From e7c14c78d0a007e946bf881bb8a00ec62f510b16 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 22 Sep 2022 23:59:07 +0530 Subject: [PATCH] Work on outputting help for commands --- tools/cli/help.go | 142 ++++++++++++++++++++++++++++++++++ tools/cli/infrastructure.go | 8 -- tools/cli/types.go | 147 +++++++++++++++++++++++++++++++----- 3 files changed, 269 insertions(+), 28 deletions(-) create mode 100644 tools/cli/help.go diff --git a/tools/cli/help.go b/tools/cli/help.go new file mode 100644 index 000000000..41dfb8fa2 --- /dev/null +++ b/tools/cli/help.go @@ -0,0 +1,142 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package cli + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + + "golang.org/x/sys/unix" + + "kitty" + "kitty/tools/cli/markup" + "kitty/tools/tty" + "kitty/tools/utils/style" +) + +var _ = fmt.Print + +func ShowError(err error) { + formatter := markup.New(tty.IsTerminal(os.Stderr.Fd())) + msg := formatter.Prettify(err.Error()) + fmt.Fprintln(os.Stderr, formatter.Err("Error")+":", msg) +} + +func (self *Command) ShowVersion() { + formatter := markup.New(tty.IsTerminal(os.Stdout.Fd())) + fmt.Fprintln(os.Stdout, formatter.Italic(self.CommandStringForUsage()), formatter.Opt(kitty.VersionString), "created by", formatter.Title("Kovid Goyal")) +} + +func format_with_indent(output io.Writer, text string, indent string, screen_width int) { + text = formatter.Prettify(text) + indented := style.WrapText(text, indent, screen_width, "#placeholder_for_formatting#") + io.WriteString(output, indented) +} + +func (self *Command) FormatSubCommands(output io.Writer, formatter *markup.Context, screen_width int) { + for _, g := range self.SubCommandGroups { + if !g.HasVisibleSubCommands() { + continue + } + fmt.Fprintln(output) + if g.Title != "" { + fmt.Fprintln(output, formatter.Title(g.Title)) + } + for _, c := range g.SubCommands { + if c.Hidden { + continue + } + fmt.Fprintln(output, " ", formatter.Opt(c.Name)) + format_with_indent(output, formatter.Prettify(c.ShortDescription), " ", screen_width) + } + } + +} + +func (self *Option) FormatOption(output io.Writer, formatter *markup.Context, screen_width int) { + for i, a := range self.Aliases { + fmt.Fprint(output, formatter.Opt(a.String())) + if i != len(self.Aliases)-1 { + fmt.Fprint(output, ", ") + } + } + defval := "" + switch self.OptionType { + case BoolOption: + default: + defval = self.Default + fallthrough + case StringOption: + if self.IsList { + defval = "" + } + } + if defval != "" { + fmt.Fprintf(output, " [=%s]", formatter.Italic(defval)) + } + fmt.Fprintln(output) + format_with_indent(output, formatter.Prettify(prepare_help_text_for_display(self.Help)), " ", screen_width) +} + +func (self *Command) ShowHelp() { + formatter := markup.New(tty.IsTerminal(os.Stdout.Fd())) + 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 + + fmt.Fprintln(&output, formatter.Title("Usage")+":", formatter.Exe(strings.TrimSpace(self.CommandStringForUsage())), + strings.TrimSpace(formatter.Prettify(self.Usage))) + fmt.Fprintln(&output) + format_with_indent(&output, formatter.Prettify(prepare_help_text_for_display(self.HelpText)), "", screen_width) + + if self.HasVisibleSubCommands() { + fmt.Fprintln(&output) + fmt.Fprintln(&output, formatter.Title("Commands")+":") + self.FormatSubCommands(&output, formatter, screen_width) + fmt.Fprintln(&output) + format_with_indent(&output, "Get help for an individual command by running:", "", screen_width) + fmt.Fprintln(&output, " ", strings.TrimSpace(self.CommandStringForUsage()), formatter.Italic("command"), "-h") + } + + group_titles, gmap := self.GetVisibleOptions() + if len(group_titles) > 0 { + fmt.Fprintln(&output) + fmt.Fprintln(&output, formatter.Title("Options")+":") + for _, title := range group_titles { + fmt.Fprintln(&output) + if title != "" { + fmt.Fprintln(&output, formatter.Title(title)) + } + for _, opt := range gmap[title] { + opt.FormatOption(&output, formatter, screen_width) + fmt.Fprintln(&output) + } + } + } + output_text := output.String() + // fmt.Printf("%#v\n", output_text) + if formatter.EscapeCodesAllowed() { + 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 { + os.Stdout.Write([]byte(output_text)) + } +} diff --git a/tools/cli/infrastructure.go b/tools/cli/infrastructure.go index 9d8f46893..ff05c89b6 100644 --- a/tools/cli/infrastructure.go +++ b/tools/cli/infrastructure.go @@ -4,7 +4,6 @@ package cli import ( "fmt" - "io" "os" "os/exec" "strings" @@ -16,7 +15,6 @@ import ( "kitty" "kitty/tools/cli/markup" "kitty/tools/tty" - "kitty/tools/utils/style" ) var RootCmd *cobra.Command @@ -67,12 +65,6 @@ func ChoicesP(flags *pflag.FlagSet, name string, short string, usage string, cho var formatter *markup.Context -func format_with_indent(output io.Writer, text string, indent string, screen_width int) { - text = formatter.Prettify(text) - indented := style.WrapText(text, indent, screen_width, "#placeholder_for_formatting#") - io.WriteString(output, indented) -} - func full_command_name(cmd *cobra.Command) string { var parent_names []string cmd.VisitParents(func(p *cobra.Command) { diff --git a/tools/cli/types.go b/tools/cli/types.go index 205714023..a9cff46e3 100644 --- a/tools/cli/types.go +++ b/tools/cli/types.go @@ -189,6 +189,15 @@ type CommandGroup struct { Title string } +func (self *CommandGroup) HasVisibleSubCommands() bool { + for _, c := range self.SubCommands { + if !c.Hidden { + return true + } + } + return false +} + func (self *CommandGroup) Clone(parent *Command) *CommandGroup { ans := CommandGroup{Title: self.Title, SubCommands: make([]*Command, 0, len(self.SubCommands))} for i, o := range self.SubCommands { @@ -263,17 +272,24 @@ func (self *OptionGroup) FindOption(name_with_hyphens string) *Option { // }}} type Command struct { // {{{ - Name, ExeName string - Usage, HelpText string - Hidden bool + Name string + Usage, ShortDescription, HelpText string + Hidden bool + + // Number of non-option arguments after which to stop parsing options. 0 means no options after the first non-option arg. AllowOptionsAfterArgs int - SubCommandIsOptional bool + // If true does not fail if the first non-option arg is not a sub-command + SubCommandIsOptional bool SubCommandGroups []*CommandGroup OptionGroups []*OptionGroup Parent *Command Args []string + Run func(cmd *Command, args []string) (int, error) + + option_map map[string]*Option + exe_name string } func (self *Command) Clone(parent *Command) *Command { @@ -356,13 +372,25 @@ func (self *Command) Validate() error { } } } - if !seen_dests["Help"] { - if seen_flags["-h"] || seen_flags["--help"] { - return &ParseError{Message: fmt.Sprintf("The --help or -h flags are assigned to an option other than Help in %s", self.Name)} + if self.Parent != nil { + if !seen_dests["Help"] { + if seen_flags["-h"] || seen_flags["--help"] { + return &ParseError{Message: fmt.Sprintf("The --help or -h flags are assigned to an option other than Help in %s", self.Name)} + } + _, err := self.Add(OptionSpec{Name: "--help -h", Type: "bool-set", Help: "Show help for this command"}) + if err != nil { + return err + } } - _, err := self.Add(OptionSpec{Name: "--help -h", Type: "bool-set", Help: "Show help for this command"}) - if err != nil { - return err + + if self.Parent.Parent == nil && !seen_dests["Version"] { + if seen_flags["--version"] { + return &ParseError{Message: fmt.Sprintf("The --version flag is assigned to an option other than Version in %s", self.Name)} + } + _, err := self.Add(OptionSpec{Name: "--version", Type: "bool-set", Help: "Show version"}) + if err != nil { + return err + } } } @@ -377,13 +405,13 @@ func (self *Command) Root(args []string) *Command { return p } -func (self *Command) CommandStringForUsage(args []string) string { +func (self *Command) CommandStringForUsage() string { names := make([]string, 0, 8) p := self for p != nil { if p.Name != "" { names = append(names, p.Name) - } else if p.ExeName != "" { + } else if p.exe_name != "" { names = append(names, p.Name) } p = p.Parent @@ -406,11 +434,17 @@ func (self *Command) ParseArgs(args []string) (*Command, error) { return nil, &ParseError{Message: "At least one arg must be supplied"} } ctx := Context{SeenCommands: make([]*Command, 0, 4)} - self.ExeName = args[0] + self.exe_name = args[0] err = self.parse_args(&ctx, args[1:]) if err != nil { return nil, err } + self.option_map = make(map[string]*Option, 128) + for _, g := range self.OptionGroups { + for _, o := range g.Options { + self.option_map[o.Name] = o + } + } return ctx.SeenCommands[len(ctx.SeenCommands)-1], nil } @@ -423,6 +457,51 @@ func (self *Command) HasSubCommands() bool { return false } +func (self *Command) HasVisibleSubCommands() bool { + for _, g := range self.SubCommandGroups { + if g.HasVisibleSubCommands() { + return true + } + } + return false +} + +func (self *Command) GetVisibleOptions() ([]string, map[string][]*Option) { + group_titles := make([]string, 0, len(self.OptionGroups)) + gmap := make(map[string][]*Option) + + add_options := func(group_title string, opts []*Option) { + if len(opts) == 0 { + return + } + x := gmap[group_title] + if x == nil { + group_titles = append(group_titles, group_title) + gmap[group_title] = opts + } else { + gmap[group_title] = append(x, opts...) + } + } + + depth := 0 + process_cmd := func(cmd *Command) { + for _, g := range cmd.OptionGroups { + gopts := make([]*Option, 0, len(g.Options)) + for _, o := range g.Options { + if !o.Hidden && o.Depth >= depth { + gopts = append(gopts, o) + } + } + add_options(g.Title, gopts) + } + } + for p := self; p != nil; p = p.Parent { + process_cmd(p) + depth++ + } + return group_titles, gmap +} + func (self *Command) FindSubCommand(name string) *Command { for _, g := range self.SubCommandGroups { c := g.FindSubCommand(name) @@ -483,12 +562,6 @@ type Context struct { } func (self *Command) GetOptionValues(pointer_to_options_struct any) error { - m := make(map[string]*Option, 128) - for _, g := range self.OptionGroups { - for _, o := range g.Options { - m[o.Name] = o - } - } val := reflect.ValueOf(pointer_to_options_struct).Elem() if val.Kind() != reflect.Struct { return fmt.Errorf("Need a pointer to a struct to set option values on") @@ -499,7 +572,7 @@ func (self *Command) GetOptionValues(pointer_to_options_struct any) error { if utils.Capitalize(field_name) != field_name || !f.CanSet() { continue } - opt := m[field_name] + opt := self.option_map[field_name] if opt == nil { return fmt.Errorf("No option with the name: %s", field_name) } @@ -547,4 +620,38 @@ func (self *Command) GetOptionValues(pointer_to_options_struct any) error { return nil } +func (self *Command) Exec(args ...string) { + root := self + for root.Parent != nil { + root = root.Parent + } + if len(args) == 0 { + args = os.Args + } + cmd, err := root.ParseArgs(args) + if err != nil { + ShowError(err) + os.Exit(1) + } + help_opt := cmd.option_map["Help"] + version_opt := cmd.option_map["Version"] + exit_code := 0 + if help_opt != nil && help_opt.parsed_value().(bool) { + cmd.ShowHelp() + os.Exit(exit_code) + } else if version_opt != nil && version_opt.parsed_value().(bool) { + cmd.ShowVersion() + os.Exit(exit_code) + } else if cmd.Run != nil { + exit_code, err = cmd.Run(cmd, cmd.Args) + if err != nil { + PrintError(err) + if exit_code == 0 { + exit_code = 1 + } + } + } + os.Exit(exit_code) +} + // }}}