Skip to content

Commit ae142d3

Browse files
authored
Add examples-check to check. (#73)
Also refactor the commands-path list and create a class for it.
1 parent e0ae1fc commit ae142d3

File tree

6 files changed

+143
-66
lines changed

6 files changed

+143
-66
lines changed

src/cli.toit

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import system
77

88
import .cache
99
import .config
10+
import .help-generator_
1011
import .parser_
12+
import .path_
1113
import .utils_
12-
import .help-generator_
1314
import .ui
1415

1516
export Ui
@@ -251,7 +252,8 @@ class Command:
251252

252253
/** Returns the help string of this command. */
253254
help --invoked-command/string=system.program-name -> string:
254-
generator := HelpGenerator [this] --invoked-command=invoked-command
255+
path := Path this --invoked-command=invoked-command
256+
generator := HelpGenerator path
255257
generator.build-all
256258
return generator.to-string
257259

@@ -273,7 +275,8 @@ class Command:
273275

274276
/** Returns the usage string of this command. */
275277
usage --invoked-command/string=system.program-name -> string:
276-
generator := HelpGenerator [this] --invoked-command=invoked-command
278+
path := Path this --invoked-command=invoked-command
279+
generator := HelpGenerator path
277280
generator.build-usage --as-section=false
278281
return generator.to-string
279282

@@ -300,8 +303,8 @@ class Command:
300303
add-ui-options_
301304
cli = Cli_ name --ui=ui --cache=null --config=null
302305
parser := Parser_ --invoked-command=invoked-command
303-
parser.parse this arguments: | path/List parameters/Parameters |
304-
invocation := Invocation.private_ cli path parameters
306+
parser.parse this arguments: | path/Path parameters/Parameters |
307+
invocation := Invocation.private_ cli path.commands parameters
305308
invocation.command.run-callback_.call invocation
306309

307310
add-ui-options_:
@@ -343,9 +346,15 @@ class Command:
343346

344347
/**
345348
Checks this command and all subcommands for errors.
349+
350+
If an error is found, an exception is thrown with a message describing the error.
351+
352+
Typically, a call to this method is added to the program's main function in an
353+
assert block.
346354
*/
347355
check --invoked-command=system.program-name:
348-
check_ --path=[invoked-command]
356+
path := Path this --invoked-command=invoked-command
357+
check_ --path=path --invoked-command=invoked-command
349358

350359
are-prefix-of-each-other_ str1/string str2/string -> bool:
351360
m := min str1.size str2.size
@@ -357,39 +366,39 @@ class Command:
357366
The $outer-long-options and $outer-short-options are the options that are
358367
available through supercommands.
359368
*/
360-
check_ --path/List --outer-long-options/Set={} --outer-short-options/Set={}:
369+
check_ --path/Path --invoked-command/string --outer-long-options/Set={} --outer-short-options/Set={}:
361370
examples_.do: it as Example
362371
aliases_.do: it as string
363372

364373
long-options := {}
365374
short-options := {}
366375
options_.do: | option/Option |
367376
if long-options.contains option.name:
368-
throw "Ambiguous option of '$(path.join " ")': --$option.name."
377+
throw "Ambiguous option of '$path.to-string': --$option.name."
369378
if outer-long-options.contains option.name:
370-
throw "Ambiguous option of '$(path.join " ")': --$option.name conflicts with global option."
379+
throw "Ambiguous option of '$path.to-string': --$option.name conflicts with global option."
371380
long-options.add option.name
372381

373382
if option.short-name:
374383
if (short-options.any: are-prefix-of-each-other_ it option.short-name):
375-
throw "Ambiguous option of '$(path.join " ")': -$option.short-name."
384+
throw "Ambiguous option of '$path.to-string': -$option.short-name."
376385
if (outer-short-options.any: are-prefix-of-each-other_ it option.short-name):
377-
throw "Ambiguous option of '$(path.join " ")': -$option.short-name conflicts with global option."
386+
throw "Ambiguous option of '$path.to-string': -$option.short-name conflicts with global option."
378387
short-options.add option.short-name
379388

380389
have-seen-optional-rest := false
381390
for i := 0; i < rest_.size; i++:
382391
option/Option := rest_[i]
383392
if option.is-multi and not i == rest_.size - 1:
384-
throw "Multi-option '$option.name' of '$(path.join " ")' must be the last rest argument."
393+
throw "Multi-option '$option.name' of '$path.to-string' must be the last rest argument."
385394
if long-options.contains option.name:
386-
throw "Rest name '$option.name' of '$(path.join " ")' already used."
395+
throw "Rest name '$option.name' of '$path.to-string' already used."
387396
if outer-long-options.contains option.name:
388-
throw "Rest name '$option.name' of '$(path.join " ")' already a global option."
397+
throw "Rest name '$option.name' of '$path.to-string' already a global option."
389398
if have-seen-optional-rest and option.is-required:
390-
throw "Required rest argument '$option.name' of '$(path.join " ")' cannot follow optional rest argument."
399+
throw "Required rest argument '$option.name' of '$path.to-string' cannot follow optional rest argument."
391400
if option.is-hidden:
392-
throw "Rest argument '$option.name' of '$(path.join " ")' cannot be hidden."
401+
throw "Rest argument '$option.name' of '$path.to-string' cannot be hidden."
393402
have-seen-optional-rest = not option.is-required
394403
long-options.add option.name
395404

@@ -407,18 +416,25 @@ class Command:
407416
names := [command.name] + command.aliases_
408417
names.do: | name/string? |
409418
if subnames.contains name:
410-
throw "Ambiguous subcommand of '$(path.join " ")': '$name'."
419+
throw "Ambiguous subcommand of '$path.to-string': '$name'."
411420
subnames.add name
412421

413-
command.check_ --path=(path + [command.name])
422+
command.check_
423+
--path=(path + command)
424+
--invoked-command=invoked-command
414425
--outer-long-options=outer-long-options
415426
--outer-short-options=outer-short-options
416427

417428
// We allow a command with a run callback if all subcommands are hidden.
418429
// As such, we could also allow commands without either. If desired, it should be
419430
// safe to remove the following check.
420431
if subcommands_.is-empty and not run-callback_:
421-
throw "Command '$(path.join " ")' has no subcommands and no run callback."
432+
throw "Command '$path.to-string' has no subcommands and no run callback."
433+
434+
if not examples_.is-empty:
435+
generator := HelpGenerator path
436+
examples_.do: | example/Example |
437+
generator.build-example_ example --example-path=path
422438

423439
find-subcommand_ name/string -> Command?:
424440
subcommands_.do: | command/Command |

src/help-generator_.toit

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
import .cli
66
import .parser_
7+
import .path_
78
import .utils_
89
import .ui
910
import system
@@ -13,10 +14,7 @@ The 'help' command that can be executed on the root command.
1314
1415
It finds the selected command and prints its help.
1516
*/
16-
help-command_ path/List arguments/List --invoked-command/string --ui/Ui:
17-
// We are modifying the path, so make a copy.
18-
path = path.copy
19-
17+
help-command_ path/Path arguments/List --ui/Ui:
2018
command/Command := path.last
2119

2220
for i := 0; i < arguments.size; i++:
@@ -32,25 +30,25 @@ help-command_ path/List arguments/List --invoked-command/string --ui/Ui:
3230
ui.abort "Unknown command: $argument"
3331
unreachable
3432
command = subcommand
35-
path.add command
33+
path += command
3634

37-
emit-help_ path --invoked-command=invoked-command --ui=ui
35+
emit-help_ path --ui=ui
3836

3937
/**
4038
Emits the help for the given command.
4139
4240
The command is identified by the $path where the command is the last element.
4341
*/
44-
emit-help_ path/List --invoked-command/string --ui/Ui:
42+
emit-help_ path/Path --ui/Ui:
4543
ui.emit --kind=Ui.RESULT
4644
--structured=:
47-
build-json-help_ path --invoked-command=invoked-command
45+
build-json-help_ path
4846
--text=:
49-
generator := HelpGenerator path --invoked-command=invoked-command
47+
generator := HelpGenerator path
5048
generator.build-all
5149
generator.to-string
5250

53-
build-json-help_ path/List --invoked-command/string -> Map:
51+
build-json-help_ path/Path -> Map:
5452
// Local block to build json objects for the given command.
5553
// Adds the converted json object to the out-map.
5654
extract-options := : | command/Command out-map/Map |
@@ -88,7 +86,8 @@ build-json-help_ path/List --invoked-command/string -> Map:
8886

8987
return {
9088
"name": command.name,
91-
"path": path.map: | command/Command | command.name,
89+
"path": path.commands.map: | command/Command | command.name,
90+
"invoked-command": path.invoked-command,
9291
"help": command.help_,
9392
"aliases": command.aliases_,
9493
"options": extract-options.call command {:},
@@ -105,14 +104,12 @@ The class also serves as a string builder for the `build_X` methods. The methods
105104
*/
106105
class HelpGenerator:
107106
command_/Command
108-
path_/List
109-
invoked-command_/string
107+
path_/Path
110108
buffer_/List := [] // Buffered string.
111109
// The index in the buffer of the last separator.
112110
last-separator-pos_/int := 0
113111

114-
constructor .path_ --invoked-command/string:
115-
invoked-command_ = invoked-command
112+
constructor .path_:
116113
command_=path_.last
117114

118115
/**
@@ -181,7 +178,7 @@ class HelpGenerator:
181178
// They are sorted by name.
182179
// For the usage line we don't care for the short names of options.
183180
184-
write_ invoked-command_ --indentation=indentation
181+
write_ path_.invoked-command --indentation=indentation
185182
has-more-options := false
186183
for i := 0; i < path_.size; i++:
187184
current-command/Command := path_[i]
@@ -376,7 +373,7 @@ class HelpGenerator:
376373
if i != 0: writeln_
377374
example-and-path := all-examples[i]
378375
example/Example := example-and-path[0]
379-
example-path/List := example-and-path[1]
376+
example-path/Path := example-and-path[1]
380377

381378
build-example_ example --example-path=example-path
382379

@@ -389,22 +386,22 @@ class HelpGenerator:
389386
If $skip-first-level is true, then does not add the examples of this command, but only
390387
those of subcommands.
391388
*/
392-
add-global-examples_ command/Command all-examples/List --path/List --skip-first-level/bool=false -> none:
389+
add-global-examples_ command/Command all-examples/List --path/Path --skip-first-level/bool=false -> none:
393390
if not skip-first-level:
394391
global-examples := (command.examples_.filter: it.global-priority > 0)
395392
all-examples.add-all (global-examples.map: [it, path])
396393

397394
command.subcommands_.do: | subcommand/Command |
398395
if subcommand.is-hidden_: continue.do
399-
add-global-examples_ subcommand all-examples --path=(path + [subcommand])
396+
add-global-examples_ subcommand all-examples --path=(path + subcommand)
400397

401398
/**
402399
Builds a single example.
403400
404401
The $example contains the description and arguments, and the $example-path is
405402
the path to the command that contains the example.
406403
*/
407-
build-example_ example/Example --example-path/List:
404+
build-example_ example/Example --example-path/Path:
408405
description := example.description.trim --right
409406
description-lines := ?
410407
if description.contains "\n": description-lines = description.split "\n"
@@ -423,11 +420,11 @@ class HelpGenerator:
423420
Builds the example command line for the given $arguments-line.
424421
The command that defined the example is identified by the $example-path.
425422
*/
426-
build-example-command-line_ arguments-line/string --example-path/List:
423+
build-example-command-line_ arguments-line/string --example-path/Path:
427424
// Start by constructing a valid command line.
428425
429426
// The prefix consists of the subcommands.
430-
prefix := example-path[1..].map: | command/Command | command.name
427+
prefix := example-path.commands[1..].map: | command/Command | command.name
431428
// Split the arguments line into individual arguments.
432429
// For example, `"foo --bar \"my password\""` is split into `["foo", "--bar", "my password"]`.
433430
example-arguments := split-arguments_ arguments-line
@@ -436,14 +433,14 @@ class HelpGenerator:
436433
// Parse it, to verify that it actually is valid.
437434
// We are also using the result to reorder the options.
438435
parser := Parser_ --invoked-command="root" --for-help-example
439-
invocation-path/List? := null
436+
invocation-path/Path? := null
440437
invocation-parameters/Parameters? := null
441438
exception := catch:
442439
parser.parse example-path.first command-line: | path parameters |
443440
invocation-path = path
444441
invocation-parameters = parameters
445442
if exception:
446-
throw "Error in example '$arguments-line': $exception"
443+
throw "Error in example '$arguments-line': $exception."
447444

448445
// For each command, collect the options that are defined on it and that were
449446
// used in the example.
@@ -528,8 +525,8 @@ class HelpGenerator:
528525
is-root := true
529526
// For examples, we don't want the full path that was used to invoke the
530527
// command (like `build/bin/artemis`), but only the basename.
531-
app-name := basename_ invoked-command_
532-
invocation-path.do: | current-command |
528+
app-name := basename_ example-path.invoked-command
529+
invocation-path.commands.do: | current-command |
533530
if is-root:
534531
is-root = false
535532
full-command.add app-name

src/parser_.toit

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
import .cli
66
import .help-generator_
7+
import .path_
78
import .ui
89
import .utils_
910
import host.pipe
@@ -29,7 +30,7 @@ class Parser_:
2930
3031
The program was called with wrong arguments.
3132
*/
32-
fatal path/List str/string:
33+
fatal path/Path str/string:
3334
if for-help-example_:
3435
throw str
3536

@@ -38,7 +39,7 @@ class Parser_:
3839
// print the usage on stderr, followed by an exit 1.
3940
ui := test-ui_ or (Ui --level=Ui.QUIET-LEVEL --printer=StderrPrinter_)
4041
ui.emit --error str
41-
help-command_ path [] --invoked-command=invoked-command_ --ui=ui
42+
help-command_ path [] --ui=ui
4243
ui.abort
4344
unreachable
4445

@@ -51,7 +52,7 @@ class Parser_:
5152
- The $Parameters that were parsed.
5253
*/
5354
parse root-command/Command arguments/List [block] -> none:
54-
path := []
55+
path := Path root-command --invoked-command=invoked-command_
5556
// Populate the options from the default values or empty lists (for multi-options)
5657
options := {:}
5758

@@ -74,12 +75,13 @@ class Parser_:
7475

7576
return-help := : | arguments/List |
7677
help-command := Command "help" --run=:: | app/Invocation |
77-
help-command_ path arguments --invoked-command=invoked-command_ --ui=app.cli.ui
78-
block.call [help-command] (Parameters.private_ {:} {})
78+
help-command_ path arguments --ui=app.cli.ui
79+
help-path := Path help-command --invoked-command=invoked-command_
80+
block.call help-path (Parameters.private_ {:} {})
7981
return
8082

8183
command/Command? := null
82-
set-command := : | new-command/Command |
84+
set-command := : | new-command/Command add-to-path/bool |
8385
new-command.options_.do: | option/Option |
8486
all-named-options[option.name] = option
8587
if option.short-name: all-short-options[option.short-name] = option
@@ -92,9 +94,9 @@ class Parser_:
9294
options[option.name] = option.default
9395

9496
command = new-command
95-
path.add command
97+
if add-to-path: path += command
9698

97-
set-command.call root-command
99+
set-command.call root-command false
98100

99101
rest := []
100102

@@ -180,7 +182,7 @@ class Parser_:
180182
return-help.call arguments[index..]
181183

182184
fatal path "Unknown command: $argument"
183-
set-command.call subcommand
185+
set-command.call subcommand true
184186

185187
else:
186188
rest.add argument

0 commit comments

Comments
 (0)