It appears that options declared in a command with subcommands can cause issues.
Summary
If I got this right, the run()
method of an instance of a ParsableCommand is never executed if the ParsableCommand has a subcommand.
If a ParsableCommand has an option and a subcommand, help and --help fail.
If a ParsableCommand and one of its subcommands have options with the same label (I don' know the proper term in SAP for, say, "--foo" in "--foo FOO"), you cannot even run the program without an error message. And the error message has usage that suggests exactly what caused the error in the first place.
All three are confusing, and the last two reflect either bugs or undefined behavior.
Examples
There are four examples, all of which refer to the same subcommand.
Subcommand
The subcommand never changes.
import ArgumentParser
struct Subcommand: ParsableCommand {
static let configuration: CommandConfiguration = .init(commandName: "sub-level")
@Option var foo: String
@Option var bar: String
public func run() {
print("SubcommandInstance.foo = \(foo)")
print("SubcommandInstance.bar = \(bar)")
}
}
Example A
Example A shows that, apparently, the run method in a top-level command is never executed:
struct Subcommand: ParsableCommand {
static let configuration: CommandConfiguration = .init(commandName: "sub-level")
@Option var foo: String
@Option var bar: String
public func run() {
print("SubcommandInstance.foo = \(foo)")
print("SubcommandInstance.bar = \(bar)")
}
}
> ./top-level sub-level --foo FOO --bar BAR
SubcommandInstance.foo = FOO
SubcommandInstance.bar = BAR
Example B
Example B shows that adding an option to the top-level messes up help, even if the option's label has nothing in common with the subcommand option names.
@main
struct TopLevelCommand_B: ParsableCommand {
@Option var baz: String
static var configuration: CommandConfiguration {
.init(commandName: "top-b", subcommands: [Subcommand.self])
}
func run() {
print("TopLevelCommand_B.baz = \(baz)")
}
}
Accordng to > ./top-level --help
, this should work, but it generates an error message.
> ./top-level help sub-level
Error: Missing expected argument '--baz <baz>'
Help: --baz <baz>
Usage: top-b --baz <baz> <subcommand>
See 'top-b --help' for more information.
This generates help for the top-level instead of the expected help for sub-level.
> ./top-level sub-level --help
USAGE: top-b --baz <baz> <subcommand>
OPTIONS:
--baz <baz>
-h, --help Show help information.
SUBCOMMANDS:
sub-level
See 'top-b help <subcommand>' for detailed help.
Example C
Example C shows what happens if baz is changed to bar at the top-level.
@main
struct TopLevelCommand_C: ParsableCommand {
@Option var bar: String
static var configuration: CommandConfiguration {
.init(commandName: "top-c", subcommands: [Subcommand.self])
}
func run() {
print("TopLevelCommand_B.baz = \(bar)")
}
}
The help issues are the same as in Example B. But now you cannot even run the program without causing an error.
> ./top-level --bar BAR1 sub-level --foo FOO --bar BAR
Error: Missing expected argument '--bar <bar>'
Help: --bar <bar>
Usage: top-c sub-level --foo <foo> --bar <bar>
See 'top-c sub-level --help' for more information.
And if you follow the suggested usage, you are in a "usage loop".
> > ./top-level sub-level --foo FOO --bar BAR
Error: Missing expected argument '--bar <bar>'
Help: --bar <bar>
Usage: top-c sub-level --foo <foo> --bar <bar>
See 'top-c sub-level --help' for more information.
Example D
Example D shows that you still cannot run the program even if bar
has a default value.
@main
struct TopLevelCommand_D: ParsableCommand {
@Option var bar: String = "barDefaultValue"
static var configuration: CommandConfiguration {
.init(commandName: "top-d", subcommands: [Subcommand.self])
}
func run() {
print("TopLevelCommand_B.baz = \(bar)")
}
}
You still get the "usage loop":
./top-level sub-level --foo FOO --bar BAR
Error: Missing expected argument '--bar <bar>'
Help: --bar <bar>
Usage: top-d sub-level --foo <foo> --bar <bar>
See 'top-d sub-level --help' for more information.
And this also does not work:
> ./top-level --bar BAR1 sub-level --foo FOO --bar BAR
> ./top-level --bar BAR1 sub-level --foo FOO --bar BAR
Error: Missing expected argument '--bar <bar>'
Help: --bar <bar>
Usage: top-d sub-level --foo <foo> --bar <bar>
See 'top-d sub-level --help' for more information.
I see --bar
twice, it is definitely not missing.
By the way, contrary to Example C, help works fine in example D.
Suggestion
Either there are a lot of bugs here, or it should be documented that declaring options in a ParsableCommand with a subcommand leads to undefined behaviour.
I would suggest the latter, since such options are, apparently (in the current version of SAP), useless.
Akin to the fatal (i.e., programmer) error that shows up in case of option label conflicts, I would suggest that a fatal error be generated if options (or a run() method) is detected in a command with a subcommand.