package cli import ( "bufio" "context" "flag" "fmt" "io" "os" "path/filepath" "reflect" "sort" "strings" "unicode" ) const ( // ignoreFlagPrefix is to ignore test flags when adding flags from other packages ignoreFlagPrefix = "test." commandContextKey = contextKey("cli.context") ) type contextKey string // Command contains everything needed to run an application that // accepts a string slice of arguments such as os.Args. A given // Command may contain Flags and sub-commands in Commands. type Command struct { // The name of the command Name string `json:"name"` // A list of aliases for the command Aliases []string `json:"aliases"` // A short description of the usage of this command Usage string `json:"usage"` // Text to override the USAGE section of help UsageText string `json:"usageText"` // A short description of the arguments of this command ArgsUsage string `json:"argsUsage"` // Version of the command Version string `json:"version"` // Longer explanation of how the command works Description string `json:"description"` // DefaultCommand is the (optional) name of a command // to run if no command names are passed as CLI arguments. DefaultCommand string `json:"defaultCommand"` // The category the command is part of Category string `json:"category"` // List of child commands Commands []*Command `json:"commands"` // List of flags to parse Flags []Flag `json:"flags"` // Boolean to hide built-in help command and help flag HideHelp bool `json:"hideHelp"` // Ignored if HideHelp is true. HideHelpCommand bool `json:"hideHelpCommand"` // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool `json:"hideVersion"` // Boolean to enable shell completion commands EnableShellCompletion bool `json:"-"` // Shell Completion generation command name ShellCompletionCommandName string `json:"-"` // The function to call when checking for shell command completions ShellComplete ShellCompleteFunc `json:"-"` // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run Before BeforeFunc `json:"-"` // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics After AfterFunc `json:"-"` // The function to call when this command is invoked Action ActionFunc `json:"-"` // Execute this function if the proper command cannot be found CommandNotFound CommandNotFoundFunc `json:"-"` // Execute this function if a usage error occurs. OnUsageError OnUsageErrorFunc `json:"-"` // Execute this function when an invalid flag is accessed from the context InvalidFlagAccessHandler InvalidFlagAccessFunc `json:"-"` // Boolean to hide this command from help or completion Hidden bool `json:"hidden"` // List of all authors who contributed (string or fmt.Stringer) // TODO: ~string | fmt.Stringer when interface unions are available Authors []any `json:"authors"` // Copyright of the binary if any Copyright string `json:"copyright"` // Reader reader to write input to (useful for tests) Reader io.Reader `json:"-"` // Writer writer to write output to Writer io.Writer `json:"-"` // ErrWriter writes error output ErrWriter io.Writer `json:"-"` // ExitErrHandler processes any error encountered while running an App before // it is returned to the caller. If no function is provided, HandleExitCoder // is used as the default behavior. ExitErrHandler ExitErrHandlerFunc `json:"-"` // Other custom info Metadata map[string]interface{} `json:"metadata"` // Carries a function which returns app specific info. ExtraInfo func() map[string]string `json:"-"` // CustomRootCommandHelpTemplate the text template for app help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomRootCommandHelpTemplate string `json:"-"` // SliceFlagSeparator is used to customize the separator for SliceFlag, the default is "," SliceFlagSeparator string `json:"sliceFlagSeparator"` // DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false DisableSliceFlagSeparator bool `json:"disableSliceFlagSeparator"` // Boolean to enable short-option handling so user can combine several // single-character bool arguments into one // i.e. foobar -o -v -> foobar -ov UseShortOptionHandling bool `json:"useShortOptionHandling"` // Enable suggestions for commands and flags Suggest bool `json:"suggest"` // Allows global flags set by libraries which use flag.XXXVar(...) directly // to be parsed through this library AllowExtFlags bool `json:"allowExtFlags"` // Treat all flags as normal arguments if true SkipFlagParsing bool `json:"skipFlagParsing"` // CustomHelpTemplate the text template for the command help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomHelpTemplate string `json:"-"` // Use longest prefix match for commands PrefixMatchCommands bool `json:"prefixMatchCommands"` // Custom suggest command for matching SuggestCommandFunc SuggestCommandFunc `json:"-"` // Flag exclusion group MutuallyExclusiveFlags []MutuallyExclusiveFlags `json:"mutuallyExclusiveFlags"` // Arguments to parse for this command Arguments []Argument `json:"arguments"` // Whether to read arguments from stdin // applicable to root command only ReadArgsFromStdin bool `json:"readArgsFromStdin"` // categories contains the categorized commands and is populated on app startup categories CommandCategories // flagCategories contains the categorized flags and is populated on app startup flagCategories FlagCategories // flags that have been applied in current parse appliedFlags []Flag // The parent of this command. This value will be nil for the // command at the root of the graph. parent *Command // the flag.FlagSet for this command flagSet *flag.FlagSet // parsed args parsedArgs Args // track state of error handling isInError bool // track state of defaults didSetupDefaults bool // whether in shell completion mode shellCompletion bool } // FullName returns the full name of the command. // For commands with parents this ensures that the parent commands // are part of the command path. func (cmd *Command) FullName() string { namePath := []string{} if cmd.parent != nil { namePath = append(namePath, cmd.parent.FullName()) } return strings.Join(append(namePath, cmd.Name), " ") } func (cmd *Command) Command(name string) *Command { for _, subCmd := range cmd.Commands { if subCmd.HasName(name) { return subCmd } } return nil } func (cmd *Command) setupDefaults(osArgs []string) { if cmd.didSetupDefaults { tracef("already did setup (cmd=%[1]q)", cmd.Name) return } cmd.didSetupDefaults = true isRoot := cmd.parent == nil tracef("isRoot? %[1]v (cmd=%[2]q)", isRoot, cmd.Name) if cmd.ShellComplete == nil { tracef("setting default ShellComplete (cmd=%[1]q)", cmd.Name) cmd.ShellComplete = DefaultCompleteWithFlags } if cmd.Name == "" && isRoot { name := filepath.Base(osArgs[0]) tracef("setting cmd.Name from first arg basename (cmd=%[1]q)", name) cmd.Name = name } if cmd.Usage == "" && isRoot { tracef("setting default Usage (cmd=%[1]q)", cmd.Name) cmd.Usage = "A new cli application" } if cmd.Version == "" { tracef("setting HideVersion=true due to empty Version (cmd=%[1]q)", cmd.Name) cmd.HideVersion = true } if cmd.Action == nil { tracef("setting default Action as help command action (cmd=%[1]q)", cmd.Name) cmd.Action = helpCommandAction } if cmd.Reader == nil { tracef("setting default Reader as os.Stdin (cmd=%[1]q)", cmd.Name) cmd.Reader = os.Stdin } if cmd.Writer == nil { tracef("setting default Writer as os.Stdout (cmd=%[1]q)", cmd.Name) cmd.Writer = os.Stdout } if cmd.ErrWriter == nil { tracef("setting default ErrWriter as os.Stderr (cmd=%[1]q)", cmd.Name) cmd.ErrWriter = os.Stderr } if cmd.AllowExtFlags { tracef("visiting all flags given AllowExtFlags=true (cmd=%[1]q)", cmd.Name) // add global flags added by other packages flag.VisitAll(func(f *flag.Flag) { // skip test flags if !strings.HasPrefix(f.Name, ignoreFlagPrefix) { cmd.Flags = append(cmd.Flags, &extFlag{f}) } }) } for _, subCmd := range cmd.Commands { tracef("setting sub-command (cmd=%[1]q) parent as self (cmd=%[2]q)", subCmd.Name, cmd.Name) subCmd.parent = cmd } cmd.ensureHelp() if !cmd.HideVersion && isRoot { tracef("appending version flag (cmd=%[1]q)", cmd.Name) cmd.appendFlag(VersionFlag) } if cmd.PrefixMatchCommands && cmd.SuggestCommandFunc == nil { tracef("setting default SuggestCommandFunc (cmd=%[1]q)", cmd.Name) cmd.SuggestCommandFunc = suggestCommand } if cmd.EnableShellCompletion || cmd.Root().shellCompletion { completionCommand := buildCompletionCommand(cmd.Name) if cmd.ShellCompletionCommandName != "" { tracef( "setting completion command name (%[1]q) from "+ "cmd.ShellCompletionCommandName (cmd=%[2]q)", cmd.ShellCompletionCommandName, cmd.Name, ) completionCommand.Name = cmd.ShellCompletionCommandName } tracef("appending completionCommand (cmd=%[1]q)", cmd.Name) cmd.appendCommand(completionCommand) } tracef("setting command categories (cmd=%[1]q)", cmd.Name) cmd.categories = newCommandCategories() for _, subCmd := range cmd.Commands { cmd.categories.AddCommand(subCmd.Category, subCmd) } tracef("sorting command categories (cmd=%[1]q)", cmd.Name) sort.Sort(cmd.categories.(*commandCategories)) tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name) for _, grp := range cmd.MutuallyExclusiveFlags { grp.propagateCategory() } tracef("setting flag categories (cmd=%[1]q)", cmd.Name) cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) if cmd.Metadata == nil { tracef("setting default Metadata (cmd=%[1]q)", cmd.Name) cmd.Metadata = map[string]any{} } if len(cmd.SliceFlagSeparator) != 0 { tracef("setting defaultSliceFlagSeparator from cmd.SliceFlagSeparator (cmd=%[1]q)", cmd.Name) defaultSliceFlagSeparator = cmd.SliceFlagSeparator } tracef("setting disableSliceFlagSeparator from cmd.DisableSliceFlagSeparator (cmd=%[1]q)", cmd.Name) disableSliceFlagSeparator = cmd.DisableSliceFlagSeparator } func (cmd *Command) setupCommandGraph() { tracef("setting up command graph (cmd=%[1]q)", cmd.Name) for _, subCmd := range cmd.Commands { subCmd.parent = cmd subCmd.setupSubcommand() subCmd.setupCommandGraph() } } func (cmd *Command) setupSubcommand() { tracef("setting up self as sub-command (cmd=%[1]q)", cmd.Name) cmd.ensureHelp() tracef("setting command categories (cmd=%[1]q)", cmd.Name) cmd.categories = newCommandCategories() for _, subCmd := range cmd.Commands { cmd.categories.AddCommand(subCmd.Category, subCmd) } tracef("sorting command categories (cmd=%[1]q)", cmd.Name) sort.Sort(cmd.categories.(*commandCategories)) tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name) for _, grp := range cmd.MutuallyExclusiveFlags { grp.propagateCategory() } tracef("setting flag categories (cmd=%[1]q)", cmd.Name) cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) } func (cmd *Command) hideHelp() bool { tracef("hide help (cmd=%[1]q)", cmd.Name) for c := cmd; c != nil; c = c.parent { if c.HideHelp { return true } } return false } func (cmd *Command) ensureHelp() { tracef("ensuring help (cmd=%[1]q)", cmd.Name) helpCommand := buildHelpCommand(true) if !cmd.hideHelp() { if cmd.Command(helpCommand.Name) == nil { if !cmd.HideHelpCommand { tracef("appending helpCommand (cmd=%[1]q)", cmd.Name) cmd.appendCommand(helpCommand) } } if HelpFlag != nil { tracef("appending HelpFlag (cmd=%[1]q)", cmd.Name) cmd.appendFlag(HelpFlag) } } } func (cmd *Command) parseArgsFromStdin() ([]string, error) { type state int const ( stateSearchForToken state = -1 stateSearchForString state = 0 ) st := stateSearchForToken linenum := 1 token := "" args := []string{} breader := bufio.NewReader(cmd.Reader) outer: for { ch, _, err := breader.ReadRune() if err == io.EOF { switch st { case stateSearchForToken: if token != "--" { args = append(args, token) } case stateSearchForString: // make sure string is not empty for _, t := range token { if !unicode.IsSpace(t) { args = append(args, token) } } } break outer } if err != nil { return nil, err } switch st { case stateSearchForToken: if unicode.IsSpace(ch) || ch == '"' { if ch == '\n' { linenum++ } if token != "" { // end the processing here if token == "--" { break outer } args = append(args, token) token = "" } if ch == '"' { st = stateSearchForString } continue } token += string(ch) case stateSearchForString: if ch != '"' { token += string(ch) } else { if token != "" { args = append(args, token) token = "" } /*else { //TODO. Should we pass in empty strings ? }*/ st = stateSearchForToken } } } tracef("parsed stdin args as %v (cmd=%[2]q)", args, cmd.Name) return args, nil } // Run is the entry point to the command graph. The positional // arguments are parsed according to the Flag and Command // definitions and the matching Action functions are run. func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) { tracef("running with arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name) cmd.setupDefaults(osArgs) if v, ok := ctx.Value(commandContextKey).(*Command); ok { tracef("setting parent (cmd=%[1]q) command from context.Context value (cmd=%[2]q)", v.Name, cmd.Name) cmd.parent = v } if cmd.parent == nil { if cmd.ReadArgsFromStdin { if args, err := cmd.parseArgsFromStdin(); err != nil { return err } else { osArgs = append(osArgs, args...) } } // handle the completion flag separately from the flagset since // completion could be attempted after a flag, but before its value was put // on the command line. this causes the flagset to interpret the completion // flag name as the value of the flag before it which is undesirable // note that we can only do this because the shell autocomplete function // always appends the completion flag at the end of the command tracef("checking osArgs %v (cmd=%[2]q)", osArgs, cmd.Name) cmd.shellCompletion, osArgs = checkShellCompleteFlag(cmd, osArgs) tracef("setting cmd.shellCompletion=%[1]v from checkShellCompleteFlag (cmd=%[2]q)", cmd.shellCompletion && cmd.EnableShellCompletion, cmd.Name) cmd.shellCompletion = cmd.EnableShellCompletion && cmd.shellCompletion } tracef("using post-checkShellCompleteFlag arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name) tracef("setting self as cmd in context (cmd=%[1]q)", cmd.Name) ctx = context.WithValue(ctx, commandContextKey, cmd) if cmd.parent == nil { cmd.setupCommandGraph() } args, err := cmd.parseFlags(&stringSliceArgs{v: osArgs}) tracef("using post-parse arguments %[1]q (cmd=%[2]q)", args, cmd.Name) if checkCompletions(ctx, cmd) { return nil } if err != nil { tracef("setting deferErr from %[1]q (cmd=%[2]q)", err, cmd.Name) deferErr = err cmd.isInError = true if cmd.OnUsageError != nil { err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) err = cmd.handleExitCoder(ctx, err) return err } fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error()) if cmd.Suggest { if suggestion, err := cmd.suggestFlagFromError(err, ""); err == nil { fmt.Fprintf(cmd.Root().ErrWriter, "%s", suggestion) } } if !cmd.hideHelp() { if cmd.parent == nil { tracef("running ShowAppHelp") if err := ShowAppHelp(cmd); err != nil { tracef("SILENTLY IGNORING ERROR running ShowAppHelp %[1]v (cmd=%[2]q)", err, cmd.Name) } } else { tracef("running ShowCommandHelp with %[1]q", cmd.Name) if err := ShowCommandHelp(ctx, cmd, cmd.Name); err != nil { tracef("SILENTLY IGNORING ERROR running ShowCommandHelp with %[1]q %[2]v", cmd.Name, err) } } } return err } if cmd.checkHelp() { return helpCommandAction(ctx, cmd) } else { tracef("no help is wanted (cmd=%[1]q)", cmd.Name) } if cmd.parent == nil && !cmd.HideVersion && checkVersion(cmd) { ShowVersion(cmd) return nil } if cmd.After != nil && !cmd.Root().shellCompletion { defer func() { if err := cmd.After(ctx, cmd); err != nil { err = cmd.handleExitCoder(ctx, err) if deferErr != nil { deferErr = newMultiError(deferErr, err) } else { deferErr = err } } }() } for _, grp := range cmd.MutuallyExclusiveFlags { if err := grp.check(cmd); err != nil { _ = ShowSubcommandHelp(cmd) return err } } if cmd.Before != nil && !cmd.Root().shellCompletion { if bctx, err := cmd.Before(ctx, cmd); err != nil { deferErr = cmd.handleExitCoder(ctx, err) return deferErr } else if bctx != nil { ctx = bctx } } tracef("running flag actions (cmd=%[1]q)", cmd.Name) if err := cmd.runFlagActions(ctx); err != nil { return err } var subCmd *Command if args.Present() { tracef("checking positional args %[1]q (cmd=%[2]q)", args, cmd.Name) name := args.First() tracef("using first positional argument as sub-command name=%[1]q (cmd=%[2]q)", name, cmd.Name) if cmd.SuggestCommandFunc != nil { name = cmd.SuggestCommandFunc(cmd.Commands, name) } subCmd = cmd.Command(name) if subCmd == nil { hasDefault := cmd.DefaultCommand != "" isFlagName := checkStringSliceIncludes(name, cmd.FlagNames()) if hasDefault { tracef("using default command=%[1]q (cmd=%[2]q)", cmd.DefaultCommand, cmd.Name) } if isFlagName || hasDefault { argsWithDefault := cmd.argsWithDefaultCommand(args) tracef("using default command args=%[1]q (cmd=%[2]q)", argsWithDefault, cmd.Name) if !reflect.DeepEqual(args, argsWithDefault) { subCmd = cmd.Command(argsWithDefault.First()) } } } } else if cmd.parent == nil && cmd.DefaultCommand != "" { tracef("no positional args present; checking default command %[1]q (cmd=%[2]q)", cmd.DefaultCommand, cmd.Name) if dc := cmd.Command(cmd.DefaultCommand); dc != cmd { subCmd = dc } } if subCmd != nil { tracef("running sub-command %[1]q with arguments %[2]q (cmd=%[3]q)", subCmd.Name, cmd.Args(), cmd.Name) return subCmd.Run(ctx, cmd.Args().Slice()) } if cmd.Action == nil { cmd.Action = helpCommandAction } else { if err := cmd.checkAllRequiredFlags(); err != nil { cmd.isInError = true _ = ShowSubcommandHelp(cmd) return err } if len(cmd.Arguments) > 0 { rargs := cmd.Args().Slice() tracef("calling argparse with %[1]v", rargs) for _, arg := range cmd.Arguments { var err error rargs, err = arg.Parse(rargs) if err != nil { tracef("calling with %[1]v (cmd=%[2]q)", err, cmd.Name) return err } } cmd.parsedArgs = &stringSliceArgs{v: rargs} } } if err := cmd.Action(ctx, cmd); err != nil { tracef("calling handleExitCoder with %[1]v (cmd=%[2]q)", err, cmd.Name) deferErr = cmd.handleExitCoder(ctx, err) } tracef("returning deferErr (cmd=%[1]q) %[2]q", cmd.Name, deferErr) return deferErr } func (cmd *Command) checkHelp() bool { tracef("checking if help is wanted (cmd=%[1]q)", cmd.Name) if HelpFlag == nil { return false } for _, name := range HelpFlag.Names() { if cmd.Bool(name) { return true } } return false } func (cmd *Command) newFlagSet() (*flag.FlagSet, error) { allFlags := cmd.allFlags() cmd.appliedFlags = append(cmd.appliedFlags, allFlags...) tracef("making new flag set (cmd=%[1]q)", cmd.Name) return newFlagSet(cmd.Name, allFlags) } func (cmd *Command) allFlags() []Flag { var flags []Flag flags = append(flags, cmd.Flags...) for _, grpf := range cmd.MutuallyExclusiveFlags { for _, f1 := range grpf.Flags { flags = append(flags, f1...) } } return flags } // useShortOptionHandling traverses Lineage() for *any* ancestors // with UseShortOptionHandling func (cmd *Command) useShortOptionHandling() bool { for _, pCmd := range cmd.Lineage() { if pCmd.UseShortOptionHandling { return true } } return false } func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, error) { fl, parseErr := flagFromError(err) if parseErr != nil { return "", err } flags := cmd.Flags hideHelp := cmd.hideHelp() if commandName != "" { subCmd := cmd.Command(commandName) if subCmd == nil { return "", err } flags = subCmd.Flags hideHelp = hideHelp || subCmd.HideHelp } suggestion := SuggestFlag(flags, fl, hideHelp) if len(suggestion) == 0 { return "", err } return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil } func (cmd *Command) parseFlags(args Args) (Args, error) { tracef("parsing flags from arguments %[1]q (cmd=%[2]q)", args, cmd.Name) cmd.parsedArgs = nil if v, err := cmd.newFlagSet(); err != nil { return args, err } else { cmd.flagSet = v } if cmd.SkipFlagParsing { tracef("skipping flag parsing (cmd=%[1]q)", cmd.Name) return cmd.Args(), cmd.flagSet.Parse(append([]string{"--"}, args.Tail()...)) } tracef("walking command lineage for persistent flags (cmd=%[1]q)", cmd.Name) for pCmd := cmd.parent; pCmd != nil; pCmd = pCmd.parent { tracef( "checking ancestor command=%[1]q for persistent flags (cmd=%[2]q)", pCmd.Name, cmd.Name, ) for _, fl := range pCmd.Flags { flNames := fl.Names() pfl, ok := fl.(LocalFlag) if !ok || pfl.IsLocal() { tracef("skipping non-persistent flag %[1]q (cmd=%[2]q)", flNames, cmd.Name) continue } tracef( "checking for applying persistent flag=%[1]q pCmd=%[2]q (cmd=%[3]q)", flNames, pCmd.Name, cmd.Name, ) applyPersistentFlag := true cmd.flagSet.VisitAll(func(f *flag.Flag) { for _, name := range flNames { if name == f.Name { applyPersistentFlag = false break } } }) if !applyPersistentFlag { tracef("not applying as persistent flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) continue } tracef("applying as persistent flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) if err := fl.Apply(cmd.flagSet); err != nil { return cmd.Args(), err } tracef("appending to applied flags flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) cmd.appliedFlags = append(cmd.appliedFlags, fl) } } tracef("parsing flags iteratively tail=%[1]q (cmd=%[2]q)", args.Tail(), cmd.Name) defer tracef("done parsing flags (cmd=%[1]q)", cmd.Name) rargs := args.Tail() posArgs := []string{} for { tracef("rearrange:1 (cmd=%[1]q) %[2]q", cmd.Name, rargs) for { tracef("rearrange:2 (cmd=%[1]q) %[2]q %[3]q", cmd.Name, posArgs, rargs) // no more args to parse. Break out of inner loop if len(rargs) == 0 { break } if strings.TrimSpace(rargs[0]) == "" { break } // stop parsing once we see a "--" if rargs[0] == "--" { posArgs = append(posArgs, rargs...) cmd.parsedArgs = &stringSliceArgs{posArgs} return cmd.parsedArgs, nil } // let flagset parse this if rargs[0][0] == '-' { break } tracef("rearrange-3 (cmd=%[1]q) check %[2]q", cmd.Name, rargs[0]) // if there is a command by that name let the command handle the // rest of the parsing if cmd.Command(rargs[0]) != nil { posArgs = append(posArgs, rargs...) cmd.parsedArgs = &stringSliceArgs{posArgs} return cmd.parsedArgs, nil } posArgs = append(posArgs, rargs[0]) // if this is the sole argument then // break from inner loop if len(rargs) == 1 { rargs = []string{} break } rargs = rargs[1:] } if err := parseIter(cmd.flagSet, cmd, rargs, cmd.Root().shellCompletion); err != nil { posArgs = append(posArgs, cmd.flagSet.Args()...) tracef("returning-1 (cmd=%[1]q) args %[2]q", cmd.Name, posArgs) cmd.parsedArgs = &stringSliceArgs{posArgs} return cmd.parsedArgs, err } tracef("rearrange-4 (cmd=%[1]q) check %[2]q", cmd.Name, cmd.flagSet.Args()) rargs = cmd.flagSet.Args() if len(rargs) == 0 || strings.TrimSpace(rargs[0]) == "" || rargs[0] == "-" { break } } posArgs = append(posArgs, cmd.flagSet.Args()...) tracef("returning-2 (cmd=%[1]q) args %[2]q", cmd.Name, posArgs) cmd.parsedArgs = &stringSliceArgs{posArgs} return cmd.parsedArgs, nil } // Names returns the names including short names and aliases. func (cmd *Command) Names() []string { return append([]string{cmd.Name}, cmd.Aliases...) } // HasName returns true if Command.Name matches given name func (cmd *Command) HasName(name string) bool { for _, n := range cmd.Names() { if n == name { return true } } return false } // VisibleCategories returns a slice of categories and commands that are // Hidden=false func (cmd *Command) VisibleCategories() []CommandCategory { ret := []CommandCategory{} for _, category := range cmd.categories.Categories() { if visible := func() CommandCategory { if len(category.VisibleCommands()) > 0 { return category } return nil }(); visible != nil { ret = append(ret, visible) } } return ret } // VisibleCommands returns a slice of the Commands with Hidden=false func (cmd *Command) VisibleCommands() []*Command { var ret []*Command for _, command := range cmd.Commands { if command.Hidden || command.Name == helpName { continue } ret = append(ret, command) } return ret } // VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain func (cmd *Command) VisibleFlagCategories() []VisibleFlagCategory { if cmd.flagCategories == nil { cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) } return cmd.flagCategories.VisibleCategories() } // VisibleFlags returns a slice of the Flags with Hidden=false func (cmd *Command) VisibleFlags() []Flag { return visibleFlags(cmd.allFlags()) } func (cmd *Command) appendFlag(fl Flag) { if !hasFlag(cmd.Flags, fl) { cmd.Flags = append(cmd.Flags, fl) } } // VisiblePersistentFlags returns a slice of [LocalFlag] with Persistent=true and Hidden=false. func (cmd *Command) VisiblePersistentFlags() []Flag { var flags []Flag for _, fl := range cmd.Root().Flags { pfl, ok := fl.(LocalFlag) if !ok || pfl.IsLocal() { continue } flags = append(flags, fl) } return visibleFlags(flags) } func (cmd *Command) appendCommand(aCmd *Command) { if !hasCommand(cmd.Commands, aCmd) { aCmd.parent = cmd cmd.Commands = append(cmd.Commands, aCmd) } } func (cmd *Command) handleExitCoder(ctx context.Context, err error) error { if cmd.parent != nil { return cmd.parent.handleExitCoder(ctx, err) } if cmd.ExitErrHandler != nil { cmd.ExitErrHandler(ctx, cmd, err) return err } HandleExitCoder(err) return err } func (cmd *Command) argsWithDefaultCommand(oldArgs Args) Args { if cmd.DefaultCommand != "" { rawArgs := append([]string{cmd.DefaultCommand}, oldArgs.Slice()...) newArgs := &stringSliceArgs{v: rawArgs} return newArgs } return oldArgs } // Root returns the Command at the root of the graph func (cmd *Command) Root() *Command { if cmd.parent == nil { return cmd } return cmd.parent.Root() } func (cmd *Command) lookupFlag(name string) Flag { for _, pCmd := range cmd.Lineage() { for _, f := range pCmd.Flags { for _, n := range f.Names() { if n == name { tracef("flag found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return f } } } } tracef("flag NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } func (cmd *Command) lookupFlagSet(name string) *flag.FlagSet { for _, pCmd := range cmd.Lineage() { if pCmd.flagSet == nil { continue } if f := pCmd.flagSet.Lookup(name); f != nil { tracef("matching flag set found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return pCmd.flagSet } } tracef("matching flag set NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) cmd.onInvalidFlag(context.TODO(), name) return nil } func (cmd *Command) checkRequiredFlag(f Flag) (bool, string) { if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { flagPresent := false flagName := "" for _, key := range f.Names() { // use the first name to return since that is the // primary flag name if flagName == "" { flagName = key } if cmd.IsSet(strings.TrimSpace(key)) { flagPresent = true break } } if !flagPresent && flagName != "" { return false, flagName } } return true, "" } func (cmd *Command) checkAllRequiredFlags() requiredFlagsErr { for pCmd := cmd; pCmd != nil; pCmd = pCmd.parent { if err := pCmd.checkRequiredFlags(); err != nil { return err } } return nil } func (cmd *Command) checkRequiredFlags() requiredFlagsErr { tracef("checking for required flags (cmd=%[1]q)", cmd.Name) missingFlags := []string{} for _, f := range cmd.appliedFlags { if ok, name := cmd.checkRequiredFlag(f); !ok { missingFlags = append(missingFlags, name) } } if len(missingFlags) != 0 { tracef("found missing required flags %[1]q (cmd=%[2]q)", missingFlags, cmd.Name) return &errRequiredFlags{missingFlags: missingFlags} } tracef("all required flags set (cmd=%[1]q)", cmd.Name) return nil } func (cmd *Command) onInvalidFlag(ctx context.Context, name string) { for cmd != nil { if cmd.InvalidFlagAccessHandler != nil { cmd.InvalidFlagAccessHandler(ctx, cmd, name) break } cmd = cmd.parent } } // NumFlags returns the number of flags set func (cmd *Command) NumFlags() int { return cmd.flagSet.NFlag() } // Set sets a context flag to a value. func (cmd *Command) Set(name, value string) error { if fs := cmd.lookupFlagSet(name); fs != nil { return fs.Set(name, value) } return fmt.Errorf("no such flag -%s", name) } // IsSet determines if the flag was actually set func (cmd *Command) IsSet(name string) bool { flSet := cmd.lookupFlagSet(name) if flSet == nil { return false } isSet := false flSet.Visit(func(f *flag.Flag) { if f.Name == name { isSet = true } }) if isSet { tracef("flag with name %[1]q found via flag set lookup (cmd=%[2]q)", name, cmd.Name) return true } fl := cmd.lookupFlag(name) if fl == nil { tracef("flag with name %[1]q NOT found; assuming not set (cmd=%[2]q)", name, cmd.Name) return false } isSet = fl.IsSet() if isSet { tracef("flag with name %[1]q is set (cmd=%[2]q)", name, cmd.Name) } else { tracef("flag with name %[1]q is NOT set (cmd=%[2]q)", name, cmd.Name) } return isSet } // LocalFlagNames returns a slice of flag names used in this // command. func (cmd *Command) LocalFlagNames() []string { names := []string{} cmd.flagSet.Visit(makeFlagNameVisitor(&names)) // Check the flags which have been set via env or file for _, f := range cmd.Flags { if f.IsSet() { names = append(names, f.Names()...) } } // Sort out the duplicates since flag could be set via multiple // paths m := map[string]struct{}{} uniqNames := []string{} for _, name := range names { if _, ok := m[name]; !ok { m[name] = struct{}{} uniqNames = append(uniqNames, name) } } return uniqNames } // FlagNames returns a slice of flag names used by the this command // and all of its parent commands. func (cmd *Command) FlagNames() []string { names := cmd.LocalFlagNames() if cmd.parent != nil { names = append(cmd.parent.FlagNames(), names...) } return names } // Lineage returns *this* command and all of its ancestor commands // in order from child to parent func (cmd *Command) Lineage() []*Command { lineage := []*Command{cmd} if cmd.parent != nil { lineage = append(lineage, cmd.parent.Lineage()...) } return lineage } // Count returns the num of occurrences of this flag func (cmd *Command) Count(name string) int { if fs := cmd.lookupFlagSet(name); fs != nil { if cf, ok := fs.Lookup(name).Value.(Countable); ok { return cf.Count() } } return 0 } // Value returns the value of the flag corresponding to `name` func (cmd *Command) Value(name string) interface{} { if fs := cmd.lookupFlagSet(name); fs != nil { tracef("value found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return fs.Lookup(name).Value.(flag.Getter).Get() } tracef("value NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } // Args returns the command line arguments associated with the // command. func (cmd *Command) Args() Args { if cmd.parsedArgs != nil { return cmd.parsedArgs } return &stringSliceArgs{v: cmd.flagSet.Args()} } // NArg returns the number of the command line arguments. func (cmd *Command) NArg() int { return cmd.Args().Len() } func hasCommand(commands []*Command, command *Command) bool { for _, existing := range commands { if command == existing { return true } } return false } func (cmd *Command) runFlagActions(ctx context.Context) error { for _, fl := range cmd.appliedFlags { isSet := false // check only local flagset for running local flag actions for _, name := range fl.Names() { cmd.flagSet.Visit(func(f *flag.Flag) { if f.Name == name { isSet = true } }) if isSet { break } } // If the flag hasnt been set on cmd line then we need to further // check if it has been set via other means. If however it has // been set by other means but it is persistent(and not set via current cmd) // do not run the flag action if !isSet { if !fl.IsSet() { continue } if pf, ok := fl.(LocalFlag); ok && !pf.IsLocal() { continue } } if af, ok := fl.(ActionableFlag); ok { if err := af.RunAction(ctx, cmd); err != nil { return err } } } return nil } func checkStringSliceIncludes(want string, sSlice []string) bool { found := false for _, s := range sSlice { if want == s { found = true break } } return found } func makeFlagNameVisitor(names *[]string) func(*flag.Flag) { return func(f *flag.Flag) { nameParts := strings.Split(f.Name, ",") name := strings.TrimSpace(nameParts[0]) for _, part := range nameParts { part = strings.TrimSpace(part) if len(part) > len(name) { name = part } } if name != "" { *names = append(*names, name) } } }