Skip to content

Enable default for option and argument arrays #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Documentation/02 Arguments, Options, and Flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,31 @@ Usage: example --user-name <user-name> <value>
See 'example --help' for more information.
```

When using the `default` parameter for an array property, the default values will not be included if additional values are passed on the command line.

```
struct Lucky: ParsableCommand {
@Argument(default: [7, 14, 21])
var numbers: [Int]

mutating func run() throws {
print("""
Your lucky numbers are:
\(numbers.map(String.init).joined(separator: " "))
""")
}
}
```

```
% lucky
Your lucky numbers are:
7 14 21
% lucky 1 2 3
Your lucky numbers are:
1 2 3
```

## Customizing option and flag names

By default, options and flags derive the name that you use on the command line from the name of the property, such as `--count` and `--index`. Camel-case names are converted to lowercase with hyphen-separated words, like `--strip-whitespace`.
Expand Down
18 changes: 10 additions & 8 deletions Sources/ArgumentParser/Parsable Properties/Argument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,44 +204,45 @@ extension Argument {

/// Creates a property that reads an array from zero or more arguments.
///
/// The property has an empty array as its default value.
///
/// - Parameters:
/// - initial: 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.
public init<Element>(
default initial: Value = [],
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
help: ArgumentHelp? = nil
)
where Element: ExpressibleByArgument, Value == Array<Element>
{
self.init(_parsedValue: .init { key in
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
let arg = ArgumentDefinition(
var arg = ArgumentDefinition(
kind: .positional,
help: help,
parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput,
update: .appendToArray(forType: Element.self, key: key),
initial: { origin, values in
values.set([], forKey: key, inputOrigin: origin)
values.set(initial, forKey: key, inputOrigin: origin)
})
arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil
return ArgumentSet(alternatives: [arg])
})
}

/// Creates a property that reads an array from zero or more arguments,
/// parsing each element with the given closure.
///
/// The property has an empty array as its default value.
///
/// - Parameters:
/// - initial: 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.
/// - transform: A closure that converts a string into this property's
/// element type or throws an error.
public init<Element>(
default initial: Value = [],
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
help: ArgumentHelp? = nil,
transform: @escaping (String) throws -> Element
Expand All @@ -250,7 +251,7 @@ extension Argument {
{
self.init(_parsedValue: .init { key in
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
let arg = ArgumentDefinition(
var arg = ArgumentDefinition(
kind: .positional,
help: help,
parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput,
Expand All @@ -266,8 +267,9 @@ extension Argument {
}
}),
initial: { origin, values in
values.set([], forKey: key, inputOrigin: origin)
values.set(initial, forKey: key, inputOrigin: origin)
})
arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil
return ArgumentSet(alternatives: [arg])
})
}
Expand Down
20 changes: 13 additions & 7 deletions Sources/ArgumentParser/Parsable Properties/Option.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,50 +309,55 @@ extension Option {
/// Creates an array property that reads its values from zero or more
/// labeled options.
///
/// This property defaults to an empty array.
///
/// - Parameters:
/// - name: A specification for what names are allowed for this flag.
/// - initial: 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 option.
public init<Element>(
name: NameSpecification = .long,
default initial: Array<Element> = [],
parsing parsingStrategy: ArrayParsingStrategy = .singleValue,
help: ArgumentHelp? = nil
) where Element: ExpressibleByArgument, Value == Array<Element> {
self.init(_parsedValue: .init { key in
let kind = ArgumentDefinition.Kind.name(key: key, specification: name)
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
let arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in
values.set([], forKey: key, inputOrigin: origin)
var arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in
values.set(initial, forKey: key, inputOrigin: origin)
})
arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil
return ArgumentSet(alternatives: [arg])
})
}

/// Creates an array property that reads its values from zero or more
/// labeled options, parsing with the given closure.
///
/// This property defaults to an empty array.
/// This property defaults to an empty array if the `initial` parameter
/// is not specified.
///
/// - Parameters:
/// - name: A specification for what names are allowed for this flag.
/// - initial: A default value to use for this property. If `initial` is
/// `nil`, this option defaults to an empty array.
/// - parsingStrategy: The behavior to use when parsing multiple values
/// from the command-line arguments.
/// - help: Information about how to use this option.
/// - transform: A closure that converts a string into this property's
/// element type or throws an error.
public init<Element>(
name: NameSpecification = .long,
default initial: Array<Element> = [],
parsing parsingStrategy: ArrayParsingStrategy = .singleValue,
help: ArgumentHelp? = nil,
transform: @escaping (String) throws -> Element
) where Value == Array<Element> {
self.init(_parsedValue: .init { key in
let kind = ArgumentDefinition.Kind.name(key: key, specification: name)
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
let arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .unary({
var arg = ArgumentDefinition.init(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .unary({
(origin, name, valueString, parsedValues) in
do {
let transformedElement = try transform(valueString)
Expand All @@ -363,8 +368,9 @@ extension Option {
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
}
}), initial: { origin, values in
values.set([], forKey: key, inputOrigin: origin)
values.set(initial, forKey: key, inputOrigin: origin)
})
arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil
return ArgumentSet(alternatives: [arg])
})
}
Expand Down
17 changes: 16 additions & 1 deletion Sources/ArgumentParser/Parsing/ParsedValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//
//===----------------------------------------------------------------------===//

struct InputKey: RawRepresentable, Equatable {
struct InputKey: RawRepresentable, Hashable {
var rawValue: String

init(rawValue: String) {
Expand All @@ -32,6 +32,7 @@ struct ParsedValues {
var value: Any
/// Where in the input that this came from.
var inputOrigin: InputOrigin
fileprivate var shouldClearArrayIfParsed = true
}

/// These are the parsed key-value pairs.
Expand Down Expand Up @@ -73,4 +74,18 @@ extension ParsedValues {
e.inputOrigin.formUnion(inputOrigin)
set(e)
}

mutating func update<A>(forKey key: InputKey, inputOrigin: InputOrigin, initial: [A], closure: (inout [A]) -> Void) {
var e = element(forKey: key) ?? Element(key: key, value: initial, inputOrigin: InputOrigin())
var v = (e.value as? [A] ) ?? initial
// The first time a value is parsed from command line, empty array of any default values.
if e.shouldClearArrayIfParsed {
v.removeAll()
e.shouldClearArrayIfParsed = false
}
closure(&v)
e.value = v
e.inputOrigin.formUnion(inputOrigin)
set(e)
}
}
69 changes: 69 additions & 0 deletions Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,72 @@ extension DefaultsEndToEndTests {
}
}

fileprivate struct Quux: ParsableArguments {
@Option(default: ["A", "B"], parsing: .upToNextOption)
var letters: [String]

@Argument(default: [1, 2])
var numbers: [Int]
}

extension DefaultsEndToEndTests {
func testParsing_ArrayDefaults() throws {
AssertParse(Quux.self, []) { qux in
XCTAssertEqual(qux.letters, ["A", "B"])
XCTAssertEqual(qux.numbers, [1, 2])
}
AssertParse(Quux.self, ["--letters", "C", "D"]) { qux in
XCTAssertEqual(qux.letters, ["C", "D"])
XCTAssertEqual(qux.numbers, [1, 2])
}
AssertParse(Quux.self, ["3", "4"]) { qux in
XCTAssertEqual(qux.letters, ["A", "B"])
XCTAssertEqual(qux.numbers, [3, 4])
}
AssertParse(Quux.self, ["3", "4", "--letters", "C", "D"]) { qux in
XCTAssertEqual(qux.letters, ["C", "D"])
XCTAssertEqual(qux.numbers, [3, 4])
}
}
}

fileprivate struct Main: ParsableCommand {
static var configuration = CommandConfiguration(
subcommands: [Sub.self],
defaultSubcommand: Sub.self
)

struct Options: ParsableArguments {
@Option(default: ["A", "B"], parsing: .upToNextOption)
var letters: [String]
}

struct Sub: ParsableCommand {
@Argument(default: [1, 2])
var numbers: [Int]

@OptionGroup()
var options: Main.Options
}
}

extension DefaultsEndToEndTests {
func testParsing_ArrayDefaults_Subcommands() {
AssertParseCommand(Main.self, Main.Sub.self, []) { sub in
XCTAssertEqual(sub.options.letters, ["A", "B"])
XCTAssertEqual(sub.numbers, [1, 2])
}
AssertParseCommand(Main.self, Main.Sub.self, ["--letters", "C", "D"]) { sub in
XCTAssertEqual(sub.options.letters, ["C", "D"])
XCTAssertEqual(sub.numbers, [1, 2])
}
AssertParseCommand(Main.self, Main.Sub.self, ["3", "4"]) { sub in
XCTAssertEqual(sub.options.letters, ["A", "B"])
XCTAssertEqual(sub.numbers, [3, 4])
}
AssertParseCommand(Main.self, Main.Sub.self, ["3", "4", "--letters", "C", "D"]) { sub in
XCTAssertEqual(sub.options.letters, ["C", "D"])
XCTAssertEqual(sub.numbers, [3, 4])
}
}
}
68 changes: 45 additions & 23 deletions Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,22 @@ fileprivate struct AlmostAllArguments: ParsableArguments {
@Argument(help: "", transform: { _ in 0 }) var d4: Int?
@Argument(default: 0, transform: { _ in 0 }) var d5: Int?

@Argument(parsing: .remaining, help: "") var e: [Int]
@Argument() var e0: [Int]
@Argument(help: "") var e1: [Int]
@Argument(parsing: .remaining) var e2: [Int]
@Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e3: [Int]
@Argument(transform: { _ in 0 }) var e4: [Int]
@Argument(help: "", transform: { _ in 0 }) var e5: [Int]
@Argument(parsing: .remaining, transform: { _ in 0 }) var e6: [Int]
@Argument(default: [1, 2], parsing: .remaining, help: "") var e: [Int]
@Argument(parsing: .remaining, help: "") var e1: [Int]
@Argument(default: [1, 2], parsing: .remaining) var e2: [Int]
@Argument(default: [1, 2], help: "") var e3: [Int]
@Argument() var e4: [Int]
@Argument(help: "") var e5: [Int]
@Argument(parsing: .remaining) var e6: [Int]
@Argument(default: [1, 2]) var e7: [Int]
@Argument(default: [1, 2], parsing: .remaining, help: "", transform: { _ in 0 }) var e8: [Int]
@Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e9: [Int]
@Argument(default: [1, 2], parsing: .remaining, transform: { _ in 0 }) var e10: [Int]
@Argument(default: [1, 2], help: "", transform: { _ in 0 }) var e11: [Int]
@Argument(transform: { _ in 0 }) var e12: [Int]
@Argument(help: "", transform: { _ in 0 }) var e13: [Int]
@Argument(parsing: .remaining, transform: { _ in 0 }) var e14: [Int]
@Argument(default: [1, 2], transform: { _ in 0 }) var e15: [Int]
}

fileprivate struct AllOptions: ParsableArguments {
Expand Down Expand Up @@ -115,21 +123,35 @@ fileprivate struct AllOptions: ParsableArguments {
@Option(parsing: .next, transform: { _ in 0 }) var d12: Int?
@Option(help: "", transform: { _ in 0 }) var d13: Int?

@Option(name: .long, parsing: .singleValue, help: "") var e: [Int]
@Option(parsing: .singleValue, help: "") var e1: [Int]
@Option(name: .long, help: "") var e2: [Int]
@Option(name: .long, parsing: .singleValue) var e3: [Int]
@Option(name: .long) var e4: [Int]
@Option(parsing: .singleValue) var e5: [Int]
@Option(help: "") var e6: [Int]

@Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int]
@Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int]
@Option(name: .long, help: "", transform: { _ in 0 }) var f2: [Int]
@Option(name: .long, parsing: .singleValue, transform: { _ in 0 }) var f3: [Int]
@Option(name: .long, transform: { _ in 0 }) var f4: [Int]
@Option(parsing: .singleValue, transform: { _ in 0 }) var f5: [Int]
@Option(help: "", transform: { _ in 0 }) var f6: [Int]
@Option(name: .long, default: [1, 2], parsing: .singleValue, help: "") var e: [Int]
@Option(default: [1, 2], parsing: .singleValue, help: "") var e1: [Int]
@Option(name: .long, parsing: .singleValue, help: "") var e2: [Int]
@Option(name: .long, default: [1, 2], help: "") var e3: [Int]
@Option(parsing: .singleValue, help: "") var e4: [Int]
@Option(default: [1, 2], help: "") var e5: [Int]
@Option(default: [1, 2], parsing: .singleValue) var e6: [Int]
@Option(name: .long, help: "") var e7: [Int]
@Option(name: .long, parsing: .singleValue) var e8: [Int]
@Option(name: .long, default: [1, 2]) var e9: [Int]
@Option(name: .long) var e10: [Int]
@Option(default: [1, 2]) var e11: [Int]
@Option(parsing: .singleValue) var e12: [Int]
@Option(help: "") var e13: [Int]

@Option(name: .long, default: [1, 2], parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int]
@Option(default: [1, 2], parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int]
@Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f2: [Int]
@Option(name: .long, default: [1, 2], help: "", transform: { _ in 0 }) var f3: [Int]
@Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f4: [Int]
@Option(default: [1, 2], help: "", transform: { _ in 0 }) var f5: [Int]
@Option(default: [1, 2], parsing: .singleValue, transform: { _ in 0 }) var f6: [Int]
@Option(name: .long, help: "", transform: { _ in 0 }) var f7: [Int]
@Option(name: .long, parsing: .singleValue, transform: { _ in 0 }) var f8: [Int]
@Option(name: .long, default: [1, 2], transform: { _ in 0 }) var f9: [Int]
@Option(name: .long, transform: { _ in 0 }) var f10: [Int]
@Option(default: [1, 2], transform: { _ in 0 }) var f11: [Int]
@Option(parsing: .singleValue, transform: { _ in 0 }) var f12: [Int]
@Option(help: "", transform: { _ in 0 }) var f13: [Int]
}

struct AllFlags: ParsableArguments {
Expand Down
6 changes: 5 additions & 1 deletion Tests/ArgumentParserUnitTests/HelpGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ extension HelpGenerationTests {
@Option(default: 20, help: "Your age.")
var age: Int

@Option(default: [7, 14], parsing: .upToNextOption, help: ArgumentHelp("Your lucky numbers.", valueName: "numbers"))
var lucky: [Int]

@Option(default: false, help: "Whether logging is enabled.")
var logging: Bool

Expand All @@ -160,7 +163,7 @@ extension HelpGenerationTests {

func testHelpWithDefaultValues() {
AssertHelp(for: D.self, equals: """
USAGE: d [<occupation>] [--name <name>] [--middle-name <middle-name>] [--age <age>] [--logging <logging>] [--optional] [--required] [--degree <degree>] [--directory <directory>]
USAGE: d [<occupation>] [--name <name>] [--middle-name <middle-name>] [--age <age>] [--lucky <numbers> ...] [--logging <logging>] [--optional] [--required] [--degree <degree>] [--directory <directory>]

ARGUMENTS:
<occupation> Your occupation. (default: --)
Expand All @@ -170,6 +173,7 @@ extension HelpGenerationTests {
--middle-name <middle-name>
Your middle name. (default: Winston)
--age <age> Your age. (default: 20)
--lucky <numbers> Your lucky numbers. (default: [7, 14])
--logging <logging> Whether logging is enabled. (default: false)
--optional/--required Vegan diet. (default: optional)
--degree <degree> Your degree. (default: bachelor)
Expand Down