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 (
|
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) {
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user