usage.go raw

   1  package arg
   2  
   3  import (
   4  	"fmt"
   5  	"io"
   6  	"strings"
   7  )
   8  
   9  // the width of the left column
  10  const colWidth = 25
  11  
  12  // Fail prints usage information to p.Config.Out and exits with status code 2.
  13  func (p *Parser) Fail(msg string) {
  14  	p.FailSubcommand(msg)
  15  }
  16  
  17  // FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
  18  // then exits with status code 2. To write usage information for a top-level
  19  // subcommand, provide just the name of that subcommand. To write usage
  20  // information for a subcommand that is nested under another subcommand, provide
  21  // a sequence of subcommand names starting with the top-level subcommand and so
  22  // on down the tree.
  23  func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
  24  	err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
  25  	if err != nil {
  26  		return err
  27  	}
  28  
  29  	fmt.Fprintln(p.config.Out, "error:", msg)
  30  	p.config.Exit(2)
  31  	return nil
  32  }
  33  
  34  // WriteUsage writes usage information to the given writer
  35  func (p *Parser) WriteUsage(w io.Writer) {
  36  	p.WriteUsageForSubcommand(w, p.subcommand...)
  37  }
  38  
  39  // WriteUsageForSubcommand writes the usage information for a specified
  40  // subcommand. To write usage information for a top-level subcommand, provide
  41  // just the name of that subcommand. To write usage information for a subcommand
  42  // that is nested under another subcommand, provide a sequence of subcommand
  43  // names starting with the top-level subcommand and so on down the tree.
  44  func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
  45  	cmd, err := p.lookupCommand(subcommand...)
  46  	if err != nil {
  47  		return err
  48  	}
  49  
  50  	var positionals, longOptions, shortOptions []*spec
  51  	for _, spec := range cmd.specs {
  52  		if spec.hidden {
  53  			continue
  54  		}
  55  		switch {
  56  		case spec.positional:
  57  			positionals = append(positionals, spec)
  58  		case spec.long != "":
  59  			longOptions = append(longOptions, spec)
  60  		case spec.short != "":
  61  			shortOptions = append(shortOptions, spec)
  62  		}
  63  	}
  64  
  65  	// print the beginning of the usage string
  66  	fmt.Fprintf(w, "Usage: %s", p.cmd.name)
  67  	for _, s := range subcommand {
  68  		fmt.Fprint(w, " "+s)
  69  	}
  70  
  71  	// write the option component of the usage message
  72  	for _, spec := range shortOptions {
  73  		// prefix with a space
  74  		fmt.Fprint(w, " ")
  75  		if !spec.required {
  76  			fmt.Fprint(w, "[")
  77  		}
  78  		fmt.Fprint(w, synopsis(spec, "-"+spec.short))
  79  		if !spec.required {
  80  			fmt.Fprint(w, "]")
  81  		}
  82  	}
  83  
  84  	for _, spec := range longOptions {
  85  		// prefix with a space
  86  		fmt.Fprint(w, " ")
  87  		if !spec.required {
  88  			fmt.Fprint(w, "[")
  89  		}
  90  		fmt.Fprint(w, synopsis(spec, "--"+spec.long))
  91  		if !spec.required {
  92  			fmt.Fprint(w, "]")
  93  		}
  94  	}
  95  
  96  	// When we parse positionals, we check that:
  97  	//  1. required positionals come before non-required positionals
  98  	//  2. there is at most one multiple-value positional
  99  	//  3. if there is a multiple-value positional then it comes after all other positionals
 100  	// Here we merely print the usage string, so we do not explicitly re-enforce those rules
 101  
 102  	// write the positionals in following form:
 103  	//    REQUIRED1 REQUIRED2
 104  	//    REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
 105  	//    REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
 106  	//    REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
 107  	//    REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
 108  	var closeBrackets int
 109  	for _, spec := range positionals {
 110  		fmt.Fprint(w, " ")
 111  		if !spec.required {
 112  			fmt.Fprint(w, "[")
 113  			closeBrackets += 1
 114  		}
 115  		if spec.cardinality == multiple {
 116  			fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
 117  		} else {
 118  			fmt.Fprint(w, spec.placeholder)
 119  		}
 120  	}
 121  	fmt.Fprint(w, strings.Repeat("]", closeBrackets))
 122  
 123  	// if the program supports subcommands, give a hint to the user about their existence
 124  	if len(cmd.subcommands) > 0 {
 125  		fmt.Fprint(w, " <command> [<args>]")
 126  	}
 127  
 128  	fmt.Fprint(w, "\n")
 129  	return nil
 130  }
 131  
 132  // print prints a line like this:
 133  //
 134  //	--option FOO            A description of the option [default: 123]
 135  //
 136  // If the text on the left is longer than a certain threshold, the description is moved to the next line:
 137  //
 138  //	--verylongoptionoption VERY_LONG_VARIABLE
 139  //	                        A description of the option [default: 123]
 140  //
 141  // If multiple "extras" are provided then they are put inside a single set of square brackets:
 142  //
 143  //	--option FOO            A description of the option [default: 123, env: FOO]
 144  func print(w io.Writer, item, description string, bracketed ...string) {
 145  	lhs := "  " + item
 146  	fmt.Fprint(w, lhs)
 147  	if description != "" {
 148  		if len(lhs)+2 < colWidth {
 149  			fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
 150  		} else {
 151  			fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
 152  		}
 153  		fmt.Fprint(w, description)
 154  	}
 155  
 156  	var brack string
 157  	for _, s := range bracketed {
 158  		if s != "" {
 159  			if brack != "" {
 160  				brack += ", "
 161  			}
 162  			brack += s
 163  		}
 164  	}
 165  
 166  	if brack != "" {
 167  		fmt.Fprintf(w, " [%s]", brack)
 168  	}
 169  	fmt.Fprint(w, "\n")
 170  }
 171  
 172  func withDefault(s string) string {
 173  	if s == "" {
 174  		return ""
 175  	}
 176  	return "default: " + s
 177  }
 178  
 179  func withEnv(env string) string {
 180  	if env == "" {
 181  		return ""
 182  	}
 183  	return "env: " + env
 184  }
 185  
 186  // WriteHelp writes the usage string followed by the full help string for each option
 187  func (p *Parser) WriteHelp(w io.Writer) {
 188  	p.WriteHelpForSubcommand(w, p.subcommand...)
 189  }
 190  
 191  // WriteHelpForSubcommand writes the usage string followed by the full help
 192  // string for a specified subcommand. To write help for a top-level subcommand,
 193  // provide just the name of that subcommand. To write help for a subcommand that
 194  // is nested under another subcommand, provide a sequence of subcommand names
 195  // starting with the top-level subcommand and so on down the tree.
 196  func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
 197  	cmd, err := p.lookupCommand(subcommand...)
 198  	if err != nil {
 199  		return err
 200  	}
 201  
 202  	var positionals, longOptions, shortOptions, envOnlyOptions []*spec
 203  	var hasVersionOption bool
 204  	for _, spec := range cmd.specs {
 205  		if spec.hidden {
 206  			continue
 207  		}
 208  
 209  		switch {
 210  		case spec.positional:
 211  			positionals = append(positionals, spec)
 212  		case spec.long != "":
 213  			longOptions = append(longOptions, spec)
 214  			if spec.long == "version" {
 215  				hasVersionOption = true
 216  			}
 217  		case spec.short != "":
 218  			shortOptions = append(shortOptions, spec)
 219  		case spec.short == "" && spec.long == "":
 220  			envOnlyOptions = append(envOnlyOptions, spec)
 221  		}
 222  	}
 223  
 224  	// obtain a flattened list of options from all ancestors
 225  	// also determine if any ancestor has a version option spec
 226  	var globals []*spec
 227  	ancestor := cmd.parent
 228  	for ancestor != nil {
 229  		for _, spec := range ancestor.specs {
 230  			if spec.long == "version" {
 231  				hasVersionOption = true
 232  				break
 233  			}
 234  		}
 235  		globals = append(globals, ancestor.specs...)
 236  		ancestor = ancestor.parent
 237  	}
 238  
 239  	if p.description != "" {
 240  		fmt.Fprintln(w, p.description)
 241  	}
 242  
 243  	if !hasVersionOption && p.version != "" {
 244  		fmt.Fprintln(w, p.version)
 245  	}
 246  
 247  	p.WriteUsageForSubcommand(w, subcommand...)
 248  
 249  	// write the list of positionals
 250  	if len(positionals) > 0 {
 251  		fmt.Fprint(w, "\nPositional arguments:\n")
 252  		for _, spec := range positionals {
 253  			print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
 254  		}
 255  	}
 256  
 257  	// write the list of options with the short-only ones first to match the usage string
 258  	if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
 259  		fmt.Fprint(w, "\nOptions:\n")
 260  		for _, spec := range shortOptions {
 261  			p.printOption(w, spec)
 262  		}
 263  		for _, spec := range longOptions {
 264  			p.printOption(w, spec)
 265  		}
 266  	}
 267  
 268  	// write the list of global options
 269  	if len(globals) > 0 {
 270  		fmt.Fprint(w, "\nGlobal options:\n")
 271  		for _, spec := range globals {
 272  			p.printOption(w, spec)
 273  		}
 274  	}
 275  
 276  	// write the list of built in options
 277  	p.printOption(w, &spec{
 278  		cardinality: zero,
 279  		long:        "help",
 280  		short:       "h",
 281  		help:        "display this help and exit",
 282  	})
 283  	if !hasVersionOption && p.version != "" {
 284  		p.printOption(w, &spec{
 285  			cardinality: zero,
 286  			long:        "version",
 287  			help:        "display version and exit",
 288  		})
 289  	}
 290  
 291  	// write the list of environment only variables
 292  	if len(envOnlyOptions) > 0 {
 293  		fmt.Fprint(w, "\nEnvironment variables:\n")
 294  		for _, spec := range envOnlyOptions {
 295  			p.printEnvOnlyVar(w, spec)
 296  		}
 297  	}
 298  
 299  	// write the list of subcommands
 300  	if len(cmd.subcommands) > 0 {
 301  		fmt.Fprint(w, "\nCommands:\n")
 302  		for _, subcmd := range cmd.subcommands {
 303  			if subcmd.hidden {
 304  				// skip this subcommand in the help message
 305  				continue
 306  			}
 307  
 308  			names := append([]string{subcmd.name}, subcmd.aliases...)
 309  			print(w, strings.Join(names, ", "), subcmd.help)
 310  		}
 311  	}
 312  
 313  	if p.epilogue != "" {
 314  		fmt.Fprintln(w, "\n"+p.epilogue)
 315  	}
 316  	return nil
 317  }
 318  
 319  func (p *Parser) printOption(w io.Writer, spec *spec) {
 320  	ways := make([]string, 0, 2)
 321  	if spec.long != "" {
 322  		ways = append(ways, synopsis(spec, "--"+spec.long))
 323  	}
 324  	if spec.short != "" {
 325  		ways = append(ways, synopsis(spec, "-"+spec.short))
 326  	}
 327  	if len(ways) > 0 {
 328  		print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
 329  	}
 330  }
 331  
 332  func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
 333  	ways := make([]string, 0, 2)
 334  	if spec.required {
 335  		ways = append(ways, "Required.")
 336  	} else {
 337  		ways = append(ways, "Optional.")
 338  	}
 339  
 340  	if spec.help != "" {
 341  		ways = append(ways, spec.help)
 342  	}
 343  
 344  	print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
 345  }
 346  
 347  func synopsis(spec *spec, form string) string {
 348  	// if the user omits the placeholder tag then we pick one automatically,
 349  	// but if the user explicitly specifies an empty placeholder then we
 350  	// leave out the placeholder in the help message
 351  	if spec.cardinality == zero || spec.placeholder == "" {
 352  		return form
 353  	}
 354  	return form + " " + spec.placeholder
 355  }
 356