From f8eb0cc694288f3df88b71be49a48593e811b263 Mon Sep 17 00:00:00 2001 From: Bassam Khouri Date: Thu, 20 Nov 2025 21:28:03 -0500 Subject: [PATCH] Augment Option to support default as flag option In some use cases, there is a need to have an option argument behave like a flag. This change introduced 4 new intialiazers to `Option` that accept a `defaultAsFlag` value. With the following usage: ```swift struct Example: ParsableCommand { @Option(defaultAsFlag: "default", help: "Set output format.") var format: String? func run() { print("Format: \(format ?? "none")") } } ``` ` The `defaultAsFlag` parameter creates a hybrid that supports both patterns: - **Flag behavior**: `--format` (sets format to "default") - **Option behavior**: `--format json` (sets format to "json") - **No usage**: format remains `nil` As a user of the command line tool, the `--help` output clearly distinguishes between the the hybrid and regular usages. ``` OPTIONS: --format [] Set output format. (default as flag: json) ```` Note the `(default as flag: ...)` text instead of regular `(default: ...)`, and the optional value syntax `[]` instead of required ``. Fixes: #829 --- Examples/default-as-flag/DefaultAsFlag.swift | 54 ++++ Package.swift | 5 + Package@swift-5.8.swift | 5 + README.md | 9 +- .../Articles/DeclaringArguments.md | 87 +++++- .../Parsable Properties/Option.swift | 271 +++++++++++++++++- .../Parsing/ArgumentDefinition.swift | 20 +- .../ArgumentParser/Parsing/ArgumentSet.swift | 125 +++++++- .../Usage/DumpHelpGenerator.swift | 2 + .../ArgumentParser/Usage/HelpGenerator.swift | 20 +- .../ArgumentParser/Usage/UsageGenerator.swift | 4 + .../DefaultAsFlagEndToEndTests.swift | 216 ++++++++++++++ .../GenerateManualTests.swift | 9 + .../testDefaultAsFlagMultiPageManual().mdoc | 77 +++++ .../testDefaultAsFlagSinglePageManual().mdoc | 53 ++++ .../DefaultAsFlagCompletionTests.swift | 73 +++++ .../DefaultAsFlagDumpHelpTests.swift | 69 +++++ ...enerationTests+AtOptionDefaultAsFlag.swift | 123 ++++++++ .../testDefaultAsFlagCompletion_Bash().bash | 211 ++++++++++++++ .../testDefaultAsFlagCompletion_Fish().fish | 74 +++++ .../testDefaultAsFlagCompletion_Zsh().zsh | 85 ++++++ .../testDefaultAsFlagDumpHelp().json | 144 ++++++++++ ...tDefaultAsFlagWithTransformDumpHelp().json | 135 +++++++++ 23 files changed, 1857 insertions(+), 14 deletions(-) create mode 100644 Examples/default-as-flag/DefaultAsFlag.swift create mode 100644 Tests/ArgumentParserEndToEndTests/DefaultAsFlagEndToEndTests.swift create mode 100644 Tests/ArgumentParserGenerateManualTests/Snapshots/testDefaultAsFlagMultiPageManual().mdoc create mode 100644 Tests/ArgumentParserGenerateManualTests/Snapshots/testDefaultAsFlagSinglePageManual().mdoc create mode 100644 Tests/ArgumentParserUnitTests/DefaultAsFlagCompletionTests.swift create mode 100644 Tests/ArgumentParserUnitTests/DefaultAsFlagDumpHelpTests.swift create mode 100644 Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOptionDefaultAsFlag.swift create mode 100644 Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Bash().bash create mode 100644 Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Fish().fish create mode 100644 Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Zsh().zsh create mode 100644 Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagDumpHelp().json create mode 100644 Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagWithTransformDumpHelp().json diff --git a/Examples/default-as-flag/DefaultAsFlag.swift b/Examples/default-as-flag/DefaultAsFlag.swift new file mode 100644 index 000000000..90d8e9bd6 --- /dev/null +++ b/Examples/default-as-flag/DefaultAsFlag.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// 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 ArgumentParser + +@main +struct DefaultAsFlag: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "A utility demonstrating defaultAsFlag options.", + discussion: """ + This command shows how defaultAsFlag options can work both as flags + and as options with values. + """ + ) + + @Option(defaultAsFlag: "default", help: "A string option with defaultAsFlag.") + var stringFlag: String? + + @Option(defaultAsFlag: 42, help: "An integer option with defaultAsFlag.") + var numberFlag: Int? + + @Option(defaultAsFlag: true, help: "A boolean option with defaultAsFlag.") + var boolFlag: Bool? + + @Option( + defaultAsFlag: "transformed", + help: "A string option with transform and defaultAsFlag.", + transform: { $0.uppercased() } + ) + var transformFlag: String? + + @Option(name: .shortAndLong, help: "A regular option for comparison.") + var regular: String? + + @Argument + var additionalArgs: [String] = [] + + func run() { + print("String flag: \(stringFlag?.description ?? "nil")") + print("Number flag: \(numberFlag?.description ?? "nil")") + print("Bool flag: \(boolFlag?.description ?? "nil")") + print("Transform flag: \(transformFlag?.description ?? "nil")") + print("Regular option: \(regular?.description ?? "nil")") + print("Additional args: \(additionalArgs)") + } +} diff --git a/Package.swift b/Package.swift index a27eaa221..4c9b8f9d3 100644 --- a/Package.swift +++ b/Package.swift @@ -78,6 +78,11 @@ var package = Package( name: "color", dependencies: ["ArgumentParser"], path: "Examples/color"), + .executableTarget( + name: "default-as-flag", + dependencies: ["ArgumentParser"], + path: "Examples/default-as-flag" + ), // Tools .executableTarget( diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift index fb860aff1..99ef2b8ad 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.8.swift @@ -79,6 +79,11 @@ var package = Package( name: "color", dependencies: ["ArgumentParser"], path: "Examples/color"), + .executableTarget( + name: "default-as-flag", + dependencies: ["ArgumentParser"], + path: "Examples/default-as-flag" + ), // Tools .executableTarget( diff --git a/README.md b/README.md index b3ead232f..d04e6e897 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ that you need to collect from the command line. Decorate each stored property with one of `ArgumentParser`'s property wrappers, and then declare conformance to `ParsableCommand` and add the `@main` attribute. (Note, for `async` renditions of `run`, conform to `AsyncParsableCommand` rather -than `ParsableCommand`.) +than `ParsableCommand`.) Finally, implement your command's logic in the `run()` method. ```swift @@ -70,7 +70,7 @@ OPTIONS: ## Documentation -For guides, articles, and API documentation see the +For guides, articles, and API documentation see the [library's documentation on the Web][docs] or in Xcode. - [ArgumentParser documentation][docs] @@ -88,6 +88,7 @@ This repository includes a few examples of using the library: - [`roll`](Examples/roll/main.swift) is a simple utility implemented as a straight-line script. - [`math`](Examples/math/Math.swift) is an annotated example of using nested commands and subcommands. - [`count-lines`](Examples/count-lines/CountLines.swift) uses `async`/`await` code in its implementation. +- [`default-as-flag`](Examples/default-as-flag/DefaultAsFlag.swift) demonstrates hybrid options that can work both as flags and as options with values. You can also see examples of `ArgumentParser` adoption among Swift project tools: @@ -104,7 +105,7 @@ The public API of version 1.0.0 of the `swift-argument-parser` package consists of non-underscored declarations that are marked public in the `ArgumentParser` module. Interfaces that aren't part of the public API may continue to change in any release, including the exact wording and formatting of the autogenerated help and error messages, -as well as the package’s examples, tests, utilities, and documentation. +as well as the package’s examples, tests, utilities, and documentation. Future minor versions of the package may introduce changes to these rules as needed. @@ -115,7 +116,7 @@ Requiring a new Swift release will only require a minor version bump. ## Adding `ArgumentParser` as a Dependency -To use the `ArgumentParser` library in a SwiftPM project, +To use the `ArgumentParser` library in a SwiftPM project, add it to the dependencies for your package and your command-line executable target: ```swift diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md b/Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md index 5ce6d8e0f..ba175d408 100644 --- a/Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md @@ -81,7 +81,7 @@ struct Lucky: ParsableCommand { ``` ``` -% lucky +% lucky Your lucky numbers are: 7 14 21 % lucky 1 2 3 @@ -327,6 +327,89 @@ If a default is not specified, the user must provide a value for that argument/o You must also always specify a default of `false` for a non-optional `Bool` flag, as in the example above. This makes the behavior consistent with both normal Swift properties (which either must be explicitly initialized or optional to initialize a `struct`/`class` containing them) and the other property types. +### Creating hybrid flag/option behavior with defaultAsFlag + +The `defaultAsFlag` parameter allows you to create options that can work both as flags (without values) and as options (with values). This provides flexible command-line interfaces where users can choose between concise flag usage or explicit value specification. + +```swift +struct Example: ParsableCommand { + @Option(defaultAsFlag: "default", help: "Set output format.") + var format: String? + + @Option(defaultAsFlag: 8080, help: "Server port.") + var port: Int? + + func run() { + print("Format: \(format ?? "none")") + print("Port: \(port ?? 3000)") + } +} +``` + +**Command-line behavior:** +``` +% example # format = nil, port = nil +% example --format # format = "default", port = nil +% example --format json # format = "json", port = nil +% example --port # format = nil, port = 8080 +% example --port 9000 # format = nil, port = 9000 +``` + +The `defaultAsFlag` parameter creates a hybrid that supports both patterns: +- **Flag behavior**: `--format` (sets format to "default") +- **Option behavior**: `--format json` (sets format to "json") +- **No usage**: format remains `nil` + +#### Type requirements + +- The property **must** be optional (`T?`) +- The `defaultAsFlag` value must be of the unwrapped type (`T`) +- All standard `ExpressibleByArgument` types are supported (String, Int, Bool, Double, etc.) + +#### Advanced usage + +You can combine `defaultAsFlag` with transform functions: + +```swift +@Option( + defaultAsFlag: "info", + help: "Set log level.", + transform: { $0.uppercased() } +) +var logLevel: String? +``` + +**Behavior:** +``` +% app --log-level # logLevel = "INFO" (transformed default) +% app --log-level debug # logLevel = "DEBUG" (transformed input) +``` + +#### Help display + +DefaultAsFlag options show special help formatting to distinguish them from regular defaults: + +``` +OPTIONS: + --format [] Set output format. (default as flag: json) + --port [] Server port. (default as flag: 8080) +``` + +Note the `(default as flag: ...)` text instead of regular `(default: ...)`, and the optional value syntax `[]` instead of required ``. + +#### Value detection + +The parser determines whether a value follows the option: + +1. **Next argument is a value** if it doesn't start with `-` and isn't another known option +2. **No value available**: Use the `defaultAsFlag` value +3. **Explicit value provided**: Parse and use that value + +This works with all parsing strategies (`.next`, `.scanningForValue`, `.unconditional`), though `.unconditional` defeats the purpose by always requiring a value. + +For complete examples and API reference, see the [`default-as-flag`](https://github.com/apple/swift-argument-parser/tree/main/Examples/default-as-flag) example. + + ### Specifying a parsing strategy When parsing a list of command-line inputs, `ArgumentParser` distinguishes between dash-prefixed keys and un-prefixed values. When looking for the value for a key, only an un-prefixed value will be selected by default. @@ -479,7 +562,7 @@ When appropriate, you can process supported arguments and ignore unknown ones by ```swift struct Example: ParsableCommand { @Flag var verbose = false - + @Argument(parsing: .allUnrecognized) var unknowns: [String] = [] diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index 024a0232a..e909c95e7 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -120,7 +120,7 @@ public struct SingleValueParsingStrategy: Hashable { /// /// For inputs such as `--foo foo`, this would parse `foo` as the /// value. However, the input `--foo --bar foo bar` would - /// result in an error. Even though two values are provided, they don’t + /// result in an error. Even though two values are provided, they don't /// succeed each option. Parsing would result in an error such as the following: /// /// Error: Missing value for '--foo ' @@ -208,7 +208,7 @@ public struct ArrayParsingStrategy: Hashable { /// the input `--files foo bar` would result in the array /// `["foo", "bar"]`. /// - /// Parsing stops as soon as there’s another option in the input such that + /// Parsing stops as soon as there's another option in the input such that /// `--files foo bar --verbose` would also set `files` to the array /// `["foo", "bar"]`. public static var upToNextOption: ArrayParsingStrategy { @@ -503,6 +503,70 @@ extension Option { }) } + /// Creates an optional property that reads its value from a labeled option, + /// with a default value when the flag is provided without an argument. + /// + /// This initializer allows providing a `defaultAsFlag` value that is used + /// when the flag is present but no value follows it: + /// + /// ```swift + /// @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path") + /// var showBinPath: String? = nil + /// ``` + /// + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided + /// implicitly by the compiler during property wrapper initialization. + /// - name: A specification for what names are allowed for this option. + /// - defaultAsFlag: The value to use when the flag is provided without an argument. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - completion: The type of command-line completion provided for this option. + public init( + wrappedValue: _OptionalNilComparisonType, + name: NameSpecification = .long, + defaultAsFlag: T, + parsing parsingStrategy: SingleValueParsingStrategy = .scanningForValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where T: ExpressibleByArgument, Value == T? { + self.init( + _parsedValue: .init { key in + let arg = ArgumentDefinition( + kind: .name(key: key, specification: name), + help: .init( + allValueStrings: T.allValueStrings, + options: [.isOptional], + help: help, + defaultValue: String(describing: defaultAsFlag), + key: key, + isComposite: false), + completion: completion ?? T.defaultCompletionKind, + parsingStrategy: parsingStrategy.base, + update: .optionalUnary( + nullaryHandler: { (origin, name, parsedValues) in + // Act like a flag - when present without value, use defaultAsFlag + parsedValues.set(defaultAsFlag, forKey: key, inputOrigin: origin) + }, + unaryHandler: { (origin, name, valueString, parsedValues) in + // Parse the provided value + guard let parsedValue = T(argument: valueString) else { + throw ParserError.unableToParseValue( + origin, name, valueString, forKey: key, originalError: nil) + } + parsedValues.set(parsedValue, forKey: key, inputOrigin: origin) + } + ), + initial: { origin, values in + values.set( + nil, forKey: key, inputOrigin: InputOrigin(element: .defaultValue) + ) + }) + + return ArgumentSet(arg) + }) + } + @available( *, deprecated, message: """ @@ -580,6 +644,68 @@ extension Option { return ArgumentSet(arg) }) } + + /// Creates an optional property that reads its value from a labeled option, + /// with a default value when the flag is provided without an argument. + /// + /// This initializer allows providing a `defaultAsFlag` value that is used + /// when the flag is present but no value follows it: + /// + /// ```swift + /// @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path") + /// var showBinPath: String? + /// ``` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this option. + /// - defaultAsFlag: The value to use when the flag is provided without an argument. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - completion: The type of command-line completion provided for this option. + public init( + name: NameSpecification = .long, + defaultAsFlag: T, + parsing parsingStrategy: SingleValueParsingStrategy = .scanningForValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where T: ExpressibleByArgument, Value == T? { + // Implementation matching the first initializer - hybrid flag/option behavior + self.init( + _parsedValue: .init { key in + let arg = ArgumentDefinition( + kind: .name(key: key, specification: name), + help: .init( + allValueStrings: T.allValueStrings, + options: [.isOptional], + help: help, + defaultValue: String(describing: defaultAsFlag), + key: key, + isComposite: false), + completion: completion ?? T.defaultCompletionKind, + parsingStrategy: parsingStrategy.base, + update: .optionalUnary( + nullaryHandler: { (origin, name, parsedValues) in + // Act like a flag - when present without value, use defaultAsFlag + parsedValues.set(defaultAsFlag, forKey: key, inputOrigin: origin) + }, + unaryHandler: { (origin, name, valueString, parsedValues) in + // Parse the provided value + guard let parsedValue = T(argument: valueString) else { + throw ParserError.unableToParseValue( + origin, name, valueString, forKey: key, originalError: nil) + } + parsedValues.set(parsedValue, forKey: key, inputOrigin: origin) + } + ), + initial: { origin, values in + values.set( + nil, forKey: key, inputOrigin: InputOrigin(element: .defaultValue) + ) + }) + + return ArgumentSet(arg) + }) + } } // MARK: - @Option Optional Initializers @@ -631,6 +757,78 @@ extension Option { }) } + /// Creates an optional property that reads its value from a labeled option, + /// parsing with the given closure, with a default value when the flag is + /// provided without an argument. + /// + /// This initializer allows providing a `defaultAsFlag` value that is used + /// when the flag is present but no value follows it: + /// + /// ```swift + /// @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path", transform: { $0.uppercased() }) + /// var showBinPath: String? = nil + /// ``` + /// + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided + /// implicitly by the compiler during property wrapper initialization. + /// - name: A specification for what names are allowed for this option. + /// - defaultAsFlag: The value to use when the flag is provided without an argument. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - completion: The type of command-line completion provided for this option. + /// - transform: A closure that converts a string into this property's + /// type, or else throws an error. + @preconcurrency + public init( + wrappedValue: _OptionalNilComparisonType, + name: NameSpecification = .long, + defaultAsFlag: T, + parsing parsingStrategy: SingleValueParsingStrategy = .scanningForValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @Sendable @escaping (String) throws -> T + ) where Value == T? { + // Implementation with hybrid flag/option behavior and transform + self.init( + _parsedValue: .init { key in + let arg = ArgumentDefinition( + kind: .name(key: key, specification: name), + help: .init( + allValueStrings: [], + options: [.isOptional], + help: help, + defaultValue: String(describing: defaultAsFlag), + key: key, + isComposite: false), + completion: completion ?? .default, + parsingStrategy: parsingStrategy.base, + update: .optionalUnary( + nullaryHandler: { (origin, name, parsedValues) in + // Act like a flag - when present without value, use defaultAsFlag + parsedValues.set(defaultAsFlag, forKey: key, inputOrigin: origin) + }, + unaryHandler: { (origin, name, valueString, parsedValues) in + // Parse the provided value using the transform + do { + let parsedValue = try transform(valueString) + parsedValues.set(parsedValue, forKey: key, inputOrigin: origin) + } catch { + throw ParserError.unableToParseValue( + origin, name, valueString, forKey: key, originalError: error) + } + } + ), + initial: { origin, values in + values.set( + nil, forKey: key, inputOrigin: InputOrigin(element: .defaultValue) + ) + }) + + return ArgumentSet(arg) + }) + } + @available( *, deprecated, message: """ @@ -706,6 +904,75 @@ extension Option { return ArgumentSet(arg) }) } + + /// Creates an optional property that reads its value from a labeled option, + /// parsing with the given closure, with a default value when the flag is + /// provided without an argument. + /// + /// This initializer allows providing a `defaultAsFlag` value that is used + /// when the flag is present but no value follows it: + /// + /// ```swift + /// @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path", transform: { $0.uppercased() }) + /// var showBinPath: String? + /// ``` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this option. + /// - defaultAsFlag: The value to use when the flag is provided without an argument. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - completion: The type of command-line completion provided for this option. + /// - transform: A closure that converts a string into this property's + /// type, or else throws an error. + @preconcurrency + public init( + name: NameSpecification = .long, + defaultAsFlag: T, + parsing parsingStrategy: SingleValueParsingStrategy = .scanningForValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @Sendable @escaping (String) throws -> T + ) where Value == T? { + // Implementation with hybrid flag/option behavior and transform + self.init( + _parsedValue: .init { key in + let arg = ArgumentDefinition( + kind: .name(key: key, specification: name), + help: .init( + allValueStrings: [], + options: [.isOptional], + help: help, + defaultValue: String(describing: defaultAsFlag), + key: key, + isComposite: false), + completion: completion ?? .default, + parsingStrategy: parsingStrategy.base, + update: .optionalUnary( + nullaryHandler: { (origin, name, parsedValues) in + // Act like a flag - when present without value, use defaultAsFlag + parsedValues.set(defaultAsFlag, forKey: key, inputOrigin: origin) + }, + unaryHandler: { (origin, name, valueString, parsedValues) in + // Parse the provided value using the transform + do { + let parsedValue = try transform(valueString) + parsedValues.set(parsedValue, forKey: key, inputOrigin: origin) + } catch { + throw ParserError.unableToParseValue( + origin, name, valueString, forKey: key, originalError: error) + } + } + ), + initial: { origin, values in + values.set( + nil, forKey: key, inputOrigin: InputOrigin(element: .defaultValue) + ) + }) + + return ArgumentSet(arg) + }) + } } // MARK: - @Option Array Initializers diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index b59aa80a8..68422a3c8 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -22,6 +22,10 @@ struct ArgumentDefinition { /// An argument that takes a string as its value. case unary(Unary) + + /// An argument that can work as both a flag (nullary) and option (unary). + /// When no value follows, uses nullaryHandler. When a value is available, uses unaryHandler. + case optionalUnary(nullaryHandler: Nullary, unaryHandler: Unary) } typealias Initial = (InputOrigin, inout ParsedValues) throws -> Void @@ -133,6 +137,9 @@ struct ArgumentDefinition { if case (.positional, .nullary) = (kind, update) { preconditionFailure("Can't create a nullary positional argument.") } + if case (.positional, .optionalUnary) = (kind, update) { + preconditionFailure("Can't create an optionalUnary positional argument.") + } self.kind = kind self.help = help @@ -157,6 +164,12 @@ extension ArgumentDefinition: CustomDebugStringConvertible { .map { $0.synopsisString } .joined(separator: ",") + " <\(valueName)>" + case (.named(let names), .optionalUnary): + return + names + .map { $0.synopsisString } + .joined(separator: ",") + + " [<\(valueName)>]" // Optional value syntax case (.positional, _): return "<\(valueName)>" case (.default, _): @@ -192,9 +205,12 @@ extension ArgumentDefinition { } var isNullary: Bool { - if case .nullary = update { + switch update { + case .nullary: return true - } else { + case .optionalUnary: + return true // Can behave as nullary + case .unary: return false } } diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 5f2728def..c5cbccd15 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -459,6 +459,124 @@ struct LenientParser { } } + mutating func parseOptionalUnaryValue( + _ argument: ArgumentDefinition, + _ parsed: ParsedArgument, + _ originElement: InputOrigin.Element, + _ nullaryHandler: ArgumentDefinition.Update.Nullary, + _ unaryHandler: ArgumentDefinition.Update.Unary, + _ result: inout ParsedValues, + _ usedOrigins: inout InputOrigin + ) throws { + let origin = InputOrigin(elements: [originElement]) + + // Helper function to check if there's a terminator between the current option and potential values + func hasTerminatorBefore(_ targetOrigin: InputOrigin.Element) -> Bool { + guard case .argumentIndex(let currentIndex) = originElement, + case .argumentIndex(let targetIndex) = targetOrigin + else { return false } + + // Check if there's a terminator between current position and target position + let terminatorIndex = inputArguments.elements.firstIndex { element in + element.isTerminator + && element.index.inputIndex > currentIndex.inputIndex + && element.index.inputIndex < targetIndex.inputIndex + } + + return terminatorIndex != nil + } + + // Try to find a value using the same logic as parseValue, but don't throw if missing + switch argument.parsingStrategy { + case .default: + // Try to get a value for this option + if let value = parsed.value { + // This was `--foo=bar` style: + try unaryHandler(origin, parsed.name, value, &result) + usedOrigins.formUnion(origin) + } else if argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement( + at: originElement) + { + // Found a joined argument + let origins = origin.inserting(origin2) + try unaryHandler(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) + } else if let (origin2, value) = inputArguments.popNextElementIfValue( + after: originElement), + !hasTerminatorBefore(origin2) + { + // Use `popNextElementIfValue(after:)` to handle cases where short option + // labels are combined - only consume if it's actually a value, not another flag + // and there's no terminator between the option and the value + let origins = origin.inserting(origin2) + try unaryHandler(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } else { + // No value found or terminator blocks access - use as flag + try nullaryHandler(origin, parsed.name, &result) + usedOrigins.formUnion(origin) + } + + case .scanningForValue: + // Similar to default, but more aggressive about finding values + if let value = parsed.value { + try unaryHandler(origin, parsed.name, value, &result) + usedOrigins.formUnion(origin) + } else if argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement( + at: originElement) + { + let origins = origin.inserting(origin2) + try unaryHandler(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) + } else if let (origin2, value) = inputArguments.popNextValue( + after: originElement), + !hasTerminatorBefore(origin2) + { + // Only consume if there's no terminator between option and value + let origins = origin.inserting(origin2) + try unaryHandler(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } else { + // No value found or terminator blocks access - use as flag + try nullaryHandler(origin, parsed.name, &result) + usedOrigins.formUnion(origin) + } + + case .unconditional: + // Use an attached value if it exists, otherwise try to consume next element + if let value = parsed.value { + try unaryHandler(origin, parsed.name, value, &result) + usedOrigins.formUnion(origin) + } else if argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement( + at: originElement) + { + let origins = origin.inserting(origin2) + try unaryHandler(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) + } else if let (origin2, value) = inputArguments.popNextElementAsValue( + after: originElement), + !hasTerminatorBefore(origin2) + { + // Only consume if there's no terminator between option and value + let origins = origin.inserting(origin2) + try unaryHandler(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } else { + // No value found or terminator blocks access - use as flag + try nullaryHandler(origin, parsed.name, &result) + usedOrigins.formUnion(origin) + } + + case .upToNextOption, .allRemainingInput, .postTerminator, .allUnrecognized: + // For other parsing strategies, fall back to flag behavior for now + try nullaryHandler(origin, parsed.name, &result) + usedOrigins.formUnion(origin) + } + } + mutating func parsePositionalValues( from unusedInput: SplitArguments, into result: inout ParsedValues @@ -629,7 +747,7 @@ struct LenientParser { switch argument.update { case .nullary(let update): - // We don’t expect a value for this option. + // We don't expect a value for this option. if let value = parsed.value { throw ParserError.unexpectedValueForOption( origin, parsed.name, value) @@ -639,6 +757,11 @@ struct LenientParser { case .unary(let update): try parseValue( argument, parsed, origin, update, &result, &usedOrigins) + case .optionalUnary(let nullaryHandler, let unaryHandler): + // Hybrid behavior: try to find a value, fall back to flag behavior + try parseOptionalUnaryValue( + argument, parsed, origin, nullaryHandler, unaryHandler, + &result, &usedOrigins) } case .terminator: // Ignore the terminator, it might get picked up as a positional value later. diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index 3ed2ac260..f80989032 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -145,6 +145,8 @@ extension ArgumentInfoV0.KindV0 { self = .flag case .unary: self = .option + case .optionalUnary: + self = .option } case .positional: self = .positional diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index 4fc313042..c64da5b9e 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -232,10 +232,24 @@ internal struct HelpGenerator { allAndDefaultValues = "(values: \(allValueStrings.joined(separator: ", ")))" case (false, true): - allAndDefaultValues = "(default: \(defaultValue))" + // Check if this is a defaultAsFlag option (uses .optionalUnary update) + switch arg.update { + case .nullary, .unary: + allAndDefaultValues = "(default: \(defaultValue))" + case .optionalUnary: + allAndDefaultValues = "(default as flag: \(defaultValue))" + } + case (true, true): - allAndDefaultValues = - "(values: \(allValueStrings.joined(separator: ", ")); default: \(defaultValue))" + // Check if this is a defaultAsFlag option for the combined case too + switch arg.update { + case .nullary, .unary: + allAndDefaultValues = + "(values: \(allValueStrings.joined(separator: ", ")); default: \(defaultValue))" + case .optionalUnary: + allAndDefaultValues = + "(values: \(allValueStrings.joined(separator: ", ")); default as flag: \(defaultValue))" + } } if arg.help.isComposite { diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index 5adc83fe6..2581e410b 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -86,6 +86,8 @@ extension ArgumentDefinition { return "\(joinedSynopsisString) <\(valueName)>" case .nullary: return joinedSynopsisString + case .optionalUnary: + return "\(joinedSynopsisString) [<\(valueName)>]" } case .positional: return "<\(valueName)>" @@ -106,6 +108,8 @@ extension ArgumentDefinition { return "\(name.synopsisString) <\(valueName)>" case .nullary: return name.synopsisString + case .optionalUnary: + return "\(name.synopsisString) [<\(valueName)>]" } case .positional: return "<\(valueName)>" diff --git a/Tests/ArgumentParserEndToEndTests/DefaultAsFlagEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultAsFlagEndToEndTests.swift new file mode 100644 index 000000000..76d5d1d9d --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/DefaultAsFlagEndToEndTests.swift @@ -0,0 +1,216 @@ +//===----------------------------------------------------------------------===// +// +// 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 ArgumentParser +import ArgumentParserTestHelpers +import XCTest + +final class DefaultAsFlagEndToEndTests: XCTestCase {} + +// MARK: - Test Cases + +extension DefaultAsFlagEndToEndTests { + + // Test struct for defaultAsFlag without transform - explicit nil + private struct CommandWithDefaultAsFlagWithoutTransformExplicitNil: + ParsableCommand + { + @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path") + var showBinPath: String? = nil + } + + // Test struct for defaultAsFlag without transform - no explicit default + private struct CommandWithDefaultAsFlagWithoutTransformNoExplicitNil: + ParsableCommand + { + @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path") + var showBinPath: String? + } + + // Test struct for defaultAsFlag with transform - explicit nil + private struct CommandWithDefaultAsFlagWithTransformExplicitNil: + ParsableCommand + { + @Option( + name: .customLong("bin-path"), defaultAsFlag: "/default/path", + transform: { $0.uppercased() }) + var showBinPath: String? = nil + } + + // Test struct for defaultAsFlag with transform - no explicit default + private struct CommandWithDefaultAsFlagWithTransformNoExplicitNil: + ParsableCommand + { + @Option( + name: .customLong("bin-path"), defaultAsFlag: "/default/path", + transform: { $0.uppercased() }) + var showBinPath: String? + } + + func testDefaultAsFlagWithExplicitNil() throws { + // When no argument is provided, should be nil + AssertParse(CommandWithDefaultAsFlagWithoutTransformExplicitNil.self, []) { + cmd in + XCTAssertNil(cmd.showBinPath) + } + + // When flag is provided without value, should use defaultAsFlag + AssertParse( + CommandWithDefaultAsFlagWithoutTransformExplicitNil.self, ["--bin-path"] + ) { cmd in + XCTAssertEqual(cmd.showBinPath, "/default/path") + } + + // When flag is provided with value, should use provided value + AssertParse( + CommandWithDefaultAsFlagWithoutTransformExplicitNil.self, + ["--bin-path", "/custom/path"] + ) { cmd in + XCTAssertEqual(cmd.showBinPath, "/custom/path") + } + } + + func testDefaultAsFlagWithoutExplicitDefault() throws { + // When no argument is provided, should be nil + AssertParse(CommandWithDefaultAsFlagWithoutTransformNoExplicitNil.self, []) + { cmd in + XCTAssertNil(cmd.showBinPath) + } + + // When flag is provided without value, should use defaultAsFlag + AssertParse( + CommandWithDefaultAsFlagWithoutTransformNoExplicitNil.self, ["--bin-path"] + ) { cmd in + XCTAssertEqual(cmd.showBinPath, "/default/path") + } + + // When flag is provided with value, should use provided value + AssertParse( + CommandWithDefaultAsFlagWithoutTransformNoExplicitNil.self, + ["--bin-path", "/custom/path"] + ) { cmd in + XCTAssertEqual(cmd.showBinPath, "/custom/path") + } + } + + func testDefaultAsFlagWithTransformWithExplicitNil() throws { + // When no argument is provided, should be nil + AssertParse(CommandWithDefaultAsFlagWithTransformExplicitNil.self, []) { + cmd in + XCTAssertNil(cmd.showBinPath) + } + + // When flag is provided without value, should use defaultAsFlag + AssertParse( + CommandWithDefaultAsFlagWithTransformExplicitNil.self, ["--bin-path"] + ) { cmd in + XCTAssertEqual(cmd.showBinPath, "/default/path") + } + + // When flag is provided with value, should use provided value with transform + AssertParse( + CommandWithDefaultAsFlagWithTransformExplicitNil.self, + ["--bin-path", "/custom/path"] + ) { cmd in + XCTAssertEqual(cmd.showBinPath, "/CUSTOM/PATH") + } + } + + func testDefaultAsFlagWithTransformWithoutExplicitDefault() throws { + // When no argument is provided, should be nil + AssertParse(CommandWithDefaultAsFlagWithTransformNoExplicitNil.self, []) { + cmd in + XCTAssertNil(cmd.showBinPath) + } + + // When flag is provided without value, should use defaultAsFlag + AssertParse( + CommandWithDefaultAsFlagWithTransformNoExplicitNil.self, ["--bin-path"] + ) { cmd in + XCTAssertEqual(cmd.showBinPath, "/default/path") + } + + // When flag is provided with value, should use provided value with transform + AssertParse( + CommandWithDefaultAsFlagWithTransformNoExplicitNil.self, + ["--bin-path", "/custom/path"] + ) { cmd in + XCTAssertEqual(cmd.showBinPath, "/CUSTOM/PATH") + } + } + + // MARK: - Tests for -- terminator behavior + + private struct CommandWithDefaultAsFlagAndArguments: ParsableCommand { + @Option(defaultAsFlag: "default") + var option: String? + + @Argument + var files: [String] = [] + } + + func testDefaultAsFlagWithTerminatorFlagBeforeTerminator() throws { + // --option -- value + // Should use defaultAsFlag value, "value" becomes positional argument + AssertParse( + CommandWithDefaultAsFlagAndArguments.self, ["--option", "--", "value"] + ) { cmd in + XCTAssertEqual(cmd.option, "default") + XCTAssertEqual(cmd.files, ["value"]) + } + } + + func testDefaultAsFlagWithTerminatorValueBeforeTerminator() throws { + // --option custom -- other + // Should use "custom" as option value, "other" becomes positional argument + AssertParse( + CommandWithDefaultAsFlagAndArguments.self, + ["--option", "custom", "--", "other"] + ) { cmd in + XCTAssertEqual(cmd.option, "custom") + XCTAssertEqual(cmd.files, ["other"]) + } + } + + func testDefaultAsFlagWithTerminatorOptionAfterTerminator() throws { + // -- --option + // Should treat "--option" as positional argument, option should be nil + AssertParse(CommandWithDefaultAsFlagAndArguments.self, ["--", "--option"]) { + cmd in + XCTAssertNil(cmd.option) + XCTAssertEqual(cmd.files, ["--option"]) + } + } + + func testDefaultAsFlagWithTerminatorComplexScenario() throws { + // --option -- --another-option value + // Should use defaultAsFlag, everything after -- is positional + AssertParse( + CommandWithDefaultAsFlagAndArguments.self, + ["--option", "--", "--another-option", "value"] + ) { cmd in + XCTAssertEqual(cmd.option, "default") + XCTAssertEqual(cmd.files, ["--another-option", "value"]) + } + } + + func testDefaultAsFlagWithTerminatorValueAfterTerminatorNotConsumed() throws { + // --option -- value1 value2 + // Should use defaultAsFlag, both values become positional arguments + AssertParse( + CommandWithDefaultAsFlagAndArguments.self, + ["--option", "--", "value1", "value2"] + ) { cmd in + XCTAssertEqual(cmd.option, "default") + XCTAssertEqual(cmd.files, ["value1", "value2"]) + } + } +} diff --git a/Tests/ArgumentParserGenerateManualTests/GenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/GenerateManualTests.swift index c2d6c583f..4854d3208 100644 --- a/Tests/ArgumentParserGenerateManualTests/GenerateManualTests.swift +++ b/Tests/ArgumentParserGenerateManualTests/GenerateManualTests.swift @@ -56,4 +56,13 @@ final class GenerateManualTests: XCTestCase { func testRollMultiPageManual() throws { try assertGenerateManual(multiPage: true, command: "roll") } + + func testDefaultAsFlagSinglePageManual() throws { + try assertGenerateManual(multiPage: false, command: "default-as-flag") + } + + func testDefaultAsFlagMultiPageManual() throws { + try assertGenerateManual(multiPage: true, command: "default-as-flag") + } + } diff --git a/Tests/ArgumentParserGenerateManualTests/Snapshots/testDefaultAsFlagMultiPageManual().mdoc b/Tests/ArgumentParserGenerateManualTests/Snapshots/testDefaultAsFlagMultiPageManual().mdoc new file mode 100644 index 000000000..f178d7b71 --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/Snapshots/testDefaultAsFlagMultiPageManual().mdoc @@ -0,0 +1,77 @@ +.\" "Generated by swift-argument-parser" +.Dd May 12, 1996 +.Dt DEFAULT-AS-FLAG 9 +.Os +.Sh NAME +.Nm default-as-flag +.Nd "A utility demonstrating defaultAsFlag options." +.Sh SYNOPSIS +.Nm +.Ar subcommand +.Op Fl -string-flag Ar string-flag +.Op Fl -number-flag Ar number-flag +.Op Fl -bool-flag Ar bool-flag +.Op Fl -transform-flag Ar transform-flag +.Op Fl -regular Ar regular +.Op Ar additional-args... +.Op Fl -help +.Sh DESCRIPTION +This command shows how defaultAsFlag options can work both as flags +and as options with values. +.Bl -tag -width 6n +.It Fl -string-flag Ar string-flag +A string option with defaultAsFlag. +.It Fl -number-flag Ar number-flag +An integer option with defaultAsFlag. +.It Fl -bool-flag Ar bool-flag +A boolean option with defaultAsFlag. +.It Fl -transform-flag Ar transform-flag +A string option with transform and defaultAsFlag. +.It Fl r , -regular Ar regular +A regular option for comparison. +.It Ar additional-args... +.It Fl h , -help +Show help information. +.El +.Sh "SEE ALSO" +.Xr default-as-flag.help 9 +.Sh AUTHORS +The +.Nm +reference was written by +.An -nosplit +.An "Jane Appleseed" , +.Mt johnappleseed@apple.com , +and +.An -nosplit +.An "The Appleseeds" +.Ao +.Mt appleseeds@apple.com +.Ac . +.\" "Generated by swift-argument-parser" +.Dd May 12, 1996 +.Dt DEFAULT-AS-FLAG.HELP 9 +.Os +.Sh NAME +.Nm "default-as-flag help" +.Nd "Show subcommand help information." +.Sh SYNOPSIS +.Nm +.Op Ar subcommands... +.Sh DESCRIPTION +.Bl -tag -width 6n +.It Ar subcommands... +.El +.Sh AUTHORS +The +.Nm +reference was written by +.An -nosplit +.An "Jane Appleseed" , +.Mt johnappleseed@apple.com , +and +.An -nosplit +.An "The Appleseeds" +.Ao +.Mt appleseeds@apple.com +.Ac . diff --git a/Tests/ArgumentParserGenerateManualTests/Snapshots/testDefaultAsFlagSinglePageManual().mdoc b/Tests/ArgumentParserGenerateManualTests/Snapshots/testDefaultAsFlagSinglePageManual().mdoc new file mode 100644 index 000000000..bacb6736a --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/Snapshots/testDefaultAsFlagSinglePageManual().mdoc @@ -0,0 +1,53 @@ +.\" "Generated by swift-argument-parser" +.Dd May 12, 1996 +.Dt DEFAULT-AS-FLAG 9 +.Os +.Sh NAME +.Nm default-as-flag +.Nd "A utility demonstrating defaultAsFlag options." +.Sh SYNOPSIS +.Nm +.Ar subcommand +.Op Fl -string-flag Ar string-flag +.Op Fl -number-flag Ar number-flag +.Op Fl -bool-flag Ar bool-flag +.Op Fl -transform-flag Ar transform-flag +.Op Fl -regular Ar regular +.Op Ar additional-args... +.Op Fl -help +.Sh DESCRIPTION +This command shows how defaultAsFlag options can work both as flags +and as options with values. +.Bl -tag -width 6n +.It Fl -string-flag Ar string-flag +A string option with defaultAsFlag. +.It Fl -number-flag Ar number-flag +An integer option with defaultAsFlag. +.It Fl -bool-flag Ar bool-flag +A boolean option with defaultAsFlag. +.It Fl -transform-flag Ar transform-flag +A string option with transform and defaultAsFlag. +.It Fl r , -regular Ar regular +A regular option for comparison. +.It Ar additional-args... +.It Fl h , -help +Show help information. +.It Em help +Show subcommand help information. +.Bl -tag -width 6n +.It Ar subcommands... +.El +.El +.Sh AUTHORS +The +.Nm +reference was written by +.An -nosplit +.An "Jane Appleseed" , +.Mt johnappleseed@apple.com , +and +.An -nosplit +.An "The Appleseeds" +.Ao +.Mt appleseeds@apple.com +.Ac . diff --git a/Tests/ArgumentParserUnitTests/DefaultAsFlagCompletionTests.swift b/Tests/ArgumentParserUnitTests/DefaultAsFlagCompletionTests.swift new file mode 100644 index 000000000..2c06de653 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/DefaultAsFlagCompletionTests.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// 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 ArgumentParserTestHelpers +import XCTest + +@testable import ArgumentParser + +final class DefaultAsFlagCompletionTests: XCTestCase { + func testDefaultAsFlagCompletion_Bash() throws { + let script = try CompletionsGenerator( + command: DefaultAsFlagCommand.self, shell: .bash + ) + .generateCompletionScript() + try assertSnapshot(actual: script, extension: "bash") + } + + func testDefaultAsFlagCompletion_Zsh() throws { + let script = try CompletionsGenerator( + command: DefaultAsFlagCommand.self, shell: .zsh + ) + .generateCompletionScript() + try assertSnapshot(actual: script, extension: "zsh") + } + + func testDefaultAsFlagCompletion_Fish() throws { + let script = try CompletionsGenerator( + command: DefaultAsFlagCommand.self, shell: .fish + ) + .generateCompletionScript() + try assertSnapshot(actual: script, extension: "fish") + } +} + +extension DefaultAsFlagCompletionTests { + struct DefaultAsFlagCommand: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "defaultasflag-test", + abstract: + "A command with defaultAsFlag options for testing completion scripts." + ) + + @Option(defaultAsFlag: "/usr/bin", completion: .directory) + var binPath: String? = nil + + @Option(defaultAsFlag: 42) + var count: Int? + + @Option(defaultAsFlag: true) + var verbose: Bool? + + @Option( + defaultAsFlag: "INFO", + completion: .list(["DEBUG", "INFO", "WARN", "ERROR"]), + transform: { $0.uppercased() } + ) + var logLevel: String? + + @Flag + var help: Bool = false + + @Argument(completion: .file()) + var input: String + } +} diff --git a/Tests/ArgumentParserUnitTests/DefaultAsFlagDumpHelpTests.swift b/Tests/ArgumentParserUnitTests/DefaultAsFlagDumpHelpTests.swift new file mode 100644 index 000000000..5d1174188 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/DefaultAsFlagDumpHelpTests.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// 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 ArgumentParserTestHelpers +import XCTest + +@testable import ArgumentParser + +final class DefaultAsFlagDumpHelpTests: XCTestCase { + func testDefaultAsFlagDumpHelp() throws { + try assertDumpHelp(type: DefaultAsFlagCommand.self) + } + + func testDefaultAsFlagWithTransformDumpHelp() throws { + try assertDumpHelp(type: DefaultAsFlagWithTransformCommand.self) + } +} + +extension DefaultAsFlagDumpHelpTests { + struct DefaultAsFlagCommand: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "A command with defaultAsFlag options for testing dump help." + ) + + @Option(name: .customLong("binary-path"), defaultAsFlag: "/usr/bin") + var binPath: String? = nil + + @Option(name: .long, defaultAsFlag: 42) + var count: Int? + + @Option(name: .long, defaultAsFlag: true) + var verbose: Bool? + + @Argument + var input: String + } + + struct DefaultAsFlagWithTransformCommand: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: + "A command with defaultAsFlag options using transforms for testing dump help." + ) + + @Option( + name: .customLong("output-dir"), + defaultAsFlag: "/default/output", + transform: { $0.uppercased() } + ) + var outputDir: String? = nil + + @Option( + name: .long, + defaultAsFlag: "INFO", + transform: { $0.lowercased() } + ) + var level: String? + + @Flag + var debug: Bool = false + } +} diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOptionDefaultAsFlag.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOptionDefaultAsFlag.swift new file mode 100644 index 000000000..78b2ac3e9 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOptionDefaultAsFlag.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------------------===// +// +// 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 ArgumentParserTestHelpers +import XCTest + +@testable import ArgumentParser + +extension HelpGenerationTests { + + struct BasicDefaultAsFlag: ParsableArguments { + @Option( + defaultAsFlag: "default", help: "A string option with defaultAsFlag.") + var stringFlag: String? + + @Option(defaultAsFlag: 42, help: "An integer option with defaultAsFlag.") + var numberFlag: Int? + + @Option(defaultAsFlag: true, help: "A boolean option with defaultAsFlag.") + var boolFlag: Bool? + + @Option( + defaultAsFlag: "transformed", + help: "A string option with defaultAsFlag and transform.") + var transformFlag: String? + + @Option(name: .shortAndLong, help: "A regular option for comparison.") + var regular: String? + } + + func testDefaultAsFlagHelpOutput() { + AssertHelp( + .default, for: BasicDefaultAsFlag.self, + equals: """ + USAGE: basic_default_as_flag [--string-flag []] [--number-flag []] [--bool-flag []] [--transform-flag []] [--regular ] + + OPTIONS: + --string-flag [] + A string option with defaultAsFlag. (default as flag: + default) + --number-flag [] + An integer option with defaultAsFlag. (default as + flag: 42) + --bool-flag [] + A boolean option with defaultAsFlag. (default as + flag: true) + --transform-flag [] + A string option with defaultAsFlag and transform. + (default as flag: transformed) + -r, --regular A regular option for comparison. + -h, --help Show help information. + + """) + } + + struct DefaultAsFlagWithShortNames: ParsableArguments { + @Option( + name: .shortAndLong, defaultAsFlag: "short", help: "Short and long names." + ) + var shortAndLong: String? + + @Option( + name: [.customShort("o")], defaultAsFlag: "s", + help: "Different short name.") + var shortOnly: String? + } + + func testDefaultAsFlagWithShortNames() { + AssertHelp( + .default, for: DefaultAsFlagWithShortNames.self, + equals: """ + USAGE: default_as_flag_with_short_names [--short-and-long []] [-o []] + + OPTIONS: + -s, --short-and-long [] + Short and long names. (default as flag: short) + -o [] Different short name. (default as flag: s) + -h, --help Show help information. + + """) + } + + struct MixedOptionTypes: ParsableArguments { + @Flag(help: "A regular flag.") + var flag: Bool = false + + @Option(defaultAsFlag: "mixed", help: "A defaultAsFlag option.") + var defaultAsFlag: String? + + @Option(help: "A regular option.") + var regular: String? + + @Argument(help: "A positional argument.") + var positional: String? + } + + func testMixedOptionTypes() { + AssertHelp( + .default, for: MixedOptionTypes.self, + equals: """ + USAGE: mixed_option_types [--flag] [--default-as-flag []] [--regular ] [] + + ARGUMENTS: + A positional argument. + + OPTIONS: + --flag A regular flag. + --default-as-flag [] + A defaultAsFlag option. (default as flag: mixed) + --regular A regular option. + -h, --help Show help information. + + """) + } +} diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Bash().bash new file mode 100644 index 000000000..9e72e5522 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Bash().bash @@ -0,0 +1,211 @@ +#!/bin/bash + +__defaultasflag-test_cursor_index_in_current_word() { + local remaining="${COMP_LINE}" + + local word + for word in "${COMP_WORDS[@]::COMP_CWORD}"; do + remaining="${remaining##*([[:space:]])"${word}"*([[:space:]])}" + done + + local -ir index="$((COMP_POINT - ${#COMP_LINE} + ${#remaining}))" + if [[ "${index}" -le 0 ]]; then + printf 0 + else + printf %s "${index}" + fi +} + +# positional arguments: +# +# - 1: the current (sub)command's count of positional arguments +# +# required variables: +# +# - flags: the flags that the current (sub)command can accept +# - options: the options that the current (sub)command can accept +# - positional_number: value ignored +# - unparsed_words: unparsed words from the current command line +# +# modified variables: +# +# - flags: remove flags for this (sub)command that are already on the command line +# - options: remove options for this (sub)command that are already on the command line +# - positional_number: set to the current positional number +# - unparsed_words: remove all flags, options, and option values for this (sub)command +__defaultasflag-test_offer_flags_options() { + local -ir positional_count="${1}" + positional_number=0 + + local was_flag_option_terminator_seen=false + local is_parsing_option_value=false + + local -ar unparsed_word_indices=("${!unparsed_words[@]}") + local -i word_index + for word_index in "${unparsed_word_indices[@]}"; do + if "${is_parsing_option_value}"; then + # This word is an option value: + # Reset marker for next word iff not currently the last word + [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]] && is_parsing_option_value=false + unset "unparsed_words[${word_index}]" + # Do not process this word as a flag or an option + continue + fi + + local word="${unparsed_words["${word_index}"]}" + if ! "${was_flag_option_terminator_seen}"; then + case "${word}" in + --) + unset "unparsed_words[${word_index}]" + # by itself -- is a flag/option terminator, but if it is the last word, it is the start of a completion + if [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]]; then + was_flag_option_terminator_seen=true + fi + continue + ;; + -*) + # ${word} is a flag or an option + # If ${word} is an option, mark that the next word to be parsed is an option value + local option + for option in "${options[@]}"; do + [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break + done + + # Remove ${word} from ${flags} or ${options} so it isn't offered again + local not_found=true + local -i index + for index in "${!flags[@]}"; do + if [[ "${flags[${index}]}" = "${word}" ]]; then + unset "flags[${index}]" + flags=("${flags[@]}") + not_found=false + break + fi + done + if "${not_found}"; then + for index in "${!options[@]}"; do + if [[ "${options[${index}]}" = "${word}" ]]; then + unset "options[${index}]" + options=("${options[@]}") + break + fi + done + fi + unset "unparsed_words[${word_index}]" + continue + ;; + esac + fi + + # ${word} is neither a flag, nor an option, nor an option value + if [[ "${positional_number}" -lt "${positional_count}" ]]; then + # ${word} is a positional + ((positional_number++)) + unset "unparsed_words[${word_index}]" + else + if [[ -z "${word}" ]]; then + # Could be completing a flag, option, or subcommand + positional_number=-1 + else + # ${word} is a subcommand or invalid, so stop processing this (sub)command + positional_number=-2 + fi + break + fi + done + + unparsed_words=("${unparsed_words[@]}") + + if\ + ! "${was_flag_option_terminator_seen}"\ + && ! "${is_parsing_option_value}"\ + && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] + then + COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) + fi +} + +__defaultasflag-test_add_completions() { + local completion + while IFS='' read -r completion; do + COMPREPLY+=("${completion}") + done < <(IFS=$'\n' compgen "${@}" -- "${cur}") +} + +__defaultasflag-test_custom_complete() { + if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then + local -ar words=("${COMP_WORDS[@]}") + else + local -ar words=("${COMP_WORDS[@]::${COMP_CWORD}}" '' "${COMP_WORDS[@]:${COMP_CWORD}}") + fi + + "${COMP_WORDS[0]}" "${@}" "${words[@]}" +} + +_defaultasflag-test() { + trap "$(shopt -p);$(shopt -po)" RETURN + shopt -s extglob + set +o history +o posix + + local -xr SAP_SHELL=bash + local -x SAP_SHELL_VERSION + SAP_SHELL_VERSION="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")" + local -r SAP_SHELL_VERSION + + local -r cur="${2}" + local -r prev="${3}" + + local -i positional_number + local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}") + + local -a flags=(--help -h --help) + local -a options=(--bin-path --count --verbose --log-level) + __defaultasflag-test_offer_flags_options 1 + + # Offer option value completions + case "${prev}" in + '--bin-path') + __defaultasflag-test_add_completions -d + return + ;; + '--count') + return + ;; + '--verbose') + return + ;; + '--log-level') + __defaultasflag-test_add_completions -W 'DEBUG'$'\n''INFO'$'\n''WARN'$'\n''ERROR' + return + ;; + esac + + # Offer positional completions + case "${positional_number}" in + 1) + __defaultasflag-test_add_completions -f + return + ;; + esac + + # Offer subcommand / subcommand argument completions + local -r subcommand="${unparsed_words[0]}" + unset 'unparsed_words[0]' + unparsed_words=("${unparsed_words[@]}") + case "${subcommand}" in + help) + # Offer subcommand argument completions + "_defaultasflag-test_${subcommand}" + ;; + *) + # Offer subcommand completions + COMPREPLY+=($(compgen -W 'help' -- "${cur}")) + ;; + esac +} + +_defaultasflag-test_help() { + : +} + +complete -o filenames -F _defaultasflag-test defaultasflag-test \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Fish().fish b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Fish().fish new file mode 100644 index 000000000..774030b89 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Fish().fish @@ -0,0 +1,74 @@ +function __defaultasflag-test_should_offer_completions_for -a expected_commands -a expected_positional_index + set -l unparsed_tokens (__defaultasflag-test_tokens -pc) + set -l positional_index 0 + set -l commands + + switch $unparsed_tokens[1] + case 'defaultasflag-test' + __defaultasflag-test_parse_subcommand 1 'bin-path=' 'count=' 'verbose=' 'log-level=' 'help' 'h/help' + switch $unparsed_tokens[1] + case 'help' + __defaultasflag-test_parse_subcommand -r 1 + end + end + + test "$commands" = "$expected_commands" -a \( -z "$expected_positional_index" -o "$expected_positional_index" -eq "$positional_index" \) +end + +function __defaultasflag-test_tokens + if test (string split -m 1 -f 1 -- . "$FISH_VERSION") -gt 3 + commandline --tokens-raw $argv + else + commandline -o $argv + end +end + +function __defaultasflag-test_parse_subcommand -S + argparse -s r -- $argv + set -l positional_count $argv[1] + set -l option_specs $argv[2..] + + set -a commands $unparsed_tokens[1] + set -e unparsed_tokens[1] + + set positional_index 0 + + while true + argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null + set unparsed_tokens $argv + set positional_index (math $positional_index + 1) + if test (count $unparsed_tokens) -eq 0 -o \( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \) + return 0 + end + set -e unparsed_tokens[1] + end +end + +function __defaultasflag-test_complete_directories + set -l token (commandline -t) + string match -- '*/' $token + set -l subdirs $token*/ + printf '%s\n' $subdirs +end + +function __defaultasflag-test_custom_completion + set -x SAP_SHELL fish + set -x SAP_SHELL_VERSION $FISH_VERSION + + set -l tokens (__defaultasflag-test_tokens -p) + if test -z (__defaultasflag-test_tokens -t) + set -l index (count (__defaultasflag-test_tokens -pc)) + set tokens $tokens[..$index] \'\' $tokens[(math $index + 1)..] + end + command $tokens[1] $argv $tokens +end + +complete -c 'defaultasflag-test' -f +complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for "defaultasflag-test"' -l 'bin-path' -rfa '(__defaultasflag-test_complete_directories)' +complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for "defaultasflag-test"' -l 'count' -rfka '' +complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for "defaultasflag-test"' -l 'verbose' -rfka '' +complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for "defaultasflag-test"' -l 'log-level' -rfka 'DEBUG INFO WARN ERROR' +complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for "defaultasflag-test"' -l 'help' +complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for "defaultasflag-test" 1' -F +complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for "defaultasflag-test"' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for "defaultasflag-test" 2' -fa 'help' -d 'Show subcommand help information.' \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Zsh().zsh new file mode 100644 index 000000000..3fff7efab --- /dev/null +++ b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagCompletion_Zsh().zsh @@ -0,0 +1,85 @@ +#compdef defaultasflag-test + +__defaultasflag-test_complete() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe -V '' non_empty_completions -- empty_completions -P $'\'\'' +} + +__defaultasflag-test_custom_complete() { + local -a completions + completions=("${(@f)"$("${command_name}" "${@}" "${command_line[@]}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + __defaultasflag-test_complete "${completions[@]:0:-1}" + fi +} + +__defaultasflag-test_cursor_index_in_current_word() { + if [[ -z "${QIPREFIX}${IPREFIX}${PREFIX}" ]]; then + printf 0 + else + printf %s "${#${(z)LBUFFER}[-1]}" + fi +} + +_defaultasflag-test() { + emulate -RL zsh -G + setopt extendedglob nullglob numericglobsort + unsetopt aliases banghist + + local -xr SAP_SHELL=zsh + local -x SAP_SHELL_VERSION + SAP_SHELL_VERSION="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" + local -r SAP_SHELL_VERSION + + local context state state_descr line + local -A opt_args + + local -r command_name="${words[1]}" + local -ar command_line=("${words[@]}") + local -ir current_word_index="$((CURRENT - 1))" + + local -i ret=1 + local -ar ___log_level=('DEBUG' 'INFO' 'WARN' 'ERROR') + local -ar arg_specs=( + '--bin-path:bin-path:_files -/' + '--count:count:' + '--verbose:verbose:' + '--log-level:log-level:{__defaultasflag-test_complete "${___log_level[@]}"}' + '--help' + ':input:_files' + '(-h --help)'{-h,--help}'[Show help information.]' + '(-): :->command' + '(-)*:: :->arg' + ) + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 + case "${state}" in + command) + local -ar subcommands=( + 'help:Show subcommand help information.' + ) + _describe -V subcommand subcommands + ;; + arg) + case "${words[1]}" in + help) + "_defaultasflag-test_${words[1]}" + ;; + esac + ;; + esac + + return "${ret}" +} + +_defaultasflag-test_help() { + local -i ret=1 + local -ar arg_specs=( + '*:subcommands:' + ) + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 + + return "${ret}" +} + +_defaultasflag-test \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagDumpHelp().json new file mode 100644 index 000000000..4ef80e79e --- /dev/null +++ b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagDumpHelp().json @@ -0,0 +1,144 @@ +{ + "command" : { + "abstract" : "A command with defaultAsFlag options for testing dump help.", + "arguments" : [ + { + "defaultValue" : "\/usr\/bin", + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "binary-path" + } + ], + "parsingStrategy" : "scanningForValue", + "preferredName" : { + "kind" : "long", + "name" : "binary-path" + }, + "shouldDisplay" : true, + "valueName" : "binary-path" + }, + { + "defaultValue" : "42", + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "count" + } + ], + "parsingStrategy" : "scanningForValue", + "preferredName" : { + "kind" : "long", + "name" : "count" + }, + "shouldDisplay" : true, + "valueName" : "count" + }, + { + "defaultValue" : "true", + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "verbose" + } + ], + "parsingStrategy" : "scanningForValue", + "preferredName" : { + "kind" : "long", + "name" : "verbose" + }, + "shouldDisplay" : true, + "valueName" : "verbose" + }, + { + "isOptional" : false, + "isRepeating" : false, + "kind" : "positional", + "parsingStrategy" : "default", + "shouldDisplay" : true, + "valueName" : "input" + }, + { + "abstract" : "Show help information.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "parsingStrategy" : "default", + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "default-as-flag-command", + "shouldDisplay" : true, + "subcommands" : [ + { + "abstract" : "Show subcommand help information.", + "arguments" : [ + { + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "parsingStrategy" : "default", + "shouldDisplay" : true, + "valueName" : "subcommands" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + }, + { + "kind" : "longWithSingleDash", + "name" : "help" + } + ], + "parsingStrategy" : "default", + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : false, + "valueName" : "help" + } + ], + "commandName" : "help", + "shouldDisplay" : true, + "superCommands" : [ + "default-as-flag-command" + ] + } + ] + }, + "serializationVersion" : 0 +} \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagWithTransformDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagWithTransformDumpHelp().json new file mode 100644 index 000000000..cc3e41844 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/Snapshots/testDefaultAsFlagWithTransformDumpHelp().json @@ -0,0 +1,135 @@ +{ + "command" : { + "abstract" : "A command with defaultAsFlag options using transforms for testing dump help.", + "arguments" : [ + { + "defaultValue" : "\/default\/output", + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "output-dir" + } + ], + "parsingStrategy" : "scanningForValue", + "preferredName" : { + "kind" : "long", + "name" : "output-dir" + }, + "shouldDisplay" : true, + "valueName" : "output-dir" + }, + { + "defaultValue" : "INFO", + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "level" + } + ], + "parsingStrategy" : "scanningForValue", + "preferredName" : { + "kind" : "long", + "name" : "level" + }, + "shouldDisplay" : true, + "valueName" : "level" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "debug" + } + ], + "parsingStrategy" : "default", + "preferredName" : { + "kind" : "long", + "name" : "debug" + }, + "shouldDisplay" : true, + "valueName" : "debug" + }, + { + "abstract" : "Show help information.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "parsingStrategy" : "default", + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "default-as-flag-with-transform-command", + "shouldDisplay" : true, + "subcommands" : [ + { + "abstract" : "Show subcommand help information.", + "arguments" : [ + { + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "parsingStrategy" : "default", + "shouldDisplay" : true, + "valueName" : "subcommands" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + }, + { + "kind" : "longWithSingleDash", + "name" : "help" + } + ], + "parsingStrategy" : "default", + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : false, + "valueName" : "help" + } + ], + "commandName" : "help", + "shouldDisplay" : true, + "superCommands" : [ + "default-as-flag-with-transform-command" + ] + } + ] + }, + "serializationVersion" : 0 +} \ No newline at end of file