| package cli |
| |
| import ( |
| "fmt" |
| "io" |
| "os" |
| "strings" |
| "text/tabwriter" |
| "text/template" |
| "unicode/utf8" |
| ) |
| |
| const ( |
| helpName = "help" |
| helpAlias = "h" |
| ) |
| |
| // this instance is to avoid recursion in the ShowCommandHelp which can |
| // add a help command again |
| var helpCommandDontUse = &Command{ |
| Name: helpName, |
| Aliases: []string{helpAlias}, |
| Usage: "Shows a list of commands or help for one command", |
| ArgsUsage: "[command]", |
| } |
| |
| var helpCommand = &Command{ |
| Name: helpName, |
| Aliases: []string{helpAlias}, |
| Usage: "Shows a list of commands or help for one command", |
| ArgsUsage: "[command]", |
| Action: func(cCtx *Context) error { |
| args := cCtx.Args() |
| argsPresent := args.First() != "" |
| firstArg := args.First() |
| |
| // This action can be triggered by a "default" action of a command |
| // or via cmd.Run when cmd == helpCmd. So we have following possibilities |
| // |
| // 1 $ app |
| // 2 $ app help |
| // 3 $ app foo |
| // 4 $ app help foo |
| // 5 $ app foo help |
| // 6 $ app foo -h (with no other sub-commands nor flags defined) |
| |
| // Case 4. when executing a help command set the context to parent |
| // to allow resolution of subsequent args. This will transform |
| // $ app help foo |
| // to |
| // $ app foo |
| // which will then be handled as case 3 |
| if cCtx.Command.Name == helpName || cCtx.Command.Name == helpAlias { |
| cCtx = cCtx.parentContext |
| } |
| |
| // Case 4. $ app help foo |
| // foo is the command for which help needs to be shown |
| if argsPresent { |
| return ShowCommandHelp(cCtx, firstArg) |
| } |
| |
| // Case 1 & 2 |
| // Special case when running help on main app itself as opposed to individual |
| // commands/subcommands |
| if cCtx.parentContext.App == nil { |
| _ = ShowAppHelp(cCtx) |
| return nil |
| } |
| |
| // Case 3, 5 |
| if (len(cCtx.Command.Subcommands) == 1 && !cCtx.Command.HideHelp && !cCtx.Command.HideHelpCommand) || |
| (len(cCtx.Command.Subcommands) == 0 && cCtx.Command.HideHelp) { |
| templ := cCtx.Command.CustomHelpTemplate |
| if templ == "" { |
| templ = CommandHelpTemplate |
| } |
| HelpPrinter(cCtx.App.Writer, templ, cCtx.Command) |
| return nil |
| } |
| |
| // Case 6, handling incorporated in the callee itself |
| return ShowSubcommandHelp(cCtx) |
| }, |
| } |
| |
| // Prints help for the App or Command |
| type helpPrinter func(w io.Writer, templ string, data interface{}) |
| |
| // Prints help for the App or Command with custom template function. |
| type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) |
| |
| // HelpPrinter is a function that writes the help output. If not set explicitly, |
| // this calls HelpPrinterCustom using only the default template functions. |
| // |
| // If custom logic for printing help is required, this function can be |
| // overridden. If the ExtraInfo field is defined on an App, this function |
| // should not be modified, as HelpPrinterCustom will be used directly in order |
| // to capture the extra information. |
| var HelpPrinter helpPrinter = printHelp |
| |
| // HelpPrinterCustom is a function that writes the help output. It is used as |
| // the default implementation of HelpPrinter, and may be called directly if |
| // the ExtraInfo field is set on an App. |
| // |
| // In the default implementation, if the customFuncs argument contains a |
| // "wrapAt" key, which is a function which takes no arguments and returns |
| // an int, this int value will be used to produce a "wrap" function used |
| // by the default template to wrap long lines. |
| var HelpPrinterCustom helpPrinterCustom = printHelpCustom |
| |
| // VersionPrinter prints the version for the App |
| var VersionPrinter = printVersion |
| |
| // ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. |
| func ShowAppHelpAndExit(c *Context, exitCode int) { |
| _ = ShowAppHelp(c) |
| os.Exit(exitCode) |
| } |
| |
| // ShowAppHelp is an action that displays the help. |
| func ShowAppHelp(cCtx *Context) error { |
| tpl := cCtx.App.CustomAppHelpTemplate |
| if tpl == "" { |
| tpl = AppHelpTemplate |
| } |
| |
| if cCtx.App.ExtraInfo == nil { |
| HelpPrinter(cCtx.App.Writer, tpl, cCtx.App) |
| return nil |
| } |
| |
| customAppData := func() map[string]interface{} { |
| return map[string]interface{}{ |
| "ExtraInfo": cCtx.App.ExtraInfo, |
| } |
| } |
| HelpPrinterCustom(cCtx.App.Writer, tpl, cCtx.App, customAppData()) |
| |
| return nil |
| } |
| |
| // DefaultAppComplete prints the list of subcommands as the default app completion method |
| func DefaultAppComplete(cCtx *Context) { |
| DefaultCompleteWithFlags(nil)(cCtx) |
| } |
| |
| func printCommandSuggestions(commands []*Command, writer io.Writer) { |
| for _, command := range commands { |
| if command.Hidden { |
| continue |
| } |
| if strings.HasSuffix(os.Getenv("0"), "zsh") { |
| for _, name := range command.Names() { |
| _, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage) |
| } |
| } else { |
| for _, name := range command.Names() { |
| _, _ = fmt.Fprintf(writer, "%s\n", name) |
| } |
| } |
| } |
| } |
| |
| func cliArgContains(flagName string) bool { |
| for _, name := range strings.Split(flagName, ",") { |
| name = strings.TrimSpace(name) |
| count := utf8.RuneCountInString(name) |
| if count > 2 { |
| count = 2 |
| } |
| flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) |
| for _, a := range os.Args { |
| if a == flag { |
| return true |
| } |
| } |
| } |
| return false |
| } |
| |
| func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { |
| cur := strings.TrimPrefix(lastArg, "-") |
| cur = strings.TrimPrefix(cur, "-") |
| for _, flag := range flags { |
| if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden { |
| continue |
| } |
| for _, name := range flag.Names() { |
| name = strings.TrimSpace(name) |
| // this will get total count utf8 letters in flag name |
| count := utf8.RuneCountInString(name) |
| if count > 2 { |
| count = 2 // reuse this count to generate single - or -- in flag completion |
| } |
| // if flag name has more than one utf8 letter and last argument in cli has -- prefix then |
| // skip flag completion for short flags example -v or -x |
| if strings.HasPrefix(lastArg, "--") && count == 1 { |
| continue |
| } |
| // match if last argument matches this flag and it is not repeated |
| if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) { |
| flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) |
| _, _ = fmt.Fprintln(writer, flagCompletion) |
| } |
| } |
| } |
| } |
| |
| func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) { |
| return func(cCtx *Context) { |
| var lastArg string |
| |
| // TODO: This shouldnt depend on os.Args rather it should |
| // depend on root arguments passed to App |
| if len(os.Args) > 2 { |
| lastArg = os.Args[len(os.Args)-2] |
| } |
| |
| if lastArg != "" { |
| if strings.HasPrefix(lastArg, "-") { |
| if cmd != nil { |
| printFlagSuggestions(lastArg, cmd.Flags, cCtx.App.Writer) |
| |
| return |
| } |
| |
| printFlagSuggestions(lastArg, cCtx.App.Flags, cCtx.App.Writer) |
| |
| return |
| } |
| } |
| |
| if cmd != nil { |
| printCommandSuggestions(cmd.Subcommands, cCtx.App.Writer) |
| return |
| } |
| |
| printCommandSuggestions(cCtx.Command.Subcommands, cCtx.App.Writer) |
| } |
| } |
| |
| // ShowCommandHelpAndExit - exits with code after showing help |
| func ShowCommandHelpAndExit(c *Context, command string, code int) { |
| _ = ShowCommandHelp(c, command) |
| os.Exit(code) |
| } |
| |
| // ShowCommandHelp prints help for the given command |
| func ShowCommandHelp(ctx *Context, command string) error { |
| commands := ctx.App.Commands |
| if ctx.Command.Subcommands != nil { |
| commands = ctx.Command.Subcommands |
| } |
| for _, c := range commands { |
| if c.HasName(command) { |
| if !ctx.App.HideHelpCommand && !c.HasName(helpName) && len(c.Subcommands) != 0 && c.Command(helpName) == nil { |
| c.Subcommands = append(c.Subcommands, helpCommandDontUse) |
| } |
| if !ctx.App.HideHelp && HelpFlag != nil { |
| c.appendFlag(HelpFlag) |
| } |
| templ := c.CustomHelpTemplate |
| if templ == "" { |
| if len(c.Subcommands) == 0 { |
| templ = CommandHelpTemplate |
| } else { |
| templ = SubcommandHelpTemplate |
| } |
| } |
| |
| HelpPrinter(ctx.App.Writer, templ, c) |
| |
| return nil |
| } |
| } |
| |
| if ctx.App.CommandNotFound == nil { |
| errMsg := fmt.Sprintf("No help topic for '%v'", command) |
| if ctx.App.Suggest && SuggestCommand != nil { |
| if suggestion := SuggestCommand(ctx.Command.Subcommands, command); suggestion != "" { |
| errMsg += ". " + suggestion |
| } |
| } |
| return Exit(errMsg, 3) |
| } |
| |
| ctx.App.CommandNotFound(ctx, command) |
| return nil |
| } |
| |
| // ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code. |
| func ShowSubcommandHelpAndExit(c *Context, exitCode int) { |
| _ = ShowSubcommandHelp(c) |
| os.Exit(exitCode) |
| } |
| |
| // ShowSubcommandHelp prints help for the given subcommand |
| func ShowSubcommandHelp(cCtx *Context) error { |
| if cCtx == nil { |
| return nil |
| } |
| // use custom template when provided (fixes #1703) |
| templ := SubcommandHelpTemplate |
| if cCtx.Command != nil && cCtx.Command.CustomHelpTemplate != "" { |
| templ = cCtx.Command.CustomHelpTemplate |
| } |
| HelpPrinter(cCtx.App.Writer, templ, cCtx.Command) |
| return nil |
| } |
| |
| // ShowVersion prints the version number of the App |
| func ShowVersion(cCtx *Context) { |
| VersionPrinter(cCtx) |
| } |
| |
| func printVersion(cCtx *Context) { |
| _, _ = fmt.Fprintf(cCtx.App.Writer, "%v version %v\n", cCtx.App.Name, cCtx.App.Version) |
| } |
| |
| // ShowCompletions prints the lists of commands within a given context |
| func ShowCompletions(cCtx *Context) { |
| c := cCtx.Command |
| if c != nil && c.BashComplete != nil { |
| c.BashComplete(cCtx) |
| } |
| } |
| |
| // ShowCommandCompletions prints the custom completions for a given command |
| func ShowCommandCompletions(ctx *Context, command string) { |
| c := ctx.Command.Command(command) |
| if c != nil { |
| if c.BashComplete != nil { |
| c.BashComplete(ctx) |
| } else { |
| DefaultCompleteWithFlags(c)(ctx) |
| } |
| } |
| } |
| |
| // printHelpCustom is the default implementation of HelpPrinterCustom. |
| // |
| // The customFuncs map will be combined with a default template.FuncMap to |
| // allow using arbitrary functions in template rendering. |
| func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { |
| const maxLineLength = 10000 |
| |
| funcMap := template.FuncMap{ |
| "join": strings.Join, |
| "subtract": subtract, |
| "indent": indent, |
| "nindent": nindent, |
| "trim": strings.TrimSpace, |
| "wrap": func(input string, offset int) string { return wrap(input, offset, maxLineLength) }, |
| "offset": offset, |
| "offsetCommands": offsetCommands, |
| } |
| |
| if customFuncs["wrapAt"] != nil { |
| if wa, ok := customFuncs["wrapAt"]; ok { |
| if waf, ok := wa.(func() int); ok { |
| wrapAt := waf() |
| customFuncs["wrap"] = func(input string, offset int) string { |
| return wrap(input, offset, wrapAt) |
| } |
| } |
| } |
| } |
| |
| for key, value := range customFuncs { |
| funcMap[key] = value |
| } |
| |
| w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) |
| t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) |
| templates := map[string]string{ |
| "helpNameTemplate": helpNameTemplate, |
| "usageTemplate": usageTemplate, |
| "descriptionTemplate": descriptionTemplate, |
| "visibleCommandTemplate": visibleCommandTemplate, |
| "copyrightTemplate": copyrightTemplate, |
| "versionTemplate": versionTemplate, |
| "visibleFlagCategoryTemplate": visibleFlagCategoryTemplate, |
| "visibleFlagTemplate": visibleFlagTemplate, |
| "visibleGlobalFlagCategoryTemplate": strings.Replace(visibleFlagCategoryTemplate, "OPTIONS", "GLOBAL OPTIONS", -1), |
| "authorsTemplate": authorsTemplate, |
| "visibleCommandCategoryTemplate": visibleCommandCategoryTemplate, |
| } |
| for name, value := range templates { |
| if _, err := t.New(name).Parse(value); err != nil { |
| if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { |
| _, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) |
| } |
| } |
| } |
| |
| err := t.Execute(w, data) |
| if err != nil { |
| // If the writer is closed, t.Execute will fail, and there's nothing |
| // we can do to recover. |
| if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { |
| _, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) |
| } |
| return |
| } |
| _ = w.Flush() |
| } |
| |
| func printHelp(out io.Writer, templ string, data interface{}) { |
| HelpPrinterCustom(out, templ, data, nil) |
| } |
| |
| func checkVersion(cCtx *Context) bool { |
| found := false |
| for _, name := range VersionFlag.Names() { |
| if cCtx.Bool(name) { |
| found = true |
| } |
| } |
| return found |
| } |
| |
| func checkHelp(cCtx *Context) bool { |
| if HelpFlag == nil { |
| return false |
| } |
| found := false |
| for _, name := range HelpFlag.Names() { |
| if cCtx.Bool(name) { |
| found = true |
| break |
| } |
| } |
| |
| return found |
| } |
| |
| func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { |
| if !a.EnableBashCompletion { |
| return false, arguments |
| } |
| |
| pos := len(arguments) - 1 |
| lastArg := arguments[pos] |
| |
| if lastArg != "--generate-bash-completion" { |
| return false, arguments |
| } |
| |
| for _, arg := range arguments { |
| // If arguments include "--", shell completion is disabled |
| // because after "--" only positional arguments are accepted. |
| // https://unix.stackexchange.com/a/11382 |
| if arg == "--" { |
| return false, arguments |
| } |
| } |
| |
| return true, arguments[:pos] |
| } |
| |
| func checkCompletions(cCtx *Context) bool { |
| if !cCtx.shellComplete { |
| return false |
| } |
| |
| if args := cCtx.Args(); args.Present() { |
| name := args.First() |
| if cmd := cCtx.Command.Command(name); cmd != nil { |
| // let the command handle the completion |
| return false |
| } |
| } |
| |
| ShowCompletions(cCtx) |
| return true |
| } |
| |
| func subtract(a, b int) int { |
| return a - b |
| } |
| |
| func indent(spaces int, v string) string { |
| pad := strings.Repeat(" ", spaces) |
| return pad + strings.Replace(v, "\n", "\n"+pad, -1) |
| } |
| |
| func nindent(spaces int, v string) string { |
| return "\n" + indent(spaces, v) |
| } |
| |
| func wrap(input string, offset int, wrapAt int) string { |
| var ss []string |
| |
| lines := strings.Split(input, "\n") |
| |
| padding := strings.Repeat(" ", offset) |
| |
| for i, line := range lines { |
| if line == "" { |
| ss = append(ss, line) |
| } else { |
| wrapped := wrapLine(line, offset, wrapAt, padding) |
| if i == 0 { |
| ss = append(ss, wrapped) |
| } else { |
| ss = append(ss, padding+wrapped) |
| } |
| |
| } |
| } |
| |
| return strings.Join(ss, "\n") |
| } |
| |
| func wrapLine(input string, offset int, wrapAt int, padding string) string { |
| if wrapAt <= offset || len(input) <= wrapAt-offset { |
| return input |
| } |
| |
| lineWidth := wrapAt - offset |
| words := strings.Fields(input) |
| if len(words) == 0 { |
| return input |
| } |
| |
| wrapped := words[0] |
| spaceLeft := lineWidth - len(wrapped) |
| for _, word := range words[1:] { |
| if len(word)+1 > spaceLeft { |
| wrapped += "\n" + padding + word |
| spaceLeft = lineWidth - len(word) |
| } else { |
| wrapped += " " + word |
| spaceLeft -= 1 + len(word) |
| } |
| } |
| |
| return wrapped |
| } |
| |
| func offset(input string, fixed int) int { |
| return len(input) + fixed |
| } |
| |
| // this function tries to find the max width of the names column |
| // so say we have the following rows for help |
| // |
| // foo1, foo2, foo3 some string here |
| // bar1, b2 some other string here |
| // |
| // We want to offset the 2nd row usage by some amount so that everything |
| // is aligned |
| // |
| // foo1, foo2, foo3 some string here |
| // bar1, b2 some other string here |
| // |
| // to find that offset we find the length of all the rows and use the max |
| // to calculate the offset |
| func offsetCommands(cmds []*Command, fixed int) int { |
| var max int = 0 |
| for _, cmd := range cmds { |
| s := strings.Join(cmd.Names(), ", ") |
| if len(s) > max { |
| max = len(s) |
| } |
| } |
| return max + fixed |
| } |