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