Work on outputting help for commands
This commit is contained in:
parent
cb2389efa5
commit
e7c14c78d0
142
tools/cli/help.go
Normal file
142
tools/cli/help.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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,10 +272,13 @@ func (self *OptionGroup) FindOption(name_with_hyphens string) *Option {
|
||||
// }}}
|
||||
|
||||
type Command struct { // {{{
|
||||
Name, ExeName string
|
||||
Usage, HelpText string
|
||||
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
|
||||
// If true does not fail if the first non-option arg is not a sub-command
|
||||
SubCommandIsOptional bool
|
||||
|
||||
SubCommandGroups []*CommandGroup
|
||||
@ -274,6 +286,10 @@ type Command struct { // {{{
|
||||
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,6 +372,7 @@ func (self *Command) Validate() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
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)}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user