Skip to content

Commit 7240ecd

Browse files
authored
Give a way to get the usage string. (#25)
Fixes #15.
1 parent b216534 commit 7240ecd

File tree

4 files changed

+105
-66
lines changed

4 files changed

+105
-66
lines changed

src/cli.toit

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
import .parser_
66
import .utils_
7+
import .help_generator_
78

89
/**
910
When the arg-parser needs to report an error, or write a help message, it
@@ -32,40 +33,40 @@ class Command:
3233
Usually constructed from the name and the arguments of the command. However, in
3334
some cases, a different (shorter) usage string is desired.
3435
*/
35-
usage/string?
36+
usage_/string?
3637

3738
/** A short (one line) description of the command. */
38-
short_help/string?
39+
short_help_/string?
3940

4041
/** A longer description of the command. */
41-
long_help/string?
42+
long_help_/string?
4243

4344
/** Examples of the command. */
44-
examples/List
45+
examples_/List
4546

4647
/** Aliases of the command. */
47-
aliases/List
48+
aliases_/List
4849

4950
/** Options to the command. */
50-
options/List
51+
options_/List
5152

5253
/** The rest arguments. */
53-
rest/List
54+
rest_/List
5455

5556
/** Whether this command should show up in the help. */
56-
is_hidden/bool
57+
is_hidden_/bool
5758

5859
/**
5960
Subcommands.
6061
Use $add to add new subcommands.
6162
*/
62-
subcommands/List
63+
subcommands_/List
6364

6465
/**
6566
The function to invoke when this command is executed.
6667
May be null, in which case at least one subcommand must be specified.
6768
*/
68-
run_callback/Lambda?
69+
run_callback_/Lambda?
6970

7071
/**
7172
Constructs a new command.
@@ -83,11 +84,19 @@ class Command:
8384
line, but it can span multiple lines/paragraphs if necessary. Use indented lines to
8485
continue paragraphs (just like toitdoc).
8586
*/
86-
constructor .name --.usage=null --.short_help=null --.long_help=null --.examples=[] \
87-
--.aliases=[] --.options=[] --.rest=[] --.subcommands=[] --hidden/bool=false \
87+
constructor .name --usage/string?=null --short_help/string?=null --long_help/string?=null --examples/List=[] \
88+
--aliases/List=[] --options/List=[] --rest/List=[] --subcommands/List=[] --hidden/bool=false \
8889
--run/Lambda?=null:
89-
run_callback = run
90-
is_hidden = hidden
90+
usage_ = usage
91+
short_help_ = short_help
92+
long_help_ = long_help
93+
examples_ = examples
94+
aliases_ = aliases
95+
options_ = options
96+
rest_ = rest
97+
subcommands_ = subcommands
98+
run_callback_ = run
99+
is_hidden_ = hidden
91100
if not subcommands.is_empty and not rest.is_empty:
92101
throw "Cannot have both subcommands and rest arguments."
93102
if run and not subcommands.is_empty:
@@ -105,11 +114,23 @@ class Command:
105114
It is an error to add a subcommand to a command that has a run callback.
106115
*/
107116
add command/Command:
108-
if not rest.is_empty:
117+
if not rest_.is_empty:
109118
throw "Cannot add subcommands to a command with rest arguments."
110-
if run_callback:
119+
if run_callback_:
111120
throw "Cannot add subcommands to a command with a run callback."
112-
subcommands.add command
121+
subcommands_.add command
122+
123+
/** Returns the help string of this command. */
124+
help --invoked_command/string=program_name -> string:
125+
generator := HelpGenerator [this] --invoked_command=invoked_command
126+
generator.build_all
127+
return generator.to_string
128+
129+
/** Returns the usage string of this command. */
130+
usage --invoked_command/string=program_name -> string:
131+
generator := HelpGenerator [this] --invoked_command=invoked_command
132+
generator.build_usage --as_section=false
133+
return generator.to_string
113134

114135
/**
115136
Runs this command.
@@ -125,7 +146,7 @@ class Command:
125146
run arguments/List --invoked_command=program_name --ui/Ui=Ui_ -> none:
126147
parser := Parser_ --ui=ui --invoked_command=invoked_command
127148
parsed := parser.parse this arguments
128-
parsed.command.run_callback.call parsed
149+
parsed.command.run_callback_.call parsed
129150

130151
/**
131152
Checks this command and all subcommands for errors.
@@ -144,12 +165,12 @@ class Command:
144165
available through supercommands.
145166
*/
146167
check_ --path/List --outer_long_options/Set={} --outer_short_options/Set={}:
147-
examples.do: it as Example
148-
aliases.do: it as string
168+
examples_.do: it as Example
169+
aliases_.do: it as string
149170

150171
long_options := {}
151172
short_options := {}
152-
options.do: | option/Option |
173+
options_.do: | option/Option |
153174
if long_options.contains option.name:
154175
throw "Ambiguous option of '$(path.join " ")': --$option.name."
155176
if outer_long_options.contains option.name:
@@ -164,9 +185,9 @@ class Command:
164185
short_options.add option.short_name
165186

166187
have_seen_optional_rest := false
167-
for i := 0; i < rest.size; i++:
168-
option/Option := rest[i]
169-
if option.is_multi and not i == rest.size - 1:
188+
for i := 0; i < rest_.size; i++:
189+
option/Option := rest_[i]
190+
if option.is_multi and not i == rest_.size - 1:
170191
throw "Multi-option '$option.name' of '$(path.join " ")' must be the last rest argument."
171192
if long_options.contains option.name:
172193
throw "Rest name '$option.name' of '$(path.join " ")' already used."
@@ -189,8 +210,8 @@ class Command:
189210
outer_short_options.add_all short_options
190211

191212
subnames := {}
192-
subcommands.do: | command/Command |
193-
names := [command.name] + command.aliases
213+
subcommands_.do: | command/Command |
214+
names := [command.name] + command.aliases_
194215
names.do: | name/string? |
195216
if subnames.contains name:
196217
throw "Ambiguous subcommand of '$(path.join " ")': '$name'."
@@ -203,12 +224,12 @@ class Command:
203224
// We allow a command with a run callback if all subcommands are hidden.
204225
// As such, we could also allow commands without either. If desired, it should be
205226
// safe to remove the following check.
206-
if subcommands.is_empty and not run_callback:
227+
if subcommands_.is_empty and not run_callback_:
207228
throw "Command '$(path.join " ")' has no subcommands and no run callback."
208229

209230
find_subcommand_ name/string -> Command?:
210-
subcommands.do: | command/Command |
211-
if command.name == name or command.aliases.contains name:
231+
subcommands_.do: | command/Command |
232+
if command.name == name or command.aliases_.contains name:
212233
return command
213234
return null
214235

@@ -305,6 +326,7 @@ abstract class Option:
305326
*/
306327
abstract parse str/string --for_help_example/bool=false -> any
307328

329+
308330
/**
309331
A string option.
310332
*/

src/help_generator_.toit

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -88,34 +88,40 @@ class HelpGenerator:
8888
global_options_ -> List:
8989
result := []
9090
for i := 0; i < path_.size - 1; i++:
91-
result.add_all path_[i].options
91+
result.add_all path_[i].options_
9292
return result
9393

9494
/**
9595
Builds the description.
9696
97-
If available, the description is the $Command.long_help, otherwise, the
98-
$Command.short_help is used. If none exists, no description is built.
97+
If available, the description is the $Command.long_help_, otherwise, the
98+
$Command.short_help_ is used. If none exists, no description is built.
9999
*/
100100
build_description -> none:
101-
if help := command_.long_help:
101+
if help := command_.long_help_:
102102
ensure_vertical_space_
103103
writeln_ (help.trim --right)
104-
else if short_help := command_.short_help:
104+
else if short_help := command_.short_help_:
105105
ensure_vertical_space_
106106
writeln_ (short_help.trim --right)
107107

108108
/**
109109
Builds the usage section.
110110
111-
If the command has a $Command.usage line, then that one is used. Otherwise the
111+
If the command has a $Command.usage_ line, then that one is used. Otherwise the
112112
usage line is built from the available options or subcommands.
113+
114+
If $as_section is true, then the section is preceded by a "Usage:" title, indented,
115+
and followed by an empty line.
113116
*/
114-
build_usage -> none:
117+
build_usage --as_section/bool=true -> none:
115118
ensure_vertical_space_
116-
writeln_ "Usage:"
117-
if command_.usage:
118-
writeln_ command_.usage --indentation=2
119+
if as_section:
120+
writeln_ "Usage:"
121+
indentation := as_section ? 2 : 0
122+
if command_.usage_:
123+
write_ command_.usage_ --indentation=indentation
124+
if as_section: writeln_
119125
return
120126

121127
// We want to construct a usage line like
@@ -125,12 +131,12 @@ class HelpGenerator:
125131
// They are sorted by name.
126132
// For the usage line we don't care for the short names of options.
127133
128-
write_ invoked_command_ --indentation=2
134+
write_ invoked_command_ --indentation=indentation
129135
has_more_options := false
130136
for i := 0; i < path_.size; i++:
131-
current_command := path_[i]
137+
current_command/Command := path_[i]
132138
if i != 0: write_ " $current_command.name"
133-
current_options := current_command.options
139+
current_options := current_command.options_
134140
sorted := current_options.sort: | a/Option b/Option | a.name.compare_to b.name
135141
sorted.do: | option/Option |
136142
if option.is_required:
@@ -140,29 +146,29 @@ class HelpGenerator:
140146
else if not option.is_hidden:
141147
has_more_options = true
142148

143-
if not command_.subcommands.is_empty: write_ " <command>"
149+
if not command_.subcommands_.is_empty: write_ " <command>"
144150
if has_more_options: write_ " [<options>]"
145-
if not command_.rest.is_empty: write_ " [--]"
146-
command_.rest.do: | option/Option |
151+
if not command_.rest_.is_empty: write_ " [--]"
152+
command_.rest_.do: | option/Option |
147153
type := option.type
148154
option_str/string := ?
149155
if type == "string": option_str = "<$option.name>"
150156
else: option_str = "<$option.name:$option.type>"
151157
if option.is_multi: option_str = "$option_str..."
152158
if not option.is_required: option_str = "[$option_str]"
153159
write_ " $option_str"
154-
writeln_
160+
if as_section: writeln_
155161

156162
/**
157163
Builds the aliases section.
158164
159165
Only generates the alias section if the command is not the root command.
160166
*/
161167
build_aliases -> none:
162-
if is_root_command_ or command_.aliases.is_empty: return
168+
if is_root_command_ or command_.aliases_.is_empty: return
163169
ensure_vertical_space_
164170
writeln_ "Aliases:"
165-
writeln_ (command_.aliases.join ", ") --indentation=2
171+
writeln_ (command_.aliases_.join ", ") --indentation=2
166172

167173
/**
168174
Builds the commands section.
@@ -173,22 +179,22 @@ class HelpGenerator:
173179
If the command is the root-command also adds the 'help' command.
174180
*/
175181
build_commands -> none:
176-
if command_.subcommands.is_empty: return
182+
if command_.subcommands_.is_empty: return
177183
ensure_vertical_space_
178184
writeln_ "Commands:"
179185

180186
commands_and_help := []
181187
has_help_subcommand := false
182-
command_.subcommands.do: | subcommand/Command |
188+
command_.subcommands_.do: | subcommand/Command |
183189
if subcommand.name == "help": has_help_subcommand = true
184-
subcommand.aliases.do: if it == "help": has_help_subcommand = true
190+
subcommand.aliases_.do: if it == "help": has_help_subcommand = true
185191

186-
if subcommand.is_hidden: continue.do
192+
if subcommand.is_hidden_: continue.do
187193

188194
help_str := ?
189-
if help := subcommand.short_help:
195+
if help := subcommand.short_help_:
190196
help_str = help
191-
else if long_help := subcommand.long_help:
197+
else if long_help := subcommand.long_help_:
192198
// Take the first paragraph (potentially multiple lines) of the long help.
193199
paragraph_index := long_help.index_of "\n\n"
194200
if paragraph_index == -1:
@@ -211,8 +217,8 @@ class HelpGenerator:
211217
Automatically adds the help option if it is not already defined.
212218
*/
213219
build_local_options -> none:
214-
build_options_ --title="Options" command_.options --add_help
215-
build_options_ --title="Rest" command_.rest --rest
220+
build_options_ --title="Options" command_.options_ --add_help
221+
build_options_ --title="Rest" command_.rest_ --rest
216222

217223
/**
218224
Builds the global options section.
@@ -313,7 +319,7 @@ class HelpGenerator:
313319
build_examples:
314320
// Get the local examples directly, as we want to keep them on top, and as we
315321
// don't want to filter them.
316-
this_examples := command_.examples.map: | example/Example | [example, path_]
322+
this_examples := command_.examples_.map: | example/Example | [example, path_]
317323
sub_examples := []
318324
add_global_examples_ command_ sub_examples --path=path_ --skip_first_level
319325
sub_examples.sort --in_place: | a/List b/List | a[0].global_priority - b[0].global_priority
@@ -342,11 +348,11 @@ class HelpGenerator:
342348
*/
343349
add_global_examples_ command/Command all_examples/List --path/List --skip_first_level/bool=false -> none:
344350
if not skip_first_level:
345-
global_examples := (command.examples.filter: it.global_priority > 0)
351+
global_examples := (command.examples_.filter: it.global_priority > 0)
346352
all_examples.add_all (global_examples.map: [it, path])
347353

348-
command.subcommands.do: | subcommand/Command |
349-
if subcommand.is_hidden: continue.do
354+
command.subcommands_.do: | subcommand/Command |
355+
if subcommand.is_hidden_: continue.do
350356
add_global_examples_ subcommand all_examples --path=(path + [subcommand])
351357

352358
/**
@@ -399,7 +405,7 @@ class HelpGenerator:
399405
for j := 0; j < parsed_path.size; j++:
400406
current_command/Command := parsed_path[j]
401407
command_level[current_command] = j
402-
current_command.options.do: | option/Option |
408+
current_command.options_.do: | option/Option |
403409
if not parsed.was_provided option.name: continue.do
404410
option_to_command["--$option.name"] = current_command
405411
if option.short_name: option_to_command["-$option.short_name"] = current_command

0 commit comments

Comments
 (0)