Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All notable changes to Commander will be documented in this file.
- Add explicit `Program.resolve(commandLine:)` and `resolve(arguments:)` entry points for generic executables and pre-trimmed argument tails.

### Fixed
- Keep tokens after a bare `--` positional instead of routing them into an unselected `remaining` option.
- Validate every registered command signature and default-subcommand target before resolving any path so malformed inactive commands fail closed.
- Reject duplicate semantic labels within each argument, option, or flag category while preserving multiple aliases declared on one definition.
- Reject required positional arguments declared after optional ones, which cannot be bound unambiguously by position.
Expand Down
17 changes: 2 additions & 15 deletions Sources/Commander/Parser/CommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public struct CommandParser: Sendable {
self.signature = signature.flattened()
}

// swiftlint:disable function_body_length
/// Tokenizes the supplied arguments and groups them into positional
/// values, options, and flags.
///
Expand All @@ -47,8 +46,6 @@ public struct CommandParser: Sendable {
var options: [String: [String]] = [:]
var flags = Set<String>()

let remainingOption = self.signature.options.first(where: { $0.parsing == .remaining })

var index = 0
while index < tokens.count {
let token = tokens[index]
Expand Down Expand Up @@ -79,16 +76,8 @@ public struct CommandParser: Sendable {
case let .argument(value):
positional.append(value)
case .terminator:
if let remainingOption {
let tail = tokens[index...].map(\.rawValue)
index = tokens.endIndex
if !tail.isEmpty {
options[remainingOption.label, default: []].append(contentsOf: tail)
}
} else {
positional.append(contentsOf: tokens[index...].map(\.rawValue))
index = tokens.endIndex
}
positional.append(contentsOf: tokens[index...].map(\.rawValue))
index = tokens.endIndex
}
}

Expand All @@ -97,8 +86,6 @@ public struct CommandParser: Sendable {
return ParsedValues(positional: positional, options: options, flags: flags)
}

// swiftlint:enable function_body_length

private struct ShortTokenContext {
let optionLookup: [CommandNameKey: OptionDefinition]
let flagLookup: [CommandNameKey: String]
Expand Down
14 changes: 13 additions & 1 deletion Tests/CommanderTests/ParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func `parses options flags and arguments`() throws {
"--include",
"a",
"b",
"--",
"--rest",
"tail1",
"tail2",
])
Expand All @@ -35,6 +35,18 @@ func `parses options flags and arguments`() throws {
#expect(values.positional == ["Project"])
}

@Test
func `terminator does not activate an unselected remaining option`() throws {
let signature = CommandSignature(
arguments: [.make(label: "values", isOptional: true, parsing: .remaining)],
options: [.make(label: "rest", names: [.long("rest")], parsing: .remaining)])

let parsed = try CommandParser(signature: signature).parse(arguments: ["--", "tail"])

#expect(parsed.positional == ["tail"])
#expect(parsed.options["rest"] == nil)
}

@Test
func `errors on unknown option`() {
let parser = CommandParser(signature: signature)
Expand Down