cmd/tailscale/cli: fix "subcommand required" errors when typod

Fixes #11672

Signed-off-by: Paul Scott <paul@tailscale.com>
pull/11772/head
Paul Scott 2 weeks ago committed by Paul Scott
parent 3ff3445e9d
commit d07ede461a

@ -99,15 +99,32 @@ func Run(args []string) (err error) {
if errors.Is(err, flag.ErrHelp) {
return nil
}
if noexec := (ffcli.NoExecError{}); errors.As(err, &noexec) {
// When the user enters an unknown subcommand, ffcli tries to run
// the closest valid parent subcommand with everything else as args,
// returning NoExecError if it doesn't have an Exec function.
cmd := noexec.Command
args := cmd.FlagSet.Args()
if len(cmd.Subcommands) > 0 {
if len(args) > 0 {
return fmt.Errorf("%s: unknown subcommand: %s", fullCmd(rootCmd, cmd), args[0])
}
subs := make([]string, 0, len(cmd.Subcommands))
for _, sub := range cmd.Subcommands {
subs = append(subs, sub.Name)
}
return fmt.Errorf("%s: missing subcommand: %s", fullCmd(rootCmd, cmd), strings.Join(subs, ", "))
}
}
return err
}
if envknob.Bool("TS_DUMP_HELP") {
walkCommands(rootCmd, func(w cmdWalk) bool {
fmt.Println("===")
c := w.cmd
// UsageFuncs are typically called during Command.Run which ensures
// FlagSet is not nil.
c := w.Command
if c.FlagSet == nil {
c.FlagSet = flag.NewFlagSet(c.Name, flag.ContinueOnError)
}
@ -182,7 +199,12 @@ change in the future.
driveCmd,
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
Exec: func(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("tailscale: unknown subcommand: %s", args[0])
}
return flag.ErrHelp
},
}
if envknob.UseWIPCode() {
rootCmd.Subcommands = append(rootCmd.Subcommands,
@ -195,8 +217,8 @@ change in the future.
}
walkCommands(rootCmd, func(w cmdWalk) bool {
if w.cmd.UsageFunc == nil {
w.cmd.UsageFunc = usageFunc
if w.UsageFunc == nil {
w.UsageFunc = usageFunc
}
return true
})
@ -220,10 +242,24 @@ var rootArgs struct {
}
type cmdWalk struct {
cmd *ffcli.Command
*ffcli.Command
parents []*ffcli.Command
}
func (w cmdWalk) Path() string {
if len(w.parents) == 0 {
return w.Name
}
var sb strings.Builder
for _, p := range w.parents {
sb.WriteString(p.Name)
sb.WriteString(" ")
}
sb.WriteString(w.Name)
return sb.String()
}
// walkCommands calls f for root and all of its nested subcommands until f
// returns false or all have been visited.
func walkCommands(root *ffcli.Command, f func(w cmdWalk) (more bool)) {
@ -243,6 +279,21 @@ func walkCommands(root *ffcli.Command, f func(w cmdWalk) (more bool)) {
walk(root, nil, f)
}
// fullCmd returns the full "tailscale ... cmd" invocation for a subcommand.
func fullCmd(root, cmd *ffcli.Command) (full string) {
walkCommands(root, func(w cmdWalk) bool {
if w.Command == cmd {
full = w.Path()
return false
}
return true
})
if full == "" {
return cmd.Name
}
return full
}
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
func usageFuncNoDefaultValues(c *ffcli.Command) string {
return usageFuncOpt(c, false)

@ -40,7 +40,7 @@ func TestShortUsage(t *testing.T) {
}
walkCommands(newRootCmd(), func(w cmdWalk) bool {
c, parents := w.cmd, w.parents
c, parents := w.Command, w.parents
// Words that we expect to be in the usage.
words := make([]string, len(parents)+1)

@ -4,7 +4,6 @@
package cli
import (
"context"
"flag"
"runtime"
"strings"
@ -26,9 +25,6 @@ services on the host to use Tailscale in more ways.
return fs
})(),
Subcommands: configureSubcommands(),
Exec: func(ctx context.Context, args []string) error {
return flag.ErrHelp
},
}
func configureSubcommands() (out []*ffcli.Command) {

@ -346,7 +346,7 @@ func outName(dst string) string {
func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
return fmt.Errorf("tailscale debug: unknown subcommand: %s", args[0])
}
var usedFlag bool
if out := debugArgs.cpuFile; out != "" {
@ -401,7 +401,7 @@ func runDebug(ctx context.Context, args []string) error {
// to subcommands.
return nil
}
return errors.New("see 'tailscale debug --help")
return errors.New("tailscale debug: subcommand or flag required")
}
func runLocalCreds(ctx context.Context, args []string) error {

@ -5,7 +5,6 @@ package cli
import (
"context"
"errors"
"fmt"
"strings"
@ -57,9 +56,6 @@ var driveCmd = &ffcli.Command{
Exec: runDriveList,
},
},
Exec: func(context.Context, []string) error {
return errors.New("drive subcommand required; run 'tailscale drive -h' for details")
},
}
// runDriveShare is the entry point for the "tailscale drive share" command.

@ -25,10 +25,6 @@ func exitNodeCmd() *ffcli.Command {
Name: "exit-node",
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 {
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
},
Subcommands: append([]*ffcli.Command{
{
Name: "list",

@ -44,12 +44,6 @@ var fileCmd = &ffcli.Command{
fileCpCmd,
fileGetCmd,
},
Exec: func(context.Context, []string) error {
// TODO(bradfitz): is there a better ffcli way to
// annotate subcommand-required commands that don't
// have an exec body of their own?
return errors.New("file subcommand required; run 'tailscale file -h' for details")
},
}
type countingReader struct {

@ -26,7 +26,7 @@ import (
var netlockCmd = &ffcli.Command{
Name: "lock",
ShortUsage: "tailscale lock <sub-command> <arguments>",
ShortUsage: "tailscale lock <subcommand> [arguments...]",
ShortHelp: "Manage tailnet lock",
LongHelp: "Manage tailnet lock",
Subcommands: []*ffcli.Command{
@ -49,6 +49,9 @@ func runNetworkLockNoSubcommand(ctx context.Context, args []string) error {
if len(args) >= 2 && args[0] == "tskey-wrap" {
return runTskeyWrapCmd(ctx, args[1:])
}
if len(args) > 0 {
return fmt.Errorf("tailscale lock: unknown subcommand: %s", args[0])
}
return runNetworkLockStatus(ctx, args)
}
@ -195,6 +198,10 @@ var nlStatusCmd = &ffcli.Command{
}
func runNetworkLockStatus(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("tailscale lock status: unexpected argument")
}
st, err := localClient.NetworkLockStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)

Loading…
Cancel
Save