I'm very happy to see the ArgumentParser library; it's much-needed and is going to make coding in Swift much better for everybody. The aggressive use of property wrappers and mirrors to avoid repeating needless boilerplate is perfect for this use case.
That said, I can't get get over some of the design choices on the API surface, primarily naming, which I think could be improved a lot, so I wanted to bring up some of the issues here. Starting with the introductory example,
import ArgumentParser
struct Repeat: ParsableCommand {
The name ParseableCommand
implies that an instance of this thing is a command that can be parsed, but that's not the case at all. Digging in the documentation for a case that actually uses an instance method, I find parseOrExit()
. That makes the type, if anything, a parser, rather than a thing that is parseable. And you can see the harm of thinking of it as a command in the Repeat.main()
, which reads like it's going to… repeat “main.”
Now, in programming there are many uses for components that “parse commands,“ for which this library would be wholly inappropriate: an interpreted shells such as ZSH or a handler for a network protocol such as AMP or IMAP are examples that spring to mind. Also there is a large category of things called “commands” that have no overlap with what this is doing. No, this is a parser for a particular category of command syntax, typically known as a “command line.” Those four characters, Line
cost little and make the domain quite clear. This leads me to the much more straightforward-sounding name CommandLineParser
. And, what do you know, the standard library already has a pseudosubmodule enum
called CommandLine
for facilities related to command lines. So not only is the use of “command line” precedented, but it even suggests a home for some of these components, in extensions to CommandLine
.
Now, I fully appreciate the idea that something is being declared, here, and we might want to emphasize its declarative nature in the naming. Personally I don't think we lose much by saying we're “declaring a command line parser,” but if you don't like that, clearly we're declaring a “command line syntax.” AFAICT there's no sense in which we're declaring a thing that is to be parsed, if we're not “parsing the syntax.”
@Flag(help: "Include a counter with each repetition.")
var includeCounter: Bool
@Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.")
var count: Int?
@Argument(help: "The phrase to repeat.")
var phrase: String
As a first-time reader of this code, the differences between a @Flag
, an @Option
, and an @Argument
weren't obvious to me. These terms are all commonly used interchangeably to mean the same thing: the general category of tokens that go on a command line after the first one, which typically names an executable. Given that this library is ArgumentParser
, is there something special about the @Argument
, such that it gets “parsed” and the others don't? I promise you if I work on a command line tool of any substance, by the graces of this good library I'll spend a very small percentage of my effort actually writing the command-line specification, and by the time I have to come back and add new… options(?)… I'll have forgotten the differences.
It turns out that (I think):
-
@Flag
is a labeled argument with no parameters -
@Option
is an argument pair consisting of a label and a parameter -
@Argument
is a positional argument, with no label.
[I'm using “label” here in a way analogous to Swift's “argument label” distinction, which is followed by a colon; on the command line labels are indicated by a leading dash or pair of dashes.]
Most sources I can find agree that these terms are—and will always be—ill defined, though the one precedent I can find that attempts to draw a distinction calls @Option
a “flag” and @Flag
a “switch.” Surely we can do better just by picking terms that are more explicit, e.g.
-
@Singular
is a labeled argument with no parameters -
@Parameterized
is an argument pair consisting of a label and a parameter -
@Positional
is a positional argument, with no label.
? These of course are not perfect, but at least they give a hint as to the differences. I'm not at all attached to these, have had a few other ideas, and hope that together we can come up with even better ones, so please take them as a starting point rather than a straw man.
func run() throws {
let repeatCount = count ?? .max
for i in 1...repeatCount {
if includeCounter {
print("\(i): \(phrase)")
} else {
print(phrase)
}
}
}
}
Repeat.main()
A few things about this:
- Presumably
run
doesn't need to be labeledthrows
? - I am unconvinced that there's a big win in gaining unqualified access to the parsed members at the point where the command is actually being executed. If the command structure is complicated enough that we are accessing the value of many parsed parameters, the program structure is probably also complicated, and at that point I imagine qualifying access (e.g.
args.repeatCount
,args.includeCounter
) is actually of great readability benefit. If there are only a few accesses to the parameters, qualifying access does little harm. - Asking people to satisfy a requirement (
run()
) and then invoke a wrapper (main
) around something that does a simple check and calls the requirement seems like a frivolous bit of complexity for examples like this, and makes it easy to forget to callmain
. Was something like this considered instead?CommandSyntax.parseAndRun { args in let repeatCount = args.count ?? .max for i in 1...args.repeatCount { if args.includeCounter { print("\(i): \(phrase)") } else { print(phrase) } } }
- I think I understand the motivation for
run
when it comes to complicated systems with multiple subcommands, though I haven't explored alternatives and there may yet be a better approach. - Calling the thing I am supposed to invoke “
main
” seems like an unnecessary reference back to C from a Swift world where users don't writemain()
functions, and it's not particularly descriptive of what's happening. What's the rationale? - I think
run()
is probably what is motivating calling these things “commands” instead of “parsers,” but to me it seems to cause more trouble for the design than the benefit it brings.
Thanks for listening,
Dave