Skip to content

Incorrect error message for .allRemainingInput options without values #750

Open

Description

@james-fletcher-01

Issue #434 reported that when a label, but no values, are provided for an array @Option property, the error message incorrectly signifies a missing option instead of a missing value. PR #435 fixed this for .upToNextOption arrays, but the bug remained for .allRemainingInput arrays.

Example

@main
struct Example: ParsableCommand {
  @Option(parsing: .remaining)
  var test: [String]

  mutating func run() throws {
    print(test)
  }
}

Expected Behaviour

$ example --test
Error: Missing value for '--test <test>'

Actual Behaviour

$ example --test
Error: Missing expected argument '--test <test> ...'

Unexpected Side-Effect

I created a commit that fixes this issue for .allRemainingInput arrays at james-fletcher-01/swift-argument-parser@1bf93bf. However, this change breaks the following test:

func testParsing_repeatingStringRemaining_2() {
  AssertParse(Baz.self, ["--names"]) { baz in
    XCTAssertFalse(baz.verbose)
    XCTAssertTrue(baz.names.isEmpty)
  }
}

This implies that for options with a default value, the expected behavior is for an option label without a value to essentially be ignored and retain the default value. For example, while the above test uses an empty array as the default value, this works as well:

struct NonEmptyDefaultValue: ParsableArguments {
  @Option(parsing: .remaining)
  var names = ["default"]
}

func testNonEmptyDefaultValue() {
  AssertParse(NonEmptyDefaultValue.self, ["--names"]) { baz in
    XCTAssertEqual(baz.names, ["default"])
  }
}

It's surprising to me that this would be the expected behaviour.

  • No other parsing strategy accepts an option label without a value. This includes single values strategies, as well as .upToNextOption.
  • In the example where the default is an empty array, it kind of makes sense. But the example I provided above seems especially counter-intuitive, since you explicitly provided an "empty option" but ended up with a non-empty value.

So if this really is expected behaviour, I can modify my changes to detect the presence of a default value and supress the error message before I submit a PR. But I would just like to get others' thoughts on this?

ArgumentParser version: main
Swift version: swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)

Checklist

  • If possible, I've reproduced the issue using the main branch of this package
  • I've searched for existing GitHub issues

Activity

  1. natecook1000 commented on Apr 4, 2025

    @natecook1000
    Member

    Thanks for filing this issue, @james-fletcher-01! So the question is what the behavior should be for .remaining options that have a default value, as right now it's permitted to provide the option name (e.g. --names) without any following values, but only when there's a default value for the option.

    I absolutely agree that the current behavior is incorrect. It would be tenable for that input to always parse as an empty array (i.e. the "remaining" input is zero items long), but falling back to the default in this case doesn't make sense. Our options are:

    1. Leave it as is, in honor of not breaking clients that are unwittingly depending on this behavior
    2. Fix it in the direction of always parsing an empty array (this leaves open the question of why not allowing this for non-defaulted options)
    3. Fix it in the direction of erroring like the non-defaulted option

    What do you think? I need to consider the source-compatibility aspect for a bit.

  2. james-fletcher-01 commented on Apr 6, 2025

    @james-fletcher-01
    Author

    Hi @natecook1000,

    I have given this significant thought over the last couple of days, and I have come to the conclusion that, if the behaviour were to be changed, turning it into an error is the better solution.

    I did almost come around to the idea of normalising it as an empty array. While it’s inconsistent with all other parsing strategies, the .remaining strategy is already quite a special case, and having an option for “clearing the default value” did seem like a reasonable design.

    However, when I came back to it from the perspective of a CLI user rather than a programmer, I realised that I can’t think of any precedent for such behaviour. I think trying to convey to the user that there is a default value that can be cleared in this way could be confusing, and for applications that need such behaviour, it would be better to make it explicit (eg. with a --no-files option).

    With that said, I understand that there are source compatibility issues regarding whether this behaviour can be changed at all, so I will leave it with you to decide what’s best!

  3. alphasafal commented on Oct 17, 2026

    @alphasafal

    Hey @natecook1000, I looked into this one and wanted to add some detail since it's been quiet for a while.

    The .upToNextOption case already has the check from #435 (the guard let first = inputArguments.elements.first, first.isValue block in ArgumentSet.swift). The .allRemainingInput case right above it doesn't. So when nothing follows --names, the consume loop never runs and the value is just whatever argument.initial(origin, &result) set, which is the default. That's where the "label with no values keeps the default" behaviour comes from. It doesn't look like it was ever intended.

    I'd also lean towards the error, same as @james-fletcher-01. It matches every other strategy. And if a tool really needs a way to clear the default, an explicit flag is clearer for users.

    On source compat, the case I can see breaking is scripts that build args like tool --names $FILES where the variable can be empty. Today that quietly falls back to the default; after the change it would error. Probably rare, but worth a line in the CHANGELOG.

    If you're ok with the error direction, I'm happy to put up a PR: add the same guard as .upToNextOption, update parsing_repeatingStringRemaining_2 to expect the error, and add a test for a non-empty default. @james-fletcher-01, if you'd still like to send yours since you already have the commit, totally fine, I don't want to step on that.

    Edit: one data point on compat. I checked apple/container (main as of Oct 16), which depends on this package. Its only .remaining use is an @Argument in MachineSet.swift, so neither the empty-array nor the error option would affect it.

  4. james-fletcher-01 commented on Oct 18, 2026

    @james-fletcher-01
    Author

    Hi @alphasafal,

    Thanks for taking a look at this. I had completely forgotten about it.

    I have rebased my branch, as it was quite out of date. I have also updated the tests I introduced, so they now use Swift Testing. The updated diff is here.

    In my original commit, I had already included a test in ErrorMessageTests.swift to validate the error message if no value is provided. So that just leaves the question, if we were to go ahead with this, what should happen to parsing_repeatingStringRemaining_2. It really does seem superfluous, so perhaps it should just be removed, and the subsequent tests should be renumbered?

  5. alphasafal commented on Nov 2, 2026

    @alphasafal

    Sorry for the slow reply. I'd keep parsing_repeatingStringRemaining_2 and flip it to expect the error, the same way the .upToNextOption tests check Qux.parse(["--names", "--other"]):

    @Test func parsing_repeatingStringRemaining_2() {
      #expect(throws: (any Error).self) { try Baz.parse(["--names"]) }
    }

    That keeps end-to-end coverage for the bare label next to the other .remaining cases and avoids renumbering the rest. Your branch already has the error-message test, so please go ahead and open the PR from it. Happy to review.

  6. james-fletcher-01 commented on Nov 9, 2026

    @james-fletcher-01
    Author

    Hi @alphasafal,

    Thanks for your input. I have updated the parsing_repeatingStringRemaining_2 test to expect the error as suggested, and submitted PR #1003.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions