go-arg/usage.go

339 lines
9.1 KiB
Go
Raw Normal View History

2015-10-31 20:32:20 -05:00
package arg
import (
"fmt"
"io"
"strings"
)
2016-01-18 10:24:21 -06:00
// the width of the left column
const colWidth = 25
// Fail prints usage information to stderr and exits with non-zero status
func (p *Parser) Fail(msg string) {
2023-10-08 19:09:05 -05:00
p.FailSubcommand(msg)
}
// FailSubcommand prints usage information for a specified subcommand to stderr,
// then exits with non-zero status. To write usage information for a top-level
// subcommand, provide just the name of that subcommand. To write usage
// information for a subcommand that is nested under another subcommand, provide
// a sequence of subcommand names starting with the top-level subcommand and so
// on down the tree.
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
2023-10-08 19:09:05 -05:00
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
if err != nil {
return err
}
fmt.Fprintln(p.config.Out, "error:", msg)
p.config.Exit(-1)
2023-10-08 19:09:05 -05:00
return nil
2015-10-31 20:32:20 -05:00
}
// WriteUsage writes usage information to the given writer
func (p *Parser) WriteUsage(w io.Writer) {
2023-10-08 19:09:05 -05:00
p.WriteUsageForSubcommand(w, p.subcommand...)
}
// WriteUsageForSubcommand writes the usage information for a specified
// subcommand. To write usage information for a top-level subcommand, provide
// just the name of that subcommand. To write usage information for a subcommand
// that is nested under another subcommand, provide a sequence of subcommand
// names starting with the top-level subcommand and so on down the tree.
func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
cmd, err := p.lookupCommand(subcommand...)
if err != nil {
return err
}
2020-12-19 17:54:03 -06:00
var positionals, longOptions, shortOptions []*spec
for _, spec := range cmd.specs {
2020-12-19 17:54:03 -06:00
switch {
case spec.positional:
2015-10-31 20:32:20 -05:00
positionals = append(positionals, spec)
2020-12-19 17:54:03 -06:00
case spec.long != "":
longOptions = append(longOptions, spec)
case spec.short != "":
shortOptions = append(shortOptions, spec)
2015-10-31 20:32:20 -05:00
}
}
2016-09-08 23:18:19 -05:00
if p.version != "" {
fmt.Fprintln(w, p.version)
}
// print the beginning of the usage string
2023-10-08 19:09:05 -05:00
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
for _, s := range subcommand {
fmt.Fprint(w, " "+s)
}
2015-10-31 20:32:20 -05:00
// write the option component of the usage message
2020-12-19 17:54:03 -06:00
for _, spec := range shortOptions {
// prefix with a space
fmt.Fprint(w, " ")
if !spec.required {
fmt.Fprint(w, "[")
}
fmt.Fprint(w, synopsis(spec, "-"+spec.short))
if !spec.required {
fmt.Fprint(w, "]")
}
}
for _, spec := range longOptions {
// prefix with a space
fmt.Fprint(w, " ")
2015-10-31 20:32:20 -05:00
if !spec.required {
fmt.Fprint(w, "[")
}
fmt.Fprint(w, synopsis(spec, "--"+spec.long))
if !spec.required {
fmt.Fprint(w, "]")
}
}
// When we parse positionals, we check that:
// 1. required positionals come before non-required positionals
// 2. there is at most one multiple-value positional
// 3. if there is a multiple-value positional then it comes after all other positionals
// Here we merely print the usage string, so we do not explicitly re-enforce those rules
// write the positionals in following form:
// REQUIRED1 REQUIRED2
// REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
// REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
// REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
// REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
var closeBrackets int
2015-10-31 20:32:20 -05:00
for _, spec := range positionals {
fmt.Fprint(w, " ")
if !spec.required {
fmt.Fprint(w, "[")
closeBrackets += 1
}
if spec.cardinality == multiple {
2019-11-29 15:22:21 -06:00
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
2015-10-31 20:32:20 -05:00
} else {
2019-11-29 15:22:21 -06:00
fmt.Fprint(w, spec.placeholder)
2015-10-31 20:32:20 -05:00
}
}
fmt.Fprint(w, strings.Repeat("]", closeBrackets))
2020-01-23 09:35:45 -06:00
// if the program supports subcommands, give a hint to the user about their existence
if len(cmd.subcommands) > 0 {
fmt.Fprint(w, " <command> [<args>]")
}
2015-10-31 20:32:20 -05:00
fmt.Fprint(w, "\n")
2023-10-08 19:09:05 -05:00
return nil
}
2023-10-08 19:09:05 -05:00
// print prints a line like this:
//
// --option FOO A description of the option [default: 123]
//
// If the text on the left is longer than a certain threshold, the description is moved to the next line:
//
// --verylongoptionoption VERY_LONG_VARIABLE
// A description of the option [default: 123]
//
// If multiple "extras" are provided then they are put inside a single set of square brackets:
//
// --option FOO A description of the option [default: 123, env: FOO]
func print(w io.Writer, item, description string, bracketed ...string) {
lhs := " " + item
2019-05-03 17:02:10 -05:00
fmt.Fprint(w, lhs)
2023-10-08 19:09:05 -05:00
if description != "" {
2019-05-03 17:02:10 -05:00
if len(lhs)+2 < colWidth {
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
} else {
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
}
2023-10-08 19:09:05 -05:00
fmt.Fprint(w, description)
2019-05-03 17:02:10 -05:00
}
2023-10-08 19:09:05 -05:00
var brack string
for _, s := range bracketed {
if s != "" {
if brack != "" {
brack += ", "
}
brack += s
}
}
2023-10-08 19:09:05 -05:00
if brack != "" {
fmt.Fprintf(w, " [%s]", brack)
}
2023-10-08 19:09:05 -05:00
fmt.Fprint(w, "\n")
}
2023-10-08 19:09:05 -05:00
func withDefault(s string) string {
if s == "" {
return ""
}
2023-10-08 19:09:05 -05:00
return "default: " + s
}
2023-10-08 19:09:05 -05:00
func withEnv(env string) string {
if env == "" {
return ""
2019-05-03 17:02:10 -05:00
}
2023-10-08 19:09:05 -05:00
return "env: " + env
2019-05-03 17:02:10 -05:00
}
// WriteHelp writes the usage string followed by the full help string for each option
func (p *Parser) WriteHelp(w io.Writer) {
2023-10-08 19:09:05 -05:00
p.WriteHelpForSubcommand(w, p.subcommand...)
}
// WriteHelpForSubcommand writes the usage string followed by the full help
// string for a specified subcommand. To write help for a top-level subcommand,
// provide just the name of that subcommand. To write help for a subcommand that
// is nested under another subcommand, provide a sequence of subcommand names
// starting with the top-level subcommand and so on down the tree.
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
cmd, err := p.lookupCommand(subcommand...)
if err != nil {
return err
}
2022-05-21 10:24:45 -05:00
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
var hasVersionOption bool
for _, spec := range cmd.specs {
switch {
case spec.positional:
positionals = append(positionals, spec)
case spec.long != "":
longOptions = append(longOptions, spec)
case spec.short != "":
shortOptions = append(shortOptions, spec)
2022-05-21 10:24:45 -05:00
case spec.short == "" && spec.long == "":
envOnlyOptions = append(envOnlyOptions, spec)
}
}
if p.description != "" {
fmt.Fprintln(w, p.description)
}
2023-10-08 19:09:05 -05:00
p.WriteUsageForSubcommand(w, subcommand...)
2015-10-31 20:32:20 -05:00
// write the list of positionals
if len(positionals) > 0 {
2017-03-08 13:44:01 -06:00
fmt.Fprint(w, "\nPositional arguments:\n")
2015-10-31 20:32:20 -05:00
for _, spec := range positionals {
2023-10-08 19:09:05 -05:00
print(w, spec.placeholder, spec.help)
2015-10-31 20:32:20 -05:00
}
}
// write the list of options with the short-only ones first to match the usage string
if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
fmt.Fprint(w, "\nOptions:\n")
for _, spec := range shortOptions {
p.printOption(w, spec)
}
for _, spec := range longOptions {
p.printOption(w, spec)
if spec.long == "version" {
hasVersionOption = true
}
}
}
// obtain a flattened list of options from all ancestors
var globals []*spec
ancestor := cmd.parent
for ancestor != nil {
2020-01-23 23:36:24 -06:00
globals = append(globals, ancestor.specs...)
ancestor = ancestor.parent
}
// write the list of global options
if len(globals) > 0 {
fmt.Fprint(w, "\nGlobal options:\n")
for _, spec := range globals {
p.printOption(w, spec)
if spec.long == "version" {
hasVersionOption = true
}
}
}
// write the list of built in options
2019-04-30 13:16:01 -05:00
p.printOption(w, &spec{
cardinality: zero,
long: "help",
short: "h",
help: "display this help and exit",
2019-04-30 13:16:01 -05:00
})
if !hasVersionOption && p.version != "" {
2019-04-30 13:16:01 -05:00
p.printOption(w, &spec{
cardinality: zero,
long: "version",
help: "display version and exit",
2019-04-30 13:16:01 -05:00
})
2016-09-08 23:18:19 -05:00
}
2019-05-03 17:02:10 -05:00
2022-05-21 10:24:45 -05:00
// write the list of environment only variables
2022-05-21 10:44:32 -05:00
if len(envOnlyOptions) > 0 {
2022-05-21 10:24:45 -05:00
fmt.Fprint(w, "\nEnvironment variables:\n")
for _, spec := range envOnlyOptions {
p.printEnvOnlyVar(w, spec)
}
}
2019-05-03 17:02:10 -05:00
// write the list of subcommands
if len(cmd.subcommands) > 0 {
2019-05-03 17:02:10 -05:00
fmt.Fprint(w, "\nCommands:\n")
for _, subcmd := range cmd.subcommands {
2023-10-08 19:09:05 -05:00
names := append([]string{subcmd.name}, subcmd.aliases...)
print(w, strings.Join(names, ", "), subcmd.help)
2019-05-03 17:02:10 -05:00
}
}
if p.epilogue != "" {
fmt.Fprintln(w, "\n"+p.epilogue)
}
2023-10-08 19:09:05 -05:00
return nil
}
2019-04-14 21:50:17 -05:00
func (p *Parser) printOption(w io.Writer, spec *spec) {
ways := make([]string, 0, 2)
2020-12-19 17:54:03 -06:00
if spec.long != "" {
ways = append(ways, synopsis(spec, "--"+spec.long))
2020-12-19 17:54:03 -06:00
}
if spec.short != "" {
ways = append(ways, synopsis(spec, "-"+spec.short))
}
if len(ways) > 0 {
2023-10-08 19:09:05 -05:00
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
}
2015-10-31 20:32:20 -05:00
}
2015-11-01 01:57:26 -05:00
2022-05-21 10:24:45 -05:00
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
ways := make([]string, 0, 2)
if spec.required {
ways = append(ways, "Required.")
} else {
ways = append(ways, "Optional.")
}
if spec.help != "" {
ways = append(ways, spec.help)
}
2023-10-08 19:09:05 -05:00
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
}
2015-11-01 01:57:26 -05:00
func synopsis(spec *spec, form string) string {
2024-03-31 09:30:12 -05:00
// if the user omits the placeholder tag then we pick one automatically,
// but if the user explicitly specifies an empty placeholder then we
// leave out the placeholder in the help message
if spec.cardinality == zero || spec.placeholder == "" {
2015-11-01 01:57:26 -05:00
return form
}
2019-11-29 15:22:21 -06:00
return form + " " + spec.placeholder
2015-11-01 01:57:26 -05:00
}