diff --git a/internal/cli/config.go b/internal/cli/config.go index 545f81a..ea477bd 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -251,6 +251,12 @@ func NewConfigCommand() *cobra.Command { Use: "config", Short: "Manage the configuration file", Long: "Commands for creating, editing, and querying the vaultik config file.", + // cobra checks Args only on a command that has a Run: without + // one, a mistyped subcommand printed help and exited 0. + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, _ []string) { + _ = cmd.Help() + }, } cmd.AddCommand(newConfigInitCommand()) diff --git a/internal/cli/database.go b/internal/cli/database.go index 04605df..1ae477b 100644 --- a/internal/cli/database.go +++ b/internal/cli/database.go @@ -15,6 +15,12 @@ func NewDatabaseCommand() *cobra.Command { Use: "database", Short: "Manage the local state database", Long: `Commands for managing the local SQLite state database.`, + // cobra checks Args only on a command that has a Run: without + // one, a mistyped subcommand printed help and exited 0. + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, _ []string) { + _ = cmd.Help() + }, } cmd.AddCommand( diff --git a/internal/cli/entry_unknown_subcommand_test.go b/internal/cli/entry_unknown_subcommand_test.go new file mode 100644 index 0000000..08c9647 --- /dev/null +++ b/internal/cli/entry_unknown_subcommand_test.go @@ -0,0 +1,43 @@ +package cli //nolint:testpackage // shares runEntry and the command-word constants + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestEntryGroupRejectsUnknownSubcommand runs each group command with a +// mistyped subcommand, as a crontab line might carry one. It must fail +// with exit 1 and name the word, as `vaultik bogus` does at the top +// level, instead of printing the group's help and exiting 0 having done +// nothing. The group name alone still prints its help. +// +//nolint:paralleltest // replaces os.Args, os.Stdout and os.Stderr +func TestEntryGroupRejectsUnknownSubcommand(t *testing.T) { + for _, testCase := range []struct { + group string + mistyped string + }{ + {group: cmdSnapshot, mistyped: "craete"}, + {group: "config", mistyped: "inti"}, + {group: cmdRemote, mistyped: "ifno"}, + {group: "database", mistyped: "delte"}, + {group: "completion", mistyped: "nosuchshell"}, + } { + t.Run(testCase.group, func(t *testing.T) { + code, stdout, stderr := runEntry(t, + testCase.group, testCase.mistyped) + + assert.Equal(t, 1, code) + assert.Empty(t, stdout) + assert.Contains(t, stderr, + `unknown command "`+testCase.mistyped+`"`) + + code, stdout, _ = runEntry(t, testCase.group) + + assert.Equal(t, 0, code) + assert.Contains(t, stdout, + programName+" "+testCase.group+" [command]") + }) + } +} diff --git a/internal/cli/remote.go b/internal/cli/remote.go index dd72e9f..4bf403f 100644 --- a/internal/cli/remote.go +++ b/internal/cli/remote.go @@ -18,6 +18,12 @@ func NewRemoteCommand() *cobra.Command { Use: "remote", Short: "Remote storage management commands", Long: "Commands for inspecting and managing remote storage", + // cobra checks Args only on a command that has a Run: without + // one, a mistyped subcommand printed help and exited 0. + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, _ []string) { + _ = cmd.Help() + }, } // Add subcommands diff --git a/internal/cli/root.go b/internal/cli/root.go index 463eb1d..ba1fcbc 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -74,6 +74,21 @@ on the source system.`, NewDatabaseCommand(), ) + // cobra adds the completion command only when Execute runs, and with + // no Run, so it skipped its own cobra.NoArgs and a mistyped shell name + // printed help and exited 0. Adding it here lets it have a Run. Its + // scripts go to os.Stdout as it is now: a later SetOut does not + // redirect them. + cmd.InitDefaultCompletionCmd() + + for _, sub := range cmd.Commands() { + if sub.Name() == "completion" { + sub.Run = func(completion *cobra.Command, _ []string) { + _ = completion.Help() + } + } + } + return cmd } diff --git a/internal/cli/snapshot.go b/internal/cli/snapshot.go index 1dc6811..e3b80a4 100644 --- a/internal/cli/snapshot.go +++ b/internal/cli/snapshot.go @@ -41,6 +41,12 @@ func NewSnapshotCommand() *cobra.Command { Use: "snapshot", Short: "Snapshot management commands", Long: "Commands for creating, listing, and managing snapshots", + // cobra checks Args only on a command that has a Run: without + // one, a mistyped subcommand printed help and exited 0. + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, _ []string) { + _ = cmd.Help() + }, } // Add subcommands