cmd/tailscale/cli: prefix all --help usages with "tailscale ...", some tidying

Also capitalises the start of all ShortHelp, allows subcommands to be hidden
with a "HIDDEN: " prefix in their ShortHelp, and adds a TS_DUMP_HELP envknob
to look at all --help messages together.

Fixes #11664

Signed-off-by: Paul Scott <paul@tailscale.com>
pull/11601/head
Paul Scott 1 month ago committed by Paul Scott
parent 9da135dd64
commit da4e92bf01

@ -17,7 +17,7 @@ var bugReportCmd = &ffcli.Command{
Name: "bugreport",
Exec: runBugReport,
ShortHelp: "Print a shareable identifier to help diagnose issues",
ShortUsage: "bugreport [note]",
ShortUsage: "tailscale bugreport [note]",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("bugreport")
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")

@ -28,7 +28,7 @@ var certCmd = &ffcli.Command{
Name: "cert",
Exec: runCert,
ShortHelp: "Get TLS certs",
ShortUsage: "cert [flags] <domain>",
ShortUsage: "tailscale cert [flags] <domain>",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("cert")
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")

@ -14,7 +14,6 @@ import (
"log"
"os"
"runtime"
"slices"
"strings"
"sync"
"text/tabwriter"
@ -95,6 +94,49 @@ func Run(args []string) (err error) {
})
})
rootCmd := newRootCmd()
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
if envknob.Bool("TS_DUMP_HELP") {
walkCommands(rootCmd, func(c *ffcli.Command) {
fmt.Println("===")
// UsageFuncs are typically called during Command.Run which ensures
// FlagSet is not nil.
if c.FlagSet == nil {
c.FlagSet = flag.NewFlagSet(c.Name, flag.ContinueOnError)
}
if c.UsageFunc != nil {
fmt.Println(c.UsageFunc(c))
} else {
fmt.Println(ffcli.DefaultUsageFunc(c))
}
})
return
}
localClient.Socket = rootArgs.socket
rootCmd.FlagSet.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
localClient.UseSocketOnly = true
}
})
err = rootCmd.Run(context.Background())
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
}
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
func newRootCmd() *ffcli.Command {
rootfs := newFlagSet("tailscale")
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled socket")
@ -134,10 +176,11 @@ change in the future.
exitNodeCmd(),
updateCmd,
whoisCmd,
debugCmd,
driveCmd,
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
UsageFunc: usageFunc,
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
}
if envknob.UseWIPCode() {
rootCmd.Subcommands = append(rootCmd.Subcommands,
@ -145,45 +188,16 @@ change in the future.
)
}
// Don't advertise these commands, but they're still explicitly available.
switch {
case slices.Contains(args, "debug"):
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
case slices.Contains(args, "drive"):
rootCmd.Subcommands = append(rootCmd.Subcommands, driveCmd)
}
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
}
for _, c := range rootCmd.Subcommands {
walkCommands(rootCmd, func(c *ffcli.Command) {
if c.UsageFunc == nil {
c.UsageFunc = usageFunc
}
}
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
localClient.Socket = rootArgs.socket
rootfs.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
localClient.UseSocketOnly = true
}
})
err = rootCmd.Run(context.Background())
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
}
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
return rootCmd
}
func fatalf(format string, a ...any) {
@ -202,6 +216,13 @@ var rootArgs struct {
socket string
}
func walkCommands(cmd *ffcli.Command, f func(*ffcli.Command)) {
f(cmd)
for _, sub := range cmd.Subcommands {
walkCommands(sub, f)
}
}
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
func usageFuncNoDefaultValues(c *ffcli.Command) string {
return usageFuncOpt(c, false)
@ -213,23 +234,32 @@ func usageFunc(c *ffcli.Command) string {
func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
var b strings.Builder
const hiddenPrefix = "HIDDEN: "
if c.ShortHelp != "" {
fmt.Fprintf(&b, "%s\n\n", c.ShortHelp)
}
fmt.Fprintf(&b, "USAGE\n")
if c.ShortUsage != "" {
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
fmt.Fprintf(&b, " %s\n", strings.ReplaceAll(c.ShortUsage, "\n", "\n "))
} else {
fmt.Fprintf(&b, " %s\n", c.Name)
}
fmt.Fprintf(&b, "\n")
if c.LongHelp != "" {
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
help, _ := strings.CutPrefix(c.LongHelp, hiddenPrefix)
fmt.Fprintf(&b, "%s\n\n", help)
}
if len(c.Subcommands) > 0 {
fmt.Fprintf(&b, "SUBCOMMANDS\n")
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
for _, subcommand := range c.Subcommands {
if strings.HasPrefix(subcommand.LongHelp, hiddenPrefix) {
continue
}
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
}
tw.Flush()
@ -242,7 +272,7 @@ func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
c.FlagSet.VisitAll(func(f *flag.Flag) {
var s string
name, usage := flag.UnquoteUsage(f)
if strings.HasPrefix(usage, "HIDDEN: ") {
if strings.HasPrefix(usage, hiddenPrefix) {
return
}
if isBoolFlag(f) {

@ -16,6 +16,7 @@ import (
qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/envknob"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
@ -29,15 +30,37 @@ import (
"tailscale.com/version/distro"
)
func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
envknob.PanicIfAnyEnvCheckedInInit()
}
func TestShortUsage_FullCmd(t *testing.T) {
t.Setenv("TAILSCALE_USE_WIP_CODE", "1")
if !envknob.UseWIPCode() {
t.Fatal("expected envknob.UseWIPCode() to be true")
}
// Some commands have more than one path from the root, so investigate all
// paths before we report errors.
ok := make(map[*ffcli.Command]bool)
root := newRootCmd()
walkCommands(root, func(c *ffcli.Command) {
if !ok[c] {
ok[c] = strings.HasPrefix(c.ShortUsage, "tailscale ") && (c.Name == "tailscale" || strings.Contains(c.ShortUsage, " "+c.Name+" ") || strings.HasSuffix(c.ShortUsage, " "+c.Name))
}
})
walkCommands(root, func(c *ffcli.Command) {
if !ok[c] {
t.Errorf("subcommand %s should show full usage ('tailscale ... %s ...') in ShortUsage (%q)", c.Name, c.Name, c.ShortUsage)
}
})
}
// geese is a collection of gooses. It need not be complete.
// But it should include anything handled specially (e.g. linux, windows)
// and at least one thing that's not (darwin, freebsd).
var geese = []string{"linux", "darwin", "windows", "freebsd"}
func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
envknob.PanicIfAnyEnvCheckedInInit()
}
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
// all flags. This will panic if a new flag creeps in that's unhandled.
//

@ -27,7 +27,7 @@ func init() {
var configureKubeconfigCmd = &ffcli.Command{
Name: "kubeconfig",
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
ShortUsage: "kubeconfig <hostname-or-fqdn>",
ShortUsage: "tailscale configure kubeconfig <hostname-or-fqdn>",
LongHelp: strings.TrimSpace(`
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.

@ -22,10 +22,11 @@ import (
// used to configure Synology devices, but is now a compatibility alias to
// "tailscale configure synology".
var configureHostCmd = &ffcli.Command{
Name: "configure-host",
Exec: runConfigureSynology,
ShortHelp: synologyConfigureCmd.ShortHelp,
LongHelp: synologyConfigureCmd.LongHelp,
Name: "configure-host",
Exec: runConfigureSynology,
ShortUsage: "tailscale configure-host",
ShortHelp: synologyConfigureCmd.ShortHelp,
LongHelp: synologyConfigureCmd.LongHelp,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure-host")
return fs
@ -33,9 +34,10 @@ var configureHostCmd = &ffcli.Command{
}
var synologyConfigureCmd = &ffcli.Command{
Name: "synology",
Exec: runConfigureSynology,
ShortHelp: "Configure Synology to enable outbound connections",
Name: "synology",
Exec: runConfigureSynology,
ShortUsage: "tailscale configure synology",
ShortHelp: "Configure Synology to enable outbound connections",
LongHelp: strings.TrimSpace(`
This command is intended to run at boot as root on a Synology device to
create the /dev/net/tun device and give the tailscaled binary permission

@ -14,8 +14,9 @@ import (
)
var configureCmd = &ffcli.Command{
Name: "configure",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
Name: "configure",
ShortUsage: "tailscale configure <subcommand>",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
LongHelp: strings.TrimSpace(`
The 'configure' set of commands are intended to provide a way to enable different
services on the host to use Tailscale in more ways.

@ -45,9 +45,10 @@ import (
)
var debugCmd = &ffcli.Command{
Name: "debug",
Exec: runDebug,
LongHelp: `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
Name: "debug",
Exec: runDebug,
ShortUsage: "tailscale debug <debug-flags | subcommand>",
LongHelp: `HIDDEN: "tailscale debug" contains misc debug facilities; it is not a stable interface.`,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
@ -58,15 +59,16 @@ var debugCmd = &ffcli.Command{
})(),
Subcommands: []*ffcli.Command{
{
Name: "derp-map",
Exec: runDERPMap,
ShortHelp: "print DERP map",
Name: "derp-map",
ShortUsage: "tailscale debug derp-map",
Exec: runDERPMap,
ShortHelp: "Print DERP map",
},
{
Name: "component-logs",
Exec: runDebugComponentLogs,
ShortHelp: "enable/disable debug logs for a component",
ShortUsage: "tailscale debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]",
Exec: runDebugComponentLogs,
ShortHelp: "Enable/disable debug logs for a component",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("component-logs")
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
@ -74,14 +76,16 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "daemon-goroutines",
Exec: runDaemonGoroutines,
ShortHelp: "print tailscaled's goroutines",
Name: "daemon-goroutines",
ShortUsage: "tailscale debug daemon-goroutines",
Exec: runDaemonGoroutines,
ShortHelp: "Print tailscaled's goroutines",
},
{
Name: "daemon-logs",
Exec: runDaemonLogs,
ShortHelp: "watch tailscaled's server logs",
Name: "daemon-logs",
ShortUsage: "tailscale debug daemon-logs",
Exec: runDaemonLogs,
ShortHelp: "Watch tailscaled's server logs",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("daemon-logs")
fs.IntVar(&daemonLogsArgs.verbose, "verbose", 0, "verbosity level")
@ -90,9 +94,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "metrics",
Exec: runDaemonMetrics,
ShortHelp: "print tailscaled's metrics",
Name: "metrics",
ShortUsage: "tailscale debug metrics",
Exec: runDaemonMetrics,
ShortHelp: "Print tailscaled's metrics",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("metrics")
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
@ -100,80 +105,95 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "env",
Exec: runEnv,
ShortHelp: "print cmd/tailscale environment",
Name: "env",
ShortUsage: "tailscale debug env",
Exec: runEnv,
ShortHelp: "Print cmd/tailscale environment",
},
{
Name: "stat",
Exec: runStat,
ShortHelp: "stat a file",
Name: "stat",
ShortUsage: "tailscale debug stat <files...>",
Exec: runStat,
ShortHelp: "Stat a file",
},
{
Name: "hostinfo",
Exec: runHostinfo,
ShortHelp: "print hostinfo",
Name: "hostinfo",
ShortUsage: "tailscale debug hostinfo",
Exec: runHostinfo,
ShortHelp: "Print hostinfo",
},
{
Name: "local-creds",
Exec: runLocalCreds,
ShortHelp: "print how to access Tailscale LocalAPI",
Name: "local-creds",
ShortUsage: "tailscale debug local-creds",
Exec: runLocalCreds,
ShortHelp: "Print how to access Tailscale LocalAPI",
},
{
Name: "restun",
Exec: localAPIAction("restun"),
ShortHelp: "force a magicsock restun",
Name: "restun",
ShortUsage: "tailscale debug restun",
Exec: localAPIAction("restun"),
ShortHelp: "Force a magicsock restun",
},
{
Name: "rebind",
Exec: localAPIAction("rebind"),
ShortHelp: "force a magicsock rebind",
Name: "rebind",
ShortUsage: "tailscale debug rebind",
Exec: localAPIAction("rebind"),
ShortHelp: "Force a magicsock rebind",
},
{
Name: "derp-set-on-demand",
Exec: localAPIAction("derp-set-homeless"),
ShortHelp: "enable DERP on-demand mode (breaks reachability)",
Name: "derp-set-on-demand",
ShortUsage: "tailscale debug derp-set-on-demand",
Exec: localAPIAction("derp-set-homeless"),
ShortHelp: "Enable DERP on-demand mode (breaks reachability)",
},
{
Name: "derp-unset-on-demand",
Exec: localAPIAction("derp-unset-homeless"),
ShortHelp: "disable DERP on-demand mode",
Name: "derp-unset-on-demand",
ShortUsage: "tailscale debug derp-unset-on-demand",
Exec: localAPIAction("derp-unset-homeless"),
ShortHelp: "Disable DERP on-demand mode",
},
{
Name: "break-tcp-conns",
Exec: localAPIAction("break-tcp-conns"),
ShortHelp: "break any open TCP connections from the daemon",
Name: "break-tcp-conns",
ShortUsage: "tailscale debug break-tcp-conns",
Exec: localAPIAction("break-tcp-conns"),
ShortHelp: "Break any open TCP connections from the daemon",
},
{
Name: "break-derp-conns",
Exec: localAPIAction("break-derp-conns"),
ShortHelp: "break any open DERP connections from the daemon",
Name: "break-derp-conns",
ShortUsage: "tailscale debug break-derp-conns",
Exec: localAPIAction("break-derp-conns"),
ShortHelp: "Break any open DERP connections from the daemon",
},
{
Name: "pick-new-derp",
Exec: localAPIAction("pick-new-derp"),
ShortHelp: "switch to some other random DERP home region for a short time",
Name: "pick-new-derp",
ShortUsage: "tailscale debug pick-new-derp",
Exec: localAPIAction("pick-new-derp"),
ShortHelp: "Switch to some other random DERP home region for a short time",
},
{
Name: "force-netmap-update",
Exec: localAPIAction("force-netmap-update"),
ShortHelp: "force a full no-op netmap update (for load testing)",
Name: "force-netmap-update",
ShortUsage: "tailscale debug force-netmap-update",
Exec: localAPIAction("force-netmap-update"),
ShortHelp: "Force a full no-op netmap update (for load testing)",
},
{
// TODO(bradfitz,maisem): eventually promote this out of debug
Name: "reload-config",
Exec: reloadConfig,
ShortHelp: "reload config",
Name: "reload-config",
ShortUsage: "tailscale debug reload-config",
Exec: reloadConfig,
ShortHelp: "Reload config",
},
{
Name: "control-knobs",
Exec: debugControlKnobs,
ShortHelp: "see current control knobs",
Name: "control-knobs",
ShortUsage: "tailscale debug control-knobs",
Exec: debugControlKnobs,
ShortHelp: "See current control knobs",
},
{
Name: "prefs",
Exec: runPrefs,
ShortHelp: "print prefs",
Name: "prefs",
ShortUsage: "tailscale debug prefs",
Exec: runPrefs,
ShortHelp: "Print prefs",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("prefs")
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
@ -181,9 +201,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "watch-ipn",
Exec: runWatchIPN,
ShortHelp: "subscribe to IPN message bus",
Name: "watch-ipn",
ShortUsage: "tailscale debug watch-ipn",
Exec: runWatchIPN,
ShortHelp: "Subscribe to IPN message bus",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("watch-ipn")
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
@ -194,9 +215,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "netmap",
Exec: runNetmap,
ShortHelp: "print the current network map",
Name: "netmap",
ShortUsage: "tailscale debug netmap",
Exec: runNetmap,
ShortHelp: "Print the current network map",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("netmap")
fs.BoolVar(&netmapArgs.showPrivateKey, "show-private-key", false, "include node private key in printed netmap")
@ -204,14 +226,17 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "via",
Name: "via",
ShortUsage: "tailscale via <site-id> <v4-cidr>\n" +
"tailscale via <v6-route>",
Exec: runVia,
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
ShortHelp: "Convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
},
{
Name: "ts2021",
Exec: runTS2021,
ShortHelp: "debug ts2021 protocol connectivity",
Name: "ts2021",
ShortUsage: "tailscale debug ts2021",
Exec: runTS2021,
ShortHelp: "Debug ts2021 protocol connectivity",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ts2021")
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
@ -221,9 +246,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "set-expire",
Exec: runSetExpire,
ShortHelp: "manipulate node key expiry for testing",
Name: "set-expire",
ShortUsage: "tailscale debug set-expire --in=1m",
Exec: runSetExpire,
ShortHelp: "Manipulate node key expiry for testing",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("set-expire")
fs.DurationVar(&setExpireArgs.in, "in", 0, "if non-zero, set node key to expire this duration from now")
@ -231,9 +257,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "dev-store-set",
Exec: runDevStoreSet,
ShortHelp: "set a key/value pair during development",
Name: "dev-store-set",
ShortUsage: "tailscale debug dev-store-set",
Exec: runDevStoreSet,
ShortHelp: "Set a key/value pair during development",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("store-set")
fs.BoolVar(&devStoreSetArgs.danger, "danger", false, "accept danger")
@ -241,14 +268,16 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "derp",
Exec: runDebugDERP,
ShortHelp: "test a DERP configuration",
Name: "derp",
ShortUsage: "tailscale debug derp",
Exec: runDebugDERP,
ShortHelp: "Test a DERP configuration",
},
{
Name: "capture",
Exec: runCapture,
ShortHelp: "streams pcaps for debugging",
Name: "capture",
ShortUsage: "tailscale debug capture",
Exec: runCapture,
ShortHelp: "Streams pcaps for debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("capture")
fs.StringVar(&captureArgs.outFile, "o", "", "path to stream the pcap (or - for stdout), leave empty to start wireshark")
@ -256,9 +285,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "portmap",
Exec: debugPortmap,
ShortHelp: "run portmap debugging",
Name: "portmap",
ShortUsage: "tailscale debug portmap",
Exec: debugPortmap,
ShortHelp: "Run portmap debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("portmap")
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
@ -270,14 +300,16 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "peer-endpoint-changes",
Exec: runPeerEndpointChanges,
ShortHelp: "prints debug information about a peer's endpoint changes",
Name: "peer-endpoint-changes",
ShortUsage: "tailscale debug peer-endpoint-changes <hostname-or-IP>",
Exec: runPeerEndpointChanges,
ShortHelp: "Prints debug information about a peer's endpoint changes",
},
{
Name: "dial-types",
Exec: runDebugDialTypes,
ShortHelp: "prints debug information about connecting to a given host or IP",
Name: "dial-types",
ShortUsage: "tailscale debug dial-types <hostname-or-IP> <port>",
Exec: runDebugDialTypes,
ShortHelp: "Prints debug information about connecting to a given host or IP",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("dial-types")
fs.StringVar(&debugDialTypesArgs.network, "network", "tcp", `network type to dial ("tcp", "udp", etc.)`)
@ -867,7 +899,7 @@ var setExpireArgs struct {
func runSetExpire(ctx context.Context, args []string) error {
if len(args) != 0 || setExpireArgs.in == 0 {
return errors.New("usage --in=<duration>")
return errors.New("usage: tailscale debug set-expire --in=<duration>")
}
return localClient.DebugSetExpireIn(ctx, setExpireArgs.in)
}
@ -966,7 +998,7 @@ func runPeerEndpointChanges(ctx context.Context, args []string) error {
}
if len(args) != 1 || args[0] == "" {
return errors.New("usage: peer-status <hostname-or-IP>")
return errors.New("usage: tailscale debug peer-endpoint-changes <hostname-or-IP>")
}
var ip string
@ -1042,7 +1074,7 @@ func runDebugDialTypes(ctx context.Context, args []string) error {
}
if len(args) != 2 || args[0] == "" || args[1] == "" {
return errors.New("usage: dial-types <hostname-or-IP> <port>")
return errors.New("usage: tailscale debug dial-types <hostname-or-IP> <port>")
}
port, err := strconv.ParseUint(args[1], 10, 16)

@ -14,7 +14,7 @@ import (
var downCmd = &ffcli.Command{
Name: "down",
ShortUsage: "down",
ShortUsage: "tailscale down",
ShortHelp: "Disconnect from Tailscale",
Exec: runDown,

@ -14,10 +14,10 @@ import (
)
const (
driveShareUsage = "drive share <name> <path>"
driveRenameUsage = "drive rename <oldname> <newname>"
driveUnshareUsage = "drive unshare <name>"
driveListUsage = "drive list"
driveShareUsage = "tailscale drive share <name> <path>"
driveRenameUsage = "tailscale drive rename <oldname> <newname>"
driveUnshareUsage = "tailscale drive unshare <name>"
driveListUsage = "tailscale drive list"
)
var driveCmd = &ffcli.Command{
@ -33,28 +33,32 @@ var driveCmd = &ffcli.Command{
UsageFunc: usageFuncNoDefaultValues,
Subcommands: []*ffcli.Command{
{
Name: "share",
Exec: runDriveShare,
ShortHelp: "[ALPHA] create or modify a share",
UsageFunc: usageFunc,
Name: "share",
ShortUsage: driveShareUsage,
Exec: runDriveShare,
ShortHelp: "[ALPHA] create or modify a share",
UsageFunc: usageFunc,
},
{
Name: "rename",
ShortHelp: "[ALPHA] rename a share",
Exec: runDriveRename,
UsageFunc: usageFunc,
Name: "rename",
ShortUsage: driveRenameUsage,
ShortHelp: "[ALPHA] rename a share",
Exec: runDriveRename,
UsageFunc: usageFunc,
},
{
Name: "unshare",
ShortHelp: "[ALPHA] remove a share",
Exec: runDriveUnshare,
UsageFunc: usageFunc,
Name: "unshare",
ShortUsage: driveUnshareUsage,
ShortHelp: "[ALPHA] remove a share",
Exec: runDriveUnshare,
UsageFunc: usageFunc,
},
{
Name: "list",
ShortHelp: "[ALPHA] list current shares",
Exec: runDriveList,
UsageFunc: usageFunc,
Name: "list",
ShortUsage: driveListUsage,
ShortHelp: "[ALPHA] list current shares",
Exec: runDriveList,
UsageFunc: usageFunc,
},
},
Exec: func(context.Context, []string) error {
@ -237,8 +241,8 @@ You can get a list of currently published shares by running:
$ tailscale drive list`
var shareLongHelpAs = `
const shareLongHelpAs = `
If you want a share to be accessed as a different user, you can use sudo to accomplish this. For example, to create the aforementioned share as "theuser", you could run:
$ sudo -u theuser tailscale drive share docs /Users/theuser/Documents`
$ sudo -u theuser tailscale drive share docs /Users/theuser/Documents`

@ -23,7 +23,7 @@ import (
func exitNodeCmd() *ffcli.Command {
return &ffcli.Command{
Name: "exit-node",
ShortUsage: "exit-node [flags]",
ShortUsage: "tailscale exit-node [flags]",
ShortHelp: "Show machines on your tailnet configured as exit nodes",
LongHelp: "Show machines on your tailnet configured as exit nodes",
Exec: func(context.Context, []string) error {
@ -32,7 +32,7 @@ func exitNodeCmd() *ffcli.Command {
Subcommands: append([]*ffcli.Command{
{
Name: "list",
ShortUsage: "exit-node list [flags]",
ShortUsage: "tailscale exit-node list [flags]",
ShortHelp: "Show exit nodes",
Exec: runExitNodeList,
FlagSet: (func() *flag.FlagSet {
@ -48,13 +48,13 @@ func exitNodeCmd() *ffcli.Command {
return []*ffcli.Command{
{
Name: "connect",
ShortUsage: "exit-node connect",
ShortUsage: "tailscale exit-node connect",
ShortHelp: "connect to most recently used exit node",
Exec: exitNodeSetUse(true),
},
{
Name: "disconnect",
ShortUsage: "exit-node disconnect",
ShortUsage: "tailscale exit-node disconnect",
ShortHelp: "disconnect from current exit node, if any",
Exec: exitNodeSetUse(false),
},

@ -38,7 +38,7 @@ import (
var fileCmd = &ffcli.Command{
Name: "file",
ShortUsage: "file <cp|get> ...",
ShortUsage: "tailscale file <cp|get> ...",
ShortHelp: "Send or receive files",
Subcommands: []*ffcli.Command{
fileCpCmd,
@ -65,7 +65,7 @@ func (c *countingReader) Read(buf []byte) (int, error) {
var fileCpCmd = &ffcli.Command{
Name: "cp",
ShortUsage: "file cp <files...> <target>:",
ShortUsage: "tailscale file cp <files...> <target>:",
ShortHelp: "Copy file(s) to a host",
Exec: runCp,
FlagSet: (func() *flag.FlagSet {
@ -412,7 +412,7 @@ func (v *onConflict) Set(s string) error {
var fileGetCmd = &ffcli.Command{
Name: "get",
ShortUsage: "file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
ShortUsage: "tailscale file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
ShortHelp: "Move files out of the Tailscale file inbox",
Exec: runFileGet,
FlagSet: (func() *flag.FlagSet {
@ -420,7 +420,7 @@ var fileGetCmd = &ffcli.Command{
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
fs.BoolVar(&getArgs.loop, "loop", false, "run get in a loop, receiving files as they come in")
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
fs.Var(&getArgs.conflict, "conflict", `behavior when a conflicting (same-named) file already exists in the target directory.
fs.Var(&getArgs.conflict, "conflict", "`behavior`"+` when a conflicting (same-named) file already exists in the target directory.
skip: skip conflicting files: leave them in the taildrop inbox and print an error. get any non-conflicting files
overwrite: overwrite existing file
rename: write to a new number-suffixed filename`)

@ -36,9 +36,9 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
Name: "funnel",
ShortHelp: "Turn on/off Funnel service",
ShortUsage: strings.Join([]string{
"funnel <serve-port> {on|off}",
"funnel status [--json]",
}, "\n "),
"tailscale funnel <serve-port> {on|off}",
"tailscale funnel status [--json]",
}, "\n"),
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",
@ -46,17 +46,16 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
"Turning off Funnel only turns off serving to the internet.",
"It does not affect serving to your tailnet.",
}, "\n"),
Exec: e.runFunnel,
UsageFunc: usageFunc,
Exec: e.runFunnel,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status",
Name: "status",
Exec: e.runServeStatus,
ShortUsage: "tailscale funnel status [--json]",
ShortHelp: "Show current serve/funnel status",
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
},
}

@ -12,8 +12,8 @@ import (
var idTokenCmd = &ffcli.Command{
Name: "id-token",
ShortUsage: "id-token <aud>",
ShortHelp: "fetch an OIDC id-token for the Tailscale machine",
ShortUsage: "tailscale id-token <aud>",
ShortHelp: "Fetch an OIDC id-token for the Tailscale machine",
Exec: runIDToken,
}

@ -16,7 +16,7 @@ import (
var ipCmd = &ffcli.Command{
Name: "ip",
ShortUsage: "ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortUsage: "tailscale ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortHelp: "Show Tailscale IP addresses",
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
Exec: runIP,

@ -12,7 +12,7 @@ import (
var licensesCmd = &ffcli.Command{
Name: "licenses",
ShortUsage: "licenses",
ShortUsage: "tailscale licenses",
ShortHelp: "Get open source license information",
LongHelp: "Get open source license information",
Exec: runLicenses,

@ -14,11 +14,10 @@ var loginArgs upArgsT
var loginCmd = &ffcli.Command{
Name: "login",
ShortUsage: "login [flags]",
ShortUsage: "tailscale login [flags]",
ShortHelp: "Log in to a Tailscale account",
LongHelp: `"tailscale login" logs this machine in to your Tailscale network.
This command is currently in alpha and may change in the future.`,
UsageFunc: usageFunc,
FlagSet: func() *flag.FlagSet {
return newUpFlagSet(effectiveGOOS(), &loginArgs, "login")
}(),

@ -13,7 +13,7 @@ import (
var logoutCmd = &ffcli.Command{
Name: "logout",
ShortUsage: "logout [flags]",
ShortUsage: "tailscale logout",
ShortHelp: "Disconnect from Tailscale and expire current node key",
LongHelp: strings.TrimSpace(`

@ -16,7 +16,7 @@ import (
var ncCmd = &ffcli.Command{
Name: "nc",
ShortUsage: "nc <hostname-or-IP> <port>",
ShortUsage: "tailscale nc <hostname-or-IP> <port>",
ShortHelp: "Connect to a port on a host, connected to stdin/stdout",
Exec: runNC,
}

@ -28,7 +28,7 @@ import (
var netcheckCmd = &ffcli.Command{
Name: "netcheck",
ShortUsage: "netcheck",
ShortUsage: "tailscale netcheck",
ShortHelp: "Print an analysis of local network conditions",
Exec: runNetcheck,
FlagSet: (func() *flag.FlagSet {

@ -26,7 +26,7 @@ import (
var netlockCmd = &ffcli.Command{
Name: "lock",
ShortUsage: "lock <sub-command> <arguments>",
ShortUsage: "tailscale lock <sub-command> <arguments>",
ShortHelp: "Manage tailnet lock",
LongHelp: "Manage tailnet lock",
Subcommands: []*ffcli.Command{
@ -61,7 +61,7 @@ var nlInitArgs struct {
var nlInitCmd = &ffcli.Command{
Name: "init",
ShortUsage: "init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
ShortUsage: "tailscale lock init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
ShortHelp: "Initialize tailnet lock",
LongHelp: strings.TrimSpace(`
@ -183,7 +183,7 @@ var nlStatusArgs struct {
var nlStatusCmd = &ffcli.Command{
Name: "status",
ShortUsage: "status",
ShortUsage: "tailscale lock status",
ShortHelp: "Outputs the state of tailnet lock",
LongHelp: "Outputs the state of tailnet lock",
Exec: runNetworkLockStatus,
@ -280,7 +280,7 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
var nlAddCmd = &ffcli.Command{
Name: "add",
ShortUsage: "add <public-key>...",
ShortUsage: "tailscale lock add <public-key>...",
ShortHelp: "Adds one or more trusted signing keys to tailnet lock",
LongHelp: "Adds one or more trusted signing keys to tailnet lock",
Exec: func(ctx context.Context, args []string) error {
@ -294,7 +294,7 @@ var nlRemoveArgs struct {
var nlRemoveCmd = &ffcli.Command{
Name: "remove",
ShortUsage: "remove [--re-sign=false] <public-key>...",
ShortUsage: "tailscale lock remove [--re-sign=false] <public-key>...",
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
Exec: runNetworkLockRemove,
@ -435,7 +435,7 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
var nlSignCmd = &ffcli.Command{
Name: "sign",
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
ShortUsage: "tailscale lock sign <node-key> [<rotation-key>] or sign <auth-key>",
ShortHelp: "Signs a node or pre-approved auth key",
LongHelp: `Either:
- signs a node key and transmits the signature to the coordination server, or
@ -479,7 +479,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
var nlDisableCmd = &ffcli.Command{
Name: "disable",
ShortUsage: "disable <disablement-secret>",
ShortUsage: "tailscale lock disable <disablement-secret>",
ShortHelp: "Consumes a disablement secret to shut down tailnet lock for the tailnet",
LongHelp: strings.TrimSpace(`
@ -508,7 +508,7 @@ func runNetworkLockDisable(ctx context.Context, args []string) error {
var nlLocalDisableCmd = &ffcli.Command{
Name: "local-disable",
ShortUsage: "local-disable",
ShortUsage: "tailscale lock local-disable",
ShortHelp: "Disables tailnet lock for this node only",
LongHelp: strings.TrimSpace(`
@ -530,7 +530,7 @@ func runNetworkLockLocalDisable(ctx context.Context, args []string) error {
var nlDisablementKDFCmd = &ffcli.Command{
Name: "disablement-kdf",
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>",
ShortUsage: "tailscale lock disablement-kdf <hex-encoded-disablement-secret>",
ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)",
LongHelp: "Computes a disablement value from a disablement secret (advanced users only)",
Exec: runNetworkLockDisablementKDF,
@ -555,7 +555,7 @@ var nlLogArgs struct {
var nlLogCmd = &ffcli.Command{
Name: "log",
ShortUsage: "log [--limit N]",
ShortUsage: "tailscale lock log [--limit N]",
ShortHelp: "List changes applied to tailnet lock",
LongHelp: "List changes applied to tailnet lock",
Exec: runNetworkLockLog,
@ -719,7 +719,7 @@ var nlRevokeKeysArgs struct {
var nlRevokeKeysCmd = &ffcli.Command{
Name: "revoke-keys",
ShortUsage: "revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
ShortUsage: "tailscale lock revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
ShortHelp: "Revoke compromised tailnet-lock keys",
LongHelp: `Retroactively revoke the specified tailnet lock keys (tlpub:abc).

@ -23,7 +23,7 @@ import (
var pingCmd = &ffcli.Command{
Name: "ping",
ShortUsage: "ping <hostname-or-IP>",
ShortUsage: "tailscale ping <hostname-or-IP>",
ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
LongHelp: strings.TrimSpace(`

@ -44,13 +44,13 @@ func newServeLegacyCommand(e *serveEnv) *ffcli.Command {
Name: "serve",
ShortHelp: "Serve content and local servers",
ShortUsage: strings.Join([]string{
"serve http:<port> <mount-point> <source> [off]",
"serve https:<port> <mount-point> <source> [off]",
"serve tcp:<port> tcp://localhost:<local-port> [off]",
"serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
"serve status [--json]",
"serve reset",
}, "\n "),
"tailscale serve http:<port> <mount-point> <source> [off]",
"tailscale serve https:<port> <mount-point> <source> [off]",
"tailscale serve tcp:<port> tcp://localhost:<local-port> [off]",
"tailscale serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
"tailscale serve status [--json]",
"tailscale serve reset",
}, "\n"),
LongHelp: strings.TrimSpace(`
*** BETA; all of this is subject to change ***
@ -91,24 +91,21 @@ EXAMPLES
local plaintext server on port 80:
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
`),
Exec: e.runServe,
UsageFunc: usageFunc,
Exec: e.runServe,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status",
ShortHelp: "Show current serve/funnel status",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
{
Name: "reset",
Exec: e.runServeReset,
ShortHelp: "reset current serve/funnel config",
ShortHelp: "Reset current serve/funnel config",
FlagSet: e.newFlags("serve-reset", nil),
UsageFunc: usageFunc,
},
},
}

@ -110,10 +110,10 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
Name: info.Name,
ShortHelp: info.ShortHelp,
ShortUsage: strings.Join([]string{
fmt.Sprintf("%s <target>", info.Name),
fmt.Sprintf("%s status [--json]", info.Name),
fmt.Sprintf("%s reset", info.Name),
}, "\n "),
fmt.Sprintf("tailscale %s <target>", info.Name),
fmt.Sprintf("tailscale %s status [--json]", info.Name),
fmt.Sprintf("tailscale %s reset", info.Name),
}, "\n"),
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name),
Exec: e.runServeCombined(subcmd),
@ -131,20 +131,20 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
UsageFunc: usageFuncNoDefaultValues,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "view current proxy configuration",
Name: "status",
ShortUsage: "tailscale " + info.Name + " status [--json]",
Exec: e.runServeStatus,
ShortHelp: "View current " + info.Name + " configuration",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
{
Name: "reset",
ShortHelp: "reset current serve/funnel config",
Exec: e.runServeReset,
FlagSet: e.newFlags("serve-reset", nil),
UsageFunc: usageFunc,
Name: "reset",
ShortUsage: "tailscale " + info.Name + " reset",
ShortHelp: "Reset current " + info.Name + " config",
Exec: e.runServeReset,
FlagSet: e.newFlags("serve-reset", nil),
},
},
}

@ -25,7 +25,7 @@ import (
var setCmd = &ffcli.Command{
Name: "set",
ShortUsage: "set [flags]",
ShortUsage: "tailscale set [flags]",
ShortHelp: "Change specified preferences",
LongHelp: `"tailscale set" allows changing specific preferences.

@ -26,7 +26,7 @@ import (
var sshCmd = &ffcli.Command{
Name: "ssh",
ShortUsage: "ssh [user@]<host> [args...]",
ShortUsage: "tailscale ssh [user@]<host> [args...]",
ShortHelp: "SSH to a Tailscale machine",
LongHelp: strings.TrimSpace(`

@ -29,7 +29,7 @@ import (
var statusCmd = &ffcli.Command{
Name: "status",
ShortUsage: "status [--active] [--web] [--json]",
ShortUsage: "tailscale status [--active] [--web] [--json]",
ShortHelp: "Show state of tailscaled and its connections",
LongHelp: strings.TrimSpace(`

@ -17,26 +17,22 @@ import (
)
var switchCmd = &ffcli.Command{
Name: "switch",
ShortHelp: "Switches to a different Tailscale account",
Name: "switch",
ShortUsage: "tailscale switch <id>",
ShortHelp: "Switches to a different Tailscale account",
LongHelp: `"tailscale switch" switches between logged in accounts. You can
use the ID that's returned from 'tailnet switch -list'
to pick which profile you want to switch to. Alternatively, you
can use the Tailnet or the account names to switch as well.
This command is currently in alpha and may change in the future.`,
FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("switch", flag.ExitOnError)
fs.BoolVar(&switchArgs.list, "list", false, "list available accounts")
return fs
}(),
Exec: switchProfile,
UsageFunc: func(*ffcli.Command) string {
return `USAGE
switch <id>
switch --list
"tailscale switch" switches between logged in accounts. You can
use the ID that's returned from 'tailnet switch -list'
to pick which profile you want to switch to. Alternatively, you
can use the Tailnet or the account names to switch as well.
This command is currently in alpha and may change in the future.`
},
}
var switchArgs struct {

@ -44,7 +44,7 @@ import (
var upCmd = &ffcli.Command{
Name: "up",
ShortUsage: "up [flags]",
ShortUsage: "tailscale up [flags]",
ShortHelp: "Connect to Tailscale, logging in if needed",
LongHelp: strings.TrimSpace(`

@ -19,7 +19,7 @@ import (
var updateCmd = &ffcli.Command{
Name: "update",
ShortUsage: "update",
ShortUsage: "tailscale update",
ShortHelp: "Update Tailscale to the latest/different version",
Exec: runUpdate,
FlagSet: (func() *flag.FlagSet {

@ -17,7 +17,7 @@ import (
var versionCmd = &ffcli.Command{
Name: "version",
ShortUsage: "version [flags]",
ShortUsage: "tailscale version [flags]",
ShortHelp: "Print Tailscale version",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("version")

@ -26,7 +26,7 @@ import (
var webCmd = &ffcli.Command{
Name: "web",
ShortUsage: "web [flags]",
ShortUsage: "tailscale web [flags]",
ShortHelp: "Run a web server for controlling Tailscale",
LongHelp: strings.TrimSpace(`

@ -17,13 +17,12 @@ import (
var whoisCmd = &ffcli.Command{
Name: "whois",
ShortUsage: "whois [--json] ip[:port]",
ShortUsage: "tailscale whois [--json] ip[:port]",
ShortHelp: "Show the machine and user associated with a Tailscale IP (v4 or v6)",
LongHelp: strings.TrimSpace(`
'tailscale whois' shows the machine and user associated with a Tailscale IP (v4 or v6).
`),
UsageFunc: usageFunc,
Exec: runWhoIs,
Exec: runWhoIs,
FlagSet: func() *flag.FlagSet {
fs := newFlagSet("whois")
fs.BoolVar(&whoIsArgs.json, "json", false, "output in JSON format")

Loading…
Cancel
Save