Skip to content

Commit

Permalink
cmd/tailscale/cli: fix "subcommand required" errors when typod
Browse files Browse the repository at this point in the history
Fixes tailscale#11672

Signed-off-by: Paul Scott <paul@tailscale.com>
  • Loading branch information
icio committed Apr 17, 2024
1 parent 3ff3445 commit d07ede4
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 27 deletions.
61 changes: 56 additions & 5 deletions cmd/tailscale/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
})
Expand All @@ -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)) {
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/tailscale/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 0 additions & 4 deletions cmd/tailscale/cli/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package cli

import (
"context"
"flag"
"runtime"
"strings"
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions cmd/tailscale/cli/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 0 additions & 4 deletions cmd/tailscale/cli/drive.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package cli

import (
"context"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 0 additions & 4 deletions cmd/tailscale/cli/exitnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 0 additions & 6 deletions cmd/tailscale/cli/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion cmd/tailscale/cli/network-lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit d07ede4

Please sign in to comment.