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