From 16141d85085f6203d32d91c03a533eadaa1d7571 Mon Sep 17 00:00:00 2001 From: Rauhul Varma Date: Fri, 26 Aug 2022 00:48:12 -0700 Subject: [PATCH] Unify @Argument and @Option initialization paths - Fixes #466. - Adds initializers to ArgumentDefinition generic over a Container type. The Container type must conform to a new internal protocol ArgumentDefinitionContainer which describes functionality like default set of help options for the argument defined by the property wrapper, etc. - Adds overloads for Optional @Arguments and @Options with default values which emit deprecation warning to guide users towards using the non-Optional versions. --- .../Parsable Properties/Argument.swift | 584 +++++++++------- .../Parsable Properties/Flag.swift | 27 +- .../Parsable Properties/Option.swift | 645 ++++++++++-------- .../ExpressibleByArgument.swift | 6 - .../Parsable Types/ParsableArguments.swift | 9 +- .../Parsing/ArgumentDefinition.swift | 280 ++++++-- .../ArgumentParser/Parsing/ArgumentSet.swift | 77 +-- .../ArgumentParser/Parsing/ParserError.swift | 2 +- .../ArgumentParser/Usage/HelpGenerator.swift | 15 +- .../TestHelpers.swift | 4 +- .../CountLinesExampleTests.swift | 2 +- .../CountLinesGenerateManualTests.swift | 4 +- Tests/ArgumentParserUnitTests/CMakeLists.txt | 2 + .../HelpGenerationTests+AtArgument.swift | 536 +++++++++++++++ .../HelpGenerationTests+AtOption.swift | 417 +++++++++++ .../HelpGenerationTests.swift | 32 +- 16 files changed, 1937 insertions(+), 705 deletions(-) create mode 100644 Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift create mode 100644 Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index 3daf38b85..409107873 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -94,66 +94,7 @@ extension Argument: CustomStringConvertible { } } -extension Argument: DecodableParsedWrapper where Value: Decodable {} - -// MARK: Property Wrapper Initializers - -extension Argument where Value: ExpressibleByArgument { - /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. - /// - /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. - private init( - initial: Value?, - help: ArgumentHelp?, - completion: CompletionKind? - ) { - self.init(_parsedValue: .init { key in - ArgumentSet(key: key, kind: .positional, parseType: Value.self, name: NameSpecification.long, default: initial, help: help, completion: completion ?? Value.defaultCompletionKind) - }) - } - - /// Creates a property with a default value provided by standard Swift default value syntax. - /// - /// This method is called to initialize an `Argument` with a default value such as: - /// ```swift - /// @Argument var foo: String = "bar" - /// ``` - /// - /// - Parameters: - /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property wrapper initialization. - /// - help: Information about how to use this argument. - public init( - wrappedValue: Value, - help: ArgumentHelp? = nil, - completion: CompletionKind? = nil - ) { - self.init( - initial: wrappedValue, - help: help, - completion: completion - ) - } - - /// Creates a property with no default value. - /// - /// This method is called to initialize an `Argument` without a default value such as: - /// ```swift - /// @Argument var foo: String - /// ``` - /// - /// - Parameters: - /// - help: Information about how to use this argument. - public init( - help: ArgumentHelp? = nil, - completion: CompletionKind? = nil - ) { - self.init( - initial: nil, - help: help, - completion: completion - ) - } -} +extension Argument: DecodableParsedWrapper where Value: Decodable { } /// The strategy to use when parsing multiple values from positional arguments /// into an array. @@ -210,95 +151,109 @@ public struct ArgumentArrayParsingStrategy: Hashable { } } +// MARK: - @Argument T: ExpressibleByArgument Initializers extension Argument { - /// Creates an optional property that reads its value from an argument. + /// Creates a property with a default value provided by standard Swift default + /// value syntax. /// - /// The argument is optional for the caller of the command and defaults to - /// `nil`. + /// This method is called to initialize an `Argument` with a default value + /// such as: + /// ```swift + /// @Argument var foo: String = "bar" + /// ``` /// - /// - Parameter help: Information about how to use this argument. - public init( + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided + /// implicitly by the compiler during property wrapper initialization. + /// - help: Information about how to use this argument. + /// - completion: Kind of completion provided to the user for this option. + public init( + wrappedValue: Value, help: ArgumentHelp? = nil, completion: CompletionKind? = nil - ) where Value == T? { + ) where Value: ExpressibleByArgument { self.init(_parsedValue: .init { key in - var arg = ArgumentDefinition( + let arg = ArgumentDefinition( + container: Bare.self, key: key, kind: .positional, + help: help, parsingStrategy: .default, - parser: T.init(argument:), - default: nil, - completion: completion ?? T.defaultCompletionKind) - arg.help.updateArgumentHelp(help: help) - return ArgumentSet(arg.optional) + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) }) } - - /// This initializer allows a user to provide a `nil` default value for an - /// optional `@Argument`-marked property without allowing a non-`nil` default - /// value. - public init( - wrappedValue _value: _OptionalNilComparisonType, + + /// Creates a property with no default value. + /// + /// This method is called to initialize an `Argument` without a default value + /// such as: + /// ```swift + /// @Argument var foo: String + /// ``` + /// + /// - Parameters: + /// - help: Information about how to use this argument. + /// - completion: Kind of completion provided to the user for this option. + public init( help: ArgumentHelp? = nil, completion: CompletionKind? = nil - ) where Value == T? { - self.init( - help: help, - completion: completion) - } - - /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. - /// - /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. - private init( - initial: Value?, - help: ArgumentHelp?, - completion: CompletionKind?, - transform: @escaping (String) throws -> Value - ) { + ) where Value: ExpressibleByArgument { self.init(_parsedValue: .init { key in - let help = ArgumentDefinition.Help(options: [], help: help, key: key) - let arg = ArgumentDefinition(kind: .positional, help: help, completion: completion ?? .default, update: .unary({ - (origin, name, valueString, parsedValues) in - do { - let transformedValue = try transform(valueString) - parsedValues.set(transformedValue, forKey: key, inputOrigin: origin) - } catch { - throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) - } - }), initial: { origin, values in - if let v = initial { - values.set(v, forKey: key, inputOrigin: origin) - } - }) + let arg = ArgumentDefinition( + container: Bare.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: .default, + initial: nil, + completion: completion) + return ArgumentSet(arg) }) } +} - /// Creates a property with a default value provided by standard Swift default value syntax, parsing with the given closure. +// MARK: - @Argument T Initializers +extension Argument { + /// Creates a property with a default value provided by standard Swift default + /// value syntax, parsing with the given closure. /// - /// This method is called to initialize an `Argument` with a default value such as: + /// This method is called to initialize an `Argument` with a default value + /// such as: /// ```swift /// @Argument(transform: baz) /// var foo: String = "bar" /// ``` /// /// - Parameters: - /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property wrapper initialization. + /// - wrappedValue: A default value to use for this property, provided + /// implicitly by the compiler during property wrapper initialization. /// - help: Information about how to use this argument. - /// - transform: A closure that converts a string into this property's type or throws an error. + /// - completion: Kind of completion provided to the user for this option. + /// - transform: A closure that converts a string into this property's type + /// or throws an error. public init( wrappedValue: Value, help: ArgumentHelp? = nil, completion: CompletionKind? = nil, transform: @escaping (String) throws -> Value ) { - self.init( - initial: wrappedValue, - help: help, - completion: completion, - transform: transform - ) + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Bare.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: .default, + transform: transform, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) } /// Creates a property with no default value, parsing with the given closure. @@ -311,216 +266,329 @@ extension Argument { /// /// - Parameters: /// - help: Information about how to use this argument. - /// - transform: A closure that converts a string into this property's type or throws an error. + /// - completion: Kind of completion provided to the user for this option. + /// - transform: A closure that converts a string into this property's + /// element type or throws an error. public init( help: ArgumentHelp? = nil, completion: CompletionKind? = nil, transform: @escaping (String) throws -> Value ) { - self.init( - initial: nil, - help: help, - completion: completion, - transform: transform - ) + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Bare.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: .default, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) } +} +// MARK: - @Argument Optional Initializers +extension Argument { + /// This initializer allows a user to provide a `nil` default value for an + /// optional `@Argument`-marked property without allowing a non-`nil` default + /// value. + /// + /// - Parameters: + /// - help: Information about how to use this argument. + /// - completion: Kind of completion provided to the user for this option. + public init( + wrappedValue _value: _OptionalNilComparisonType, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where T: ExpressibleByArgument, Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: .default, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } - /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. + @available(*, deprecated, message: """ + Optional @Arguments with default values should be declared as non-Optional. + """) + public init( + wrappedValue: Value, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where T: ExpressibleByArgument, Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: .default, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) + } + + /// Creates an optional property that reads its value from an argument. + /// + /// The argument is optional for the caller of the command and defaults to + /// `nil`. /// - /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. - private init( - initial: Value?, - parsingStrategy: ArgumentArrayParsingStrategy, - help: ArgumentHelp?, - completion: CompletionKind? - ) - where Element: ExpressibleByArgument, Value == Array - { + /// - Parameters: + /// - help: Information about how to use this argument. + /// - completion: Kind of completion provided to the user for this option. + public init( + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where T: ExpressibleByArgument, Value == Optional { self.init(_parsedValue: .init { key in - // Assign the initial-value setter and help text for default value based on if an initial value was provided. - let setInitialValue: ArgumentDefinition.Initial - let helpDefaultValue: String? - if let initial = initial { - setInitialValue = { origin, values in - values.set(initial, forKey: key, inputOrigin: origin) - } - helpDefaultValue = !initial.isEmpty ? initial.defaultValueDescription : nil - } else { - setInitialValue = { _, _ in } - helpDefaultValue = nil - } + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: .default, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } +} - let help = ArgumentDefinition.Help( - allValues: Element.allValueStrings, - options: [.isOptional, .isRepeating], +// MARK: - @Argument Optional Initializers +extension Argument { + /// This initializer allows a user to provide a `nil` default value for an + /// optional `@Argument`-marked property without allowing a non-`nil` default + /// value. + /// + /// - Parameters: + /// - help: Information about how to use this argument. + /// - completion: Kind of completion provided to the user for this option. + /// - transform: A closure that converts a string into this property's + /// element type or throws an error. + public init( + wrappedValue _value: _OptionalNilComparisonType, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> T + ) where Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .positional, help: help, - key: key - ) - var arg = ArgumentDefinition( + parsingStrategy: .default, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } + + @available(*, deprecated, message: """ + Optional @Arguments with default values should be declared as non-Optional. + """) + public init( + wrappedValue: Value, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> T + ) where Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, kind: .positional, help: help, - completion: completion ?? Element.defaultCompletionKind, - parsingStrategy: parsingStrategy.base, - update: .appendToArray(forType: Element.self, key: key), - initial: setInitialValue) - arg.help.defaultValue = helpDefaultValue + parsingStrategy: .default, + transform: transform, + initial: wrappedValue, + completion: completion) + return ArgumentSet(arg) }) } + /// Creates an optional property that reads its value from an argument. + /// + /// The argument is optional for the caller of the command and defaults to + /// `nil`. + /// + /// - Parameters: + /// - help: Information about how to use this argument. + /// - completion: Kind of completion provided to the user for this option. + /// - transform: A closure that converts a string into this property's + /// element type or throws an error. + public init( + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> T + ) where Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: .default, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } +} + +// MARK: - @Argument Array Initializers +extension Argument { /// Creates a property that reads an array from zero or more arguments. /// /// - Parameters: /// - initial: A default value to use for this property. - /// - parsingStrategy: The behavior to use when parsing multiple values - /// from the command-line arguments. + /// - parsingStrategy: The behavior to use when parsing multiple values from + /// the command-line arguments. /// - help: Information about how to use this argument. - public init( - wrappedValue: Value, + /// - completion: Kind of completion provided to the user for this option. + public init( + wrappedValue: Array, parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, help: ArgumentHelp? = nil, completion: CompletionKind? = nil - ) - where Element: ExpressibleByArgument, Value == Array - { - self.init( - initial: wrappedValue, - parsingStrategy: parsingStrategy, - help: help, - completion: completion - ) + ) where T: ExpressibleByArgument, Value == Array { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: parsingStrategy.base, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) } - /// Creates a property with no default value that reads an array from zero or more arguments. + /// Creates a property with no default value that reads an array from zero or + /// more arguments. /// - /// This method is called to initialize an array `Argument` with no default value such as: + /// This method is called to initialize an array `Argument` with no default + /// value such as: /// ```swift /// @Argument() /// var foo: [String] /// ``` /// /// - Parameters: - /// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments. + /// - parsingStrategy: The behavior to use when parsing multiple values from + /// the command-line arguments. /// - help: Information about how to use this argument. - public init( + /// - completion: Kind of completion provided to the user for this option. + public init( parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, help: ArgumentHelp? = nil, completion: CompletionKind? = nil - ) - where Element: ExpressibleByArgument, Value == Array - { - self.init( - initial: nil, - parsingStrategy: parsingStrategy, - help: help, - completion: completion - ) - } - - /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. - /// - /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. - private init( - initial: Value?, - parsingStrategy: ArgumentArrayParsingStrategy, - help: ArgumentHelp?, - completion: CompletionKind?, - transform: @escaping (String) throws -> Element - ) - where Value == Array - { + ) where T: ExpressibleByArgument, Value == Array { self.init(_parsedValue: .init { key in - // Assign the initial-value setter and help text for default value based on if an initial value was provided. - let setInitialValue: ArgumentDefinition.Initial - let helpDefaultValue: String? - if let initial = initial { - setInitialValue = { origin, values in - values.set(initial, forKey: key, inputOrigin: origin) - } - helpDefaultValue = !initial.isEmpty ? "\(initial)" : nil - } else { - setInitialValue = { _, _ in } - helpDefaultValue = nil - } - - let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) - var arg = ArgumentDefinition( + let arg = ArgumentDefinition( + container: Array.self, + key: key, kind: .positional, help: help, - completion: completion ?? .default, parsingStrategy: parsingStrategy.base, - update: .unary({ - (origin, name, valueString, parsedValues) in - do { - let transformedElement = try transform(valueString) - parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { - $0.append(transformedElement) - }) - } catch { - throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) - } - }), - initial: setInitialValue) - arg.help.defaultValue = helpDefaultValue + initial: nil, + completion: completion) + return ArgumentSet(arg) }) } +} +// MARK: - @Argument Array Initializers +extension Argument { /// Creates a property that reads an array from zero or more arguments, /// parsing each element with the given closure. /// /// - Parameters: - /// - initial: A default value to use for this property. - /// - parsingStrategy: The behavior to use when parsing multiple values - /// from the command-line arguments. + /// - wrappedValue: A default value to use for this property. + /// - parsingStrategy: The behavior to use when parsing multiple values from + /// the command-line arguments. /// - help: Information about how to use this argument. + /// - completion: Kind of completion provided to the user for this option. /// - transform: A closure that converts a string into this property's /// element type or throws an error. - public init( - wrappedValue: Value, + public init( + wrappedValue: Array, parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, help: ArgumentHelp? = nil, completion: CompletionKind? = nil, - transform: @escaping (String) throws -> Element - ) - where Value == Array - { - self.init( - initial: wrappedValue, - parsingStrategy: parsingStrategy, - help: help, - completion: completion, - transform: transform - ) + transform: @escaping (String) throws -> T + ) where Value == Array { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) } - /// Creates a property with no default value that reads an array from zero or more arguments, parsing each element with the given closure. + /// Creates a property with no default value that reads an array from zero or + /// more arguments, parsing each element with the given closure. /// - /// This method is called to initialize an array `Argument` with no default value such as: + /// This method is called to initialize an array `Argument` with no default + /// value such as: /// ```swift /// @Argument(transform: baz) /// var foo: [String] /// ``` /// /// - Parameters: - /// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments. + /// - parsingStrategy: The behavior to use when parsing multiple values from + /// the command-line arguments. /// - help: Information about how to use this argument. - /// - transform: A closure that converts a string into this property's element type or throws an error. - public init( + /// - completion: Kind of completion provided to the user for this option. + /// - transform: A closure that converts a string into this property's + /// element type or throws an error. + public init( parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, help: ArgumentHelp? = nil, completion: CompletionKind? = nil, - transform: @escaping (String) throws -> Element - ) - where Value == Array - { - self.init( - initial: nil, - parsingStrategy: parsingStrategy, - help: help, - completion: completion, - transform: transform - ) + transform: @escaping (String) throws -> T + ) where Value == Array { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .positional, + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) } } diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index 8b0dc7060..249ac403b 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -400,7 +400,13 @@ extension Flag where Value: EnumerableFlag { let caseKey = InputKey(rawValue: String(describing: value)) let name = Value.name(for: value) let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help - let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: helpForCase, defaultValue: defaultValue, key: key, isComposite: !hasCustomCaseHelp) + let help = ArgumentDefinition.Help( + allValues: [], + options: initial != nil ? [.isOptional] : [], + help: helpForCase, + defaultValue: defaultValue, + key: key, + isComposite: !hasCustomCaseHelp) return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: initial, update: .nullary({ (origin, name, values) in hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) })) @@ -489,7 +495,15 @@ extension Flag { let caseKey = InputKey(rawValue: String(describing: value)) let name = Element.name(for: value) let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help - let help = ArgumentDefinition.Help(options: .isOptional, help: helpForCase, key: key, isComposite: !hasCustomCaseHelp) + + let help = ArgumentDefinition.Help( + allValues: [], + options: [.isOptional], + help: helpForCase, + defaultValue: nil, + key: key, + isComposite: !hasCustomCaseHelp) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) })) @@ -514,7 +528,14 @@ extension Flag { let caseKey = InputKey(rawValue: String(describing: value)) let name = Element.name(for: value) let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help - let help = ArgumentDefinition.Help(options: .isOptional, help: helpForCase, key: key, isComposite: !hasCustomCaseHelp) + let help = ArgumentDefinition.Help( + allValues: [], + options: [.isOptional], + help: helpForCase, + defaultValue: nil, + key: key, + isComposite: !hasCustomCaseHelp) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: initial, update: .nullary({ (origin, name, values) in values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { $0.append(value) diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index 05f464d81..93afc00fd 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -101,103 +101,6 @@ extension Option: CustomStringConvertible { extension Option: DecodableParsedWrapper where Value: Decodable {} -// MARK: Property Wrapper Initializers - -extension Option where Value: ExpressibleByArgument { - /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. - /// - /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. - private init( - name: NameSpecification, - initial: Value?, - parsingStrategy: SingleValueParsingStrategy, - help: ArgumentHelp?, - completion: CompletionKind? - ) { - self.init(_parsedValue: .init { key in - ArgumentSet( - key: key, - kind: .name(key: key, specification: name), - parsingStrategy: parsingStrategy.base, - parseType: Value.self, - name: name, - default: initial, help: help, completion: completion ?? Value.defaultCompletionKind) - } - ) - } - - /// Creates a property with a default value provided by standard Swift default value syntax. - @available(*, deprecated, message: "Swap the order of your 'help' and 'completion' arguments.") - public init( - wrappedValue: Value, - name: NameSpecification = .long, - parsing parsingStrategy: SingleValueParsingStrategy = .next, - completion: CompletionKind?, - help: ArgumentHelp? - ) { - self.init( - name: name, - initial: wrappedValue, - parsingStrategy: parsingStrategy, - help: help, - completion: completion) - } - - /// Creates a property with a default value provided by standard Swift default value syntax. - /// - /// This method is called to initialize an `Option` with a default value such as: - /// ```swift - /// @Option var foo: String = "bar" - /// ``` - /// - /// - 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 flag. - /// - parsingStrategy: The behavior to use when looking for this option's value. - /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. - public init( - wrappedValue: Value, - name: NameSpecification = .long, - parsing parsingStrategy: SingleValueParsingStrategy = .next, - help: ArgumentHelp? = nil, - completion: CompletionKind? = nil - ) { - self.init( - name: name, - initial: wrappedValue, - parsingStrategy: parsingStrategy, - help: help, - completion: completion) - } - - /// Creates a property with no default value. - /// - /// This method is called to initialize an `Option` without a default value such as: - /// ```swift - /// @Option var foo: String - /// ``` - /// - /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when looking for this option's value. - /// - help: Information about how to use this option. - /// - completion: Kind of completion provided to the user for this option. - public init( - name: NameSpecification = .long, - parsing parsingStrategy: SingleValueParsingStrategy = .next, - help: ArgumentHelp? = nil, - completion: CompletionKind? = nil - ) { - self.init( - name: name, - initial: nil, - parsingStrategy: parsingStrategy, - help: help, - completion: completion) - } -} - /// The strategy to use when parsing a single value from `@Option` arguments. /// /// - SeeAlso: ``ArrayParsingStrategy`` @@ -329,103 +232,112 @@ public struct ArrayParsingStrategy: Hashable { } } +// MARK: - @Option T: ExpressibleByArgument Initializers extension Option { - /// Creates a property that reads its value from a labeled option. + /// Creates a property with a default value provided by standard Swift default value syntax. /// - /// If the property has an `Optional` type, or you provide a non-`nil` - /// value for the `initial` parameter, specifying this option is not - /// required. + /// This method is called to initialize an `Option` with a default value such as: + /// ```swift + /// @Option var foo: String = "bar" + /// ``` /// /// - 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 flag. - /// - parsingStrategy: The behavior to use when looking for this option's - /// value. + /// - parsingStrategy: The behavior to use when looking for this option's value. /// - help: Information about how to use this option. /// - completion: Kind of completion provided to the user for this option. - public init( + public init( + wrappedValue: Value, name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, help: ArgumentHelp? = nil, completion: CompletionKind? = nil - ) where Value == T? { + ) where Value: ExpressibleByArgument { self.init(_parsedValue: .init { key in - var arg = ArgumentDefinition( + let arg = ArgumentDefinition( + container: Bare.self, key: key, kind: .name(key: key, specification: name), + help: help, parsingStrategy: parsingStrategy.base, - parser: T.init(argument:), - default: nil, - completion: completion ?? T.defaultCompletionKind) - arg.help.updateArgumentHelp(help: help) - return ArgumentSet(arg.optional) + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) }) } - /// This initializer allows a user to provide a `nil` default value for an - /// optional `@Option`-marked property without allowing a non-`nil` default - /// value. - public init( - wrappedValue _value: _OptionalNilComparisonType, + @available(*, deprecated, message: """ + Swap the order of the 'help' and 'completion' arguments. + """) + public init( + wrappedValue: Value, name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, - help: ArgumentHelp? = nil, - completion: CompletionKind? = nil - ) where Value == T? { + completion: CompletionKind?, + help: ArgumentHelp? + ) where Value: ExpressibleByArgument { self.init( + wrappedValue: wrappedValue, name: name, parsing: parsingStrategy, help: help, - completion: completion - ) + completion: completion) } - /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. + /// Creates a property with no default value. /// - /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. - private init( - name: NameSpecification, - initial: Value?, - parsingStrategy: SingleValueParsingStrategy, - help: ArgumentHelp?, - completion: CompletionKind?, - transform: @escaping (String) throws -> Value - ) { + /// This method is called to initialize an `Option` without a default value such as: + /// ```swift + /// @Option var foo: String + /// ``` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where Value: ExpressibleByArgument { self.init(_parsedValue: .init { key in - let kind = ArgumentDefinition.Kind.name(key: key, specification: name) - let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: help, key: key) - var arg = ArgumentDefinition(kind: kind, help: help, completion: completion ?? .default, parsingStrategy: parsingStrategy.base, update: .unary({ - (origin, name, valueString, parsedValues) in - do { - let transformedValue = try transform(valueString) - parsedValues.set(transformedValue, forKey: key, inputOrigin: origin) - } catch { - throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) - } - }), initial: { origin, values in - if let v = initial { - values.set(v, forKey: key, inputOrigin: origin) - } - }) - arg.help.options.formUnion(ArgumentDefinition.Help.Options(type: Value.self)) - arg.help.defaultValue = initial.map { "\($0)" } + let arg = ArgumentDefinition( + container: Bare.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + initial: nil, + completion: completion) + return ArgumentSet(arg) - }) + }) } +} - /// Creates a property with a default value provided by standard Swift default value syntax, parsing with the given closure. +// MARK: - @Option T Initializers +extension Option { + /// Creates a property with a default value provided by standard Swift default value syntax. /// /// This method is called to initialize an `Option` with a default value such as: /// ```swift - /// @Option(transform: baz) - /// var foo: String = "bar" + /// @Option var foo: String = "bar" /// ``` + /// /// - Parameters: - /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property wrapper initialization. + /// - 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 flag. /// - parsingStrategy: The behavior to use when looking for this option's value. /// - help: Information about how to use this option. /// - completion: Kind of completion provided to the user for this option. - /// - transform: A closure that converts a string into this property's type or throws an error. + /// - transform: A closure that converts a string into this property's + /// element type or throws an error. public init( wrappedValue: Value, name: NameSpecification = .long, @@ -434,22 +346,26 @@ extension Option { completion: CompletionKind? = nil, transform: @escaping (String) throws -> Value ) { - self.init( - name: name, - initial: wrappedValue, - parsingStrategy: parsingStrategy, - help: help, - completion: completion, - transform: transform - ) + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Bare.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) } - /// Creates a property with no default value, parsing with the given closure. + /// Creates a property with no default value. /// - /// This method is called to initialize an `Option` with no default value such as: + /// This method is called to initialize an `Option` without a default value such as: /// ```swift - /// @Option(transform: baz) - /// var foo: String + /// @Option var foo: String /// ``` /// /// - Parameters: @@ -457,7 +373,6 @@ extension Option { /// - parsingStrategy: The behavior to use when looking for this option's value. /// - help: Information about how to use this option. /// - completion: Kind of completion provided to the user for this option. - /// - transform: A closure that converts a string into this property's type or throws an error. public init( name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, @@ -465,56 +380,220 @@ extension Option { completion: CompletionKind? = nil, transform: @escaping (String) throws -> Value ) { - self.init( - name: name, - initial: nil, - parsingStrategy: parsingStrategy, - help: help, - completion: completion, - transform: transform - ) + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Bare.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) } +} +// MARK: - @Option Optional Initializers +extension Option { + /// This initializer allows a user to provide a `nil` default value for an + /// optional `@Option`-marked property without allowing a non-`nil` default + /// value. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. + /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. + public init( + wrappedValue _value: _OptionalNilComparisonType, + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where T: ExpressibleByArgument, Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } + + @available(*, deprecated, message: """ + Optional @Options with default values should be declared as non-Optional. + """) + public init( + wrappedValue: Value, + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where T: ExpressibleByArgument, Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + initial: wrappedValue, + completion: completion) - /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. + return ArgumentSet(arg) + }) + } + + /// Creates a property that reads its value from a labeled option. + /// + /// If the property has an `Optional` type, or you provide a non-`nil` + /// value for the `initial` parameter, specifying this option is not + /// required. /// - /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. - private init( - initial: [Element]?, - name: NameSpecification, - parsingStrategy: ArrayParsingStrategy, - help: ArgumentHelp?, - completion: CompletionKind? - ) where Element: ExpressibleByArgument, Value == Array { + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. + /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where T: ExpressibleByArgument, Value == Optional { self.init(_parsedValue: .init { key in - // Assign the initial-value setter and help text for default value based on if an initial value was provided. - let setInitialValue: ArgumentDefinition.Initial - let helpDefaultValue: String? - if let initial = initial { - setInitialValue = { origin, values in - values.set(initial, forKey: key, inputOrigin: origin) - } - helpDefaultValue = !initial.isEmpty ? initial.defaultValueDescription : nil - } else { - setInitialValue = { _, _ in } - helpDefaultValue = nil - } + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } +} + +// MARK: - @Option Optional Initializers +extension Option { + /// Creates a property with a default value provided by standard Swift default + /// value syntax, parsing with the given closure. + /// + /// This method is called to initialize an `Option` with a default value such as: + /// ```swift + /// @Option(transform: baz) + /// var foo: String = "bar" + /// ``` + /// + /// - 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 flag. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. + /// - transform: A closure that converts a string into this property's type + /// or throws an error. + public init( + wrappedValue _value: _OptionalNilComparisonType, + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> T + ) where Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } + + @available(*, deprecated, message: """ + Optional @Options with default values should be declared as non-Optional. + """) + public init( + wrappedValue: Value, + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> T + ) where Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) + } - let kind = ArgumentDefinition.Kind.name(key: key, specification: name) - let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) - var arg = ArgumentDefinition( - kind: kind, + /// Creates a property with no default value, parsing with the given closure. + /// + /// This method is called to initialize an `Option` with no default value such as: + /// ```swift + /// @Option(transform: baz) + /// var foo: String + /// ``` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. + /// - transform: A closure that converts a string into this property's type or throws an error. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> T + ) where Value == Optional { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Optional.self, + key: key, + kind: .name(key: key, specification: name), help: help, - completion: completion ?? Element.defaultCompletionKind, parsingStrategy: parsingStrategy.base, - update: .appendToArray(forType: Element.self, key: key), - initial: setInitialValue - ) - arg.help.defaultValue = helpDefaultValue + transform: transform, + initial: nil, + completion: completion) + return ArgumentSet(arg) }) } +} +// MARK: - @Option Array Initializers +extension Option { /// Creates an array property that reads its values from zero or more /// labeled options. /// @@ -525,20 +604,25 @@ extension Option { /// from the command-line arguments. /// - help: Information about how to use this option. /// - completion: Kind of completion provided to the user for this option. - public init( - wrappedValue: [Element], + public init( + wrappedValue: Value, name: NameSpecification = .long, parsing parsingStrategy: ArrayParsingStrategy = .singleValue, help: ArgumentHelp? = nil, completion: CompletionKind? = nil - ) where Element: ExpressibleByArgument, Value == Array { - self.init( - initial: wrappedValue, - name: name, - parsingStrategy: parsingStrategy, - help: help, - completion: completion - ) + ) where T: ExpressibleByArgument, Value == Array { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) } /// Creates an array property with no default value that reads its values from zero or more labeled options. @@ -554,71 +638,29 @@ extension Option { /// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments. /// - help: Information about how to use this option. /// - completion: Kind of completion provided to the user for this option. - public init( + public init( name: NameSpecification = .long, parsing parsingStrategy: ArrayParsingStrategy = .singleValue, help: ArgumentHelp? = nil, completion: CompletionKind? = nil - ) where Element: ExpressibleByArgument, Value == Array { - self.init( - initial: nil, - name: name, - parsingStrategy: parsingStrategy, - help: help, - completion: completion - ) - } - - - /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. - /// - /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. - private init( - initial: [Element]?, - name: NameSpecification, - parsingStrategy: ArrayParsingStrategy, - help: ArgumentHelp?, - completion: CompletionKind?, - transform: @escaping (String) throws -> Element - ) where Value == Array { + ) where T: ExpressibleByArgument, Value == Array { self.init(_parsedValue: .init { key in - // Assign the initial-value setter and help text for default value based on if an initial value was provided. - let setInitialValue: ArgumentDefinition.Initial - let helpDefaultValue: String? - if let initial = initial { - setInitialValue = { origin, values in - values.set(initial, forKey: key, inputOrigin: origin) - } - helpDefaultValue = !initial.isEmpty ? "\(initial)" : nil - } else { - setInitialValue = { _, _ in } - helpDefaultValue = nil - } - - let kind = ArgumentDefinition.Kind.name(key: key, specification: name) - let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) - var arg = ArgumentDefinition( - kind: kind, + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .name(key: key, specification: name), help: help, - completion: completion ?? .default, parsingStrategy: parsingStrategy.base, - update: .unary({ (origin, name, valueString, parsedValues) in - do { - let transformedElement = try transform(valueString) - parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { - $0.append(transformedElement) - }) - } catch { - throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) - } - }), - initial: setInitialValue - ) - arg.help.defaultValue = helpDefaultValue + initial: nil, + completion: completion) + return ArgumentSet(arg) }) } +} +// MARK: - @Option Array Initializers +extension Option { /// Creates an array property that reads its values from zero or more /// labeled options, parsing with the given closure. /// @@ -635,25 +677,31 @@ extension Option { /// - completion: Kind of completion provided to the user for this option. /// - transform: A closure that converts a string into this property's /// element type or throws an error. - public init( - wrappedValue: [Element], + public init( + wrappedValue: Value, name: NameSpecification = .long, parsing parsingStrategy: ArrayParsingStrategy = .singleValue, help: ArgumentHelp? = nil, completion: CompletionKind? = nil, - transform: @escaping (String) throws -> Element - ) where Value == Array { - self.init( - initial: wrappedValue, - name: name, - parsingStrategy: parsingStrategy, - help: help, - completion: completion, - transform: transform - ) + transform: @escaping (String) throws -> T + ) where Value == Array { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) } - /// Creates an array property with no default value that reads its values from zero or more labeled options, parsing each element with the given closure. + /// Creates an array property with no default value that reads its values from + /// zero or more labeled options, parsing each element with the given closure. /// /// This method is called to initialize an array `Option` with no default value such as: /// ```swift @@ -663,24 +711,31 @@ extension Option { /// /// - Parameters: /// - name: A specification for what names are allowed for this flag. - /// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments. + /// - parsingStrategy: The behavior to use when parsing multiple values from + /// the command-line arguments. /// - help: Information about how to use this option. /// - completion: Kind of completion provided to the user for this option. - /// - transform: A closure that converts a string into this property's element type or throws an error. - public init( + /// - transform: A closure that converts a string into this property's + /// element type or throws an error. + public init( name: NameSpecification = .long, parsing parsingStrategy: ArrayParsingStrategy = .singleValue, help: ArgumentHelp? = nil, completion: CompletionKind? = nil, - transform: @escaping (String) throws -> Element - ) where Value == Array { - self.init( - initial: nil, - name: name, - parsingStrategy: parsingStrategy, - help: help, - completion: completion, - transform: transform - ) + transform: @escaping (String) throws -> T + ) where Value == Array { + self.init(_parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) } } diff --git a/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift b/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift index 240f0820a..6e67a1412 100644 --- a/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift +++ b/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift @@ -105,9 +105,3 @@ extension Float: ExpressibleByArgument {} extension Double: ExpressibleByArgument {} extension Bool: ExpressibleByArgument {} - -extension Array where Element: ExpressibleByArgument { - var defaultValueDescription: String { - map { $0.defaultValueDescription }.joined(separator: ", ") - } -} diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index 061492d5a..88b51e20c 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -280,7 +280,7 @@ extension ArgumentSet { let a: [ArgumentSet] = Mirror(reflecting: type.init()) .children - .compactMap { child in + .compactMap { child -> ArgumentSet? in guard var codingKey = child.label else { return nil } if let parsed = child.value as? ArgumentSetProvider { @@ -294,9 +294,12 @@ extension ArgumentSet { let key = InputKey(rawValue: codingKey) return parsed.argumentSet(for: key) } else { + let arg = ArgumentDefinition( + unparsedKey: codingKey, + default: nilOrValue(child.value)) + // Save a non-wrapped property as is - return ArgumentSet( - ArgumentDefinition(unparsedKey: codingKey, default: nilOrValue(child.value))) + return ArgumentSet(arg) } } self.init( diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index 7773b80f6..9b83ff5ce 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -38,19 +38,6 @@ struct ArgumentDefinition { } struct Help { - var options: Options - - // `ArgumentHelp` members - var abstract: String = "" - var discussion: String = "" - var valueName: String = "" - var visibility: ArgumentVisibility = .default - - var defaultValue: String? - var keys: [InputKey] - var allValues: [String] = [] - var isComposite: Bool = false - struct Options: OptionSet { var rawValue: UInt @@ -58,24 +45,29 @@ struct ArgumentDefinition { static let isRepeating = Options(rawValue: 1 << 1) } - init(allValues: [String] = [], options: Options = [], help: ArgumentHelp? = nil, defaultValue: String? = nil, key: InputKey, isComposite: Bool = false) { + var options: Options + var defaultValue: String? + var keys: [InputKey] + var allValues: [String] + var isComposite: Bool + var abstract: String + var discussion: String + var valueName: String + var visibility: ArgumentVisibility + + init( + allValues: [String], + options: Options, + help: ArgumentHelp?, + defaultValue: String?, + key: InputKey, + isComposite: Bool + ) { self.options = options self.defaultValue = defaultValue self.keys = [key] self.allValues = allValues self.isComposite = isComposite - updateArgumentHelp(help: help) - } - - init(type: T.Type, options: Options = [], help: ArgumentHelp? = nil, defaultValue: String? = nil, key: InputKey) { - self.options = options - self.defaultValue = defaultValue - self.keys = [key] - self.allValues = type.allValueStrings - updateArgumentHelp(help: help) - } - - mutating func updateArgumentHelp(help: ArgumentHelp?) { self.abstract = help?.abstract ?? "" self.discussion = help?.discussion ?? "" self.valueName = help?.valueName ?? "" @@ -208,38 +200,230 @@ extension ArgumentDefinition.Kind { } } -extension ArgumentDefinition.Update { - static func appendToArray(forType type: A.Type, key: InputKey) -> ArgumentDefinition.Update { - return ArgumentDefinition.Update.unary { - (origin, name, value, values) in - guard let v = A(argument: value) else { - throw ParserError.unableToParseValue(origin, name, value, forKey: key) - } - values.update(forKey: key, inputOrigin: origin, initial: [A](), closure: { - $0.append(v) + +// MARK: - Common @Argument, @Option, Unparsed Initializer Path +extension ArgumentDefinition { + // MARK: Unparsed Keys + /// Creates an argument definition for a property that isn't parsed from the + /// command line. + /// + /// This initializer is used for any property defined on a `ParsableArguments` + /// type that isn't decorated with one of ArgumentParser's property wrappers. + init(unparsedKey: String, default defaultValue: Any?) { + self.init( + container: Bare.self, + key: InputKey(rawValue: unparsedKey), + kind: .default, + allValues: [], + help: .private, + defaultValueDescription: nil, + parsingStrategy: .default, + parser: { (key, origin, name, valueString) in + throw ParserError.unableToParseValue( + origin, name, valueString, forKey: key, originalError: nil) + }, + initial: defaultValue, + completion: nil) + } + + init( + container: Container.Type, + key: InputKey, + kind: ArgumentDefinition.Kind, + help: ArgumentHelp?, + parsingStrategy: ParsingStrategy, + initial: Container.Initial?, + completion: CompletionKind? + ) where Container: ArgumentDefinitionContainerExpressibleByArgument { + self.init( + container: Container.self, + key: key, + kind: kind, + allValues: Container.Contained.allValueStrings, + help: help, + defaultValueDescription: Container.defaultValueDescription(initial), + parsingStrategy: parsingStrategy, + parser: { (key, origin, name, valueString) -> Container.Contained in + guard let value = Container.Contained(argument: valueString) else { + throw ParserError.unableToParseValue( + origin, name, valueString, forKey: key, originalError: nil) + } + return value + }, + initial: initial, + completion: completion ?? Container.Contained.defaultCompletionKind) + } + + init( + container: Container.Type, + key: InputKey, + kind: ArgumentDefinition.Kind, + help: ArgumentHelp?, + parsingStrategy: ParsingStrategy, + transform: @escaping (String) throws -> Container.Contained, + initial: Container.Initial?, + completion: CompletionKind? + ) where Container: ArgumentDefinitionContainer { + self.init( + container: Container.self, + key: key, + kind: kind, + allValues: [], + help: help, + defaultValueDescription: nil, + parsingStrategy: parsingStrategy, + parser: { (key, origin, name, valueString) -> Container.Contained in + do { + return try transform(valueString) + } catch { + throw ParserError.unableToParseValue( + origin, name, valueString, forKey: key, originalError: error) + } + }, + initial: initial, + completion: completion) + } + + private init( + container: Container.Type, + key: InputKey, + kind: ArgumentDefinition.Kind, + allValues: [String], + help: ArgumentHelp?, + defaultValueDescription: String?, + parsingStrategy: ParsingStrategy, + parser: @escaping (InputKey, InputOrigin, Name?, String) throws -> Container.Contained, + initial: Container.Initial?, + completion: CompletionKind? + ) where Container: ArgumentDefinitionContainer { + self.init( + kind: kind, + help: .init( + allValues: allValues, + options: Container.helpOptions.union(initial != nil ? [.isOptional] : []), + help: help, + defaultValue: defaultValueDescription, + key: key, + isComposite: false), + completion: completion ?? .default, + parsingStrategy: parsingStrategy, + update: .unary({ (origin, name, valueString, parsedValues) in + let value = try parser(key, origin, name, valueString) + Container.update( + parsedValues: &parsedValues, + value: value, + key: key, + origin: origin) + }), + initial: { origin, values in + let inputOrigin: InputOrigin + switch kind { + case .default: + inputOrigin = InputOrigin(element: .defaultValue) + case .named, .positional: + inputOrigin = origin + } + values.set(initial, forKey: key, inputOrigin: inputOrigin) }) - } } } -// MARK: - Help Options +// MARK: - Abstraction over T, Option, Array +protocol ArgumentDefinitionContainer { + associatedtype Contained + associatedtype Initial -protocol ArgumentHelpOptionProvider { static var helpOptions: ArgumentDefinition.Help.Options { get } + static func update( + parsedValues: inout ParsedValues, + value: Contained, + key: InputKey, + origin: InputOrigin) } -extension Optional: ArgumentHelpOptionProvider { - static var helpOptions: ArgumentDefinition.Help.Options { - return [.isOptional] +protocol ArgumentDefinitionContainerExpressibleByArgument: + ArgumentDefinitionContainer where Contained: ExpressibleByArgument { + static func defaultValueDescription(_ initial: Initial?) -> String? +} + +enum Bare { } + +extension Bare: ArgumentDefinitionContainer { + typealias Contained = T + typealias Initial = T + + static var helpOptions: ArgumentDefinition.Help.Options { [] } + + static func update( + parsedValues: inout ParsedValues, + value: Contained, + key: InputKey, + origin: InputOrigin + ) { + parsedValues.set(value, forKey: key, inputOrigin: origin) } } -extension ArgumentDefinition.Help.Options { - init(type: A.Type) { - if let t = type as? ArgumentHelpOptionProvider.Type { - self = t.helpOptions - } else { - self = [] - } +extension Bare: ArgumentDefinitionContainerExpressibleByArgument +where Contained: ExpressibleByArgument { + static func defaultValueDescription(_ initial: T?) -> String? { + guard let initial = initial else { return nil } + return initial.defaultValueDescription + } +} + +extension Optional: ArgumentDefinitionContainer { + typealias Contained = Wrapped + typealias Initial = Wrapped + + static var helpOptions: ArgumentDefinition.Help.Options { [.isOptional] } + + static func update( + parsedValues: inout ParsedValues, + value: Contained, + key: InputKey, + origin: InputOrigin + ) { + parsedValues.set(value, forKey: key, inputOrigin: origin) + } +} + +extension Optional: ArgumentDefinitionContainerExpressibleByArgument +where Contained: ExpressibleByArgument { + static func defaultValueDescription(_ initial: Initial?) -> String? { + guard let initial = initial else { return nil } + return initial.defaultValueDescription + } +} + +extension Array: ArgumentDefinitionContainer { + typealias Contained = Element + typealias Initial = Array + + static var helpOptions: ArgumentDefinition.Help.Options { [.isRepeating] } + + static func update( + parsedValues: inout ParsedValues, + value: Element, + key: InputKey, + origin: InputOrigin + ) { + parsedValues.update( + forKey: key, + inputOrigin: origin, + initial: .init(), + closure: { $0.append(value) }) + } +} + +extension Array: ArgumentDefinitionContainerExpressibleByArgument +where Element: ExpressibleByArgument { + static func defaultValueDescription(_ initial: Array?) -> String? { + guard let initial = initial else { return nil } + guard !initial.isEmpty else { return nil } + return initial + .lazy + .map { $0.defaultValueDescription } + .joined(separator: ", ") } } diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 2eee0a4cc..4c7c88ea7 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -73,7 +73,13 @@ extension ArgumentSet { let helpOptions: ArgumentDefinition.Help.Options = initialValue != nil ? .isOptional : [] let defaultValueString = initialValue == true ? "true" : nil - let help = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: defaultValueString, key: key) + let help = ArgumentDefinition.Help( + allValues: [], + options: helpOptions, + help: help, + defaultValue: defaultValueString, + key: key, + isComposite: false) let arg = ArgumentDefinition(kind: .name(key: key, specification: name), help: help, completion: .default, update: .nullary({ (origin, name, values) in values.set(true, forKey: key, inputOrigin: origin) }), initial: { origin, values in @@ -117,8 +123,8 @@ extension ArgumentSet { { let helpOptions: ArgumentDefinition.Help.Options = required ? [] : .isOptional - let enableHelp = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: initialValue.map(String.init), key: key, isComposite: true) - let disableHelp = ArgumentDefinition.Help(options: [.isOptional], help: help, key: key) + let enableHelp = ArgumentDefinition.Help(allValues: [], options: helpOptions, help: help, defaultValue: initialValue.map(String.init), key: key, isComposite: true) + let disableHelp = ArgumentDefinition.Help(allValues: [], options: [.isOptional], help: help, defaultValue: nil, key: key, isComposite: false) let (enableNames, disableNames) = inversion.enableDisableNamePair(for: key, name: name) @@ -138,7 +144,7 @@ extension ArgumentSet { /// Creates an argument set for an incrementing integer flag. static func counter(key: InputKey, name: NameSpecification, help: ArgumentHelp?) -> ArgumentSet { - let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + let help = ArgumentDefinition.Help(allValues: [], options: [.isOptional, .isRepeating], help: help, defaultValue: nil, key: key, isComposite: false) let arg = ArgumentDefinition(kind: .name(key: key, specification: name), help: help, completion: .default, update: .nullary({ (origin, name, values) in guard let a = values.element(forKey: key)?.value, let b = a as? Int else { throw ParserError.invalidState @@ -151,69 +157,6 @@ extension ArgumentSet { } } -// MARK: - - -extension ArgumentSet { - /// Create a unary / argument that parses the string as `A`. - init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ArgumentDefinition.ParsingStrategy = .default, parseType type: A.Type, name: NameSpecification, default initial: A?, help: ArgumentHelp?, completion: CompletionKind) { - var arg = ArgumentDefinition(key: key, kind: kind, parsingStrategy: parsingStrategy, parser: A.init(argument:), default: initial, completion: completion) - arg.help.updateArgumentHelp(help: help) - arg.help.defaultValue = initial.map { "\($0.defaultValueDescription)" } - self.init(arg) - } -} - -extension ArgumentDefinition { - /// Create a unary / argument that parses using the given closure. - init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .default, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind) { - self.init(key: key, kind: kind, parsingStrategy: parsingStrategy, parser: parser, parseType: type, default: initial, completion: completion, help: ArgumentDefinition.Help(key: key)) - } - - /// Create a unary / argument that parses using the given closure. - init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .default, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind) { - self.init(key: key, kind: kind, parsingStrategy: parsingStrategy, parser: parser, parseType: type, default: initial, completion: completion, help: ArgumentDefinition.Help(type: A.self, key: key)) - } - - private init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .default, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind, help: ArgumentDefinition.Help) { - self.init(kind: kind, help: help, completion: completion, parsingStrategy: parsingStrategy, update: .unary({ (origin, name, value, values) in - guard let v = parser(value) else { - throw ParserError.unableToParseValue(origin, name, value, forKey: key) - } - values.set(v, forKey: key, inputOrigin: origin) - }), initial: { origin, values in - switch kind { - case .default: - values.set(initial, forKey: key, inputOrigin: InputOrigin(element: .defaultValue)) - case .named, .positional: - values.set(initial, forKey: key, inputOrigin: origin) - } - }) - - self.help.options.formUnion(ArgumentDefinition.Help.Options(type: type)) - self.help.defaultValue = initial.map { "\($0)" } - if initial != nil { - self = self.optional - } - } -} - -extension ArgumentDefinition { - /// Creates an argument definition for a property that isn't parsed from the - /// command line. - /// - /// This initializer is used for any property defined on a `ParsableArguments` - /// type that isn't decorated with one of ArgumentParser's property wrappers. - init(unparsedKey: String, default defaultValue: Any?) { - self.init( - key: InputKey(rawValue: unparsedKey), - kind: .default, - parser: { _ in nil }, - default: defaultValue, - completion: .default) - help.updateArgumentHelp(help: .private) - } -} - // MARK: - Parsing from SplitArguments extension ArgumentSet { /// Parse the given input for this set of defined arguments. diff --git a/Sources/ArgumentParser/Parsing/ParserError.swift b/Sources/ArgumentParser/Parsing/ParserError.swift index 8756f9d3f..ed104983b 100644 --- a/Sources/ArgumentParser/Parsing/ParserError.swift +++ b/Sources/ArgumentParser/Parsing/ParserError.swift @@ -31,7 +31,7 @@ enum ParserError: Error { case duplicateExclusiveValues(previous: InputOrigin, duplicate: InputOrigin, originalInput: [String]) /// We need a value for the given key, but it’s not there. Some non-optional option or argument is missing. case noValue(forKey: InputKey) - case unableToParseValue(InputOrigin, Name?, String, forKey: InputKey, originalError: Error? = nil) + case unableToParseValue(InputOrigin, Name?, String, forKey: InputKey, originalError: Error?) case missingSubcommand case userValidationError(Error) case noArguments(Error) diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index 3f09f13b3..1bf3df3ca 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -308,9 +308,12 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type return ArgumentDefinition( kind: .named([.long("version")]), help: .init( + allValues: [], options: [.isOptional], help: "Show the version.", - key: InputKey(rawValue: "")), + defaultValue: nil, + key: InputKey(rawValue: ""), + isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) ) @@ -322,9 +325,12 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type return ArgumentDefinition( kind: .named(names), help: .init( + allValues: [], options: [.isOptional], help: "Show help information.", - key: InputKey(rawValue: "")), + defaultValue: nil, + key: InputKey(rawValue: ""), + isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) ) @@ -334,9 +340,12 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type return ArgumentDefinition( kind: .named([.long("experimental-dump-help")]), help: .init( + allValues: [], options: [.isOptional], help: ArgumentHelp("Dump help information as JSON."), - key: InputKey(rawValue: "")), + defaultValue: nil, + key: InputKey(rawValue: ""), + isComposite: false), completion: .default, update: .nullary({ _, _, _ in }) ) diff --git a/Sources/ArgumentParserTestHelpers/TestHelpers.swift b/Sources/ArgumentParserTestHelpers/TestHelpers.swift index 9a0338c25..2dbbd761e 100644 --- a/Sources/ArgumentParserTestHelpers/TestHelpers.swift +++ b/Sources/ArgumentParserTestHelpers/TestHelpers.swift @@ -257,7 +257,7 @@ extension XCTest { if #available(macOS 10.13, *) { guard (try? process.run()) != nil else { - XCTFail("Couldn't run command process.", file: (file), line: line) + XCTFail("Couldn't run command process.", file: file, line: line) return } } else { @@ -275,7 +275,7 @@ extension XCTest { AssertEqualStringsIgnoringTrailingWhitespace(expected, errorActual + outputActual, file: file, line: line) } - XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: (file), line: line) + XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: file, line: line) #else throw XCTSkip("Not supported on this platform") #endif diff --git a/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift b/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift index 56b54d5c1..53577cbde 100644 --- a/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift @@ -25,7 +25,7 @@ final class CountLinesExampleTests: XCTestCase { func testCountLinesHelp() throws { guard #available(macOS 12, *) else { return } let helpText = """ - USAGE: count-lines [--prefix ] [--verbose] + USAGE: count-lines [] [--prefix ] [--verbose] ARGUMENTS: A file to count lines in. If omitted, counts the diff --git a/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift index ab045db29..fa3d18a40 100644 --- a/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift +++ b/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift @@ -26,7 +26,7 @@ final class CountLinesGenerateManualTests: XCTestCase { .Nm count-lines .Sh SYNOPSIS .Nm - .Ar input-file + .Op Ar input-file .Op Fl -prefix Ar prefix .Op Fl -verbose .Op Fl -help @@ -68,7 +68,7 @@ final class CountLinesGenerateManualTests: XCTestCase { .Nm count-lines .Sh SYNOPSIS .Nm - .Ar input-file + .Op Ar input-file .Op Fl -prefix Ar prefix .Op Fl -verbose .Op Fl -help diff --git a/Tests/ArgumentParserUnitTests/CMakeLists.txt b/Tests/ArgumentParserUnitTests/CMakeLists.txt index 2599ff895..476219c7b 100644 --- a/Tests/ArgumentParserUnitTests/CMakeLists.txt +++ b/Tests/ArgumentParserUnitTests/CMakeLists.txt @@ -2,6 +2,8 @@ add_library(UnitTests ParsableArgumentsValidationTests.swift ErrorMessageTests.swift HelpGenerationTests.swift + HelpGenerationTests+AtArgument.swift + HelpGenerationTests+AtOption.swift NameSpecificationTests.swift SplitArgumentTests.swift StringSnakeCaseTests.swift diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift new file mode 100644 index 000000000..a939b1cdb --- /dev/null +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift @@ -0,0 +1,536 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 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 ArgumentParserTestHelpers +@testable import ArgumentParser + +// This set of tests assert the help output matches the expected value for all +// valid combinations of @Argument. + +extension HelpGenerationTests { + enum AtArgumentTransform { + // Not ExpressibleByArgument + struct A { } + + struct BareNoDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A + } + + struct BareDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A = A() + } + + struct OptionalNoDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A? + } + + struct OptionalDefaultNil: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A? = nil + } + + struct OptionalDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A? = A() + } + + struct ArrayNoDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: [A] + } + + struct ArrayDefaultEmpty: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: [A] = [] + } + + struct ArrayDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: [A] = [A()] + } + } + + func testAtArgumentTransform_BareNoDefault() { + AssertHelp( + .default, + for: AtArgumentTransform.BareNoDefault.self, + equals: """ + USAGE: bare-no-default + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentTransform_BareDefault() { + AssertHelp( + .default, + for: AtArgumentTransform.BareDefault.self, + equals: """ + USAGE: bare-default [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentTransform_OptionalNoDefault() { + AssertHelp( + .default, + for: AtArgumentTransform.OptionalNoDefault.self, + equals: """ + USAGE: optional-no-default [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentTransform_OptionalDefaultNil() { + AssertHelp( + .default, + for: AtArgumentTransform.OptionalDefaultNil.self, + equals: """ + USAGE: optional-default-nil [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentTransform_OptionalDefault() { + AssertHelp( + .default, + for: AtArgumentTransform.OptionalDefault.self, + equals: """ + USAGE: optional-default [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentTransform_ArrayNoDefault() { + AssertHelp( + .default, + for: AtArgumentTransform.ArrayNoDefault.self, + equals: """ + USAGE: array-no-default ... + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentTransform_ArrayDefaultEmpty() { + AssertHelp( + .default, + for: AtArgumentTransform.ArrayDefaultEmpty.self, + equals: """ + USAGE: array-default-empty [ ...] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentTransform_ArrayDefault() { + AssertHelp( + .default, + for: AtArgumentTransform.ArrayDefault.self, + equals: """ + USAGE: array-default [ ...] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } +} + +extension HelpGenerationTests { + enum AtArgumentEBA { + // ExpressibleByArgument + struct A: ExpressibleByArgument { + static var allValueStrings: [String] { ["A()"] } + var defaultValueDescription: String { "A()" } + init() { } + init?(argument: String) { self.init() } + } + + struct BareNoDefault: ParsableCommand { + @Argument(help: "example") + var arg0: A + } + + struct BareDefault: ParsableCommand { + @Argument(help: "example") + var arg0: A = A() + } + + struct OptionalNoDefault: ParsableCommand { + @Argument(help: "example") + var arg0: A? + } + + struct OptionalDefaultNil: ParsableCommand { + @Argument(help: "example") + var arg0: A? = nil + } + + struct OptionalDefault: ParsableCommand { + @Argument(help: "example") + var arg0: A? = A() + } + + struct ArrayNoDefault: ParsableCommand { + @Argument(help: "example") + var arg0: [A] + } + + struct ArrayDefaultEmpty: ParsableCommand { + @Argument(help: "example") + var arg0: [A] = [] + } + + struct ArrayDefault: ParsableCommand { + @Argument(help: "example") + var arg0: [A] = [A()] + } + } + + func testAtArgumentEBA_BareNoDefault() { + AssertHelp( + .default, + for: AtArgumentEBA.BareNoDefault.self, + equals: """ + USAGE: bare-no-default + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBA_BareDefault() { + AssertHelp( + .default, + for: AtArgumentEBA.BareDefault.self, + equals: """ + USAGE: bare-default [] + + ARGUMENTS: + example (default: A()) + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBA_OptionalNoDefault() { + AssertHelp( + .default, + for: AtArgumentEBA.OptionalNoDefault.self, + equals: """ + USAGE: optional-no-default [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBA_OptionalDefaultNil() { + AssertHelp( + .default, + for: AtArgumentEBA.OptionalDefaultNil.self, + equals: """ + USAGE: optional-default-nil [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBA_ArrayNoDefault() { + AssertHelp( + .default, + for: AtArgumentEBA.ArrayNoDefault.self, + equals: """ + USAGE: array-no-default ... + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBA_ArrayDefaultEmpty() { + AssertHelp( + .default, + for: AtArgumentEBA.ArrayDefaultEmpty.self, + equals: """ + USAGE: array-default-empty [ ...] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBA_ArrayDefault() { + AssertHelp( + .default, + for: AtArgumentEBA.ArrayDefault.self, + equals: """ + USAGE: array-default [ ...] + + ARGUMENTS: + example (default: A()) + + OPTIONS: + -h, --help Show help information. + + """) + } +} + +extension HelpGenerationTests { + enum AtArgumentEBATransform { + // ExpressibleByArgument with Transform + struct A: ExpressibleByArgument { + static var allValueStrings: [String] { ["A()"] } + var defaultValueDescription: String { "A()" } + init() { } + init?(argument: String) { self.init() } + } + + struct BareNoDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A + } + + struct BareDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A = A() + } + + struct OptionalNoDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A? + } + + struct OptionalDefaultNil: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A? = nil + } + + struct OptionalDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: A? = A() + } + + struct ArrayNoDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: [A] + } + + struct ArrayDefaultEmpty: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: [A] = [] + } + + struct ArrayDefault: ParsableCommand { + @Argument(help: "example", transform: { _ in A() }) + var arg0: [A] = [A()] + } + } + + func testAtArgumentEBATransform_BareNoDefault() { + AssertHelp( + .default, + for: AtArgumentEBATransform.BareNoDefault.self, + equals: """ + USAGE: bare-no-default + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBATransform_BareDefault() { + AssertHelp( + .default, + for: AtArgumentEBATransform.BareDefault.self, + equals: """ + USAGE: bare-default [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBATransform_OptionalNoDefault() { + AssertHelp( + .default, + for: AtArgumentEBATransform.OptionalNoDefault.self, + equals: """ + USAGE: optional-no-default [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBATransform_OptionalDefaultNil() { + AssertHelp( + .default, + for: AtArgumentEBATransform.OptionalDefaultNil.self, + equals: """ + USAGE: optional-default-nil [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBATransform_OptionalDefault() { + AssertHelp( + .default, + for: AtArgumentEBATransform.OptionalDefault.self, + equals: """ + USAGE: optional-default [] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBATransform_ArrayNoDefault() { + AssertHelp( + .default, + for: AtArgumentEBATransform.ArrayNoDefault.self, + equals: """ + USAGE: array-no-default ... + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBATransform_ArrayDefaultEmpty() { + AssertHelp( + .default, + for: AtArgumentEBATransform.ArrayDefaultEmpty.self, + equals: """ + USAGE: array-default-empty [ ...] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testAtArgumentEBATransform_ArrayDefault() { + AssertHelp( + .default, + for: AtArgumentEBATransform.ArrayDefault.self, + equals: """ + USAGE: array-default [ ...] + + ARGUMENTS: + example + + OPTIONS: + -h, --help Show help information. + + """) + } +} diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift new file mode 100644 index 000000000..5e8b85419 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift @@ -0,0 +1,417 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 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 ArgumentParserTestHelpers +@testable import ArgumentParser + +// This set of tests assert the help output matches the expected value for all +// valid combinations of @Option. + +extension HelpGenerationTests { + enum AtOptionTransform { + // Not ExpressibleByArgument + struct A { } + + struct BareNoDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A + } + + struct BareDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A = A() + } + + struct OptionalNoDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A? + } + + struct OptionalDefaultNil: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A? = nil + } + + struct OptionalDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A? = A() + } + + struct ArrayNoDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: [A] + } + + struct ArrayDefaultEmpty: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: [A] = [] + } + + struct ArrayDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: [A] = [A()] + } + } + + func testAtOptionTransform_BareNoDefault() { + AssertHelp(.default, for: AtOptionTransform.BareNoDefault.self, equals: """ + USAGE: bare-no-default --arg0 + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionTransform_BareDefault() { + AssertHelp(.default, for: AtOptionTransform.BareDefault.self, equals: """ + USAGE: bare-default [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionTransform_OptionalNoDefault() { + AssertHelp(.default, for: AtOptionTransform.OptionalNoDefault.self, equals: """ + USAGE: optional-no-default [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionTransform_OptionalDefaultNil() { + AssertHelp(.default, for: AtOptionTransform.OptionalDefaultNil.self, equals: """ + USAGE: optional-default-nil [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionTransform_OptionalDefault() { + AssertHelp(.default, for: AtOptionTransform.OptionalDefault.self, equals: """ + USAGE: optional-default [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionTransform_ArrayNoDefault() { + AssertHelp(.default, for: AtOptionTransform.ArrayNoDefault.self, equals: """ + USAGE: array-no-default --arg0 ... + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionTransform_ArrayDefaultEmpty() { + AssertHelp(.default, for: AtOptionTransform.ArrayDefaultEmpty.self, equals: """ + USAGE: array-default-empty [--arg0 ...] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionTransform_ArrayDefault() { + AssertHelp(.default, for: AtOptionTransform.ArrayDefault.self, equals: """ + USAGE: array-default [--arg0 ...] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } +} + +extension HelpGenerationTests { + enum AtOptionEBA { + // ExpressibleByArgument + struct A: ExpressibleByArgument { + init() { } + init?(argument: String) { self.init() } + } + + struct BareNoDefault: ParsableCommand { + @Option(help: "example") + var arg0: A + } + + struct BareDefault: ParsableCommand { + @Option(help: "example") + var arg0: A = A() + } + + struct OptionalNoDefault: ParsableCommand { + @Option(help: "example") + var arg0: A? + } + + struct OptionalDefaultNil: ParsableCommand { + @Option(help: "example") + var arg0: A? = nil + } + + struct OptionalDefault: ParsableCommand { + @Option(help: "example") + var arg0: A? = A() + } + + struct ArrayNoDefault: ParsableCommand { + @Option(help: "example") + var arg0: [A] + } + + struct ArrayDefaultEmpty: ParsableCommand { + @Option(help: "example") + var arg0: [A] = [] + } + + struct ArrayDefault: ParsableCommand { + @Option(help: "example") + var arg0: [A] = [A()] + } + } + + func testAtOptionEBA_BareNoDefault() { + AssertHelp(.default, for: AtOptionEBA.BareNoDefault.self, equals: """ + USAGE: bare-no-default --arg0 + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBA_BareDefault() { + AssertHelp(.default, for: AtOptionEBA.BareDefault.self, equals: """ + USAGE: bare-default [--arg0 ] + + OPTIONS: + --arg0 example (default: A()) + -h, --help Show help information. + + """) + } + + func testAtOptionEBA_OptionalNoDefault() { + AssertHelp(.default, for: AtOptionEBA.OptionalNoDefault.self, equals: """ + USAGE: optional-no-default [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBA_OptionalDefaultNil() { + AssertHelp(.default, for: AtOptionEBA.OptionalDefaultNil.self, equals: """ + USAGE: optional-default-nil [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBA_ArrayNoDefault() { + AssertHelp(.default, for: AtOptionEBA.ArrayNoDefault.self, equals: """ + USAGE: array-no-default --arg0 ... + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBA_ArrayDefaultEmpty() { + AssertHelp(.default, for: AtOptionEBA.ArrayDefaultEmpty.self, equals: """ + USAGE: array-default-empty [--arg0 ...] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBA_ArrayDefault() { + AssertHelp(.default, for: AtOptionEBA.ArrayDefault.self, equals: """ + USAGE: array-default [--arg0 ...] + + OPTIONS: + --arg0 example (default: A()) + -h, --help Show help information. + + """) + } +} + +extension HelpGenerationTests { + enum AtOptionEBATransform { + // ExpressibleByArgument with Transform + struct A: ExpressibleByArgument { + init() { } + init?(argument: String) { self.init() } + } + + struct BareNoDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A + } + + struct BareDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A = A() + } + + struct OptionalNoDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A? + } + + struct OptionalDefaultNil: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A? = nil + } + + struct OptionalDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: A? = A() + } + + struct ArrayNoDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: [A] + } + + struct ArrayDefaultEmpty: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: [A] = [] + } + + struct ArrayDefault: ParsableCommand { + @Option(help: "example", transform: { _ in A() }) + var arg0: [A] = [A()] + } + } + + func testAtOptionEBATransform_BareNoDefault() { + AssertHelp(.default, for: AtOptionEBATransform.BareNoDefault.self, equals: """ + USAGE: bare-no-default --arg0 + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBATransform_BareDefault() { + AssertHelp(.default, for: AtOptionEBATransform.BareDefault.self, equals: """ + USAGE: bare-default [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBATransform_OptionalNoDefault() { + AssertHelp(.default, for: AtOptionEBATransform.OptionalNoDefault.self, equals: """ + USAGE: optional-no-default [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBATransform_OptionalDefaultNil() { + AssertHelp(.default, for: AtOptionEBATransform.OptionalDefaultNil.self, equals: """ + USAGE: optional-default-nil [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBATransform_OptionalDefault() { + AssertHelp(.default, for: AtOptionEBATransform.OptionalDefault.self, equals: """ + USAGE: optional-default [--arg0 ] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBATransform_ArrayNoDefault() { + AssertHelp(.default, for: AtOptionEBATransform.ArrayNoDefault.self, equals: """ + USAGE: array-no-default --arg0 ... + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBATransform_ArrayDefaultEmpty() { + AssertHelp(.default, for: AtOptionEBATransform.ArrayDefaultEmpty.self, equals: """ + USAGE: array-default-empty [--arg0 ...] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } + + func testAtOptionEBATransform_ArrayDefault() { + AssertHelp(.default, for: AtOptionEBATransform.ArrayDefault.self, equals: """ + USAGE: array-default [--arg0 ...] + + OPTIONS: + --arg0 example + -h, --help Show help information. + + """) + } +} diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index f963d89e5..aca783e92 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -197,25 +197,25 @@ extension HelpGenerationTests { func testHelpWithDefaultValues() { AssertHelp(.default, for: D.self, equals: """ - USAGE: d [] [--name ] [--age ] [--logging ] [--lucky ...] [--optional] [--required] [--degree ] [--directory ] [--manual ] [--unspecial ] [--special ] + USAGE: d [] [--name ] [--age ] [--logging ] [--lucky ...] [--optional] [--required] [--degree ] [--directory ] [--manual ] [--unspecial ] [--special ] - ARGUMENTS: - Your occupation. (default: --) + ARGUMENTS: + Your occupation. (default: --) - OPTIONS: - --name Your name. (default: John) - --age Your age. (default: 20) - --logging Whether logging is enabled. (default: false) - --lucky Your lucky numbers. (default: 7, 14) - --optional/--required Vegan diet. (default: optional) - --degree Your degree. (default: bachelor) - --directory Directory. (default: current directory) - --manual Manual Option. (default: default-value) - --unspecial Unspecialized Synthesized (default: one) - --special Specialized Synthesized (default: Apple) - -h, --help Show help information. + OPTIONS: + --name Your name. (default: John) + --age Your age. (default: 20) + --logging Whether logging is enabled. (default: false) + --lucky Your lucky numbers. (default: 7, 14) + --optional/--required Vegan diet. (default: optional) + --degree Your degree. + --directory Directory. (default: current directory) + --manual Manual Option. (default: default-value) + --unspecial Unspecialized Synthesized (default: one) + --special Specialized Synthesized (default: Apple) + -h, --help Show help information. - """) + """) } struct E: ParsableCommand {