Flag default value: where did it come from?

I encountered some awkward ergonomics when migrating DrString to ArgumentParser.

tl;dr

I wish there's a way to to know whether a Flag's value is determined by actual presence in the command line arguments, or by default.

Details

Today, a user could supply the default value for a flag by either omitting it from command arguments, or explicitly supplying it. And as far as I know, there's no way to distinguish these two. The design space for CLI is, therefore, constrained such that we must not care about user's intention when it comes to these default values.

In my app, a user could specify an option in a config file, or they could do it as a command line argument. Crucially, when an option is present in both, the CLI value take precedence so that they can temporarily deviate from linting/formatting option of the codebase. To make this work, I have to use an @Option() flagName Bool? as opposed to @Flag() flagName: Bool to get the information I needed.

But that means the user must say --flag-name true as opposed to simply --flag-name, which is a regression in ergonomics.


I can't think of any obvious way to improve this in ArgumentParser. And, given that I've only worked with it for a few hours, it's entirely possible that I didn't think of/know of an obvious alternative. Thought I'd share here for some help/discussion :)

While I'm here: having used quite a few command line argument parsers from the Python and Swift community in the last 2 decades, I find ArgumentParser to be a delight to learn and use. The feature set is well rounded, API design thoughtful and clever. I'm more excited that ever for Swift's future in the CLI!

2 Likes

Could you make the type Bool?, with a default of nil?

IIUC, the argument you're using is one of Flag type, where user only use the name to indicate presence, and in its absence, use the config file setting.

main --flag-name // true
main // config file

It would be the same as

@Flag(name: .custom(...))
var argFlagName

var flagName { self.argFlagName || self.config.flagName }

which doesn't seem to be parser concern to me.


Another way I could interpret this is as a mix of option/flag,

main --flag-name // true
main --flag-name=true // true
main --flag-name=false // false
main // config file

which I don't have a good answer to.

As I mentioned in my original post, that is the solution I had to go with and it has ergonomics issues because user now must supply true or false in command line.

Correct.

No. The config file value is deliberately identical to the command line flags in my design. This is critical because there are a large number of possible arguments.

I'm still not quite seeing the problem here. A typical Boolean flag is an expression of whether the user specifies it on the command line or not. That is, if it's false, that means the user left it out, and if it's true, that means the user provided it.

Are you looking for a way to let a user state that the value should be true or false by using separate flags? If that's the case, you could look at the flag inversion options, though I don't think you can have those with an optional Bool currently. Another approach could be to define a CaseIterable enum and use an optional version of that as the type of your flag. These are both covered in this section of the docs: https://github.com/apple/swift-argument-parser/blob/master/Documentation/02%20Arguments%2C%20Options%2C%20and%20Flags.md#using-flag-inversions-enumerations-and-counts

I think he wants a UserDefaults.registrationSomain-style back up.

The problem is I have a separate source (config file) for each of my flags. Let's say for a boolean flag "x" defined in ArgumentParser, the default value is false. In the config file, user marks the flag true, but they want to use CLI to override it.

Thinking in terms of code, how do I distinguish, let's say, cli --no-x and just cli? Because in either case, as far as x is concerned, the value is false according to ArgumentParser. But in my design, those two are different intentions from the user.

1 Like

Makes sense! It sounds like we need another @Flag initializer, to let you write this:

struct CLI: ParsableCommand {
    @Flag(inversion: .prefixedNo) var x: Bool?
}

You can do it today like this:

struct CLI: ParsableCommand {
    enum X: String, CaseIterable {
        case x, noX
    }

    @Flag() var _x: X?

    var x: Bool? {
        _x.map { $0 == .x }
    }
}
3 Likes

Ah, interesting. This is what you meant earlier by

Another approach could be to define a CaseIterable enum and use an optional version of that as the type of your flag.

I'll give this a try. Thanks!

Follow up: the workaround Nate mentioned works and meets my needs.

Also, took a stab at the new initializer: Add @Flag initializer for Optional<Bool> by dduan · Pull Request #54 · apple/swift-argument-parser · GitHub

1 Like