// License: GPLv3 Copyright: 2022, Kovid Goyal, package cli import ( "fmt" "os" "os/exec" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/sys/unix" "kitty" "kitty/tools/cli/markup" "kitty/tools/tty" ) var RootCmd *cobra.Command func key_in_slice(vals []string, key string) bool { for _, q := range vals { if q == key { return true } } return false } type ChoicesVal struct { name, Choice string allowed []string } type choicesVal ChoicesVal func (i *choicesVal) String() string { return ChoicesVal(*i).Choice } func (i *choicesVal) Type() string { return "string" } func (i *choicesVal) Set(s string) error { (*i).Choice = s return nil } func newChoicesVal(val ChoicesVal, p *ChoicesVal) *choicesVal { *p = val return (*choicesVal)(p) } func add_choices(flags *pflag.FlagSet, p *ChoicesVal, choices []string, name string, short string, usage string) { usage = strings.TrimSpace(usage) + "\n" + "Choices: " + strings.Join(choices, ", ") value := ChoicesVal{Choice: choices[0], allowed: choices} flags.VarP(newChoicesVal(value, p), name, short, usage) } func Choices(flags *pflag.FlagSet, name string, usage string, choices ...string) *ChoicesVal { p := new(ChoicesVal) add_choices(flags, p, choices, name, "", usage) return p } func ChoicesP(flags *pflag.FlagSet, name string, short string, usage string, choices ...string) *ChoicesVal { p := new(ChoicesVal) add_choices(flags, p, choices, name, short, usage) return p } var formatter *markup.Context func full_command_name(cmd *cobra.Command) string { var parent_names []string cmd.VisitParents(func(p *cobra.Command) { parent_names = append([]string{p.Name()}, parent_names...) }) parent_names = append(parent_names, cmd.Name()) return strings.Join(parent_names, " ") } func show_usage(cmd *cobra.Command, use_pager bool) error { screen_width := 80 if formatter.EscapeCodesAllowed() { var sz *unix.Winsize var tty_size_err error for { sz, tty_size_err = unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) if tty_size_err != unix.EINTR { break } } if tty_size_err == nil && sz.Col < 80 { screen_width = int(sz.Col) } } var output strings.Builder use := cmd.Use idx := strings.Index(use, " ") if idx > -1 { use = use[idx+1:] } else { use = "" } fmt.Fprintln(&output, formatter.Title("Usage")+":", formatter.Exe(full_command_name(cmd)), use) fmt.Fprintln(&output) if len(cmd.Long) > 0 { format_with_indent(&output, cmd.Long, "", screen_width) } else if len(cmd.Short) > 0 { format_with_indent(&output, cmd.Short, "", screen_width) } if cmd.HasAvailableSubCommands() { fmt.Fprintln(&output) fmt.Fprintln(&output, formatter.Title("Commands")+":") for _, child := range cmd.Commands() { if child.Hidden { continue } fmt.Fprintln(&output, " ", formatter.Opt(child.Name())) format_with_indent(&output, child.Short, " ", screen_width) } fmt.Fprintln(&output) format_with_indent(&output, "Get help for an individual command by running:", "", screen_width) fmt.Fprintln(&output, " ", full_command_name(cmd), formatter.Italic("command"), "-h") } if cmd.HasAvailableFlags() { options_title := cmd.Annotations["options_title"] if len(options_title) == 0 { options_title = "Options" } fmt.Fprintln(&output) fmt.Fprintln(&output, formatter.Title(options_title)+":") flag_set := cmd.LocalFlags() flag_set.VisitAll(func(flag *pflag.Flag) { fmt.Fprint(&output, formatter.Opt(" --"+flag.Name)) if flag.Shorthand != "" { fmt.Fprint(&output, ", ", formatter.Opt("-"+flag.Shorthand)) } defval := "" switch flag.Value.Type() { default: if flag.DefValue != "" { defval = fmt.Sprintf("[=%s]", formatter.Italic(flag.DefValue)) } case "stringArray": if flag.DefValue != "[]" { defval = fmt.Sprintf("[=%s]", formatter.Italic(flag.DefValue)) } case "bool": case "count": } if defval != "" { fmt.Fprint(&output, " ", defval) } fmt.Fprintln(&output) msg := flag.Usage switch flag.Name { case "help": msg = "Print this help message" case "version": msg = "Print the version of " + RootCmd.Name() + ": " + formatter.Italic(RootCmd.Version) } format_with_indent(&output, msg, " ", screen_width) fmt.Fprintln(&output) }) } if cmd.Annotations["usage-suffix"] != "" { fmt.Fprintln(&output, cmd.Annotations["usage-suffix"]) } else { fmt.Fprintln(&output, formatter.Italic(RootCmd.Name()), formatter.Opt(kitty.VersionString), "created by", formatter.Title("Kovid Goyal")) } output_text := output.String() // fmt.Printf("%#v\n", output_text) if use_pager && formatter.EscapeCodesAllowed() && cmd.Annotations["allow-pager"] != "no" { pager := exec.Command(kitty.DefaultPager[0], kitty.DefaultPager[1:]...) pager.Stdin = strings.NewReader(output_text) pager.Stdout = os.Stdout pager.Stderr = os.Stderr pager.Run() } else { cmd.OutOrStdout().Write([]byte(output_text)) } return nil } func FlagNormalizer(name string) string { return strings.ReplaceAll(name, "_", "-") } func DisallowArgs(cmd *cobra.Command, args []string) error { if cmd.HasSubCommands() { if len(args) == 0 { return fmt.Errorf("No sub-command specified. Use %s -h to get a list of available sub-commands", full_command_name(cmd)) } cmd.SuggestionsMinimumDistance = 2 suggestions := cmd.SuggestionsFor(args[0]) es := "Not a valid subcommand: " + args[0] trailer := fmt.Sprintf("Use %s to get a list of available sub-commands", formatter.Bold(full_command_name(cmd)+" -h")) if len(suggestions) > 0 { es += "\nDid you mean?\n" for _, s := range suggestions { es += fmt.Sprintf("\t%s\n", formatter.Italic(s)) } es += trailer } else { es += ". " + trailer } return fmt.Errorf("%s", es) } return nil } func CreateCommand(cmd *cobra.Command) *cobra.Command { cmd.Annotations = make(map[string]string) cmd.SilenceErrors = true cmd.SilenceUsage = true cmd.PersistentFlags().SortFlags = false cmd.Flags().SortFlags = false cmd.Flags().SetNormalizeFunc(func(fs *pflag.FlagSet, name string) pflag.NormalizedName { return pflag.NormalizedName(FlagNormalizer(name)) }) cmd.PersistentFlags().SetNormalizeFunc(cmd.Flags().GetNormalizeFunc()) if !cmd.Runnable() { cmd.Args = DisallowArgs cmd.RunE = func(cmd *cobra.Command, args []string) error { return nil } } return cmd } func show_help(cmd *cobra.Command, args []string) { show_usage(cmd, true) } func PrintError(err error) { fmt.Println(formatter.Err("Error")+":", err) } func Init(root *cobra.Command) { vs := kitty.VersionString if kitty.VCSRevision != "" { vs = vs + " (" + kitty.VCSRevision + ")" } formatter = markup.New(tty.IsTerminal(os.Stdout.Fd())) RootCmd = root root.Version = vs root.SetUsageFunc(func(cmd *cobra.Command) error { return show_usage(cmd, false) }) root.SetHelpFunc(show_help) root.SetHelpCommand(&cobra.Command{Hidden: true}) root.CompletionOptions.DisableDefaultCmd = true } func Execute(root *cobra.Command) error { return root.Execute() } type FlagValGetter struct { Flags *pflag.FlagSet Err error } func (self *FlagValGetter) String(name string) string { if self.Err != nil { return "" } ans, err := self.Flags.GetString(name) self.Err = err return ans }