Work on outputting help for commands

This commit is contained in:
Kovid Goyal 2022-09-22 23:59:07 +05:30
parent cb2389efa5
commit e7c14c78d0
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 269 additions and 28 deletions

142
tools/cli/help.go Normal file
View File

@ -0,0 +1,142 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
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))
}
}

View File

@ -4,7 +4,6 @@ package cli
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@ -16,7 +15,6 @@ import (
"kitty" "kitty"
"kitty/tools/cli/markup" "kitty/tools/cli/markup"
"kitty/tools/tty" "kitty/tools/tty"
"kitty/tools/utils/style"
) )
var RootCmd *cobra.Command var RootCmd *cobra.Command
@ -67,12 +65,6 @@ func ChoicesP(flags *pflag.FlagSet, name string, short string, usage string, cho
var formatter *markup.Context 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 { func full_command_name(cmd *cobra.Command) string {
var parent_names []string var parent_names []string
cmd.VisitParents(func(p *cobra.Command) { cmd.VisitParents(func(p *cobra.Command) {

View File

@ -189,6 +189,15 @@ type CommandGroup struct {
Title string 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 { func (self *CommandGroup) Clone(parent *Command) *CommandGroup {
ans := CommandGroup{Title: self.Title, SubCommands: make([]*Command, 0, len(self.SubCommands))} ans := CommandGroup{Title: self.Title, SubCommands: make([]*Command, 0, len(self.SubCommands))}
for i, o := range self.SubCommands { for i, o := range self.SubCommands {
@ -263,10 +272,13 @@ func (self *OptionGroup) FindOption(name_with_hyphens string) *Option {
// }}} // }}}
type Command struct { // {{{ type Command struct { // {{{
Name, ExeName string Name string
Usage, HelpText string Usage, ShortDescription, HelpText string
Hidden bool 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 AllowOptionsAfterArgs int
// If true does not fail if the first non-option arg is not a sub-command
SubCommandIsOptional bool SubCommandIsOptional bool
SubCommandGroups []*CommandGroup SubCommandGroups []*CommandGroup
@ -274,6 +286,10 @@ type Command struct { // {{{
Parent *Command Parent *Command
Args []string 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 { func (self *Command) Clone(parent *Command) *Command {
@ -356,6 +372,7 @@ func (self *Command) Validate() error {
} }
} }
} }
if self.Parent != nil {
if !seen_dests["Help"] { if !seen_dests["Help"] {
if seen_flags["-h"] || seen_flags["--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)} return &ParseError{Message: fmt.Sprintf("The --help or -h flags are assigned to an option other than Help in %s", self.Name)}
@ -366,6 +383,17 @@ func (self *Command) Validate() error {
} }
} }
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
}
}
}
return nil return nil
} }
@ -377,13 +405,13 @@ func (self *Command) Root(args []string) *Command {
return p return p
} }
func (self *Command) CommandStringForUsage(args []string) string { func (self *Command) CommandStringForUsage() string {
names := make([]string, 0, 8) names := make([]string, 0, 8)
p := self p := self
for p != nil { for p != nil {
if p.Name != "" { if p.Name != "" {
names = append(names, p.Name) names = append(names, p.Name)
} else if p.ExeName != "" { } else if p.exe_name != "" {
names = append(names, p.Name) names = append(names, p.Name)
} }
p = p.Parent 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"} return nil, &ParseError{Message: "At least one arg must be supplied"}
} }
ctx := Context{SeenCommands: make([]*Command, 0, 4)} ctx := Context{SeenCommands: make([]*Command, 0, 4)}
self.ExeName = args[0] self.exe_name = args[0]
err = self.parse_args(&ctx, args[1:]) err = self.parse_args(&ctx, args[1:])
if err != nil { if err != nil {
return nil, err 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 return ctx.SeenCommands[len(ctx.SeenCommands)-1], nil
} }
@ -423,6 +457,51 @@ func (self *Command) HasSubCommands() bool {
return false 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 { func (self *Command) FindSubCommand(name string) *Command {
for _, g := range self.SubCommandGroups { for _, g := range self.SubCommandGroups {
c := g.FindSubCommand(name) c := g.FindSubCommand(name)
@ -483,12 +562,6 @@ type Context struct {
} }
func (self *Command) GetOptionValues(pointer_to_options_struct any) error { 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() val := reflect.ValueOf(pointer_to_options_struct).Elem()
if val.Kind() != reflect.Struct { if val.Kind() != reflect.Struct {
return fmt.Errorf("Need a pointer to a struct to set option values on") 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() { if utils.Capitalize(field_name) != field_name || !f.CanSet() {
continue continue
} }
opt := m[field_name] opt := self.option_map[field_name]
if opt == nil { if opt == nil {
return fmt.Errorf("No option with the name: %s", field_name) 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 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)
}
// }}} // }}}