diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift index 5cf0d7aa..02c675df 100644 --- a/Sources/ArgumentParser/Parsing/SplitArguments.swift +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -592,7 +592,14 @@ func parseIndividualArg(_ arg: String, at position: Int) throws case 0: return [.value(arg, index: index)] case 1: - // Long option: + // If the remainder is a numeric value (e.g. "-5", "-3.14"), treat it as a value + // to allow passing negative numbers to positional arguments. + // This preserves typical short/long option parsing while enabling + // negative numeric literals to be consumed as values. + if remainder.allSatisfy({ $0.isNumber }) || isDecimalNumber(remainder) { + return [.value(arg, index: index)] + } + // Otherwise, treat as an option (short or long-with-single-dash) let parsed = try ParsedArgument(longArgWithSingleDashRemainder: remainder) // Short options: @@ -636,6 +643,22 @@ func parseIndividualArg(_ arg: String, at position: Int) throws } } +/// Detects a simple decimal number like "123" or "3.14" (no sign). +private func isDecimalNumber(_ s: Substring) -> Bool { + // must contain exactly one dot or none; all other chars are digits + var dotCount = 0 + for ch in s { + if ch == "." { + dotCount += 1 + if dotCount > 1 { return false } + } else if !ch.isNumber { + return false + } + } + // not just a dot + return !s.isEmpty && !(s.count == 1 && s.first == ".") +} + extension SplitArguments { /// Parses the given input into an array of `Element`. /// diff --git a/Tests/ArgumentParserUnitTests/NegativeNumberArgumentTests.swift b/Tests/ArgumentParserUnitTests/NegativeNumberArgumentTests.swift new file mode 100644 index 00000000..248af2fc --- /dev/null +++ b/Tests/ArgumentParserUnitTests/NegativeNumberArgumentTests.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParser + +final class NegativeNumberArgumentTests: XCTestCase { + struct Absolute: ParsableCommand { + @Argument var number: Int + } + + func testParsesNegativeIntegerAsArgument() throws { + let cmd = try Absolute.parse(["-5"]) // should be treated as value, not option + XCTAssertEqual(cmd.number, -5) + } + + struct FloatArg: ParsableCommand { + @Argument var value: Double + } + + func testParsesNegativeDoubleAsArgument() throws { + let cmd = try FloatArg.parse(["-3.14"]) // negative decimal + XCTAssertEqual(cmd.value, -3.14, accuracy: 1e-9) + } +} +