Skip to content

Commit 8bf9dd9

Browse files
committed
Addressing PR comments
1 parent ec6f11b commit 8bf9dd9

4 files changed

Lines changed: 60 additions & 139 deletions

File tree

Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -333,32 +333,26 @@ The `defaultAsFlag` parameter allows you to create options that can work both as
333333

334334
```swift
335335
struct Example: ParsableCommand {
336-
@Option(defaultAsFlag: "default", help: "Set output format.")
337-
var format: String?
338-
339-
@Option(defaultAsFlag: 8080, help: "Server port.")
340-
var port: Int?
336+
@Option(defaultAsFlag: "json", help: "Set the export format.")
337+
var export: String?
341338

342339
func run() {
343-
print("Format: \(format ?? "none")")
344-
print("Port: \(port ?? 3000)")
340+
print("Export: \(format ?? "<don't export>")")
345341
}
346342
}
347343
```
348344

349345
**Command-line behavior:**
350346
```
351-
% example # format = nil, port = nil
352-
% example --format # format = "default", port = nil
353-
% example --format json # format = "json", port = nil
354-
% example --port # format = nil, port = 8080
355-
% example --port 9000 # format = nil, port = 9000
347+
% example # export = nil
348+
% example --export # export = "json"
349+
% example --format yaml # format = "yaml"
356350
```
357351

358352
The `defaultAsFlag` parameter creates a hybrid that supports both patterns:
359-
- **Flag behavior**: `--format` (sets format to "default")
360-
- **Option behavior**: `--format json` (sets format to "json")
361-
- **No usage**: format remains `nil`
353+
- **Flag behavior**: `--export` (sets format to "json")
354+
- **Option behavior**: `--export yaml` (sets format to "yaml")
355+
- **No usage**: `export` remains `nil`
362356

363357
#### Type requirements
364358

Sources/ArgumentParser/Parsable Properties/Option.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ extension Option {
504504
}
505505

506506
/// Creates an optional property that reads its value from a labeled option,
507-
/// with a default value when the flag is provided without an argument.
507+
/// with a default value when the flag is provided without a value.
508508
///
509509
/// This initializer allows providing a `defaultAsFlag` value that is used
510510
/// when the flag is present but no value follows it:
@@ -518,7 +518,7 @@ extension Option {
518518
/// - wrappedValue: A default value to use for this property, provided
519519
/// implicitly by the compiler during property wrapper initialization.
520520
/// - name: A specification for what names are allowed for this option.
521-
/// - defaultAsFlag: The value to use when the flag is provided without an argument.
521+
/// - defaultAsFlag: The value to use when the flag is provided without a value.
522522
/// - parsingStrategy: The behavior to use when looking for this option's value.
523523
/// - help: Information about how to use this option.
524524
/// - completion: The type of command-line completion provided for this option.
@@ -646,7 +646,7 @@ extension Option {
646646
}
647647

648648
/// Creates an optional property that reads its value from a labeled option,
649-
/// with a default value when the flag is provided without an argument.
649+
/// with a default value when the flag is provided without a value.
650650
///
651651
/// This initializer allows providing a `defaultAsFlag` value that is used
652652
/// when the flag is present but no value follows it:
@@ -658,7 +658,7 @@ extension Option {
658658
///
659659
/// - Parameters:
660660
/// - name: A specification for what names are allowed for this option.
661-
/// - defaultAsFlag: The value to use when the flag is provided without an argument.
661+
/// - defaultAsFlag: The value to use when the flag is provided without a value.
662662
/// - parsingStrategy: The behavior to use when looking for this option's value.
663663
/// - help: Information about how to use this option.
664664
/// - completion: The type of command-line completion provided for this option.
@@ -759,7 +759,7 @@ extension Option {
759759

760760
/// Creates an optional property that reads its value from a labeled option,
761761
/// parsing with the given closure, with a default value when the flag is
762-
/// provided without an argument.
762+
/// provided without a value.
763763
///
764764
/// This initializer allows providing a `defaultAsFlag` value that is used
765765
/// when the flag is present but no value follows it:
@@ -773,7 +773,7 @@ extension Option {
773773
/// - wrappedValue: A default value to use for this property, provided
774774
/// implicitly by the compiler during property wrapper initialization.
775775
/// - name: A specification for what names are allowed for this option.
776-
/// - defaultAsFlag: The value to use when the flag is provided without an argument.
776+
/// - defaultAsFlag: The value to use when the flag is provided without a value.
777777
/// - parsingStrategy: The behavior to use when looking for this option's value.
778778
/// - help: Information about how to use this option.
779779
/// - completion: The type of command-line completion provided for this option.
@@ -907,7 +907,7 @@ extension Option {
907907

908908
/// Creates an optional property that reads its value from a labeled option,
909909
/// parsing with the given closure, with a default value when the flag is
910-
/// provided without an argument.
910+
/// provided without a value.
911911
///
912912
/// This initializer allows providing a `defaultAsFlag` value that is used
913913
/// when the flag is present but no value follows it:
@@ -919,7 +919,7 @@ extension Option {
919919
///
920920
/// - Parameters:
921921
/// - name: A specification for what names are allowed for this option.
922-
/// - defaultAsFlag: The value to use when the flag is provided without an argument.
922+
/// - defaultAsFlag: The value to use when the flag is provided without a value.
923923
/// - parsingStrategy: The behavior to use when looking for this option's value.
924924
/// - help: Information about how to use this option.
925925
/// - completion: The type of command-line completion provided for this option.

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 43 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,25 @@ struct LenientParser {
290290
}
291291
}
292292

293+
/// Helper function to check if there's a terminator between the current option and target origin
294+
private func hasTerminatorBetween(
295+
_ originElement: InputOrigin.Element,
296+
_ targetOrigin: InputOrigin.Element
297+
) -> Bool {
298+
guard case .argumentIndex(let currentIndex) = originElement,
299+
case .argumentIndex(let targetIndex) = targetOrigin
300+
else { return false }
301+
302+
// Check if there's a terminator between current position and target position
303+
let terminatorIndex = inputArguments.elements.firstIndex { element in
304+
element.isTerminator
305+
&& element.index.inputIndex > currentIndex.inputIndex
306+
&& element.index.inputIndex < targetIndex.inputIndex
307+
}
308+
309+
return terminatorIndex != nil
310+
}
311+
293312
mutating func parseValue(
294313
_ argument: ArgumentDefinition,
295314
_ parsed: ParsedArgument,
@@ -315,10 +334,11 @@ struct LenientParser {
315334
try update(origins, parsed.name, String(value), &result)
316335
usedOrigins.formUnion(origins)
317336
} else if let (origin2, value) = inputArguments.popNextElementIfValue(
318-
after: originElement)
337+
after: originElement),
338+
!hasTerminatorBetween(originElement, origin2)
319339
{
320340
// Use `popNextElementIfValue(after:)` to handle cases where short option
321-
// labels are combined
341+
// labels are combined, but only if there's no terminator between them
322342
let origins = origin.inserting(origin2)
323343
try update(origins, parsed.name, value, &result)
324344
usedOrigins.formUnion(origins)
@@ -341,10 +361,11 @@ struct LenientParser {
341361
try update(origins, parsed.name, String(value), &result)
342362
usedOrigins.formUnion(origins)
343363
} else if let (origin2, value) = inputArguments.popNextValue(
344-
after: originElement)
364+
after: originElement),
365+
!hasTerminatorBetween(originElement, origin2)
345366
{
346367
// Use `popNext(after:)` to handle cases where short option
347-
// labels are combined
368+
// labels are combined, but only if there's no terminator between them
348369
let origins = origin.inserting(origin2)
349370
try update(origins, parsed.name, value, &result)
350371
usedOrigins.formUnion(origins)
@@ -366,16 +387,16 @@ struct LenientParser {
366387
let origins = origin.inserting(origin2)
367388
try update(origins, parsed.name, String(value), &result)
368389
usedOrigins.formUnion(origins)
369-
} else {
370-
guard
371-
let (origin2, value) = inputArguments.popNextElementAsValue(
372-
after: originElement)
373-
else {
374-
throw errorForMissingValue(originElement, parsed)
375-
}
390+
} else if let (origin2, value) = inputArguments.popNextElementAsValue(
391+
after: originElement),
392+
!hasTerminatorBetween(originElement, origin2)
393+
{
394+
// Only consume if there's no terminator between option and value
376395
let origins = origin.inserting(origin2)
377396
try update(origins, parsed.name, value, &result)
378397
usedOrigins.formUnion(origins)
398+
} else {
399+
throw errorForMissingValue(originElement, parsed)
379400
}
380401

381402
case .allRemainingInput:
@@ -468,112 +489,20 @@ struct LenientParser {
468489
_ result: inout ParsedValues,
469490
_ usedOrigins: inout InputOrigin
470491
) throws {
471-
let origin = InputOrigin(elements: [originElement])
472-
473-
// Helper function to check if there's a terminator between the current option and potential values
474-
func hasTerminatorBefore(_ targetOrigin: InputOrigin.Element) -> Bool {
475-
guard case .argumentIndex(let currentIndex) = originElement,
476-
case .argumentIndex(let targetIndex) = targetOrigin
477-
else { return false }
478-
479-
// Check if there's a terminator between current position and target position
480-
let terminatorIndex = inputArguments.elements.firstIndex { element in
481-
element.isTerminator
482-
&& element.index.inputIndex > currentIndex.inputIndex
483-
&& element.index.inputIndex < targetIndex.inputIndex
484-
}
485-
486-
return terminatorIndex != nil
487-
}
488-
489-
// Try to find a value using the same logic as parseValue, but don't throw if missing
490-
switch argument.parsingStrategy {
491-
case .default:
492-
// Try to get a value for this option
493-
if let value = parsed.value {
494-
// This was `--foo=bar` style:
495-
try unaryHandler(origin, parsed.name, value, &result)
496-
usedOrigins.formUnion(origin)
497-
} else if argument.allowsJoinedValue,
498-
let (origin2, value) = inputArguments.extractJoinedElement(
499-
at: originElement)
500-
{
501-
// Found a joined argument
502-
let origins = origin.inserting(origin2)
503-
try unaryHandler(origins, parsed.name, String(value), &result)
504-
usedOrigins.formUnion(origins)
505-
} else if let (origin2, value) = inputArguments.popNextElementIfValue(
506-
after: originElement),
507-
!hasTerminatorBefore(origin2)
508-
{
509-
// Use `popNextElementIfValue(after:)` to handle cases where short option
510-
// labels are combined - only consume if it's actually a value, not another flag
511-
// and there's no terminator between the option and the value
512-
let origins = origin.inserting(origin2)
513-
try unaryHandler(origins, parsed.name, value, &result)
514-
usedOrigins.formUnion(origins)
515-
} else {
516-
// No value found or terminator blocks access - use as flag
517-
try nullaryHandler(origin, parsed.name, &result)
518-
usedOrigins.formUnion(origin)
519-
}
520-
521-
case .scanningForValue:
522-
// Similar to default, but more aggressive about finding values
523-
if let value = parsed.value {
524-
try unaryHandler(origin, parsed.name, value, &result)
525-
usedOrigins.formUnion(origin)
526-
} else if argument.allowsJoinedValue,
527-
let (origin2, value) = inputArguments.extractJoinedElement(
528-
at: originElement)
529-
{
530-
let origins = origin.inserting(origin2)
531-
try unaryHandler(origins, parsed.name, String(value), &result)
532-
usedOrigins.formUnion(origins)
533-
} else if let (origin2, value) = inputArguments.popNextValue(
534-
after: originElement),
535-
!hasTerminatorBefore(origin2)
536-
{
537-
// Only consume if there's no terminator between option and value
538-
let origins = origin.inserting(origin2)
539-
try unaryHandler(origins, parsed.name, value, &result)
540-
usedOrigins.formUnion(origins)
541-
} else {
542-
// No value found or terminator blocks access - use as flag
543-
try nullaryHandler(origin, parsed.name, &result)
544-
usedOrigins.formUnion(origin)
545-
}
546-
547-
case .unconditional:
548-
// Use an attached value if it exists, otherwise try to consume next element
549-
if let value = parsed.value {
550-
try unaryHandler(origin, parsed.name, value, &result)
551-
usedOrigins.formUnion(origin)
552-
} else if argument.allowsJoinedValue,
553-
let (origin2, value) = inputArguments.extractJoinedElement(
554-
at: originElement)
555-
{
556-
let origins = origin.inserting(origin2)
557-
try unaryHandler(origins, parsed.name, String(value), &result)
558-
usedOrigins.formUnion(origins)
559-
} else if let (origin2, value) = inputArguments.popNextElementAsValue(
560-
after: originElement),
561-
!hasTerminatorBefore(origin2)
562-
{
563-
// Only consume if there's no terminator between option and value
564-
let origins = origin.inserting(origin2)
565-
try unaryHandler(origins, parsed.name, value, &result)
566-
usedOrigins.formUnion(origins)
567-
} else {
568-
// No value found or terminator blocks access - use as flag
492+
do {
493+
// Try to parse as a unary value first using the main parseValue logic
494+
try parseValue(argument, parsed, originElement, unaryHandler, &result, &usedOrigins)
495+
} catch let error as ParserError {
496+
switch error {
497+
case .missingValueForOption, .missingValueOrUnknownCompositeOption:
498+
// Fall back to flag behavior when no value is available
499+
let origin = InputOrigin(elements: [originElement])
569500
try nullaryHandler(origin, parsed.name, &result)
570501
usedOrigins.formUnion(origin)
502+
default:
503+
// Re-throw other parser errors
504+
throw error
571505
}
572-
573-
case .upToNextOption, .allRemainingInput, .postTerminator, .allUnrecognized:
574-
// For other parsing strategies, fall back to flag behavior for now
575-
try nullaryHandler(origin, parsed.name, &result)
576-
usedOrigins.formUnion(origin)
577506
}
578507
}
579508

Sources/ArgumentParser/Usage/HelpGenerator.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,6 @@ internal struct HelpGenerator {
232232
allAndDefaultValues =
233233
"(values: \(allValueStrings.joined(separator: ", ")))"
234234
case (false, true):
235-
// Check if this is a defaultAsFlag option (uses .optionalUnary update)
236235
switch arg.update {
237236
case .nullary, .unary:
238237
allAndDefaultValues = "(default: \(defaultValue))"
@@ -241,7 +240,6 @@ internal struct HelpGenerator {
241240
}
242241

243242
case (true, true):
244-
// Check if this is a defaultAsFlag option for the combined case too
245243
switch arg.update {
246244
case .nullary, .unary:
247245
allAndDefaultValues =

0 commit comments

Comments
 (0)