diff --git a/lib/utils/cli.go b/lib/utils/cli.go index 7f64603297988..70087b93a0573 100644 --- a/lib/utils/cli.go +++ b/lib/utils/cli.go @@ -295,6 +295,18 @@ func InitCLIParser(appName, appHelp string) (app *kingpin.Application) { return app.UsageTemplate(createUsageTemplate()) } +// InitHiddenCLIParser initializes a `kingpin.Application` that does not terminate the application +// or write any usage information to os.Stdout. Can be used in scenarios where multiple `kingpin.Application` +// instances are needed without interfering with subsequent parsing. Usage output is completely suppressed, +// and the default global `--help` flag is ignored to prevent the application from exiting. +func InitHiddenCLIParser() (app *kingpin.Application) { + app = kingpin.New("", "") + app.UsageWriter(io.Discard) + app.Terminate(func(i int) {}) + + return app +} + // createUsageTemplate creates an usage template for kingpin applications. func createUsageTemplate(opts ...func(*usageTemplateOptions)) string { opt := &usageTemplateOptions{ diff --git a/tool/tctl/common/tctl.go b/tool/tctl/common/tctl.go index a8f671c5ad37e..cb3afd2fc9bc8 100644 --- a/tool/tctl/common/tctl.go +++ b/tool/tctl/common/tctl.go @@ -90,12 +90,11 @@ func TryRun(ctx context.Context, commands []CLICommand, args []string) error { utils.InitLogger(utils.LoggingForCLI, slog.LevelWarn) var ccf tctlcfg.GlobalCLIFlags - muApp := utils.InitCLIParser("tctl", GlobalHelpString) + muApp := utils.InitHiddenCLIParser() muApp.Flag("auth-server", fmt.Sprintf("Attempts to connect to specific auth/proxy address(es) instead of local auth [%v]", defaults.AuthConnectAddr().Addr)). Envar(authAddrEnvVar). StringsVar(&ccf.AuthServerAddr) - // We need to parse the arguments before executing managed updates to identify // the profile name and the required version for the current cluster. // All other commands and flags may change between versions, so full parsing @@ -104,9 +103,6 @@ func TryRun(ctx context.Context, commands []CLICommand, args []string) error { slog.WarnContext(ctx, "can't identify current profile", "error", err) } - // app is the command line parser - app := utils.InitCLIParser("tctl", GlobalHelpString) - // cfg (teleport auth server configuration) is going to be shared by all // commands cfg := servicecfg.MakeDefaultConfig() @@ -124,6 +120,9 @@ func TryRun(ctx context.Context, commands []CLICommand, args []string) error { return trace.Wrap(err) } + // app is the command line parser + app := utils.InitCLIParser("tctl", GlobalHelpString) + // Each command will add itself to the CLI parser. for i := range commands { commands[i].Initialize(app, &ccf, cfg) diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 280b8193ba513..4ca081920f62f 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -827,10 +827,9 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { // All other commands and flags may change between versions, so full parsing // should be performed only after managed updates are applied. var proxyArg string - muApp := utils.InitCLIParser("tsh", "") + muApp := utils.InitHiddenCLIParser() muApp.Flag("proxy", "Teleport proxy address").Envar(proxyEnvVar).Hidden().StringVar(&proxyArg) muApp.Flag("check-update", "Check for availability of managed update.").Envar(toolsCheckUpdateEnvVar).Hidden().BoolVar(&cf.checkManagedUpdates) - if _, err := muApp.Parse(utils.FilterArguments(args, muApp.Model())); err != nil { slog.WarnContext(ctx, "can't identify current profile", "error", err) } diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 5fdb1b2080862..9f5779e5f08a5 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -464,6 +464,29 @@ func TestNoEnvVars(t *testing.T) { require.NoError(t, trace.NewAggregate(err, ctx.Err())) } +// TestDefaultPrintUsage verifies that the main `kingpin.Application` parser has not been +// previously terminated, and that it correctly prints the usage message when using the +// global `--help` flag or the `help` command, and both are identical. +func TestDefaultPrintUsage(t *testing.T) { + t.Parallel() + testExecutable, err := os.Executable() + require.NoError(t, err) + + ctx := context.Background() + + cmd := exec.CommandContext(ctx, testExecutable, "version", "--help") + cmd.Env = []string{fmt.Sprintf("%s=1", tshBinMainTestEnv), "TELEPORT_TOOLS_VERSION=off"} + flagOutput, err := cmd.CombinedOutput() + require.NoError(t, err) + require.Contains(t, string(flagOutput), "Print the tsh client and Proxy server versions for the current context") + + cmd = exec.CommandContext(ctx, testExecutable, "help", "version") + cmd.Env = []string{fmt.Sprintf("%s=1", tshBinMainTestEnv), "TELEPORT_TOOLS_VERSION=off"} + commandOutput, err := cmd.CombinedOutput() + require.NoError(t, err) + require.Equal(t, string(flagOutput), string(commandOutput)) +} + func TestFailedLogin(t *testing.T) { t.Parallel() tmpHomePath := t.TempDir()