Skip to content

Commit c36d222

Browse files
committed
feat: treat '-<number>' tokens as values to allow negative numeric arguments (Fixes #31)
- Update SplitArguments to classify '-5' and '-3.14' as values, not options - Add unit tests for negative Int and Double positional arguments
1 parent 04695ec commit c36d222

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

Sources/ArgumentParser/Parsing/SplitArguments.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,14 @@ func parseIndividualArg(_ arg: String, at position: Int) throws
592592
case 0:
593593
return [.value(arg, index: index)]
594594
case 1:
595-
// Long option:
595+
// If the remainder is a numeric value (e.g. "-5", "-3.14"), treat it as a value
596+
// to allow passing negative numbers to positional arguments.
597+
// This preserves typical short/long option parsing while enabling
598+
// negative numeric literals to be consumed as values.
599+
if remainder.allSatisfy({ $0.isNumber }) || isDecimalNumber(remainder) {
600+
return [.value(arg, index: index)]
601+
}
602+
// Otherwise, treat as an option (short or long-with-single-dash)
596603
let parsed = try ParsedArgument(longArgWithSingleDashRemainder: remainder)
597604

598605
// Short options:
@@ -636,6 +643,22 @@ func parseIndividualArg(_ arg: String, at position: Int) throws
636643
}
637644
}
638645

646+
/// Detects a simple decimal number like "123" or "3.14" (no sign).
647+
private func isDecimalNumber(_ s: Substring) -> Bool {
648+
// must contain exactly one dot or none; all other chars are digits
649+
var dotCount = 0
650+
for ch in s {
651+
if ch == "." {
652+
dotCount += 1
653+
if dotCount > 1 { return false }
654+
} else if !ch.isNumber {
655+
return false
656+
}
657+
}
658+
// not just a dot
659+
return !s.isEmpty && !(s.count == 1 && s.first == ".")
660+
}
661+
639662
extension SplitArguments {
640663
/// Parses the given input into an array of `Element`.
641664
///
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
import ArgumentParser
14+
15+
final class NegativeNumberArgumentTests: XCTestCase {
16+
struct Absolute: ParsableCommand {
17+
@Argument var number: Int
18+
}
19+
20+
func testParsesNegativeIntegerAsArgument() throws {
21+
let cmd = try Absolute.parse(["-5"]) // should be treated as value, not option
22+
XCTAssertEqual(cmd.number, -5)
23+
}
24+
25+
struct FloatArg: ParsableCommand {
26+
@Argument var value: Double
27+
}
28+
29+
func testParsesNegativeDoubleAsArgument() throws {
30+
let cmd = try FloatArg.parse(["-3.14"]) // negative decimal
31+
XCTAssertEqual(cmd.value, -3.14, accuracy: 1e-9)
32+
}
33+
}
34+

0 commit comments

Comments
 (0)