From f6ac7b8118ff5d1bc0faee7f37bf6f8fd8f95602 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 20 Feb 2020 08:07:01 -0800 Subject: [PATCH] Initial import of ArgumentParser --- .gitignore | 5 + CODE_OF_CONDUCT.md | 55 ++ CONTRIBUTING.md | 10 + Documentation/01 Getting Started.md | 292 +++++++++ .../02 Arguments, Options, and Flags.md | 362 +++++++++++ Documentation/03 Commands and Subcommands.md | 178 ++++++ Documentation/04 Customizing Help.md | 170 ++++++ Documentation/05 Validation and Errors.md | 82 +++ .../06 Manual Parsing and Testing.md | 115 ++++ Examples/math/main.swift | 186 ++++++ Examples/repeat/main.swift | 37 ++ Examples/roll/SplitMix64.swift | 26 + Examples/roll/main.swift | 48 ++ LICENSE.txt | 211 +++++++ Package.swift | 57 ++ README.md | 102 ++++ .../Parsable Properties/Argument.swift | 157 +++++ .../Parsable Properties/ArgumentHelp.swift | 51 ++ .../Parsable Properties/Flag.swift | 317 ++++++++++ .../NameSpecification.swift | 149 +++++ .../Parsable Properties/Option.swift | 312 ++++++++++ .../Parsable Properties/OptionGroup.swift | 93 +++ .../Parsable Properties/ValidationError.swift | 62 ++ .../Parsable Types/CommandConfiguration.swift | 76 +++ .../ExpressibleByArgument.swift | 68 +++ .../Parsable Types/ParsableArguments.swift | 190 ++++++ .../Parsable Types/ParsableCommand.swift | 88 +++ .../Parsing/ArgumentDecoder.swift | 242 ++++++++ .../Parsing/ArgumentDefinition.swift | 211 +++++++ .../ArgumentParser/Parsing/ArgumentSet.swift | 468 ++++++++++++++ .../Parsing/ArgumentSetSequence.swift | 63 ++ .../Parsing/CommandParser.swift | 242 ++++++++ .../ArgumentParser/Parsing/InputOrigin.swift | 90 +++ Sources/ArgumentParser/Parsing/Name.swift | 77 +++ Sources/ArgumentParser/Parsing/Parsed.swift | 87 +++ .../ArgumentParser/Parsing/ParsedValues.swift | 86 +++ .../ArgumentParser/Parsing/ParserError.swift | 40 ++ .../Parsing/SplitArguments.swift | 515 ++++++++++++++++ .../ArgumentParser/Usage/HelpCommand.swift | 47 ++ .../ArgumentParser/Usage/HelpGenerator.swift | 267 ++++++++ .../ArgumentParser/Usage/MessageInfo.swift | 105 ++++ .../ArgumentParser/Usage/UsageGenerator.swift | 318 ++++++++++ .../Utilities/StringExtensions.swift | 147 +++++ Sources/ArgumentParser/Utilities/Tree.swift | 97 +++ Sources/TestHelpers/StringHelpers.swift | 28 + Sources/TestHelpers/TestHelpers.swift | 180 ++++++ .../CustomParsingEndToEndTests.swift | 168 +++++ .../EndToEndTests/DefaultsEndToEndTests.swift | 338 +++++++++++ Tests/EndToEndTests/EnumEndToEndTests.swift | 53 ++ Tests/EndToEndTests/FlagsEndToEndTests.swift | 243 ++++++++ .../LongNameWithShortDashEndToEndTests.swift | 109 ++++ .../NestedCommandEndToEndTests.swift | 149 +++++ .../OptionGroupEndToEndTests.swift | 110 ++++ .../EndToEndTests/OptionalEndToEndTests.swift | 208 +++++++ .../PositionalEndToEndTests.swift | 231 +++++++ .../RawRepresentableEndToEndTests.swift | 48 ++ .../RepeatingEndToEndTests.swift | 335 ++++++++++ .../ShortNameEndToEndTests.swift | 139 +++++ Tests/EndToEndTests/SimpleEndToEndTests.swift | 117 ++++ .../SingleValueParsingStrategyTests.swift | 85 +++ .../SubcommandEndToEndTests.swift | 147 +++++ .../ValidationEndToEndTests.swift | 100 +++ Tests/ExampleTests/MathExampleTests.swift | 118 ++++ Tests/ExampleTests/RepeatExampleTests.swift | 69 +++ Tests/ExampleTests/RollDiceExampleTests.swift | 54 ++ Tests/PackageManagerTests/HelpTests.swift | 224 +++++++ .../PackageManager/Clean.swift | 19 + .../PackageManager/Config.swift | 55 ++ .../PackageManager/Describe.swift | 28 + .../PackageManager/GenerateXcodeProject.swift | 45 ++ .../PackageManager/Options.swift | 102 ++++ Tests/PackageManagerTests/Tests.swift | 86 +++ Tests/UnitTests/ErrorMessageTests.swift | 111 ++++ Tests/UnitTests/HelpGenerationTests.swift | 77 +++ Tests/UnitTests/NameSpecificationTests.swift | 80 +++ Tests/UnitTests/SplitArgumentTests.swift | 574 ++++++++++++++++++ Tests/UnitTests/StringWrappingTests.swift | 133 ++++ Tests/UnitTests/TreeTests.swift | 55 ++ Tests/UnitTests/UsageGenerationTests.swift | 114 ++++ 79 files changed, 11333 insertions(+) create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Documentation/01 Getting Started.md create mode 100644 Documentation/02 Arguments, Options, and Flags.md create mode 100644 Documentation/03 Commands and Subcommands.md create mode 100644 Documentation/04 Customizing Help.md create mode 100644 Documentation/05 Validation and Errors.md create mode 100644 Documentation/06 Manual Parsing and Testing.md create mode 100644 Examples/math/main.swift create mode 100644 Examples/repeat/main.swift create mode 100644 Examples/roll/SplitMix64.swift create mode 100644 Examples/roll/main.swift create mode 100644 LICENSE.txt create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/ArgumentParser/Parsable Properties/Argument.swift create mode 100644 Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift create mode 100644 Sources/ArgumentParser/Parsable Properties/Flag.swift create mode 100644 Sources/ArgumentParser/Parsable Properties/NameSpecification.swift create mode 100644 Sources/ArgumentParser/Parsable Properties/Option.swift create mode 100644 Sources/ArgumentParser/Parsable Properties/OptionGroup.swift create mode 100644 Sources/ArgumentParser/Parsable Properties/ValidationError.swift create mode 100644 Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift create mode 100644 Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift create mode 100644 Sources/ArgumentParser/Parsable Types/ParsableArguments.swift create mode 100644 Sources/ArgumentParser/Parsable Types/ParsableCommand.swift create mode 100644 Sources/ArgumentParser/Parsing/ArgumentDecoder.swift create mode 100644 Sources/ArgumentParser/Parsing/ArgumentDefinition.swift create mode 100644 Sources/ArgumentParser/Parsing/ArgumentSet.swift create mode 100644 Sources/ArgumentParser/Parsing/ArgumentSetSequence.swift create mode 100644 Sources/ArgumentParser/Parsing/CommandParser.swift create mode 100644 Sources/ArgumentParser/Parsing/InputOrigin.swift create mode 100644 Sources/ArgumentParser/Parsing/Name.swift create mode 100644 Sources/ArgumentParser/Parsing/Parsed.swift create mode 100644 Sources/ArgumentParser/Parsing/ParsedValues.swift create mode 100644 Sources/ArgumentParser/Parsing/ParserError.swift create mode 100644 Sources/ArgumentParser/Parsing/SplitArguments.swift create mode 100644 Sources/ArgumentParser/Usage/HelpCommand.swift create mode 100644 Sources/ArgumentParser/Usage/HelpGenerator.swift create mode 100644 Sources/ArgumentParser/Usage/MessageInfo.swift create mode 100644 Sources/ArgumentParser/Usage/UsageGenerator.swift create mode 100644 Sources/ArgumentParser/Utilities/StringExtensions.swift create mode 100644 Sources/ArgumentParser/Utilities/Tree.swift create mode 100644 Sources/TestHelpers/StringHelpers.swift create mode 100644 Sources/TestHelpers/TestHelpers.swift create mode 100644 Tests/EndToEndTests/CustomParsingEndToEndTests.swift create mode 100644 Tests/EndToEndTests/DefaultsEndToEndTests.swift create mode 100644 Tests/EndToEndTests/EnumEndToEndTests.swift create mode 100644 Tests/EndToEndTests/FlagsEndToEndTests.swift create mode 100644 Tests/EndToEndTests/LongNameWithShortDashEndToEndTests.swift create mode 100644 Tests/EndToEndTests/NestedCommandEndToEndTests.swift create mode 100644 Tests/EndToEndTests/OptionGroupEndToEndTests.swift create mode 100644 Tests/EndToEndTests/OptionalEndToEndTests.swift create mode 100644 Tests/EndToEndTests/PositionalEndToEndTests.swift create mode 100644 Tests/EndToEndTests/RawRepresentableEndToEndTests.swift create mode 100644 Tests/EndToEndTests/RepeatingEndToEndTests.swift create mode 100644 Tests/EndToEndTests/ShortNameEndToEndTests.swift create mode 100644 Tests/EndToEndTests/SimpleEndToEndTests.swift create mode 100644 Tests/EndToEndTests/SingleValueParsingStrategyTests.swift create mode 100644 Tests/EndToEndTests/SubcommandEndToEndTests.swift create mode 100644 Tests/EndToEndTests/ValidationEndToEndTests.swift create mode 100644 Tests/ExampleTests/MathExampleTests.swift create mode 100644 Tests/ExampleTests/RepeatExampleTests.swift create mode 100644 Tests/ExampleTests/RollDiceExampleTests.swift create mode 100644 Tests/PackageManagerTests/HelpTests.swift create mode 100644 Tests/PackageManagerTests/PackageManager/Clean.swift create mode 100644 Tests/PackageManagerTests/PackageManager/Config.swift create mode 100644 Tests/PackageManagerTests/PackageManager/Describe.swift create mode 100644 Tests/PackageManagerTests/PackageManager/GenerateXcodeProject.swift create mode 100644 Tests/PackageManagerTests/PackageManager/Options.swift create mode 100644 Tests/PackageManagerTests/Tests.swift create mode 100644 Tests/UnitTests/ErrorMessageTests.swift create mode 100644 Tests/UnitTests/HelpGenerationTests.swift create mode 100644 Tests/UnitTests/NameSpecificationTests.swift create mode 100644 Tests/UnitTests/SplitArgumentTests.swift create mode 100644 Tests/UnitTests/StringWrappingTests.swift create mode 100644 Tests/UnitTests/TreeTests.swift create mode 100644 Tests/UnitTests/UsageGenerationTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..0c62eec19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +.swiftpm diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..2b0a60355 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,55 @@ +# Code of Conduct +To be a truly great community, Swift.org needs to welcome developers from all walks of life, +with different backgrounds, and with a wide range of experience. A diverse and friendly +community will have more great ideas, more unique perspectives, and produce more great +code. We will work diligently to make the Swift community welcoming to everyone. + +To give clarity of what is expected of our members, Swift.org has adopted the code of conduct +defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source +communities, and we think it articulates our values well. The full text is copied below: + +### Contributor Code of Conduct v1.3 +As contributors and maintainers of this project, and in the interest of fostering an open and +welcoming community, we pledge to respect all people who contribute through reporting +issues, posting feature requests, updating documentation, submitting pull requests or patches, +and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, sexual +orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or +nationality. + +Examples of unacceptable behavior by participants include: +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing other’s private information, such as physical or electronic addresses, without explicit permission +- Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, +commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of +Conduct, or to ban temporarily or permanently any contributor for other behaviors that they +deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and +consistently applying these principles to every aspect of managing this project. Project +maintainers who do not follow or enforce the Code of Conduct may be permanently removed +from the project team. + +This code of conduct applies both within project spaces and in public spaces when an +individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +contacting a project maintainer at [conduct@swift.org](mailto:conduct@swift.org). All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and appropriate to the +circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter +of an incident. + +*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](http://contributor-covenant.org/version/1/3/0/).* + +### Reporting +A working group of community members is committed to promptly addressing any [reported +issues](mailto:conduct@swift.org). Working group members are volunteers appointed by the project lead, with a +preference for individuals with varied backgrounds and perspectives. Membership is expected +to change regularly, and may grow or shrink. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..e36bf969f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,10 @@ +By submitting a pull request, you represent that you have the right to license +your contribution to Apple and the community, and agree by submitting the patch +that your contributions are licensed under the [Swift +license](https://swift.org/LICENSE.txt). + +--- + +Before submitting the pull request, please make sure you have tested your +changes and that they follow the Swift project [guidelines for contributing +code](https://swift.org/contributing/#contributing-code). diff --git a/Documentation/01 Getting Started.md b/Documentation/01 Getting Started.md new file mode 100644 index 000000000..dbb1ad457 --- /dev/null +++ b/Documentation/01 Getting Started.md @@ -0,0 +1,292 @@ +# Getting Started with `ArgumentParser` + +Learn to set up and customize a simple command-line tool. + +This guide walks through building an example command. You'll learn about the different tools that `ArgumentParser` provides for defining a command's options, customizing the interface, and providing help text for your user. + +## Adding `ArgumentParser` as a Dependency + +First, we need to add `swift-argument-parser` as a dependency to our package, +and then include `"ArgumentParser"` as a dependency for our executable target. +Our "Package.swift" file ends up looking like this: + +```swift +// swift-tools-version:5.2 +import PackageDescription + +let package = Package( + name: "random", + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.0.1"), + ], + targets: [ + .target( + name: "count", + dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]), + ] +) +``` + +## Building Our First Command + +Let's write a tool called `count` that reads an input file, counts the words, and writes the result to an output file. + +We can run our `count` tool like this: + +``` +% count readme.md readme.counts +Counting words in 'readme.md' and writing the result into 'readme.counts'. +``` + +We'll define the initial version of the command as a type that conforms to the `ParsableCommand` protocol: + +```swift +import ArgumentParser + +struct Count: ParsableCommand { + @Argument() + var inputFile: String + + @Argument() + var outputFile: String + + func run() throws { + print(""" + Counting words in '\(inputFile)' \ + and writing the result into '\(outputFile)'. + """) + + // Read 'inputFile', count the words, and save to 'outputFile'. + } +} + +Count.main() +``` + +In the code above, the `inputFile` and `outputFile` properties use the `@Argument` property wrapper. `ArgumentParser` uses this wrapper to denote a positional command-line input — because `inputFile` is specified first in the `Count` type, it's the first value read from the command-line, and `outputFile` is read second. + +We've implemented the command's logic in its `run()` method. Here, we're printing out a message confirming the names of the files the user gave. (You can find a full implementation of the completed command at the end of this guide.) + +Finally, you tell the parser to execute the `Count` command by calling its static `main()` method. This method parses the command-line arguments, verifies that they match up with what we've defined in `Count`, and either calls the `run()` method or exits with a helpful message. + + +## Working with Named Options + +Our `count` tool may have a usability problem — it's not immediately clear whether a user should provide the input file first, or the output file. Instead of using positional arguments for our two inputs, let's specify that they should be labeled options: + +``` +% count --input-file readme.md --output-file readme.counts +Counting words in 'readme.md' and writing the result into 'readme.counts'. +``` + +We do this by using the `@Option` property wrapper instead of `@Argument`: + +```swift +struct Count: ParsableCommand { + @Option() + var inputFile: String + + @Option() + var outputFile: String + + func run() throws { + print(""" + Counting words in '\(inputFile)' \ + and writing the result into '\(outputFile)'. + """) + + // Read 'inputFile', count the words, and save to 'outputFile'. + } +} +``` + +The `@Option` property wrapper denotes a command-line input that looks like `--name value`, deriving its name from the name of your property. + +This interface has a trade-off for the users of our `count` tool: With `@Argument`, users don't need to type as much, but have to remember whether the input file or the output file needs to be given first. Using `@Option` makes the user type a little more, but the distinction between values is explicit. Options are order-independent, as well, so the user can name the input and output files in either order: + +``` +% count --output-file readme.counts --input-file readme.md +Counting words in 'readme.md' and writing the result into 'readme.counts'. +``` + +## Adding a Flag + +Next, we want to add a `--verbose` flag to our tool, and only print the message if the user specifies that option: + +``` +% count --input-file readme.md --output-file readme.counts +(no output) +% count --verbose --input-file readme.md --output-file readme.counts +Counting words in 'readme.md' and writing the result into 'readme.counts'. +``` + +Let's change our `Count` type to look like this: + +```swift +struct Count: ParsableCommand { + @Option() + var inputFile: String + + @Option() + var outputFile: String + + @Flag() + var verbose: Bool + + func run() throws { + if verbose { + print(""" + Counting words in '\(inputFile)' \ + and writing the result into '\(outputFile)'. + """) + } + + // Read 'inputFile', count the words, and save to 'outputFile'. + } +} +``` + +The `@Flag` property wrapper denotes a command-line input that looks like `--name`, deriving its name from the name of your property. Flags are most frequently used for Boolean values, like the `verbose` property here. + + +## Using Custom Names + +We can customize the names of our options and add an alternative to the `verbose` flag, so that users can specify `-v` instead of `--verbose`. The new interface will look like this: + +``` +% count -v -i readme.md -o readme.counts +Counting words in 'readme.md' and writing the result into 'readme.counts'. +% count --input readme.md --output readme.counts -v +Counting words in 'readme.md' and writing the result into 'readme.counts'. +% count -o readme.counts -i readme.md --verbose +Counting words in 'readme.md' and writing the result into 'readme.counts'. +``` + +Customize the input names by passing `name` parameters to the `@Option` and `@Flag` initializers: + +```swift +struct Count: ParsableCommand { + @Option(name: [.short, .customLong("input")]) + var inputFile: String + + @Option(name: [.short, .customLong("output")]) + var outputFile: String + + @Flag(name: .shortAndLong) + var verbose: Bool + + func run() throws { ... } +} +``` + +The default name specification is `.long`, which uses a property's with a two-dash prefix. `.short` uses only the first letter of a property's name with a single-dash prefix, and allows combining groups of short options. You can specify custom short and long names with the `.customShort(_:)` and `.customLong(_:)` methods, respectively, or use the combined `.shortAndLong` property to specify the common case of both the short and long derived names. + +## Providing Help + +`ArgumentParser` automatically generates help for any command when a user provides the `-h` or `--help` flags: + +``` +% count --help +USAGE: count --input --output [--verbose] + +OPTIONS: + -i, --input + -o, --output + -v, --verbose + -h, --help Show help information. +``` + +This is a great start — you can see that all the custom names are visible, and the help shows that values are expected for the `--input` and `--output` options. However, our custom options and flag don't have any descriptive text. Let's add that now, by passing string literals as the `help` parameter: + +```swift +struct Count: ParsableCommand { + @Option(name: [.short, .customLong("input")], help: "A file to read.") + var inputFile: String + + @Option(name: [.short, .customLong("output")], help: "A file to save word counts to.") + var outputFile: String + + @Flag(name: .shortAndLong, help: "Print status updates while counting.") + var verbose: Bool + + func run() throws { ... } +} +``` + +The help screen now includes descriptions for each parameter: + +``` +% count -h +USAGE: count --input --output [--verbose] + +OPTIONS: + -i, --input A file to read. + -o, --output A file to save word counts to. + -v, --verbose Print status updates while counting. + -h, --help Show help information. + +``` + +## The Complete Utility + +As promised, here's the complete `count` command, for your experimentation: + +```swift +struct Count: ParsableCommand { + static let configuration = CommandConfiguration(abstract: "Word counter.") + + @Option(name: [.short, .customLong("input")], help: "A file to read.") + var inputFile: String + + @Option(name: [.short, .customLong("output")], help: "A file to save word counts to.") + var outputFile: String + + @Flag(name: .shortAndLong, help: "Print status updates while counting.") + var verbose: Bool + + func run() throws { + if verbose { + print(""" + Counting words in '\(inputFile)' \ + and writing the result into '\(outputFile)'. + """) + } + + guard let input = try? String(contentsOfFile: inputFile) else { + throw RuntimeError("Couldn't read from '\(inputFile)'!") + } + + let words = input.components(separatedBy: .whitespacesAndNewlines) + .map { word in + word.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + .lowercased() + } + .compactMap { word in word.isEmpty ? nil : word } + + let counts = Dictionary(grouping: words, by: { $0 }) + .mapValues { $0.count } + .sorted(by: { $0.value > $1.value }) + + if verbose { + print("Found \(counts.count) words.") + } + + let output = counts.map { word, count in "\(word): \(count)" } + .joined(separator: "\n") + + guard let _ = try? output.write(toFile: outputFile, atomically: true, encoding: .utf8) else { + throw RuntimeError("Couldn't write to '\(outputFile)'!") + } + } +} + +struct RuntimeError: Error, CustomStringConvertible { + var description: String + + init(_ description: String) { + self.description = description + } +} + +Count.main() +``` diff --git a/Documentation/02 Arguments, Options, and Flags.md b/Documentation/02 Arguments, Options, and Flags.md new file mode 100644 index 000000000..c6fd9086e --- /dev/null +++ b/Documentation/02 Arguments, Options, and Flags.md @@ -0,0 +1,362 @@ +# Declaring Arguments, Options, and Flags + +Use the `@Argument`, `@Option` and `@Flag` property wrappers to declare the command-line interface for your command. + +When creating commands, you can define three primary kinds of command-line inputs: + +- *Arguments* are values given by a user, and are read in order from first to last. For example, this command is called with three file names as arguments: + + ``` + % example file1.swift file2.swift file3.swift + ``` + +- *Options* are named key-value pairs. Keys start with one or two dashes (`-` or `--`), and a user can separate the key and value with an equal sign (`=`) or a space. This command is called with two options: + + ``` + % example --count=5 --index 2 + ``` + +- *Flags* are like options, but without a paired value. Instead, their presence indicates a particular value (usually `true`). This command is called with two flags: + + ``` + % example --verbose --strip-whitespace + ``` + +The three preceding examples could be calls of this `Example` command: + +```swift +struct Example: ParsableCommand { + @Argument() var files: [String] + + @Option() var count: Int? + + @Option(default: 0) var index: Int + + @Flag() var verbose: Bool + + @Flag() var stripWhitespace: Bool +} +``` + +This example shows how `ArgumentParser` provides defaults that speed up your initial development process: + +- Option and flag names are derived from the names of your command's properties. +- Whether arguments are required and what kinds of inputs are valid is based on your properties' types. + +In this example, all of the properties have default values — Boolean flags always default to `false`, optional properties default to `nil`, and arrays default to an empty array. An option or argument with a `default` parameter can also be omitted by the user. + +Users must provide values for all properties with no implicit or specified default. For example, this command would require one integer argument and a string with the key `--user-name`. + +```swift +struct Example: ParsableCommand { + @Option() + var userName: String + + @Argument() + var value: Int +} +``` + +When called without both values, the command exits with an error: + +``` +% example 5 +Error: Missing '--user-name ' +Usage: example --user-name +% example --user-name kjohnson +Error: Missing '' +Usage: example --user-name +``` + +## 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`. + +You can override this default by specifying one or more name specifications in the `@Option` or `@Flag` initializers. This command demonstrates the four name specifications: + +```swift +struct Example: ParsableCommand { + @Flag(name: .long) // Same as the default + var stripWhitespace: Bool + + @Flag(name: .short) + var verbose: Bool + + @Option(name: .customLong("count")) + var iterationCount: Int + + @Option(name: [.customShort("I"), .long]) + var inputFile: String +} +``` + +* Specifying `.long` or `.short` uses the property's name as the source of the command-line name. Long names use the whole name, prefixed by two dashes, while short names are a single character prefixed by a single dash. In this example, the `stripWhitespace` and `verbose` flags are specified in this way: + + ``` + % example --strip-whitespace -v + ``` + +* Specifying `.customLong(_:)` or `.customShort(_:)` uses the given string or character as the long or short name for the property. + + ``` + % example --count 10 -I file1.swift + ``` + +* Use array literal syntax to specify multiple names. The `inputFile` property can alternatively be given with the default long name: + + ``` + % example --input-file file1.swift + ``` + +> **Note:** You can also pass `withSingleDash: true` to `.customLong` to create a single-dash flag or option, such as `-verbose`. Use this name specification only when necessary, such as when migrating a legacy command line interface. Using long names with a single-dash prefix can lead to ambiguity with combined short names: it may not be obvious whether `-file` is a single option or the combination of the four short options `-f`, `-i`, `-l`, and `-e`. + +## Parsing custom types + +Arguments and options can be parsed from any type that conforms to the `ExpressibleByArgument` protocol. Standard library integer and floating-point types, strings, and Booleans all conform to `ExpressibleByArgument`. + +You can make your own custom types conform to `ExpressibleByArgument` by implementing `init(argument:)`: + +```swift +struct Path { + var pathString: String + + init?(argument: String) { + self.pathString = argument + } +} + +struct Example: ParsableCommand { + @Argument() var inputFile: Path +} +``` + +The library provides a default implementation for `RawRepresentable` types, like string-backed enumerations, so you only need to declare conformance. + +```swift +enum ReleaseMode: String, ExpressibleByArgument { + case debug, release +} + +struct Example: ParsableCommand { + @Option() var mode: ReleaseMode + + func run() throws { + print(mode) + } +} +``` + +The user can provide the raw values on the command line, which are then converted to your custom type. Only valid values are allowed: + +``` +% example --mode release +release +% example --mode future +Error: The value 'future' is invalid for '--mode ' +``` + +To use a non-`ExpressibleByArgument` type for an argument or option, you can instead provide a throwing `transform` function that converts the parsed string to your desired type. This is a good idea for custom types that are more complex than a `RawRepresentable` type, or for types you don't define yourself. + +```swift +enum Format { + case text + case other(String) + + init(_ string: String) throws { + if string == "text" { + self = .text + } else { + self = .other(string) + } + } +} + +struct Example: ParsableCommand { + @Argument(transform: Format.init) + var format: Format +} +``` + +Throw an error from the `transform` function to indicate that the user provided an invalid value for that type. + +## Using flag inversions, enumerations, and counts + +Flags are most frequently used for `Bool` properties, with a default value of `false`. You can generate a `true`/`false` pair of flags by specifying a flag inversion: + +```swift +struct Example: ParsableCommand { + @Flag(inversion: .prefixedNo) + var index: Bool + + @Flag(inversion: .prefixedEnableDisable) + var requiredElement: Bool + + func run() throws { + print(index, requiredElement) + } +} +``` + +Since these flags are non-optional and don't have default values, they're now required when calling the command. The specified prefixes are prepended to the long names for the flags: + +``` +% example --index --enable-required-element +true true +% example --no-index --disable-required-element +false false +``` + +You can also use flags with types that are `CaseIterable` and `RawRepresentable` with a string raw value. This is useful for providing custom names for a Boolean value, for an exclusive choice between more than two names, or for collecting multiple values from a set of defined choices. + +```swift +enum CacheMethod: String, CaseIterable { + case inMemoryCache + case persistentCache +} + +enum Color: String, CaseIterable { + case pink, purple, silver +} + +struct Example: ParsableCommand { + @Flag() var cacheMethod: CacheMethod + + @Flag() var colors: [Color] + + func run() throws { + print(cacheMethod) + print(colors) + } +} +``` + +The flag names in this case are drawn from the raw values: + +``` +% example --in-memory-cache --pink --silver +.inMemoryCache +[.pink, .silver] +% example +Error: Missing one of: '--in-memory-cache', '--persistent-cache' +``` + +Finally, when a flag is of type `Int`, the value is parsed as a count of the number of times that the flag is specified. + +```swift +struct Example: ParsableCommand { + @Flag(name: .shortAndLong) + var verbose: Int + + func run() throws { + print("Verbosity level: \(verbose)") + } +} +``` + +`verbose` in this example defaults to zero, and counts the number of times that `-v` or `--verbose` is given. + +``` +% example --verbose +Verbosity level: 1 +% example -vvvv +Verbosity level: 4 +``` + +## Specifying a parsing strategy + +When parsing a list of command-line inputs, `ArgumentParser` distinguishes between dash-prefixed keys and un-prefixed values. When looking for the value for a key, only an un-prefixed value will be selected by default. + +For example, this command defines a `--verbose` flag, a `--name` option, and an optional `file` argument: + +```swift +struct Example: ParsableCommand { + @Flag() var verbose: Bool + @Option() var name: String + @Argument() var file: String? + + func run() throws { + print("Verbose: \(verbose), name: \(name), file: \(file ?? "none")") + } +} +``` + +When calling this command, the value for `--name` must be given immediately after the key. If the `--verbose` flag is placed in between, parsing fails with an error: + +``` +% example --verbose --name Tomás +Verbose: true, name: Tomás, file: none +% example --name --verbose Tomás +Error: Missing value for '--name ' +Usage: example [--verbose] --name [] +``` + +Parsing options as arrays is similar — only adjacent key-value pairs are recognized by default. + +## Alternative single-value parsing strategies + +You can change this behavior by providing a different parsing strategy in the `@Option` initializer. **Be careful when selecting any of the alternative parsing strategies** — they may lead your command-line tool to have unexpected behavior for users! + +The `.unconditional` parsing strategy uses the immediate next input for the value of the option, even if it starts with a dash. If `name` were instead defined as `@Option(parsing: .unconditional) var name: String`, the second attempt would result in `"--verbose"` being read as the value of `name`: + +``` +% example --name --verbose Tomás +Verbose: false, name: --verbose, file: Tomás +``` + +The `.scanningForValue` strategy, on the other hand, looks ahead in the list of command-line inputs and uses the first un-prefixed value as the input, even if that requires skipping over other flags or options. If `name` were defined as `@Option(parsing: . scanningForValue) var name: String`, the parser would look ahead to find `Tomás`, then pick up parsing where it left off to get the `--verbose` flag: + +``` +% example --name --verbose Tomás +Verbose: true, name: Tomás, file: none +``` + +## Alternative array parsing strategies + +The default strategy for parsing options as arrays is to read each value from a key-value pair. For example, this command expects zero or more input file names: + +```swift +struct Example: ParsableCommand { + @Option() var file: [String] + + @Flag() var verbose: Bool + + func run() throws { + print("Verbose: \(verbose), files: \(file)") + } +} +``` + +As with single values, each time the user provides the `--file` key, they must also provide a value: + +``` +% example --verbose --file file1.swift --file file2.swift +Verbose: true, files: ["file1.swift", "file2.swift"] +% example --file --verbose file1.swift --file file2.swift +Error: Missing value for '--file ' +Usage: example [--file ...] [--verbose] +``` + +The `.unconditionalSingleValue` parsing strategy uses whatever input follows the key as its value, even if that input is dash-prefixed. If `file` were defined as `@Option(parsing: .unconditionalSingleValue) var file: [String]`, then the resulting array could include strings that look like options: + +``` +% example --file file1.swift --file --verbose +Verbose: false, files: ["file1.swift", "--verbose"] +``` + +The `.upToNextOption` parsing strategy uses the inputs that follow the option key until reaching a dash-prefixed input. If `file` were defined as `@Option(parsing: . upToNextOption) var file: [String]`, then the user could specify multiple files without repeating `--file`: + +``` +% example --file file1.swift file2.swift +Verbose: false, files: ["file1.swift", "file2.swift"] +% example --file file1.swift file2.swift --verbose +Verbose: true, files: ["file1.swift", "file2.swift"] +``` + +Finally, the `.remaining` parsing strategy uses all the inputs that follow the option key, regardless of their prefix. If `file` were defined as `@Option(parsing: .remaining) var file: [String]`, then the user would need to specify `--verbose` before the `--file` key for it to be recognized as a flag: + +``` +% example --verbose --file file1.swift file2.swift +Verbose: true, files: ["file1.swift", "file2.swift"] +% example --file file1.swift file2.swift --verbose +Verbose: false, files: ["file1.swift", "file2.swift", "--verbose"] +``` diff --git a/Documentation/03 Commands and Subcommands.md b/Documentation/03 Commands and Subcommands.md new file mode 100644 index 000000000..9bd02c7d3 --- /dev/null +++ b/Documentation/03 Commands and Subcommands.md @@ -0,0 +1,178 @@ +# Defining Commands and Subcommands + +When command-line programs grow larger, it can be useful to divide them into a group of smaller programs, providing an interface through subcommands. Utilities such as `git` and the Swift package manager are able to provide varied interfaces for each of their sub-functions by implementing subcommands such as `git branch` or `swift package init`. + +Generally, these subcommands each have their own configuration options, as well as options that are shared across several or all aspects of the larger program. + +You can build a program with commands and subcommands by defining multiple command types and specifying each command's subcommands in its configuration. For example, here's the interface of a `math` utility that performs operations on a series of values given at the command line. + +``` +% math add 10 15 7 +32 +% math multiply 10 15 7 +1050 +% math stats average 3 4 13 15 15 +10.0 +% math stats average --kind median 3 4 13 15 15 +13.0 +% math stats +OVERVIEW: Calculate descriptive statistics. + +USAGE: math stats + +OPTIONS: + -h, --help Show help information. + +SUBCOMMANDS: + average Print the average of the values. + stdev Print the standard deviation of the values. + quantiles Print the quantiles of the values (TBD). +``` + +Start by defining the root `Math` command. You can provide a static `configuration` property for a command that specifies its subcommands and a default subcommand, if any. + +```swift +struct Math: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "A utility for performing maths.", + subcommands: [Add.self, Multiply.self, Statistics.self], + defaultSubcommand: Add.self) +} +``` + +`Math` lists its three subcommands by their types; we'll see the definitions of `Math`, `Multiply`, and `Statistics` below. `Add` is also given as a default subcommand — this means that it is selected if a user leaves out a subcommand name: + +``` +% math 10 15 7 +32 +``` + +Next, define a `ParsableArguments` type with properties that will be shared across multiple subcommands. Types that conform to `ParsableArguments` can be parsed from command-line arguments, but don't provide any execution through a `run()` method. + +In this case, the `Options` type accepts a `--hexidecimal-output` flag and expects a list of integers. + +```swift +struct Options: ParsableArguments { + @Flag(name: [.long, .customShort("x")], help: "Use hexadecimal notation for the result.") + var hexadecimalOutput: Bool + + @Argument(help: "A group of integers to operate on.") + var values: [Int] +} +``` + +It's time to define our first two subcommands: `Add` and `Multiply`. Both of these subcommands include the arguments defined in the `Options` type by denoting that property with the `@OptionGroup()` property wrapper. `@OptionGroup` doesn't define any new arguments for a command; instead, it splats in the arguments defined by another `ParsableArguments` type. + +```swift +extension Math { + struct Add: ParsableCommand { + static var configuration + = CommandConfiguration(abstract: "Print the sum of the values.") + + @OptionGroup + var options: Math.Options + + func run() { + let result = options.values.reduce(0, +) + print(format(result: result, usingHex: options.hexadecimalOutput)) + } + } + + struct Multiply: ParsableCommand { + static var configuration + = CommandConfiguration(abstract: "Print the product of the values.") + + @OptionGroup + var options: Math.Options + + func run() { + let result = options.values.reduce(1, *) + print(format(result: result, usingHex: options.hexadecimalOutput)) + } + } +} +``` + +Next, we'll define `Statistics`, the third subcommand of `Math`. The `Statistics` command specifies a custom command name (`stats`) in its configuration, overriding the default derived from the type name (`statistics`). It also declares two additional subcommands, meaning that it acts as a forked branch in the command tree, and not a leaf. + +```swift +extension Math { + struct Statistics: ParsableCommand { + static var configuration = CommandConfiguration( + commandName: "stats", + abstract: "Calculate descriptive statistics.", + subcommands: [Average.self, StandardDeviation.self]) + } +} +``` + +Let's finish our subcommands with the `Average` and `StandardDeviation` types. Each of them has slightly different arguments, so they don't use the `Options` type defined above. Each subcommand is ultimately independent, and can specify a combination of shared and unique arguments. + +```swift +extension Math.Statistics { + struct Average: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Print the average of the values.") + + enum Kind: String, ExpressibleByArgument { + case mean, median, mode + } + + @Option(default: .mean, help: "The kind of average to provide.") + var kind: Kind + + @Argument(help: "A group of floating-point values to operate on.") + var values: [Double] + + func calculateMean() -> Double { ... } + func calculateMedian() -> Double { ... } + func calculateMode() -> [Double] { ... } + + func run() { + switch kind { + case .mean: + print(calculateMean()) + case .median: + print(calculateMedian()) + case .mode: + let result = calculateMode() + .map(String.init(describing:)) + .joined(separator: " ") + print(result) + } + } + } + + struct StandardDeviation: ParsableCommand { + static var configuration = CommandConfiguration( + commandName: "stdev", + abstract: "Print the standard deviation of the values.") + + @Argument(help: "A group of floating-point values to operate on.") + var values: [Double] + + func run() { + if values.isEmpty { + print(0.0) + } else { + let sum = values.reduce(0, +) + let mean = sum / Double(values.count) + let squaredErrors = values + .map { $0 - mean } + .map { $0 * $0 } + let variance = squaredErrors.reduce(0, +) + let result = variance.squareRoot() + print(result) + } + } + } +} +``` + +Last but not least, we kick off parsing and execution with a call to the static `main` method on the type at the root of our command tree. The call to main parses the command-line arguments, determines whether a subcommand was selected, and then instantiates and calls the `run()` method on that particular subcommand. + +```swift +Math.main() +``` + +That's it for this doubly-nested `math` command! This example is also provided as a part of the `swift-argument-parser` repository, so you can see it all together and experiment with it [here](https://github.com/apple/swift-argument-parser/blob/master/Examples/math/main.swift). diff --git a/Documentation/04 Customizing Help.md b/Documentation/04 Customizing Help.md new file mode 100644 index 000000000..758bdb84d --- /dev/null +++ b/Documentation/04 Customizing Help.md @@ -0,0 +1,170 @@ +# Customizing Help + +Support your users (and yourself) by providing rich help for arguments and commands. + +You can provide help text when declaring any `@Argument`, `@Option`, or `@Flag` by passing a string literal as the `help` parameter: + +```swift +struct Example: ParsableCommand { + @Flag(help: "Display extra information while processing.") + var verbose: Bool + + @Option(default: 0, help: "The number of extra lines to show.") + var extraLines: Int + + @Argument(help: "The input file.") + var inputFile: String? +} +``` + +Users see these strings in the automatically-generated help screen, which is triggered by the `-h` or `--help` flags, by default: + +``` +% example --help +USAGE: example [--verbose] [--extra-lines ] + +ARGUMENTS: + The input file. + +OPTIONS: + --verbose Display extra information while processing. + --extra-lines + The number of extra lines to show. (default: 0) + -h, --help Show help information. +``` + +## Customizing Help for Arguments + +You can have more control over the help text by passing an `ArgumentHelp` instance instead. The `ArgumentHelp` type can include an abstract (which is what the string literal becomes), a discussion, a value name to use in the usage string, and a Boolean that indicates whether the argument should be visible in the help screen. + +Here's the same command with some extra customization: + +```swift +struct Example: ParsableCommand { + @Flag(help: "Display extra information while processing.") + var verbose: Bool + + @Option(default: 0, help: ArgumentHelp( + "The number of extra lines to show.", + valueName: "n")) + var extraLines: Int + + @Argument(help: ArgumentHelp( + "The input file.", + discussion: "If no input file is provided, the tool reads from stdin.", + valueName: "file")) + var inputFile: String? +} +``` + +...and the help screen: + +``` +USAGE: example [--verbose] [--extra-lines ] [] + +ARGUMENTS: + The input file. + If no input file is provided, the tool reads from stdin. + +OPTIONS: + --verbose Display extra information while processing. + --extra-lines The number of extra lines to show. (default: 0) + -h, --help Show help information. +``` + +## Customizing Help for Commands + +In addition to the configuring the command name and subcommands, as described in [Command and Subcommands](03%20Commands%20and%20Subcommands.md), you can also configure a command's help text by providing and abstract and discussion. + +```swift +struct Repeat: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Repeats your input phrase.", + discussion: """ + Prints to stdout forever, or until you halt the program. + """) + + @Argument(help: "The phrase to repeat.") + var phrase: String + + func run() throws { + while true { print(phrase) } + } +} +``` + +The abstract and discussion appear in the generated help screen: + +``` +% repeat --help +OVERVIEW: Repeats your input phrase. + +Prints to stdout forever, or until you halt the program. + +USAGE: repeat + +ARGUMENTS: + The phrase to repeat. + +OPTIONS: + -h, --help Show help information. + +% repeat hello! +hello! +hello! +hello! +hello! +hello! +hello! +... +``` + +## Modifying the Help Flag Names + +Users can see the help screen for a command by passing either `-h` or `--help` flag, by default. If you need to use one of those flags for another purpose, you can provide alternative names when configuring a root command. + +```swift +struct Example: ParsableCommand { + static let configuration: CommandConfiguration( + helpName: [.long, .customShort("?")]) + + @Option(name: .shortAndLong, help: "The number of history entries to show.") + var historyDepth: Int + + func run() throws { + printHistory(depth: historyDepth) + } +} +``` + +When running the command, `-h` matches the short name of the `historyDepth` property, and `-?` displays the help screen. + +``` +% example -h 3 +... +% example -? +USAGE: example --history-depth + +ARGUMENTS: + The phrase to repeat. + +OPTIONS: + -h, --history-depth The number of history entries to show. + -?, --help Show help information. +``` + +## Hiding Arguments and Commands + +You may want to suppress features under development or experimental flags from the generated help screen. You can hide an argument or a subcommand by passing `shouldDisplay: false` to the property wrapper or `CommandConfiguration` initializers, respectively. + +`ArgumentHelp` include a `.hidden` static property that makes it even simpler to hide arguments: + +```swift +struct Example: ParsableCommand { + @Flag(help: .hidden) + var experimentalEnableWidgets: Bool +} +``` + + + diff --git a/Documentation/05 Validation and Errors.md b/Documentation/05 Validation and Errors.md new file mode 100644 index 000000000..62d7bace7 --- /dev/null +++ b/Documentation/05 Validation and Errors.md @@ -0,0 +1,82 @@ +# Validation and Errors + +Provide helpful feedback to users when things go wrong. + +## Validating Command-Line Input + +While `ArgumentParser` validates that the inputs given by your user match the requirements and types that you define in each command, there are some requirements that can't easily be described in Swift's type system, such as the number of elements in an array, or an expected integer value. + +To validate your commands properties after parsing, implement the `validate()` method on any `ParsableCommand` or `ParsableArguments` type. Throwing an error from the `validate()` method causes the program to print a message to standard error and exit with an error code, preventing the `run()` method from being called with invalid inputs. + +Here's a command that prints out one or more random elements from the list you provide. Its `validate()` method catches three different errors that a user can make, and throws a relevant error for each one. + +```swift +struct Select: ParsableCommand { + @Option(default: 1) + var count: Int + + @Argument() + var elements: [String] + + mutating func validate() throws { + guard count >= 1 else { + throw ValidationError("Please specify a 'count' of at least 1.") + } + + guard !elements.isEmpty else { + throw ValidationError("Please provide at least one element to choose from.") + } + + guard count <= elements.count else { + throw ValidationError("Please specify a 'count' less than the number of elements.") + } + } + + func run() { + print(elements.shuffled().prefix(count).joined(separator: "\n")) + } +} +``` + +When you provide useful error messages, they can guide new users to success with your command-line tool! + +``` +% select +Error: Please provide at least one element to choose from. +Usage: select [--count ] [ ...] +% select --count 2 hello +Error: Please specify a 'count' less than the number of elements. +Usage: select [--count ] [ ...] +% select --count 0 hello hey hi howdy +Error: Please specify a 'count' of at least 1. +Usage: select [--count ] [ ...] +% select --count 2 hello hey hi howdy +howdy +hey +``` + +## Handling Post-Validation Errors + +The `ValidationError` type is a special `ArgumentParser` error — a validation error's message is always accompanied by an appropriate usage string. You can throw other errors, from either the `validate()` or `run`()` method to indicate that something has gone wrong that isn't validation-specific. + +```swift +struct LineCount: ParsableCommand { + @Argument() var file: String + + func run() throws { + let contents = try String(contentsOfFile: file, encoding: .utf8) + let lines = contents.split(separator: "\n") + print(lines.count) + } +} +``` + +The throwing `String(contentsOfFile:encoding:)` initializer fails when the user specifies an invalid file. `ArgumentParser` prints its error message to standard error and exits with an error code. + +``` +% line-count file1.swift +37 +% line-count non-existing-file.swift +Error: The file “non-existing-file.swift” couldn’t be opened because +there is no such file. +``` diff --git a/Documentation/06 Manual Parsing and Testing.md b/Documentation/06 Manual Parsing and Testing.md new file mode 100644 index 000000000..4aba8f949 --- /dev/null +++ b/Documentation/06 Manual Parsing and Testing.md @@ -0,0 +1,115 @@ +# Manual Parsing and Testing + +Provide your own array of command-line inputs and work with parsed results by calling alternatives to `main()`. + +For most programs, calling the static `main()` method on the root command type is all that's necessary. That single call parses the command-line arguments to find the correct command from your tree of nested subcommands, instantiates and validates the result, and executes the chosen command. For more control, however, you can perform each of those steps manually. + +## Parsing Arguments + +For simple Swift scripts, and for those who prefer a straight-down-the-left-edge-of-the-screen scripting style, you can define a single `ParsableArguments` type to parse explicitly from the command-line arguments. + +Let's implement the `Select` command discussed in [Validation and Errors](05%20Validation%20and%20Errors.md), but using a scripty style instead of the typical command. First, we define the options as a `ParsableArguments` type: + +```swift +struct SelectOptions: ParsableArguments { + @Option(default: 1) + var count: Int + + @Argument() + var elements: [String] +} +``` + +The next step is to parse our options from the command-line input: + +```swift +let options = SelectOptions.parseOrExit() +``` + +The static `parseOrExit()` method either returns a fully initialized instance of the type, or exits with an error message and code. Alternatively, you can call the throwing `parse()` method if you'd like to catch any errors that arise during parsing. + +We can perform validation on the inputs and exit the script if necessary: + +```swift +guard let options.elements.count >= options.count else { + let error = ValidationError("Please specify a 'count' less than the number of elements.") + SelectOptions.exit(withError: error) +} +``` + +As you would expect, the `exit(withError:)` method includes usage information when you pass it a `ValidationError`. + +Finally, we print out the requested number of elements: + +```swift +let chosen = options.elements + .shuffled() + .prefix(options.count) +print(chosen.joined(separator: "\n")) +``` + +## Parsing Commands + +Manually parsing commands is a little more complex than parsing a simple `ParsableArguments` type. The result of parsing from a tree of subcommands may be of a different type than the root of the tree, so the static `parseAsRoot()` method returns a type-erased `ParsableCommand`. + +Let's see how this works by using the `Math` command and subcommands defined in [Commands and Subcommands](03%20Commands%20and%20Subcommands.md). This time, instead of calling `Math.main()`, we'll call `Math.parseAsRoot()`, and switch over the result: + +```swift +do { + let command = try Math.parseAsRoot() + + switch command { + case let command as Math.Add: + print("You chose to add \(command.options.values.count) values.") + command.run() + default: + print("You chose to do something else.") + try command.run() + } +} catch { + Math.exit(withError: error) +} +``` +Our new logic intercepts the command between validation and running, and outputs an additional message: + +``` +% math 10 15 7 +You chose to add 3 values. +32 +% math multiply 10 15 7 +You chose to do something else. +1050 +``` + +## Providing Command-Line Input + +All of the parsing methods — `parse()`, `parseOrExit()`, and `parseAsRoot()` — can optionally take an array of command-line inputs as an argument. You can use this capability to test your commands, to perform pre-parse filtering of the command-line arguments, or to manually execute commands from within the same or another target. + +Let's update our `select` script above to strip out any words that contain all capital letters before parsing the inputs. + +```swift +let noShoutingArguments = CommandLine.arguments.filter { phrase in + phrase.uppercased() != phrase +} +let options = SelectOptions.parseOrExit(noShoutingArguments) +``` + +Now when we call our command, the parser won't even see the capitalized words — `HEY` won't ever be printed: + +``` +% select hi howdy HEY --count 2 +hi +howdy +% select hi howdy HEY --count 2 +howdy +hi +``` + + + + + + + + + diff --git a/Examples/math/main.swift b/Examples/math/main.swift new file mode 100644 index 000000000..6291a46d0 --- /dev/null +++ b/Examples/math/main.swift @@ -0,0 +1,186 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +struct Math: ParsableCommand { + // Customize your command's help and subcommands by implementing the + // `configuration` property. + static var configuration = CommandConfiguration( + // Optional abstracts and discussions are used for help output. + abstract: "A utility for performing maths.", + + // Pass an array to `subcommands` to set up a nested tree of subcommands. + // With language support for type-level introspection, this could be + // provided by automatically finding nested `ParsableCommand` types. + subcommands: [Add.self, Multiply.self, Statistics.self], + + // A default subcommand, when provided, is automatically selected if a + // subcommand is not given on the command line. + defaultSubcommand: Add.self) + +} + +struct Options: ParsableArguments { + @Flag(name: [.customLong("hex-output"), .customShort("x")], + help: "Use hexadecimal notation for the result.") + var hexadecimalOutput: Bool + + @Argument( + help: "A group of integers to operate on.") + var values: [Int] +} + +extension Math { + static func format(_ result: Int, usingHex: Bool) -> String { + usingHex ? String(result, radix: 16) + : String(result) + } + + struct Add: ParsableCommand { + static var configuration = + CommandConfiguration(abstract: "Print the sum of the values.") + + // The `@OptionGroup` attribute includes the flags, options, and + // arguments defined by another `ParsableArguments` type. + @OptionGroup() + var options: Options + + func run() { + let result = options.values.reduce(0, +) + print(format(result, usingHex: options.hexadecimalOutput)) + } + } + + struct Multiply: ParsableCommand { + static var configuration = + CommandConfiguration(abstract: "Print the product of the values.") + + @OptionGroup() + var options: Options + + func run() { + let result = options.values.reduce(1, *) + print(format(result, usingHex: options.hexadecimalOutput)) + } + } +} + +// In practice, these nested types could be broken out into different files. +extension Math { + struct Statistics: ParsableCommand { + static var configuration = CommandConfiguration( + // Command names are automatically generated from the type name + // by default; you can specify an override here. + commandName: "stats", + abstract: "Calculate descriptive statistics.", + subcommands: [Average.self, StandardDeviation.self, Quantiles.self]) + } +} + +extension Math.Statistics { + struct Average: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Print the average of the values.") + + enum Kind: String, ExpressibleByArgument { + case mean, median, mode + } + + @Option(default: .mean, help: "The kind of average to provide.") + var kind: Kind + + @Argument(help: "A group of floating-point values to operate on.") + var values: [Double] + + func calculateMean() -> Double { + guard !values.isEmpty else { + return 0 + } + + let sum = values.reduce(0, +) + return sum / Double(values.count) + } + + func calculateMedian() -> Double { + guard !values.isEmpty else { + return 0 + } + + let sorted = values.sorted() + let mid = sorted.count / 2 + if sorted.count.isMultiple(of: 2) { + return sorted[mid - 1] + sorted[mid] / 2 + } else { + return sorted[mid] + } + } + + func calculateMode() -> [Double] { + guard !values.isEmpty else { + return [] + } + + let grouped = Dictionary(grouping: values, by: { $0 }) + let highestFrequency = grouped.lazy.map { $0.value.count }.max()! + return grouped.filter { _, v in v.count == highestFrequency } + .map { k, _ in k } + } + + func run() { + switch kind { + case .mean: + print(calculateMean()) + case .median: + print(calculateMedian()) + case .mode: + let result = calculateMode() + .map(String.init(describing:)) + .joined(separator: " ") + print(result) + } + } + } + + struct StandardDeviation: ParsableCommand { + static var configuration = CommandConfiguration( + commandName: "stdev", + abstract: "Print the standard deviation of the values.") + + @Argument(help: "A group of floating-point values to operate on.") + var values: [Double] + + func run() { + if values.isEmpty { + print(0.0) + } else { + let sum = values.reduce(0, +) + let mean = sum / Double(values.count) + let squaredErrors = values + .map { $0 - mean } + .map { $0 * $0 } + let variance = squaredErrors.reduce(0, +) + let result = variance.squareRoot() + print(result) + } + } + } + + struct Quantiles: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Print the quantiles of the values (TBD).") + + @Argument(help: "A group of floating-point values to operate on.") + var values: [Double] + } +} + +Math.main() diff --git a/Examples/repeat/main.swift b/Examples/repeat/main.swift new file mode 100644 index 000000000..da74e6851 --- /dev/null +++ b/Examples/repeat/main.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +struct Repeat: ParsableCommand { + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? + + @Flag(help: "Include a counter with each repetition.") + var includeCounter: Bool + + @Argument(help: "The phrase to repeat.") + var phrase: String + + func run() throws { + let repeatCount = count ?? .max + + for i in 1...repeatCount { + if includeCounter { + print("\(i): \(phrase)") + } else { + print(phrase) + } + } + } +} + +Repeat.main() diff --git a/Examples/roll/SplitMix64.swift b/Examples/roll/SplitMix64.swift new file mode 100644 index 000000000..a1d7a4190 --- /dev/null +++ b/Examples/roll/SplitMix64.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct SplitMix64: RandomNumberGenerator { + private var state: UInt64 + + init(seed: UInt64) { + self.state = seed + } + + mutating func next() -> UInt64 { + self.state &+= 0x9e3779b97f4a7c15 + var z: UInt64 = self.state + z = (z ^ (z &>> 30)) &* 0xbf58476d1ce4e5b9 + z = (z ^ (z &>> 27)) &* 0x94d049bb133111eb + return z ^ (z &>> 31) + } +} diff --git a/Examples/roll/main.swift b/Examples/roll/main.swift new file mode 100644 index 000000000..b400239a0 --- /dev/null +++ b/Examples/roll/main.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +struct RollOptions: ParsableArguments { + @Option(default: 1, help: ArgumentHelp("Rolls the dice times.", valueName: "n")) + var times: Int + + @Option(default: 6, help: ArgumentHelp( + "Rolls an -sided dice.", + discussion: "Use this option to override the default value of a six-sided die.", + valueName: "m")) + var sides: Int + + @Option(help: "A seed to use for repeatable random generation.") + var seed: Int? + + @Flag(name: .shortAndLong, help: "Show all roll results.") + var verbose: Bool +} + +// If you prefer writing in a "script" style, you can call `parseOrExit()` to +// parse a single `ParsableArguments` type from command-line arguments. +let options = RollOptions.parseOrExit() + +let seed = options.seed ?? .random(in: .min ... .max) +var rng = SplitMix64(seed: UInt64(truncatingIfNeeded: seed)) + +let rolls = (1...options.times).map { _ in + Int.random(in: 1...options.sides, using: &rng) +} + +if options.verbose { + for (number, roll) in zip(1..., rolls) { + print("Roll \(number): \(roll)") + } +} + +print(rolls.reduce(0, +)) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..61b0c7819 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,211 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..a6ff8be06 --- /dev/null +++ b/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version:5.1 +//===----------------------------------------------------------*- 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 PackageDescription + +let package = Package( + name: "swift-argument-parser", + products: [ + .library( + name: "ArgumentParser", + targets: ["ArgumentParser"]), + ], + dependencies: [], + targets: [ + .target( + name: "ArgumentParser", + dependencies: []), + .target( + name: "TestHelpers", + dependencies: ["ArgumentParser"]), + + .target( + name: "roll", + dependencies: ["ArgumentParser"], + path: "Examples/roll"), + .target( + name: "math", + dependencies: ["ArgumentParser"], + path: "Examples/math"), + .target( + name: "repeat", + dependencies: ["ArgumentParser"], + path: "Examples/repeat"), + + .testTarget( + name: "EndToEndTests", + dependencies: ["ArgumentParser", "TestHelpers"]), + .testTarget( + name: "UnitTests", + dependencies: ["ArgumentParser", "TestHelpers"]), + .testTarget( + name: "PackageManagerTests", + dependencies: ["ArgumentParser", "TestHelpers"]), + .testTarget( + name: "ExampleTests", + dependencies: ["TestHelpers"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 000000000..9f8e55c98 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Swift Argument Parser + +## Usage + +Begin by declaring a type that defines the information you need to collect from the command line. +Decorate each stored property with one of `ArgumentParser`'s property wrappers, +and declare conformance to `ParsableCommand`. + +```swift +import ArgumentParser + +struct Repeat: ParsableCommand { + @Flag(help: "Include a counter with each repetition.") + var includeCounter: Bool + + @Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.") + var count: Int? + + @Argument(help: "The phrase to repeat.") + var phrase: String +} +``` + +Next, implement the `run()` method on your type, +and kick off execution by calling the type's static `main()` method. +The `ArgumentParser` library parses the command-line arguments, +instantiates your command type, and then either executes your custom `run()` method +or exits with useful a message. + +```swift +extension Repeat { + func run() throws { + let repeatCount = count ?? .max + + for i in 1...repeatCount { + if includeCounter { + print("\(i): \(phrase)") + } else { + print(phrase) + } + } + } +} + +Repeat.main() +``` + +`ArgumentParser` uses your properties' names and type information, +along with the details you provide using property wrappers, +to supply useful error messages and detailed help: + +``` +$ repeat hello --count 3 +hello +hello +hello +$ repeat +Error: Missing required value for argument 'phrase'. +Usage: repeat [--count ] [--include-counter] +$ repeat --help +USAGE: repeat [--count ] [--include-counter] + +ARGUMENTS: + The phrase to repeat. + +OPTIONS: + --include-counter Include a counter with each repetition. + -c, --count The number of times to repeat 'phrase'. + -h, --help Show help for this command. +``` + +## Examples + +This repository includes a few examples of using the library: + +- [`repeat`](Examples/repeat/main.swift) is the example shown above. +- [`roll`](Examples/roll/main.swift) is a simple utility implemented as a straight-line script. +- [`math`](Examples/math/main.swift) is an annotated example of using nested commands and subcommands. + +You can also see examples of `ArgumentParser` adoption among Swift project tools: + +- [`indexstore-db`](https://github.com/apple/indexstore-db/pulls) is a simple utility with two commands. +- [`swift-format`](https://github.com/apple/swift-format/pulls) uses some advanced features, like custom option values and hidden flags. + +## Adding `ArgumentParser` as a Dependency + +Add the following line to the dependencies in your `Package.swift` file: + +```swift +.package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.1"), +``` + +...and then include `"ArgumentParser"` as a dependency for your executable target: + +```swift +.product(name: "ArgumentParser", package: "swift-argument-parser"), +``` + +> **Note:** Because `ArgumentParser` is under active development, +source-stability is only guaranteed within minor versions (e.g. between `0.0.3` and `0.0.4`). +If you don't want potentially source-breaking package updates, +you can specify your package dependency using `.upToNextMinorVersion(from: "0.0.1")` instead. diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift new file mode 100644 index 000000000..a80762f69 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -0,0 +1,157 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A wrapper that represents a positional command-line argument. +/// +/// Positional arguments are specified without a label, and must appear in +/// the command line arguments in declaration order. +/// +/// struct Options: ParsableArguments { +/// @Argument var name: String +/// @Argument var greeting: String? +/// } +/// +/// This program has two positional arguments; `name` is required, while +/// `greeting` is optional. It could be evoked as either `command Joseph Hello` +/// or simply `command Joseph`. +@propertyWrapper +public struct Argument: + Decodable, ParsedWrapper +{ + internal var _parsedValue: Parsed + + internal init(_parsedValue: Parsed) { + self._parsedValue = _parsedValue + } + + public init(from decoder: Decoder) throws { + try self.init(_decoder: decoder) + } + + /// The value presented by this property wrapper. + public var wrappedValue: Value { + get { + switch _parsedValue { + case .value(let v): + return v + case .definition: + fatalError("Trying to read value from definition.") + } + } + set { + _parsedValue = .value(newValue) + } + } +} + +extension Argument: CustomStringConvertible { + public var description: String { + switch _parsedValue { + case .value(let v): + return String(describing: v) + case .definition: + return "Argument(*definition*)" + } + } +} + +extension Argument: DecodableParsedWrapper where Value: Decodable {} + +// MARK: Property Wrapper Initializers + +extension Argument where Value: ExpressibleByArgument { + /// Creates a property that reads its value from an argument. + /// + /// If the property has an `Optional` type, the argument is optional and + /// defaults to `nil`. + /// + /// - Parameter help: Information about how to use this argument. + public init( + help: ArgumentHelp? = nil + ) { + self.init(_parsedValue: .init { key in + ArgumentSet(key: key, kind: .positional, parseType: Value.self, name: NameSpecification.long, default: nil, help: help) + }) + } +} + +extension Argument { + /// Creates a property that reads its value from an argument, parsing with + /// the given closure. + /// + /// - 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. + public init( + help: ArgumentHelp? = nil, + transform: @escaping (String) throws -> Value + ) { + self.init(_parsedValue: .init { key in + let help = ArgumentDefinition.Help(options: [], help: help, key: key) + let arg = ArgumentDefinition(kind: .positional, help: help, update: .unary({ + (origin, _, valueString, parsedValues) in + parsedValues.set(try transform(valueString), forKey: key, inputOrigin: origin) + })) + return ArgumentSet(alternatives: [arg]) + }) + } + + /// Creates a property that reads an array from zero or more arguments. + /// + /// The property has an empty array as its default value. + /// + /// - Parameter help: Information about how to use this argument. + public init( + help: ArgumentHelp? = nil + ) + where Element: ExpressibleByArgument, Value == Array + { + self.init(_parsedValue: .init { key in + let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + let arg = ArgumentDefinition(kind: .positional, help: help, update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in + values.set([], forKey: key, inputOrigin: origin) + }) + 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: + /// - 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( + help: ArgumentHelp? = nil, + transform: @escaping (String) throws -> Element + ) + where Value == Array + { + self.init(_parsedValue: .init { key in + let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + let arg = ArgumentDefinition(kind: .positional, help: help, update: .unary({ + (origin, name, valueString, parsedValues) in + let element = try transform(valueString) + parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { + $0.append(element) + }) + }), + initial: { origin, values in + values.set([], forKey: key, inputOrigin: origin) + }) + return ArgumentSet(alternatives: [arg]) + }) + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift b/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift new file mode 100644 index 000000000..13c751239 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// Help information for a command-line argument. +public struct ArgumentHelp { + /// A short description of the argument. + public var abstract: String = "" + + /// An expanded description of the argument, in plain text form. + public var discussion: String = "" + + /// An alternative name to use for the argument's value when showing usage + /// information. + public var valueName: String? + + /// A Boolean value indicating whether this argument should be shown in + /// the extended help display. + public var shouldDisplay: Bool = true + + /// Creates a new help instance. + public init( + _ abstract: String = "", + discussion: String = "", + valueName: String? = nil, + shouldDisplay: Bool = true) + { + self.abstract = abstract + self.discussion = discussion + self.valueName = valueName + self.shouldDisplay = shouldDisplay + } + + /// A `Help` instance that hides an argument from the extended help display. + public static var hidden: ArgumentHelp { + ArgumentHelp(shouldDisplay: false) + } +} + +extension ArgumentHelp: ExpressibleByStringInterpolation { + public init(stringLiteral value: String) { + self.abstract = value + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift new file mode 100644 index 000000000..a0f986e6b --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -0,0 +1,317 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A wrapper that represents a command-line flag. +/// +/// A flag is a defaulted Boolean or integer value that can be changed by +/// specifying the flag at the command line. For example: +/// +/// struct Options: ParsableArguments { +/// @Flag var verbose: Bool +/// } +/// +/// `verbose` has a default value of `false`, but becomes `true` if `--verbose` +/// is provided on the command line. +/// +/// A flag can have a value that is a `Bool`, an `Int`, or any `CaseIterable` +/// type. When using a `CaseIterable` type as a flag, the individual cases +/// form the flags that are used on the command line. +/// +/// struct Options { +/// enum Operation: CaseIterable, ... { +/// case add +/// case multiply +/// } +/// +/// @Flag var operation: Operation +/// } +/// +/// // usage: command --add +/// // or: command --multiply +@propertyWrapper +public struct Flag: Decodable, ParsedWrapper { + internal var _parsedValue: Parsed + + internal init(_parsedValue: Parsed) { + self._parsedValue = _parsedValue + } + + public init(from decoder: Decoder) throws { + try self.init(_decoder: decoder) + } + + /// The value presented by this property wrapper. + public var wrappedValue: Value { + get { + switch _parsedValue { + case .value(let v): + return v + case .definition: + fatalError("Trying to read value from definition.") + } + } + set { + _parsedValue = .value(newValue) + } + } +} + +extension Flag: CustomStringConvertible { + public var description: String { + switch _parsedValue { + case .value(let v): + return String(describing: v) + case .definition: + return "Flag(*definition*)" + } + } +} + +extension Flag: DecodableParsedWrapper where Value: Decodable {} + +/// The options for converting a Boolean flag into a `true`/`false` pair. +public enum FlagInversion { + /// Adds a matching flag with a `no-` prefix to represent `false`. + /// + /// For example, the `shouldRender` property in this declaration is set to + /// `true` when a user provides `--render` and to `false` when the user + /// provides `--no-render`: + /// + /// @Flag(name: .customLong("render"), inversion: .prefixedNo) + /// var shouldRender: Bool + case prefixedNo + + /// Uses matching flags with `enable-` and `disable-` prefixes. + /// + /// For example, the `extraOutput` property in this declaration is set to + /// `true` when a user provides `--enable-extra-output` and to `false` when + /// the user provides `--disable-extra-output`: + /// + /// @Flag(inversion: .prefixedEnableDisable) + /// var extraOutput: Bool + case prefixedEnableDisable +} + +/// The options for treating enumeration-based flags as exclusive. +public enum FlagExclusivity { + /// Only one of the enumeration cases may be provided. + case exclusive + + /// The first enumeration case that is provided is used. + case chooseFirst + + /// The last enumeration case that is provided is used. + case chooseLast +} + +extension Flag where Value == Bool { + /// Creates a Boolean property that reads its value from the presence of a + /// flag. + /// + /// This property defaults to a value of `false`. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - help: Information about how to use this flag. + public init( + name: NameSpecification = .long, + help: ArgumentHelp? = nil + ) { + self.init(_parsedValue: .init { key in + .flag(key: key, name: name, help: help) + }) + } + + /// Creates a Boolean property that reads its value from the presence of + /// one or more inverted flags. + /// + /// Use this initializer to create a Boolean flag with an on/off pair. With + /// the following declaration, for example, the user can specify either + /// `--use-https` or `--no-use-https` to set the `useHTTPS` flag to `true` + /// or `false`, respectively. + /// + /// @Flag(inversion: .prefixedNo) + /// var useHTTPS: Bool + /// + /// To customize the names of the two states further, define a + /// `CaseIterable` enumeration with a case for each state, and use that + /// as the type for your flag. In this case, the user can specify either + /// `--use-production-server` or `--use-development-server` to set the + /// flag's value. + /// + /// enum ServerChoice { + /// case useProductionServer + /// case useDevelopmentServer + /// } + /// + /// @Flag() var serverChoice: ServerChoice + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - initial: The default value for this flag. + /// - inversion: The method for converting this flags name into an on/off + /// pair. + /// - help: Information about how to use this flag. + public init( + name: NameSpecification = .long, + default initial: Bool? = false, + inversion: FlagInversion, + help: ArgumentHelp? = nil + ) { + self.init(_parsedValue: .init { key in + .flag(key: key, name: name, default: initial, inversion: inversion, help: help) + }) + } +} + +extension Flag where Value == Int { + /// Creates a integer property that gets its value from the number of times + /// a flag appears. + /// + /// This property defaults to a value of zero. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - help: Information about how to use this flag. + public init( + name: NameSpecification = .long, + help: ArgumentHelp? = nil + ) { + self.init(_parsedValue: .init { key in + .counter(key: key, name: name, help: help) + }) + } +} + +extension Flag where Value: CaseIterable, Value: RawRepresentable, Value.RawValue == String { + /// Creates a property that gets its value from the presence of a flag, + /// where the allowed flags are defined by a case-iterable type. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. If `initial` is + /// non-`nil`, this flag is not required. + /// - exclusivity: The behavior to use when multiple flags are specified. + /// - help: Information about how to use this flag. + public init( + name: NameSpecification = .long, + default initial: Value? = nil, + exclusivity: FlagExclusivity = .exclusive, + help: ArgumentHelp? = nil + ) { + self.init(_parsedValue: .init { key in + // This gets flipped to `true` the first time one of these flags is + // encountered. + var hasUpdated = false + + let args = Value.allCases.map { value -> ArgumentDefinition in + let caseKey = InputKey(rawValue: value.rawValue) + let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: help, key: key) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in + // TODO: We should catch duplicate flags that hit a single part of + // an exclusive argument set in the value parsing, not here. + let previous = values.element(forKey: key) + switch (hasUpdated, previous, exclusivity) { + case (true, let p?, .exclusive): + // This value has already been set. + throw ParserError.duplicateExclusiveValues(previous: p.inputOrigin, duplicate: origin) + case (false, _, _), (_, _, .chooseLast): + values.set(value, forKey: key, inputOrigin: origin) + default: + break + } + hasUpdated = true + })) + } + return exclusivity == .exclusive + ? ArgumentSet(exclusive: args) + : ArgumentSet(additive: args) + }) + } +} + +extension Flag { + /// Creates a property that gets its value from the presence of a flag, + /// where the allowed flags are defined by a case-iterable type. + /// + /// This property has a default value of `nil`; specifying the flag in the + /// command-line arguments is not required. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - exclusivity: The behavior to use when multiple flags are specified. + /// - help: Information about how to use this flag. + public init( + name: NameSpecification = .long, + exclusivity: FlagExclusivity = .exclusive, + help: ArgumentHelp? = nil + ) where Value == Element?, Element: CaseIterable, Element: RawRepresentable, Element.RawValue == String { + self.init(_parsedValue: .init { key in + // This gets flipped to `true` the first time one of these flags is + // encountered. + var hasUpdated = false + + let args = Element.allCases.map { value -> ArgumentDefinition in + let caseKey = InputKey(rawValue: value.rawValue) + let help = ArgumentDefinition.Help(options: .isOptional, help: help, key: key) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in + if hasUpdated && exclusivity == .exclusive { + throw ParserError.unexpectedExtraValues([(origin, String(describing: value))]) + } + if !hasUpdated || exclusivity == .chooseLast { + values.set(value, forKey: key, inputOrigin: origin) + } + hasUpdated = true + })) + } + return exclusivity == .exclusive + ? ArgumentSet(exclusive: args) + : ArgumentSet(additive: args) + }) + } + + /// Creates an array property that gets its values from the presence of + /// zero or more flags, where the allowed flags are defined by a + /// case-iterable type. + /// + /// This property has an empty array as its default value. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - help: Information about how to use this flag. + public init( + name: NameSpecification = .long, + help: ArgumentHelp? = nil + ) where Value == Array, Element: CaseIterable, Element: RawRepresentable, Element.RawValue == String { + self.init(_parsedValue: .init { key in + let args = Element.allCases.map { value -> ArgumentDefinition in + let caseKey = InputKey(rawValue: value.rawValue) + let help = ArgumentDefinition.Help(options: .isOptional, help: help, key: key) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: [Element](), update: .nullary({ (origin, name, values) in + values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { + $0.append(value) + }) + })) + } + return ArgumentSet(additive: args) + }) + } +} + +extension ArgumentDefinition { + static func flag(name: NameSpecification, key: InputKey, caseKey: InputKey, help: Help, parsingStrategy: ArgumentDefinition.ParsingStrategy, initialValue: V?, update: Update) -> ArgumentDefinition { + return ArgumentDefinition(kind: .name(key: caseKey, specification: name), help: help, parsingStrategy: parsingStrategy, update: update, initial: { origin, values in + if let initial = initialValue { + values.set(initial, forKey: key, inputOrigin: origin) + } + }) + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift new file mode 100644 index 000000000..a3912bced --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift @@ -0,0 +1,149 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A specification for how to represent a property as a command-line argument +/// label. +public struct NameSpecification: ExpressibleByArrayLiteral { + public enum Element: Hashable { + /// Use the property's name, converted to lowercase with words separated by + /// hyphens. + /// + /// For example, a property named `allowLongNames` would be converted to the + /// label `--allow-long-names`. + case long + + /// Use the given string instead of the property's name. + /// + /// To create a single-dash argument, pass `true` as `withSingleDash`. Note + /// that combining single-dash options and options with short, + /// single-character names can lead to ambiguities for the user. + case customLong(_ name: String, withSingleDash: Bool = false) + + /// Use the first character of the property's name as a short option label. + /// + /// For example, a property named `verbose` would be converted to the + /// label `-v`. Short labels can be combined into groups. + case short + + /// Use the given character as a short option label. + /// + /// Short labels can be combined into groups. + case customShort(Character) + } + var elements: Set + + public init(_ sequence: S) where S : Sequence, Element == S.Element { + self.elements = Set(sequence) + } + + public init(arrayLiteral elements: Element...) { + self.init(elements) + } +} + +extension NameSpecification { + /// Use the property's name, converted to lowercase with words separated by + /// hyphens. + /// + /// For example, a property named `allowLongNames` would be converted to the + /// label `--allow-long-names`. + public static var long: NameSpecification { [.long] } + + /// Use the given string instead of the property's name. + /// + /// To create a single-dash argument, pass `true` as `withSingleDash`. Note + /// that combining single-dash options and options with short, + /// single-character names can lead to ambiguities for the user. + public static func customLong(_ name: String, withSingleDash: Bool = false) -> NameSpecification { + [.customLong(name, withSingleDash: withSingleDash)] + } + + /// Use the first character of the property's name as a short option label. + /// + /// For example, a property named `verbose` would be converted to the + /// label `-v`. Short labels can be combined into groups. + public static var short: NameSpecification { [.short] } + + /// Use the given character as a short option label. + /// + /// Short labels can be combined into groups. + public static func customShort(_ char: Character) -> NameSpecification { + [.customShort(char)] + } + + /// Combine the `.short` and `.long` specifications to allow both long + /// and short labels. + /// + /// For example, a property named `verbose` would be converted to both the + /// long `--verbose` and short `-v` labels. + public static var shortAndLong: NameSpecification { [.long, .short] } +} + +extension NameSpecification.Element { + /// Creates the argument name for this specification element. + internal func name(for key: InputKey) -> Name? { + switch self { + case .long: + return .long(key.rawValue.convertedToSnakeCase(separator: "-")) + case .short: + guard let c = key.rawValue.first else { fatalError("Key '\(key.rawValue)' has not characters to form short option name.") } + return .short(c) + case .customLong(let name, let withSingleDash): + return withSingleDash + ? .longWithSingleDash(name) + : .long(name) + case .customShort(let name): + return .short(name) + } + } +} + +extension NameSpecification { + /// Creates the argument names for each element in the name specification. + internal func makeNames(_ key: InputKey) -> [Name] { + return elements.compactMap { $0.name(for: key) } + } +} + +extension FlagInversion { + /// Creates the enable and disable name(s) for the given flag. + internal func enableDisableNamePair(for key: InputKey, name: NameSpecification) -> ([Name], [Name]) { + + func makeNames(withPrefix prefix: String, includingShort: Bool) -> [Name] { + return name.elements.compactMap { element -> Name? in + switch element { + case .short, .customShort: + return includingShort ? element.name(for: key) : nil + case .long: + let modifiedKey = InputKey(rawValue: key.rawValue.addingIntercappedPrefix(prefix)) + return element.name(for: modifiedKey) + case .customLong(let name, let withSingleDash): + let modifiedName = name.addingPrefixWithAutodetectedStyle(prefix) + let modifiedElement = NameSpecification.Element.customLong(modifiedName, withSingleDash: withSingleDash) + return modifiedElement.name(for: key) + } + } + } + + switch (self) { + case .prefixedNo: + return ( + name.makeNames(key), + makeNames(withPrefix: "no", includingShort: false) + ) + case .prefixedEnableDisable: + return ( + makeNames(withPrefix: "enable", includingShort: true), + makeNames(withPrefix: "disable", includingShort: false) + ) + } + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift new file mode 100644 index 000000000..9305c8226 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -0,0 +1,312 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A wrapper that represents a command-line option. +/// +/// An option is a value that can be specified as a named value on the command +/// line. An option can have a default values specified as part of its +/// declaration; options with optional `Value` types impicitly have `nil` as +/// their default value. +/// +/// struct Options: ParsableArguments { +/// @Option(default: "Hello") var greeting: String +/// @Option var name: String +/// @Option var age: Int? +/// } +/// +/// `greeting` has a default value of `"Hello"`, which can be overridden by +/// providing a different string as an argument. `age` defaults to `nil`, while +/// `name` is a required argument because it is non-`nil` and has no default +/// value. +@propertyWrapper +public struct Option: Decodable, ParsedWrapper { + internal var _parsedValue: Parsed + + internal init(_parsedValue: Parsed) { + self._parsedValue = _parsedValue + } + + public init(from decoder: Decoder) throws { + try self.init(_decoder: decoder) + } + + /// The value presented by this property wrapper. + public var wrappedValue: Value { + get { + switch _parsedValue { + case .value(let v): + return v + case .definition: + fatalError("Trying to read value from definition.") + } + } + set { + _parsedValue = .value(newValue) + } + } +} + +extension Option: CustomStringConvertible { + public var description: String { + switch _parsedValue { + case .value(let v): + return String(describing: v) + case .definition: + return "Option(*definition*)" + } + } +} + +extension Option: DecodableParsedWrapper where Value: Decodable {} + +// MARK: Property Wrapper Initializers + +extension Option where Value: ExpressibleByArgument { + /// Creates a property that reads its value from an 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. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. + /// - help: Information about how to use this option. + public init( + name: NameSpecification = .long, + default initial: Value? = nil, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil + ) { + self.init(_parsedValue: .init { key in + ArgumentSet( + key: key, + kind: .name(key: key, specification: name), + parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + parseType: Value.self, + name: name, + default: initial, help: help) + }) + } +} + +/// The strategy to use when parsing a single value from `@Option` arguments. +/// +/// - SeeAlso: `ArrayParsingStrategy`` +public enum SingleValueParsingStrategy { + /// Parse the input after the option. Expect it to be a value. + /// + /// For input such as `--foo foo` this would parse `foo` as the + /// value. However, for the input `--foo --bar foo bar` would + /// result in a error. Even though two values are provided, they don’t + /// succeed each option. Parsing would result in an error such as + /// + /// Error: Missing value for '--foo ' + /// Usage: command [--foo ] + /// + /// This is the **default behavior** for `@Option`-wrapped properties. + case next + + /// Parse the next input, even if it could be interpreted as an option or + /// flag. + /// + /// For input such as `--foo --bar baz`, if `.unconditional` is used for `foo`, + /// this would read `--bar` as the value for `foo` and would use `baz` as + /// the next positional argument. + /// + /// This allows reading negative numeric values, or capturing flags to be + /// passed through to another program, since the leading hyphen is normally + /// interpreted as the start of another option. + /// + /// - Note: This is usually *not* what users would expect. Use with caution. + case unconditional + + /// Parse the next input, as long as that input can't be interpreted as + /// an option or flag. + /// + /// - Note: This will skip other options and _read ahead_ in the input + /// to find the next available value. This may be *unexpected* for users. + /// Use with caution. + /// + /// For example, if `--foo` takes an values, then the input `--foo --bar bar` + /// would be parsed such that the value `bar` is used for `--foo`. + case scanningForValue +} + +/// The strategy to use when parsing multiple values from `@Option` arguments into an +/// array. +public enum ArrayParsingStrategy { + /// Parse one value per option, joining multiple into an array. + /// + /// For example, for a parsable type with a property defined as + /// `@Option(parsing: .singleValue) var read: [String]` + /// the input `--read foo --read bar` would result in the array + /// `["foo", "bar"]`. The same would be true for the input + /// `--read=foo --read=bar`. + /// + /// - Note: This follows the default behavior of differentiating between values and options. As + /// such the value for this option will be the next value (non-option) in the input. For the + /// above example, the input `--read --name Foo Bar` would parse `Foo` into + /// `read` (and `Bar` into `name`). + case singleValue + + /// Parse the value immediatly after the option while allowing repeating options, joining multiple into an array. + /// + /// This is identical to `.singleValue` except that the value will be read + /// from the input immediatly after the option even it it could be interpreted as an option. + /// + /// For example, for a parsable type with a property defined as + /// `@Option(parsing: .unconditionalSingleValue) var read: [String]` + /// the input `--read foo --read bar` would result in the array + /// `["foo", "bar"]` -- just as it would have been the case for `.singleValue`. + /// + /// - Note: However, the input `--read --name Foo Bar --read baz` would result in + /// `read` being set to the array `["--name", "baz"]`. This is usually *not* what users + /// would expect. Use with caution. + case unconditionalSingleValue + + /// Parse all values up to the next option. + /// + /// For example, for a parsable type with a property defined as + /// `@Option(parsing: .upToNextOption) var files: [String]` + /// the input `--files foo bar` would result in the array + /// `["foo", "bar"]`. + /// + /// Parsing stops as soon as there’s another option in the input, such that + /// `--files foo bar --verbose` would also set `files` to the array + /// `["foo", "bar"]`. + case upToNextOption + + /// Parse all remaining arguments into an array. + /// + /// `.remaining` can be used for capturing pass-through flags. For example, for + /// a parsable type defined as + /// `@Option(parsing: .remaining) var passthrough: [String]`: + /// + /// $ cmd --passthrough --foo 1 --bar 2 -xvf + /// ------------ + /// options.passthrough == ["--foo", "1", "--bar", "2", "-xvf"] + /// + /// - Note: This will read all input following the option, without attempting to do any parsing. This is + /// usually *not* what users would expect. Use with caution. + /// + /// Consider using a trailing `@Argument` instead, and letting users explicitly turn off parsing + /// through the terminator `--`. That is the more common approach. For example: + /// ```swift + /// struct Options: ParsableArguments { + /// @Option() + /// var name: String + /// + /// @Argument() + /// var remainder: [String] + /// } + /// ``` + /// would allow to parse the input `--name Foo -- Bar --baz` such that the `remainder` + /// would hold the values `["Bar", "--baz"]`. + case remaining +} + +extension Option { + /// Creates a property that reads its value from an labeled option, parsing + /// with the given closure. + /// + /// If the property has an `Optional` type, or you provide a non-`nil` + /// value for the `initial` parameter, specifying this option is not + /// required. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. + /// - help: Information about how to use this option. + /// - transform: A closure that converts a string into this property's + /// type or throws an error. + public init( + name: NameSpecification = .long, + default initial: Value? = nil, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + transform: @escaping (String) throws -> Value + ) { + self.init(_parsedValue: .init { key in + let kind = ArgumentDefinition.Kind.name(key: key, specification: name) + let help = ArgumentDefinition.Help(options: [], help: help, key: key) + let arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .unary({ + (origin, _, valueString, parsedValues) in + parsedValues.set(try transform(valueString), forKey: key, inputOrigin: origin) + }), initial: { origin, values in + if let v = initial { + values.set(v, forKey: key, inputOrigin: origin) + } + }) + return ArgumentSet(alternatives: [arg]) + }) + } + + /// 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. + /// - parsingStrategy: The behavior to use when parsing multiple values + /// from the command-line arguments. + /// - help: Information about how to use this option. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil + ) where Element: ExpressibleByArgument, Value == Array { + 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) + }) + 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. + /// + /// - 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. + /// - 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( + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + transform: @escaping (String) throws -> Element + ) where Value == Array { + 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({ + (origin, name, valueString, parsedValues) in + let element = try transform(valueString) + parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { + $0.append(element) + }) + }), + initial: { origin, values in + values.set([], forKey: key, inputOrigin: origin) + }) + return ArgumentSet(alternatives: [arg]) + }) + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift new file mode 100644 index 000000000..ac4c1a4aa --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A wrapper that transparently includes a parsable type. +/// +/// Use an option group to include a group of options, flags, or arguments +/// declared in a parsable type. +/// +/// struct GlobalOptions: ParsableArguments { +/// @Flag(name: .shortAndLong) +/// var verbose: Bool +/// +/// @Argument() +/// var values: [Int] +/// } +/// +/// struct Options: ParsableArguments { +/// @Option() +/// var name: String +/// +/// @OptionGroup() +/// var globals: GlobalOptions +/// } +/// +/// The flag and positional arguments declared as part of `GlobalOptions` are +/// included when parsing `Options`. +@propertyWrapper +public struct OptionGroup: Decodable, ParsedWrapper { + internal var _parsedValue: Parsed + + internal init(_parsedValue: Parsed) { + self._parsedValue = _parsedValue + } + + public init(from decoder: Decoder) throws { + if let d = decoder as? SingleValueDecoder, + let value = try? d.previousValue(Value.self) + { + self.init(_parsedValue: .value(value)) + } else { + try self.init(_decoder: decoder) + } + + do { + try wrappedValue.validate() + } catch { + throw ParserError.userValidationError(error) + } + } + + /// The value presented by this property wrapper. + public var wrappedValue: Value { + get { + switch _parsedValue { + case .value(let v): + return v + case .definition: + fatalError("Trying to read value from definition.") + } + } + set { + _parsedValue = .value(newValue) + } + } +} + +extension OptionGroup: CustomStringConvertible { + public var description: String { + switch _parsedValue { + case .value(let v): + return String(describing: v) + case .definition: + return "OptionGroup(*definition*)" + } + } +} + +extension OptionGroup { + /// Creates a property that represents another parsable type. + public init() { + self.init(_parsedValue: .init { _ in + ArgumentSet(Value.self) + }) + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/ValidationError.swift b/Sources/ArgumentParser/Parsable Properties/ValidationError.swift new file mode 100644 index 000000000..23dc9b358 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/ValidationError.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// An error type that is presented to the user as an error with parsing their +/// command-line input. +public struct ValidationError: Error, CustomStringConvertible { + var message: String + + /// Creates a new validation error with the given message. + public init(_ message: String) { + self.message = message + } + + public var description: String { + message + } +} + +/// An error type that represents a clean (i.e. non-error state) exit of the +/// utility. +/// +/// Throwing a `CleanExit` instance from a `validate` or `run` method, or +/// passing it to `exit(with:)`, exits a program with exit code `0`. +public enum CleanExit: Error, CustomStringConvertible { + /// Treat this error as a help request and display the full help message. + /// + /// You can use this case to simulate the user specifying one of the help + /// flags or subcommands. + /// + /// - Parameter command: The command type to offer help for, if different + /// from the root command. + case helpRequest(ParsableCommand.Type? = nil) + + /// Treat this error as a clean exit with the given message. + case message(String) + + public var description: String { + switch self { + case .helpRequest: return "--help" + case .message(let message): return message + } + } + + /// Treat this error as a help request and display the full help message. + /// + /// You can use this case to simulate the user specifying one of the help + /// flags or subcommands. + /// + /// - Parameter command: A command to offer help for, if different from + /// the root command. + public static func helpRequest(_ command: ParsableCommand) -> CleanExit { + return .helpRequest(type(of: command)) + } +} diff --git a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift new file mode 100644 index 000000000..10e27efc4 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// The configuration for a command. +public struct CommandConfiguration { + /// The name of the command to use on the command line. + /// + /// If `nil`, the command name is derived by converting the name of + /// the command type to hyphen-separated lowercase case words. + public var commandName: String? + + /// A one-line description of this command. + public var abstract: String + + /// A longer description of this command, to be shown in the extended help + /// display. + public var discussion: String + + /// A Boolean value indicating whether this command should be shown in + /// the extended help display. + public var shouldDisplay: Bool + + /// An array of the types that define subcommands for this command. + public var subcommands: [ParsableCommand.Type] + + /// The default command type to run if no subcommand is given. + public var defaultSubcommand: ParsableCommand.Type? + + /// Flag names to be used for help. + public var helpNames: NameSpecification + + /// Creates the configuration for a command. + /// + /// - Parameters: + /// - commandName: The name of the command to use on the command line. If + /// `commandName` is `nil`, the command name is derived by converting + /// the name of the command type to hyphen-separated lowercase case + /// words. + /// - abstract: A one-line description of the command. + /// - discussion: A longer description of the command. + /// - shouldDisplay: A Boolean value indicating whether the command + /// should be shown in the extended help display. + /// - subcommands: An array of the types that define subcommands for the + /// command. + /// - defaultSubcommand: The default command type to run if no subcommand + /// is given. + /// - helpNames: The flag names to use for requesting help, simulating + /// a Boolean property named `help`. + public init( + commandName: String? = nil, + abstract: String = "", + discussion: String = "", + shouldDisplay: Bool = true, + subcommands: [ParsableCommand.Type] = [], + defaultSubcommand: ParsableCommand.Type? = nil, + helpNames: NameSpecification = [.short, .long] + ) { + self.commandName = commandName + self.abstract = abstract + self.discussion = discussion + self.shouldDisplay = shouldDisplay + self.subcommands = subcommands + self.defaultSubcommand = defaultSubcommand + self.helpNames = helpNames + } +} + + diff --git a/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift b/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift new file mode 100644 index 000000000..7cc371ac5 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A type that can be expressed as a command-line argument. +public protocol ExpressibleByArgument { + /// Creates a new instance of this type from a command-line-specified + /// argument. + init?(argument: String) +} + +extension String: ExpressibleByArgument { + public init?(argument: String) { + self = argument + } +} + +extension Optional: ExpressibleByArgument where Wrapped: ExpressibleByArgument { + public init?(argument: String) { + if let value = Wrapped(argument: argument) { + self = value + } else { + return nil + } + } +} + +extension RawRepresentable where Self: ExpressibleByArgument, RawValue: ExpressibleByArgument { + public init?(argument: String) { + if let value = RawValue(argument: argument) { + self.init(rawValue: value) + } else { + return nil + } + } +} + +// MARK: LosslessStringConvertible + +extension LosslessStringConvertible { + public init?(argument: String) { + self.init(argument) + } +} + +extension Int: ExpressibleByArgument {} +extension Int8: ExpressibleByArgument {} +extension Int16: ExpressibleByArgument {} +extension Int32: ExpressibleByArgument {} +extension Int64: ExpressibleByArgument {} +extension UInt: ExpressibleByArgument {} +extension UInt8: ExpressibleByArgument {} +extension UInt16: ExpressibleByArgument {} +extension UInt32: ExpressibleByArgument {} +extension UInt64: ExpressibleByArgument {} + +extension Float: ExpressibleByArgument {} +extension Double: ExpressibleByArgument {} + +extension Bool: ExpressibleByArgument {} + diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift new file mode 100644 index 000000000..d052044fd --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -0,0 +1,190 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +#if os(Linux) +import Glibc +let _exit: (Int32) -> Never = Glibc.exit +#else +import Darwin +let _exit: (Int32) -> Never = Darwin.exit +#endif + +/// A type that can be parsed from a program's command-line arguments. +/// +/// When you implement a `ParsableArguments` type, all properties must be declared with +/// one of the four property wrappers provided by the `ArgumentParser` library. +public protocol ParsableArguments: Decodable { + /// Creates an instance of this parsable type using the definitions + /// given by each property's wrapper. + init() + + /// Validates the properties of the instance after parsing. + /// + /// Implement this method to perform validation or other processing after + /// creating a new instance from command-line arguments. + mutating func validate() throws +} + +/// A type that provides the `ParsableCommand` interface to a `ParsableArguments` type. +struct _WrappedParsableCommand: ParsableCommand { + static var _commandName: String { + let name = String(describing: P.self).convertedToSnakeCase() + + // If the type is named something like "TransformOptions", we only want + // to use "transform" as the command name. + if let optionsRange = name.range(of: "_options"), + optionsRange.upperBound == name.endIndex + { + return String(name[...self + } +} + +// MARK: - API + +extension ParsableArguments { + /// Parses a new instance of this type from command-line arguments. + /// + /// - Parameter arguments: An array of arguments to use for parsing. If + /// `arguments` is `nil`, this uses the program's command-line arguments. + /// - Returns: A new instance of this type. + public static func parse( + _ arguments: [String]? = nil + ) throws -> Self { + // Parse the command and unwrap the result if necessary. + switch try self.asCommand.parseAsRoot(arguments) { + case is HelpCommand: + throw ParserError.helpRequested + case let result as _WrappedParsableCommand: + return result.options + case var result as Self: + do { + try result.validate() + } catch { + throw ParserError.userValidationError(error) + } + return result + default: + // TODO: this should be a "wrong command" message + throw ParserError.invalidState + } + } + + /// Returns a brief message for the given error. + /// + /// - Parameter error: An error to generate a message for. + /// - Returns: A message that can be displayed to the user. + public static func message( + for error: Error + ) -> String { + MessageInfo(error: error, type: self).message + } + + /// Returns a full message for the given error, including usage information, + /// if appropriate. + /// + /// - Parameter error: An error to generate a message for. + /// - Returns: A message that can be displayed to the user. + public static func fullMessage( + for error: Error + ) -> String { + MessageInfo(error: error, type: self).fullText + } + + /// Terminates execution with a message and exit code that is appropriate + /// for the given error. + /// + /// If the `error` parameter is `nil`, this method prints nothing and exits + /// with code `EXIT_SUCCESS`. If `error` represents a help request or + /// another `CleanExit` error, this method prints help information and + /// exits with code `EXIT_SUCCESS`. Otherwise, this method prints a relevant + /// error message and exits with code `EXIT_FAILURE`. + /// + /// - Parameter error: The error to use when exiting, if any. + public static func exit( + withError error: Error? = nil + ) -> Never { + guard let error = error else { + _exit(EXIT_SUCCESS) + } + + let messageInfo = MessageInfo(error: error, type: self) + if messageInfo.shouldExitCleanly { + print(messageInfo.fullText) + _exit(EXIT_SUCCESS) + } else { + print(messageInfo.fullText, to: &standardError) + _exit(EXIT_FAILURE) + } + } + + /// Parses a new instance of this type from command-line arguments or exits + /// with a relevant message. + /// + /// - Parameter arguments: An array of arguments to use for parsing. If + /// `arguments` is `nil`, this uses the program's command-line arguments. + public static func parseOrExit( + _ arguments: [String]? = nil + ) -> Self { + do { + return try parse(arguments) + } catch { + exit(withError: error) + } + } +} + +protocol ArgumentSetProvider { + func argumentSet(for key: InputKey) -> ArgumentSet +} + +extension ArgumentSet { + init(_ type: ParsableArguments.Type) { + let a: [ArgumentSet] = Mirror(reflecting: type.init()) + .children + .compactMap { child in + guard + var codingKey = child.label, + let parsed = child.value as? ArgumentSetProvider + else { return nil } + + // Property wrappers have underscore-prefixed names + codingKey = String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) + + let key = InputKey(rawValue: codingKey) + return parsed.argumentSet(for: key) + } + self.init(additive: a) + } +} diff --git a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift new file mode 100644 index 000000000..5fe8cb6ab --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A type that can be executed as part of a nested tree of commands. +public protocol ParsableCommand: ParsableArguments { + /// Configuration for this command, including subcommands and custom help + /// text. + static var configuration: CommandConfiguration { get } + + /// *For internal use only:* The name for the command on the command line. + /// + /// This is generated from the configuration, if given, or from the type + /// name if not. This is a customization point so that a WrappedParsable + /// can pass through the wrapped type's name. + static var _commandName: String { get } + + /// Runs this command. + /// + /// After implementing this method, you can run your command-line + /// application by calling the static `main()` method on the root type. + /// This method has a default implementation that prints help text + /// for this command. + func run() throws +} + +extension ParsableCommand { + public static var _commandName: String { + configuration.commandName ?? + String(describing: Self.self).convertedToSnakeCase(separator: "-") + } + + public static var configuration: CommandConfiguration { + CommandConfiguration() + } + + public func run() throws { + throw CleanExit.helpRequest(self) + } +} + +// MARK: - API + +extension ParsableCommand { + /// Parses an instance of this type, or one of its subcommands, from + /// command-line arguments. + /// + /// - Parameter arguments: An array of arguments to use for parsing. If + /// `arguments` is `nil`, this uses the program's command-line arguments. + /// - Returns: A new instance of this type, one of its subcommands, or an + /// command type internal to the `ArgumentParser` library. + public static func parseAsRoot( + _ arguments: [String]? = nil + ) throws -> ParsableCommand { + var parser = CommandParser(self) + let arguments = arguments ?? Array(CommandLine.arguments.dropFirst()) + var result = try parser.parse(arguments: arguments).get() + do { + try result.validate() + } catch { + throw ParserError.userValidationError(error) + } + return result + } + + /// Parses an instance of this type, or one of its subcommands, from + /// command-line arguments and calls its `run()` method, exiting cleanly + /// or with a relevant error message. + /// + /// - Parameter arguments: An array of arguments to use for parsing. If + /// `arguments` is `nil`, this uses the program's command-line arguments. + public static func main(_ arguments: [String]? = nil) -> Never { + do { + let command = try parseAsRoot(arguments) + try command.run() + exit() + } catch { + exit(withError: error) + } + } +} diff --git a/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift new file mode 100644 index 000000000..76d1dc868 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift @@ -0,0 +1,242 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A decoder that decodes from parsed command-line arguments. +final class ArgumentDecoder: Decoder { + init(values: ParsedValues, previouslyParsedValues: [(ParsableCommand.Type, ParsableCommand)] = []) { + self.values = values + self.previouslyParsedValues = previouslyParsedValues + self.usedOrigins = InputOrigin() + + // Mark the terminator position(s) as used: + values.elements.filter { $0.key == .terminator }.forEach { + usedOrigins.formUnion($0.inputOrigin) + } + } + + let values: ParsedValues + var usedOrigins: InputOrigin + var nextCommandIndex = 0 + var previouslyParsedValues: [(ParsableCommand.Type, ParsableCommand)] = [] + + var codingPath: [CodingKey] = [] + + var userInfo: [CodingUserInfoKey : Any] = [:] + + func container(keyedBy type: K.Type) throws -> KeyedDecodingContainer where K: CodingKey { + let container = ParsedArgumentsContainer(for: self, keyType: K.self, codingPath: codingPath) + return KeyedDecodingContainer(container) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw Error.topLevelHasNoUnkeyedContainer + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + throw Error.topLevelHasNoSingleValueContainer + } +} + +extension ArgumentDecoder { + fileprivate func element(forKey key: InputKey) -> ParsedValues.Element? { + guard let element = values.element(forKey: key) else { return nil } + usedOrigins.formUnion(element.inputOrigin) + return element + } +} + +extension ArgumentDecoder { + enum Error: Swift.Error { + case topLevelHasNoUnkeyedContainer + case topLevelHasNoSingleValueContainer + case singleValueDecoderHasNoContainer + case wrongKeyType(CodingKey.Type, CodingKey.Type) + } +} + +final class ParsedArgumentsContainer: KeyedDecodingContainerProtocol where K : CodingKey { + var codingPath: [CodingKey] + + let decoder: ArgumentDecoder + + init(for decoder: ArgumentDecoder, keyType: K.Type, codingPath: [CodingKey]) { + self.codingPath = codingPath + self.decoder = decoder + } + + var allKeys: [K] { + fatalError() + } + + fileprivate func element(forKey key: K) -> ParsedValues.Element? { + let k = InputKey(key) + return decoder.element(forKey: k) + } + + func contains(_ key: K) -> Bool { + return element(forKey: key) != nil + } + + func decodeNil(forKey key: K) throws -> Bool { + return !contains(key) + } + + func decode(_ type: T.Type, forKey key: K) throws -> T where T : Decodable { + let subDecoder = SingleValueDecoder(userInfo: decoder.userInfo, underlying: decoder, codingPath: codingPath + [key], key: InputKey(key), parsedElement: element(forKey: key)) + return try type.init(from: subDecoder) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + fatalError() + } + + func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { + fatalError() + } + + func superDecoder() throws -> Decoder { + fatalError() + } + + func superDecoder(forKey key: K) throws -> Decoder { + fatalError() + } +} + +struct SingleValueDecoder: Decoder { + var userInfo: [CodingUserInfoKey : Any] + var underlying: ArgumentDecoder + var codingPath: [CodingKey] + var key: InputKey + var parsedElement: ParsedValues.Element? + + func container(keyedBy type: K.Type) throws -> KeyedDecodingContainer where K: CodingKey { + return KeyedDecodingContainer(ParsedArgumentsContainer(for: underlying, keyType: type, codingPath: codingPath)) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + guard let e = parsedElement else { + throw ParserError.noValue(forKey: InputKey(rawValue: codingPath.last!.stringValue)) + } + guard let a = e.value as? [Any] else { + throw ParserError.invalidState + } + return UnkeyedContainer(codingPath: codingPath, parsedElement: e, array: ArrayWrapper(a)) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + return SingleValueContainer(underlying: self, codingPath: codingPath, parsedElement: parsedElement) + } + + func previousValue(_ type: T.Type) throws -> T { + for (previousType, value) in underlying.previouslyParsedValues { + if type == previousType { + return value as! T + } + } + throw ParserError.invalidState + } + + struct SingleValueContainer: SingleValueDecodingContainer { + var underlying: SingleValueDecoder + var codingPath: [CodingKey] + var parsedElement: ParsedValues.Element? + + func decodeNil() -> Bool { + return parsedElement == nil + } + + func decode(_ type: T.Type) throws -> T where T : Decodable { + guard let e = parsedElement else { + throw ParserError.noValue(forKey: InputKey(rawValue: codingPath.last!.stringValue)) + } + guard let s = e.value as? T else { + throw InternalParseError.wrongType(e.value, forKey: e.key) + } + return s + } + } + + struct UnkeyedContainer: UnkeyedDecodingContainer { + var codingPath: [CodingKey] + var parsedElement: ParsedValues.Element + var array: ArrayWrapperProtocol + + var count: Int? { + return array.count + } + + var isAtEnd: Bool { + return array.isAtEnd + } + + var currentIndex: Int { + return array.currentIndex + } + + mutating func decodeNil() throws -> Bool { + return false + } + + mutating func decode(_ type: T.Type) throws -> T where T : Decodable { + guard let next = array.getNext() else { fatalError() } + guard let t = next as? T else { + throw InternalParseError.wrongType(next, forKey: parsedElement.key) + } + return t + } + + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + fatalError() + } + + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + fatalError() + } + + mutating func superDecoder() throws -> Decoder { + fatalError() + } + } +} + +/// A type-erasing wrapper for consuming elements of an array. +protocol ArrayWrapperProtocol { + var count: Int? { get } + var isAtEnd: Bool { get } + var currentIndex: Int { get } + mutating func getNext() -> Any? +} + +struct ArrayWrapper: ArrayWrapperProtocol { + var base: [A] + var currentIndex: Int + + init(_ a: [A]) { + self.base = a + self.currentIndex = a.startIndex + } + + var count: Int? { + return base.count + } + + var isAtEnd: Bool { + return base.endIndex <= currentIndex + } + + mutating func getNext() -> Any? { + guard currentIndex < base.endIndex else { return nil } + let next = base[currentIndex] + currentIndex += 1 + return next + } +} diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift new file mode 100644 index 000000000..5872b6822 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -0,0 +1,211 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct ArgumentDefinition { + enum Update { + typealias Nullary = (InputOrigin, Name?, inout ParsedValues) throws -> Void + typealias Unary = (InputOrigin, Name?, String, inout ParsedValues) throws -> Void + + case nullary(Nullary) + case unary(Unary) + } + + typealias Initial = (InputOrigin, inout ParsedValues) throws -> Void + + enum Kind { + case named([Name]) + case positional + } + + struct Help { + var options: Options + var help: ArgumentHelp? + var discussion: String? + var defaultValue: String? + var keys: [InputKey] + + struct Options: OptionSet { + var rawValue: UInt + + static let isOptional = Options(rawValue: 1 << 0) + static let isRepeating = Options(rawValue: 1 << 1) + } + + init(options: Options = [], help: ArgumentHelp? = nil, defaultValue: String? = nil, key: InputKey) { + self.options = options + self.help = help + self.defaultValue = defaultValue + self.keys = [key] + } + } + + /// This folds the public `ArrayParsingStrategy` and `SingleValueParsingStrategy` + /// into a single enum. + enum ParsingStrategy { + /// Expect the next `SplitArguments.Element` to be a value and parse it. Will fail if the next + /// input is an option. + case nextAsValue + /// Parse the next `SplitArguments.Element.value` + case scanningForValue + /// Parse the next `SplitArguments.Element` as a value, regardless of its type. + case unconditional + /// Parse multiple `SplitArguments.Element.value` up to the next non-`.value` + case upToNextOption + /// Parse all remaining `SplitArguments.Element` as a values, regardless of its type. + case allRemainingInput + } + + var kind: Kind + var help: Help + var parsingStrategy: ParsingStrategy + var update: Update + var initial: Initial + + var names: [Name] { + switch kind { + case .named(let n): return n + case .positional: return [] + } + } + + var valueName: String { + return help.help?.valueName + ?? preferredNameForSynopsis?.valueString + ?? help.keys.first?.rawValue.convertedToSnakeCase(separator: "-") + ?? "value" + } + + init(kind: Kind, help: Help, parsingStrategy: ParsingStrategy = .nextAsValue, update: Update, initial: @escaping Initial = { _, _ in }) { + if case (.positional, .nullary) = (kind, update) { + preconditionFailure("Can't create a nullary positional argument.") + } + + self.kind = kind + self.help = help + self.parsingStrategy = parsingStrategy + self.update = update + self.initial = initial + } +} + +extension ArgumentDefinition.ParsingStrategy { + init(_ other: SingleValueParsingStrategy) { + switch other { + case .next: + self = .nextAsValue + case .scanningForValue: + self = .scanningForValue + case .unconditional: + self = .unconditional + } + } + + init(_ other: ArrayParsingStrategy) { + switch other { + case .singleValue: + self = .scanningForValue + case .unconditionalSingleValue: + self = .unconditional + case .upToNextOption: + self = .upToNextOption + case .remaining: + self = .allRemainingInput + } + } +} + +extension ArgumentDefinition: CustomDebugStringConvertible { + var debugDescription: String { + switch (kind, update) { + case (.named(let names), .nullary): + return names + .map { $0.synopsisString } + .joined(separator: ",") + case (.named(let names), .unary): + return names + .map { $0.synopsisString } + .joined(separator: ",") + + " <\(valueName)>" + case (.positional, _): + return "<\(valueName)>" + } + } +} + +extension ArgumentDefinition { + var optional: ArgumentDefinition { + var result = self + result.help.options.insert(.isOptional) + return result + } + + var nonOptional: ArgumentDefinition { + var result = self + result.help.options.remove(.isOptional) + return result + } +} + +extension ArgumentDefinition { + var isPositional: Bool { + if case .positional = kind { + return true + } + return false + } + + var isRepeatingPositional: Bool { + isPositional && help.options.contains(.isRepeating) + } +} + +extension ArgumentDefinition.Kind { + static func name(key: InputKey, specification: NameSpecification) -> ArgumentDefinition.Kind { + let names = specification.makeNames(key) + return ArgumentDefinition.Kind.named(names) + } +} + +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: - Help Options + +protocol ArgumentHelpOptionProvider { + static var helpOptions: ArgumentDefinition.Help.Options { get } +} + +extension Optional: ArgumentHelpOptionProvider { + static var helpOptions: ArgumentDefinition.Help.Options { + return [.isOptional] + } +} + +extension ArgumentDefinition.Help.Options { + init(type: A.Type) { + if let t = type as? ArgumentHelpOptionProvider.Type { + self = t.helpOptions + } else { + self = [] + } + } +} diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift new file mode 100644 index 000000000..d8ba81e88 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -0,0 +1,468 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A nested tree of argument definitions. +/// +/// The main reason for having a nested representation is to build help output. +/// For output like: +/// +/// Usage: mytool [-v | -f] +/// +/// The `-v | -f` part is one *set* that’s optional, ` ` is +/// another. Both of these can then be combined into a third set. +struct ArgumentSet { + enum Content { + /// A leaf list of arguments. + case arguments([ArgumentDefinition]) + /// A node with additional `[ArgumentSet]` + case sets([ArgumentSet]) + } + var content: Content + var kind: Kind + + /// Used to generate _help_ text. + enum Kind { + /// Independent + case additive + /// Mutually exclusive + case exclusive + /// Several ways of achieving the same behaviour. Should only display one. + case alternatives + } + + init(arguments: [ArgumentDefinition], kind: Kind) { + self.content = .arguments(arguments) + self.kind = kind + // TODO: Move this check into a separate validation pass for completed + // argument sets. + precondition( + self.hasValidArguments, + "Can't have a positional argument following an array of positional arguments.") + } + + init(sets: [ArgumentSet], kind: Kind) { + self.content = .sets(sets) + self.kind = kind + precondition( + self.hasValidArguments, + "Can't have a positional argument following an array of positional arguments.") + } +} + +extension ArgumentSet: CustomDebugStringConvertible { + var debugDescription: String { + switch content { + case .arguments(let args): + return args + .map { $0.debugDescription } + .joined(separator: " / ") + case .sets(let sets): + return sets + .map { "{\($0.debugDescription)}" } + .joined(separator: " / ") + } + } +} + +extension ArgumentSet { + init() { + self.init(arguments: [], kind: .additive) + } + + init(_ arg: ArgumentDefinition) { + self.init(arguments: [arg], kind: .additive) + } + + init(additive args: [ArgumentDefinition]) { + self.init(arguments: args, kind: .additive) + } + + init(exclusive args: [ArgumentDefinition]) { + self.init(arguments: args, kind: args.count == 1 ? .additive : .exclusive) + } + + init(alternatives args: [ArgumentDefinition]) { + self.init(arguments: args, kind: args.count == 1 ? .additive : .alternatives) + } + + init(additive sets: [ArgumentSet]) { + self.init(sets: sets, kind: .additive) + } +} + +extension ArgumentSet { + var hasPositional: Bool { + switch content { + case .arguments(let arguments): + return arguments.contains(where: { $0.isPositional }) + case .sets(let sets): + return sets.contains(where: { $0.hasPositional }) + } + } + + var hasRepeatingPositional: Bool { + switch content { + case .arguments(let arguments): + return arguments.contains(where: { $0.isRepeatingPositional }) + case .sets(let sets): + return sets.contains(where: { $0.hasRepeatingPositional }) + } + } + + /// A Boolean value indicating whether this set has valid positional + /// arguments. + /// + /// For positional arguments to be valid, there must be at most one + /// positional array argument, and it must be the last positional argument + /// in the argument list. Any other configuration leads to ambiguity in + /// parsing the arguments. + var hasValidArguments: Bool { + // Exclusive and alternative argument sets must each individually + // satisfy this requirement. + guard kind == .additive else { + switch content { + case .arguments: return true + case .sets(let sets): return sets.allSatisfy({ $0.hasValidArguments }) + } + } + + switch content { + case .arguments(let arguments): + guard let repeatedPositional = arguments.firstIndex(where: { $0.isRepeatingPositional }) + else { return true } + + let hasPositionalFollowingRepeated = arguments[repeatedPositional...] + .dropFirst() + .contains(where: { $0.isPositional }) + return !hasPositionalFollowingRepeated + + case .sets(let sets): + guard let repeatedPositional = sets.firstIndex(where: { $0.hasRepeatingPositional }) + else { return true } + let hasPositionalFollowingRepeated = sets[repeatedPositional...] + .dropFirst() + .contains(where: { $0.hasPositional }) + + return !hasPositionalFollowingRepeated + } + } +} + +// MARK: Flag + +extension ArgumentSet { + /// Creates an argument set for a single Boolean flag. + static func flag(key: InputKey, name: NameSpecification, help: ArgumentHelp?) -> ArgumentSet { + let help = ArgumentDefinition.Help(options: .isOptional, help: help, key: key) + let arg = ArgumentDefinition(kind: .name(key: key, specification: name), help: help, update: .nullary({ (origin, name, values) in + values.set(true, forKey: key, inputOrigin: origin) + }), initial: { origin, values in + values.set(false, forKey: key, inputOrigin: origin) + }) + return ArgumentSet(arg) + } + + /// Creates an argument set for a pair of inverted Boolean flags. + static func flag(key: InputKey, name: NameSpecification, default initialValue: Bool?, inversion: FlagInversion, help: ArgumentHelp?) -> ArgumentSet { + // The flag is required if initialValue is `nil`, otherwise it's optional + let helpOptions: ArgumentDefinition.Help.Options = initialValue != nil ? .isOptional : [] + + let help = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: initialValue.map(String.init), key: key) + let (enableNames, disableNames) = inversion.enableDisableNamePair(for: key, name: name) + + let enableArg = ArgumentDefinition(kind: .named(enableNames), help: help, update: .nullary({ (origin, name, values) in + values.set(true, forKey: key, inputOrigin: origin) + }), initial: { origin, values in + if let initialValue = initialValue { + values.set(initialValue, forKey: key, inputOrigin: origin) + } + }) + let disableArg = ArgumentDefinition(kind: .named(disableNames), help: ArgumentDefinition.Help(options: [.isOptional], key: key), update: .nullary({ (origin, name, values) in + values.set(false, forKey: key, inputOrigin: origin) + }), initial: { _, _ in }) + return ArgumentSet(exclusive: [enableArg, disableArg]) + } + + /// 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 arg = ArgumentDefinition(kind: .name(key: key, specification: name), help: help, update: .nullary({ (origin, name, values) in + guard let a = values.element(forKey: key)?.value, let b = a as? Int else { + throw ParserError.invalidState + } + values.set(b + 1, forKey: key, inputOrigin: origin) + }), initial: { origin, values in + values.set(0, forKey: key, inputOrigin: origin) + }) + return ArgumentSet(arg) + } +} + +// MARK: - + +extension ArgumentSet { + /// Create a unary / argument that parses the string as `A`. + init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ArgumentDefinition.ParsingStrategy = .nextAsValue, parseType type: A.Type, name: NameSpecification, default initial: A?, help: ArgumentHelp?) { + var arg = ArgumentDefinition(key: key, kind: kind, parsingStrategy: parsingStrategy, parser: A.init(argument:), default: initial) + arg.help.help = help + arg.help.defaultValue = initial.map { "\($0)" } + self.init(arg) + } +} + +extension ArgumentDefinition { + /// Create a unary / argument that parses using the given closure. + fileprivate init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .nextAsValue, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?) { + let initalValueCreator: (InputOrigin, inout ParsedValues) throws -> Void + if let initialValue = initial { + initalValueCreator = { origin, values in + values.set(initialValue, forKey: key, inputOrigin: origin) + } + } else { + initalValueCreator = { _, _ in } + } + + self.init(kind: kind, help: ArgumentDefinition.Help(key: key), 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: initalValueCreator) + + help.options.formUnion(ArgumentDefinition.Help.Options(type: type)) + help.defaultValue = initial.map { "\($0)" } + if initial != nil { + self = self.optional + } + } +} + +// MARK: - Parsing from SplitArguments +extension ArgumentSet { + /// Parse the given input (`SplitArguments`) for the given `commandStack` of previously parsed sommands. + /// + /// This method will gracefully fail if there are extra arguments that it doesn’t understand. Hence the + /// *lenient* name. If so, it will return `.partial`. + /// + /// When dealing with commands, this will be called iteratively in order to find + /// the matching command(s). + /// + /// - Parameter all: The input (from the command line) that needs to be parsed + /// - Parameter commandStack: commands that have been parsed + func lenientParse(_ all: SplitArguments) throws -> LenientParsedValues { + // Create a local, mutable copy of the arguments: + var set = all + + func parseValue( + _ argument: ArgumentDefinition, + _ parsed: ParsedArgument, + _ originElement: InputOrigin.Element, + _ update: ArgumentDefinition.Update.Unary, + _ result: inout ParsedValues, + _ usedOrigins: inout InputOrigin + ) throws { + let origin = InputOrigin(elements: [originElement]) + switch argument.parsingStrategy { + case .nextAsValue: + // We need a value for this option. + if let value = parsed.value { + // This was `--foo=bar` style: + try update(origin, parsed.name, value, &result) + } else if let (origin2, value) = set.popNextElementIfValue(after: originElement) { + // Use `popNextElementIfValue(after:)` to handle cases where short option + // labels are combined + let origins = origin.inserting(origin2) + try update(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } else { + throw ParserError.missingValueForOption(origin, parsed.name) + } + + case .scanningForValue: + // We need a value for this option. + if let value = parsed.value { + // This was `--foo=bar` style: + try update(origin, parsed.name, value, &result) + } else if let (origin2, value) = set.popNextValue(after: originElement) { + // Use `popNext(after:)` to handle cases where short option + // labels are combined + let origins = origin.inserting(origin2) + try update(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } else { + throw ParserError.missingValueForOption(origin, parsed.name) + } + + case .unconditional: + // Use an attached value if it exists... + if let value = parsed.value { + // This was `--foo=bar` style: + try update(origin, parsed.name, value, &result) + usedOrigins.formUnion(origin) + } else { + guard let (origin2, value) = set.popNextElementAsValue(after: originElement) else { + throw ParserError.missingValueForOption(origin, parsed.name) + } + let origins = origin.inserting(origin2) + try update(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } + + case .allRemainingInput: + // Reset initial value with the found input origins: + try argument.initial(origin, &result) + + // Use an attached value if it exists... + if let value = parsed.value { + // This was `--foo=bar` style: + try update(origin, parsed.name, value, &result) + usedOrigins.formUnion(origin) + } + + // ...and then consume the rest of the arguments + while let (origin2, value) = set.popNextElementAsValue(after: originElement) { + let origins = origin.inserting(origin2) + try update(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } + + case .upToNextOption: + // Reset initial value with the found source index + try argument.initial(origin, &result) + + // Use an attached value if it exists... + if let value = parsed.value { + // This was `--foo=bar` style: + try update(origin, parsed.name, value, &result) + usedOrigins.formUnion(origin) + } + + // ...and then consume the arguments until hitting an option + while let (origin2, value) = set.popNextElementIfValue() { + let origins = origin.inserting(origin2) + try update(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } + } + } + + var result = ParsedValues(elements: [], originalInput: all.originalInput) + var positionalValues: [(InputOrigin.Element, String)] = [] + var usedOrigins = InputOrigin() + + try setInitialValues(into: &result) + + // Loop over all arguments: + while let (origin, next) = set.popNext() { + defer { + set.removeAll(in: usedOrigins) + } + + switch next { + case let .value(v): + positionalValues.append((origin, v)) + case let .option(parsed): + guard + let argument: ArgumentDefinition = try? first(matching: parsed, at: origin) + else { continue } + + switch argument.update { + case let .nullary(update): + // We need don’t expect a value for this option. + guard parsed.value == nil else { + throw ParserError.unexpectedValueForOption(origin, parsed.name, parsed.value!) + } + try update([origin], parsed.name, &result) + usedOrigins.insert(origin) + case let .unary(update): + try parseValue(argument, parsed, origin, update, &result, &usedOrigins) + } + case .terminator: + // Mark the terminator as used: + result.set(ParsedValues.Element(key: .terminator, value: 0, inputOrigin: [origin])) + } + } + + // We have parsed all non-positional values at this point. + // Next: parse / consume the positional values. + do { + try parsePositionalValues(from: positionalValues, into: &result) + } catch { + switch error { + case ParserError.unexpectedExtraValues: + // There were more positional values than we could parse. + // If we‘re using sub-commands, that could be expected. + return .partial(result, error) + default: + throw error + } + } + return .success(result) + } +} + +extension ArgumentSet { + /// Fills the given `ParsedValues` instance with initial values from this + /// argument set. + func setInitialValues(into parsed: inout ParsedValues) throws { + for arg in self { + try arg.initial(InputOrigin(), &parsed) + } + } +} + +extension ArgumentSet { + /// Find an `ArgumentDefinition` that matches the given `ParsedArgument`. + /// + /// As we iterate over the values from the command line, we try to find a + /// definition that matches the particular element. + /// - Parameters: + /// - parsed: The argument from the command line + /// - origin: The where `parsed` came from. + /// - Returns: The matching definition. + func first( + matching parsed: ParsedArgument, + at origin: InputOrigin.Element + ) throws -> ArgumentDefinition { + guard let match = first(where: { $0.names.contains(parsed.name) }) else { + throw ParserError.unknownOption(origin, parsed.name) + } + return match + } + + func parsePositionalValues( + from values: [(InputOrigin.Element, String)], + into result: inout ParsedValues + ) throws { + guard !values.isEmpty else { return } + var remainingValues = values[values.startIndex.. Iterator { + return Iterator(set: self) + } + + var underestimatedCount: Int { return 0 } + + struct Iterator: IteratorProtocol { + enum Content { + case arguments(ArraySlice) + case sets([Iterator]) + case empty + } + + var content: Content + + init(set: ArgumentSet) { + switch set.content { + case .arguments(let a): + self.content = .arguments(a[a.startIndex.. ArgumentDefinition? { + switch content { + case .arguments(var a): + guard !a.isEmpty else { return nil } + let n = a.remove(at: 0) + content = .arguments(a) + return n + case .sets(var sets): + defer { + content = .sets(sets) + } + while true { + guard !sets.isEmpty else { return nil } + if let n = sets[0].next() { + return n + } + sets.remove(at: 0) + } + case .empty: + return nil + } + } + } +} diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift new file mode 100644 index 000000000..5027ff225 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -0,0 +1,242 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct CommandError: Error { + var commandStack: [ParsableCommand.Type] + var parserError: ParserError +} + +struct HelpRequested: Error {} + +struct CommandParser { + let commandTree: Tree + var currentNode: Tree + var parsedValues: [(type: ParsableCommand.Type, decodedResult: ParsableCommand)] = [] + + var commandStack: [ParsableCommand.Type] { + let result = parsedValues.map { $0.type } + if currentNode.element == result.last { + return result + } else { + return result + [currentNode.element] + } + } + + init(_ rootCommand: ParsableCommand.Type) { + self.commandTree = Tree(root: rootCommand) + self.currentNode = commandTree + + // A command tree that has a depth greater than zero gets a `help` + // subcommand. + if !commandTree.isLeaf { + commandTree.addChild(Tree(HelpCommand.self)) + } + } +} + +extension CommandParser { + /// Consumes the next argument in `split` if it matches a subcommand at the + /// current node of the command tree. + /// + /// If a matching subcommand is found, the subcommand argument is consumed + /// in `split`. + /// + /// - Returns: A node for the matched subcommand if one was found; + /// otherwise, `nil`. + fileprivate func consumeNextCommand(split: inout SplitArguments) -> Tree? { + if let command = split.peekNextValue() { + if let subcommandNode = currentNode.firstChild(withName: command.1) { + _ = split.popNextValue() + return subcommandNode + } + } + return nil + } + + /// Throws a `HelpRequested` error if the user has specified either of the + /// built in help flags. + func checkForHelpFlag(_ split: SplitArguments) throws { + guard !split.contains(anyOf: self.commandTree.element.getHelpNames()) else { + throw HelpRequested() + } + } + + /// Returns the last parsed value if there are no remaining unused arguments. + /// + /// If there are remaining arguments, or if no commands have been parsed, + /// this throws an error. + fileprivate func extractLastParsedValue(_ split: SplitArguments) throws -> ParsableCommand { + try checkForHelpFlag(split) + + // We should have used up all arguments at this point: + guard split.isEmpty else { + let extra = split.coalescedExtraElements() + throw ParserError.unexpectedExtraValues(extra) + } + + guard let lastParsed = parsedValues.last else { + throw ParserError.invalidState + } + + return lastParsed.decodedResult + } + + /// Extracts the current command from `split`, throwing if decoding isn't + /// possible. + fileprivate mutating func parseCurrent(_ split: inout SplitArguments) throws { + // Build the argument set (i.e. information on how to parse): + let commandArguments = ArgumentSet(currentNode.element) + + // Parse the arguments into a ParsedValues: + let parsedResult = try commandArguments.lenientParse(split) + + let values: ParsedValues + switch parsedResult { + case .success(let v): + values = v + case .partial(let v, let e): + values = v + if currentNode.isLeaf { + throw e + } + } + + // Decode the values from ParsedValues into the ParsableCommand: + let decoder = ArgumentDecoder(values: values, previouslyParsedValues: parsedValues) + var decodedResult: ParsableCommand + do { + decodedResult = try currentNode.element.init(from: decoder) + } catch let error { + // If decoding this command failed, see if they were asking for + // help before propagating that parsing failure. + try checkForHelpFlag(split) + throw error + } + + // Decoding was successful, so remove the arguments that were used + // by the decoder. + split.removeAll(in: decoder.usedOrigins) + + // Save this decoded result to add to the next command. + parsedValues.append((currentNode.element, decodedResult)) + } + + /// Starting with the current node, extracts commands out of `split` and + /// descends into sub-commands as far as possible. + internal mutating func descendingParse(_ split: inout SplitArguments) throws { + while true { + try parseCurrent(&split) + + // Look for next command in the argument list. + if let nextCommand = consumeNextCommand(split: &split) { + currentNode = nextCommand + continue + } + + // Look for the help flag before falling back to a default command. + try checkForHelpFlag(split) + + // No command was found, so fall back to the default subcommand. + if let defaultSubcommand = currentNode.element.configuration.defaultSubcommand { + guard let subcommandNode = currentNode.firstChild(equalTo: defaultSubcommand) else { + throw ParserError.invalidState + } + currentNode = subcommandNode + continue + } + + // No more subcommands to parse. + return + } + } + + /// Returns the fully-parsed matching command for `arguments`, or an + /// appropriate error. + /// + /// - Parameter arguments: The array of arguments to parse. This should not + /// include the command name as the first argument. + mutating func parse(arguments: [String]) -> Result { + var split: SplitArguments + do { + split = try SplitArguments(arguments: arguments) + } catch let error as ParserError { + return .failure(CommandError(commandStack: [commandTree.element], parserError: error)) + } catch { + return .failure(CommandError(commandStack: [commandTree.element], parserError: .invalidState)) + } + + do { + try descendingParse(&split) + let result = try extractLastParsedValue(split) + + // HelpCommand is a valid result, but needs extra information about + // the tree from the parser to build its stack of commands. + if var helpResult = result as? HelpCommand { + try helpResult.buildCommandStack(with: self) + return .success(helpResult) + } + return .success(result) + } catch let error as CommandError { + return .failure(error) + } catch let error as ParserError { + return .failure(CommandError(commandStack: commandStack, parserError: error)) + } catch is HelpRequested { + return .success(HelpCommand(commandStack: commandStack)) + } catch { + return .failure(CommandError(commandStack: commandStack, parserError: .invalidState)) + } + } +} + +extension CommandParser { + /// Builds an array of commands that matches the given command names. + /// + /// This stops building the stack if it encounters any "command names" that + /// aren't in the command tree, so it's okay to pass a list of arbitrary + /// commands. Will always return at least the root of the command tree. + func commandStack(for commandNames: [String]) -> [ParsableCommand.Type] { + var node = commandTree + var result = [node.element] + + for name in commandNames { + guard let nextNode = node.firstChild(withName: name) else { + // Reached a non-command argument. + // Ignore anything after this point + return result + } + result.append(nextNode.element) + node = nextNode + } + + return result + } + + func commandStack(for subcommand: ParsableCommand.Type) -> [ParsableCommand.Type] { + let path = commandTree.path(to: subcommand) + return path.isEmpty + ? [commandTree.element] + : path + } +} + +extension SplitArguments { + func contains(anyOf names: [Name]) -> Bool { + self.elements.contains { + switch $0.element { + case .option(.name(let name)), + .option(.nameWithValue(let name, _)): + return names.contains(name) + default: + return false + } + } + } +} diff --git a/Sources/ArgumentParser/Parsing/InputOrigin.swift b/Sources/ArgumentParser/Parsing/InputOrigin.swift new file mode 100644 index 000000000..fee4eb1c4 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/InputOrigin.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// Specifies where a given input came from. +/// +/// When reading from the command line, a value might originate from multiple indices. +/// +/// This is usually an index into the `SplitArguments`. +/// In some cases it can be multiple indices. +struct InputOrigin: Equatable, ExpressibleByArrayLiteral { + enum Element: Comparable, Hashable { + case argumentIndex(SplitArguments.Index) + } + + private var _elements: Set = [] + var elements: [Element] { + get { + Array(_elements).sorted() + } + set { + _elements = Set(newValue) + } + } + + init() { + } + + init(elements: [Element]) { + _elements = Set(elements) + } + + init(element: Element) { + _elements = Set([element]) + } + + init(arrayLiteral elements: InputOrigin.Element...) { + self.init(elements: elements) + } + + static func argumentIndex(_ index: SplitArguments.Index) -> InputOrigin { + return InputOrigin(elements: [.argumentIndex(index)]) + } + + mutating func insert(_ other: InputOrigin.Element) { + guard !_elements.contains(other) else { return } + _elements.insert(other) + } + + func inserting(_ other: InputOrigin.Element) -> InputOrigin { + guard !_elements.contains(other) else { return self } + var result = self + result.insert(other) + return result + } + + mutating func formUnion(_ other: InputOrigin) { + _elements.formUnion(other._elements) + } + + func union(_ other: InputOrigin) -> InputOrigin { + var result = self + result._elements.formUnion(other._elements) + return result + } + + func isSubset(of other: Self) -> Bool { + return _elements.isSubset(of: other._elements) + } + + func forEach(_ closure: (Element) -> Void) { + _elements.forEach(closure) + } +} + +extension InputOrigin.Element { + static func < (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.argumentIndex(let l), .argumentIndex(let r)): + return l < r + } + } +} diff --git a/Sources/ArgumentParser/Parsing/Name.swift b/Sources/ArgumentParser/Parsing/Name.swift new file mode 100644 index 000000000..e3925d5d3 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/Name.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +enum Name: Equatable { + /// A name (usually multi-character) prefixed with `--` (2 dashes) or equivalent. + case long(String) + /// A single character name prefixed with `-` (1 dash) or equivalent. + /// + /// Usually supports mixing multiple short names with a single dash, i.e. `-ab` is equivalent to `-a -b`. + case short(Character) + /// A name (usually multi-character) prefixed with `-` (1 dash). + case longWithSingleDash(String) + + init(_ baseName: Substring) { + assert(baseName.first == "-", "Attempted to create name for unprefixed argument") + if baseName.hasPrefix("--") { + self = .long(String(baseName.dropFirst(2))) + } else if baseName.count == 2 { // single character "-x" style + self = .short(baseName.last!) + } else { // long name with single dash + self = .longWithSingleDash(String(baseName.dropFirst())) + } + } + + static func == (lhs: Name, rhs: Name) -> Bool { + switch (lhs, rhs) { + case (.long(let lhs), .long(let rhs)): + return lhs == rhs + case (.short(let lhs), .short(let rhs)): + return lhs == rhs + case (.longWithSingleDash(let lhs), .longWithSingleDash(let rhs)): + return lhs == rhs + default: + return false + } + } +} + +extension Name { + var synopsisString: String { + switch self { + case .long(let n): + return "--\(n)" + case .short(let n): + return "-\(n)" + case .longWithSingleDash(let n): + return "-\(n)" + } + } + + var valueString: String { + switch self { + case .long(let n): + return n + case .short(let n): + return String(n) + case .longWithSingleDash(let n): + return n + } + } +} + +// short argument names based on the synopsisString +// this will put the single - options before the -- options +extension Name: Comparable { + static func < (lhs: Name, rhs: Name) -> Bool { + return lhs.synopsisString < rhs.synopsisString + } +} diff --git a/Sources/ArgumentParser/Parsing/Parsed.swift b/Sources/ArgumentParser/Parsing/Parsed.swift new file mode 100644 index 000000000..d32c826bd --- /dev/null +++ b/Sources/ArgumentParser/Parsing/Parsed.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +enum Parsed { + /// The definition of how this value is to be parsed from command line arguments. + /// + /// Internally, this wraps an `ArgumentSet`, but that’s not `public` since it’s + /// an implementation detail. + public struct Definition { + var makeSet: (InputKey) -> ArgumentSet + } + + case value(Value) + case definition(Definition) + + internal init(_ makeSet: @escaping (InputKey) -> ArgumentSet) { + self = .definition(Definition(makeSet: makeSet)) + } +} + +/// A type that wraps a `Parsed` instance to act as a property wrapper. +/// +/// This protocol simplifies the implementations of property wrappers that +/// wrap the `Parsed` type. +internal protocol ParsedWrapper: Decodable, ArgumentSetProvider { + associatedtype Value + var _parsedValue: Parsed { get } + init(_parsedValue: Parsed) +} + +/// A `Parsed`-wrapper whose value type knows how to decode itself. Types that +/// conform to this protocol can initialize their values directly from a +/// `Decoder`. +internal protocol DecodableParsedWrapper: ParsedWrapper + where Value: Decodable +{ + init(_parsedValue: Parsed) +} + +extension ParsedWrapper { + init(_decoder: Decoder) throws { + guard let d = _decoder as? SingleValueDecoder else { + throw ParserError.invalidState + } + guard let value = d.parsedElement?.value as? Value else { + throw ParserError.noValue(forKey: d.parsedElement?.key ?? d.key) + } + + self.init(_parsedValue: .value(value)) + } + + func argumentSet(for key: InputKey) -> ArgumentSet { + switch _parsedValue { + case .value: + fatalError("Trying to get the argument set from a resolved/parsed property.") + case .definition(let a): + return a.makeSet(key) + } + } +} + +extension ParsedWrapper where Value: Decodable { + init(_decoder: Decoder) throws { + var value: Value + + do { + value = try Value.init(from: _decoder) + } catch { + if let d = _decoder as? SingleValueDecoder, + let v = d.parsedElement?.value as? Value { + value = v + } else { + throw error + } + } + + self.init(_parsedValue: .value(value)) + } +} diff --git a/Sources/ArgumentParser/Parsing/ParsedValues.swift b/Sources/ArgumentParser/Parsing/ParsedValues.swift new file mode 100644 index 000000000..9a975b45e --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ParsedValues.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct InputKey: RawRepresentable, Equatable { + var rawValue: String + + init(rawValue: String) { + self.rawValue = rawValue + } + + init(_ codingKey: C) { + self.rawValue = codingKey.stringValue + } + + static let terminator = InputKey(rawValue: "__terminator") +} + +/// The resulting values after parsing the command line arguments. +/// +/// This is a flat key-value list of values. +struct ParsedValues { + struct Element { + var key: InputKey + var value: Any + /// Where in the input that this came from. + var inputOrigin: InputOrigin + } + + /// These are the parsed key-value pairs. + var elements: [Element] = [] + + /// This is the *original* array of arguments that this was parsed from. + /// + /// This is used for error output generation. + var originalInput: [String] + + public init(elements: [Element] = [], originalInput: [String]) { + self.elements = elements + self.originalInput = originalInput + } +} + +enum LenientParsedValues { + case success(ParsedValues) + case partial(ParsedValues, Swift.Error) +} + +extension ParsedValues { + mutating func set(_ new: Any, forKey key: InputKey, inputOrigin: InputOrigin) { + set(Element(key: key, value: new, inputOrigin: inputOrigin)) + } + + mutating func set(_ element: Element) { + if let index = elements.firstIndex(where: { $0.key == element.key }) { + // Merge the source values. We need to keep track + // of any previous source indexes we have used for + // this key. + var e = element + e.inputOrigin.formUnion(elements[index].inputOrigin) + elements[index] = e + } else { + elements.append(element) + } + } + + func element(forKey key: InputKey) -> Element? { + return elements.first(where: { $0.key == key }) + } + + mutating func update(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 + closure(&v) + e.value = v + e.inputOrigin.formUnion(inputOrigin) + set(e) + } +} diff --git a/Sources/ArgumentParser/Parsing/ParserError.swift b/Sources/ArgumentParser/Parsing/ParserError.swift new file mode 100644 index 000000000..197a35809 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ParserError.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// Gets thrown while parsing and will be handled by the error output generation. +enum ParserError: Error { + case helpRequested + case notImplemented + case invalidState + case unknownOption(InputOrigin.Element, Name) + case invalidOption(String) + case nonAlphanumericShortOption(Character) + /// The option was there, but its value is missing, e.g. `--name` but no value for the `name`. + case missingValueForOption(InputOrigin, Name) + case unexpectedValueForOption(InputOrigin.Element, Name, String) + case unexpectedExtraValues([(InputOrigin, String)]) + case duplicateExclusiveValues(previous: InputOrigin, duplicate: InputOrigin) + /// 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) + case missingSubcommand + case userValidationError(Error) +} + +/// These are errors used internally to the parsing, and will not be exposed to the help generation +enum InternalParseError: Error { + case wrongType(Any, forKey: InputKey) + case subcommandNameMismatch + case subcommandLevelMismatch(Int, Int) + case subcommandLevelMissing(Int) + case subcommandLevelDuplicated(Int) + case expectedCommandButNoneFound +} diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift new file mode 100644 index 000000000..79a65f51d --- /dev/null +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -0,0 +1,515 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// A single `-f`, `--foo`, or `--foo=bar`. +/// +/// When parsing, we might see "--foo" or "--foo=bar". +enum ParsedArgument: Equatable, CustomStringConvertible { + /// `--foo` or `-f` + case name(Name) + /// `--foo=bar` + case nameWithValue(Name, String) + + init(_ str: S) where S.SubSequence == Substring { + let indexOfEqualSign = str.firstIndex(of: "=") ?? str.endIndex + let (baseName, value) = (str[.. Bool { + lhs.rawValue < rhs.rawValue + } + + var next: InputIndex { + InputIndex(rawValue: rawValue + 1) + } + } + + /// The index into an input index position. + /// + /// E.g. the input `"-vh"` will be split into the elements `-v`, and `-h` + /// each with its own subindex. + enum SubIndex: Hashable, Comparable { + case complete + case sub(Int) + + static func <(lhs: SubIndex, rhs: SubIndex) -> Bool { + switch (lhs, rhs) { + case (.complete, .sub): + return true + case (.sub(let l), .sub(let r)) where l < r: + return true + default: + return false + } + } + } + + /// Tracks both the index into the original input and the index into the split arguments (array of elements). + struct Index: Hashable, Comparable { + static func < (lhs: SplitArguments.Index, rhs: SplitArguments.Index) -> Bool { + if lhs.inputIndex < rhs.inputIndex { + return true + } else if lhs.inputIndex == rhs.inputIndex { + return lhs.subIndex < rhs.subIndex + } else { + return false + } + } + + var inputIndex: InputIndex + var subIndex: SubIndex + } + + var elements: [(index: Index, element: Element)] + var originalInput: [String] +} + +extension SplitArguments.Element: CustomDebugStringConvertible { + var debugDescription: String { + switch self { + case .option(.name(let name)): + return name.synopsisString + case .option(.nameWithValue(let name, let value)): + return name.synopsisString + "; value '\(value)'" + case .value(let value): + return "value '\(value)'" + case .terminator: + return "terminator" + } + } +} + +extension SplitArguments.Index: CustomStringConvertible { + var description: String { + switch subIndex { + case .complete: return "\(inputIndex.rawValue)" + case .sub(let sub): return "\(inputIndex.rawValue).\(sub)" + } + } +} + +extension SplitArguments: CustomStringConvertible { + var description: String { + guard !isEmpty else { return "" } + return elements + .map { (index, element) -> String in + switch element { + case .option(.name(let name)): + return "[\(index)] \(name.synopsisString)" + case .option(.nameWithValue(let name, let value)): + return "[\(index)] \(name.synopsisString)='\(value)'" + case .value(let value): + return "[\(index)] '\(value)'" + case .terminator: + return "[\(index)] --" + } + } + .joined(separator: " ") + } +} + +extension SplitArguments.Element { + var isValue: Bool { + switch self { + case .value: return true + case .option, .terminator: return false + } + } +} + +extension SplitArguments { + var isEmpty: Bool { + elements.isEmpty + } + + subscript(position: Index) -> Element? { + return elements.first { + $0.0 == position + }?.1 + } + + mutating func popNext() -> (InputOrigin.Element, Element)? { + guard let (index, value) = elements.first else { return nil } + elements.remove(at: 0) + return (.argumentIndex(index), value) + } + + func peekNext() -> (InputOrigin.Element, Element)? { + guard let (index, value) = elements.first else { return nil } + return (.argumentIndex(index), value) + } + + /// Pops the element immediately after the given index, if it is a `.value`. + /// + /// This is used to get the next value in `-fb name` where `name` is the + /// value for `-f` or `--foo name` where `name` is the value for `--foo`. + /// If `--foo` expects a value, input of `--foo --bar name` will return + /// `nil`, since the option `--bar` comes before the value `name`. + mutating func popNextElementIfValue(after origin: InputOrigin.Element) -> (InputOrigin.Element, String)? { + // Look for the index of the input that comes from immediately after + // `origin` in the input string. We look at the input index so that + // packed short options can be followed, in order, by their values. + // e.g. "-fn f-value n-value" + guard + case .argumentIndex(let after) = origin, + let elementIndex = elements.firstIndex(where: { $0.0.inputIndex > after.inputIndex }) + else { return nil } + + // Only succeed if the element is a value (not prefixed with a dash) + guard case .value(let value) = elements[elementIndex].1 + else { return nil } + + let matchedArgumentIndex = elements[elementIndex].0 + elements.remove(at: elementIndex) + return (.argumentIndex(matchedArgumentIndex), value) + } + + /// Pops the next `.value` after the given index. + /// + /// This is used to get the next value in `-f -b name` where `name` is the value of `-f`. + mutating func popNextValue(after origin: InputOrigin.Element) -> (InputOrigin.Element, String)? { + guard case .argumentIndex(let after) = origin else { return nil } + for (index, element) in elements.enumerated() { + guard + element.0 > after, + case .value(let value) = element.1 + else { continue } + elements.remove(at: index) + return (.argumentIndex(element.0), value) + } + return nil + } + + /// Pops the element after the given index as a value. + /// + /// This will re-interpret `.option` and `.terminator` as values, i.e. + /// read from the `originalInput`. + /// + /// For an input such as `--a --b foo`, if passed the origin of `--a`, + /// this will first pop the value `--b`, then the value `foo`. + mutating func popNextElementAsValue(after origin: InputOrigin.Element) -> (InputOrigin.Element, String)? { + guard case .argumentIndex(let after) = origin else { return nil } + // Elements are sorted by their `InputIndex`. Find the first `InputIndex` + // after `origin`: + guard let unconditionalIndex = elements.first(where: { (index, _) in index.inputIndex > after.inputIndex })?.0.inputIndex else { return nil } + let nextIndex = Index(inputIndex: unconditionalIndex, subIndex: .complete) + // Remove all elements with this `InputIndex`: + remove(at: nextIndex) + // Return the original input + return (.argumentIndex(nextIndex), originalInput[unconditionalIndex.rawValue]) + } + + /// Pops the next element if it is a value. + /// + /// If the current elements are `--b foo`, this will return `nil`. If the + /// elements are `foo --b`, this will return the value `foo`. + mutating func popNextElementIfValue() -> (InputOrigin.Element, String)? { + guard + let (index, element) = elements.first, + case .value(let value) = element + else { return nil } + elements.remove(at: 0) + return (.argumentIndex(index), value) + } + + /// Finds and "pops" the next element that is a value. + /// + /// If the current elements are `--a --b foo`, this will remove and return + /// `foo`. + mutating func popNextValue() -> (Index, String)? { + guard let idx = elements.firstIndex(where: { + switch $0.element { + case .option: return false + case .value: return true + case .terminator: return false + } + }) else { return nil } + let e = elements[idx] + elements.remove(at: idx) + guard case let .value(v) = e.element else { fatalError() } + return (e.index, v) + } + + func peekNextValue() -> (Index, String)? { + guard let idx = elements.firstIndex(where: { + switch $0.element { + case .option: return false + case .value: return true + case .terminator: return false + } + }) else { return nil } + let e = elements[idx] + guard case let .value(v) = e.element else { fatalError() } + return (e.index, v) + } + + /// Removes the element(s) at the given `Index`. + /// + /// - Note: This may remove multiple elements. + /// + /// For combined _short_ arguments such as `-ab`, these will gets parsed into + /// 3 elements: The _long with short dash_ `ab`, and 2 _short_ `a` and `b`. All of these + /// will have the same `inputIndex` but different `subIndex`. When either of the short ones + /// is removed, that will remove the _long with short dash_ as well. Likewise, if the + /// _long with short dash_ is removed, that will remove both of the _short_ elements. + mutating func remove(at position: Index) { + if case .complete = position.subIndex { + // When removing a `.complete`, we need to remove _all_ + // elements that have the same `InputIndex`. + elements.removeAll { (index, _) -> Bool in + index.inputIndex == position.inputIndex + } + } else { + // When removing a `.sub` (i.e. non-`.complete`), we need to + // remove any `.complete`. + elements.removeAll { (index, _) -> Bool in + index == position || + ((index.inputIndex == position.inputIndex) && (index.subIndex == .complete)) + } + } + } + + mutating func removeAll(in origin: InputOrigin) { + origin.forEach { + remove(at: $0) + } + } + + /// Removes the element(s) at the given position. + /// + /// Note that this may remove multiple elements. + mutating func remove(at origin: InputOrigin.Element) { + guard case .argumentIndex(let i) = origin else { return } + remove(at: i) + } + + func coalescedExtraElements() -> [(InputOrigin, String)] { + let completeIndexes: [InputIndex] = elements + .compactMap { + guard case .complete = $0.0.subIndex else { return nil } + return $0.0.inputIndex + } + + // Now return all elements that are either: + // 1) `.complete` + // 2) `.sub` but not in `completeIndexes` + + let extraElements: [(Index, Element)] = elements.filter { + switch $0.0.subIndex { + case .complete: + return true + case .sub: + return !completeIndexes.contains($0.0.inputIndex) + } + } + return extraElements.map { index, element -> (InputOrigin, String) in + let input: String + switch index.subIndex { + case .complete: + input = originalInput[index.inputIndex.rawValue] + case .sub: + if case .option(let option) = element { + input = String(describing: option) + } else { + // Odd case. Fall back to entire input at that index: + input = originalInput[index.inputIndex.rawValue] + } + } + return (.argumentIndex(index), input) + } + } +} + +extension SplitArguments { + /// Parses the given input into an array of `Element`. + /// + /// - Parameter arguments: The input from the command line. + init(arguments: [String]) throws { + self.init(elements: [], originalInput: arguments) + + var inputIndex = InputIndex(rawValue: 0) + + func append(_ element: SplitArguments.Element, sub: Int? = nil) { + let subIndex = sub.flatMap { SubIndex.sub($0) } ?? SubIndex.complete + let index = Index(inputIndex: inputIndex, subIndex: subIndex) + elements.append((index, element)) + } + + var args = arguments[arguments.startIndex.. Name) throws { + if let equalIdx = remainder.firstIndex(of: "=") { + let name = remainder[remainder.startIndex.. [ParsedArgument] { + var result: [ParsedArgument] = [] + var remainder = shortArgRemainder + while let char = remainder.popFirst() { + guard char.isLetter || char.isNumber else { + throw ParserError.nonAlphanumericShortOption(char) + } + result.append(.name(.short(char))) + } + return result + } +} diff --git a/Sources/ArgumentParser/Usage/HelpCommand.swift b/Sources/ArgumentParser/Usage/HelpCommand.swift new file mode 100644 index 000000000..4bfa143af --- /dev/null +++ b/Sources/ArgumentParser/Usage/HelpCommand.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct HelpCommand: ParsableCommand { + static var configuration = CommandConfiguration(commandName: "help") + + @Argument() var subcommands: [String] + + private(set) var commandStack: [ParsableCommand.Type] = [] + + init() {} + + func run() { + print(generateHelp()) + } + + mutating func buildCommandStack(with parser: CommandParser) throws { + commandStack = parser.commandStack(for: subcommands) + } + + func generateHelp() -> String { + return HelpGenerator(commandStack: commandStack).rendered + } + + enum CodingKeys: CodingKey { + case subcommands + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self._subcommands = Argument(_parsedValue: .value(try container.decode([String].self, forKey: .subcommands))) + } + + init(commandStack: [ParsableCommand.Type]) { + self.commandStack = commandStack + self._subcommands = Argument(_parsedValue: .value(commandStack.map { $0._commandName })) + } +} + diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift new file mode 100644 index 000000000..60bea6db9 --- /dev/null +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -0,0 +1,267 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +internal struct HelpGenerator { + static var helpIndent = 2 + static var labelColumnWidth = 26 + static var screenWidth: Int { + _screenWidthOverride ?? _terminalWidth() + } + + internal static var _screenWidthOverride: Int? = nil + + struct Usage { + public var components: [String] + + public init(components: [String]) { + self.components = components + } + + var rendered: String { + components + .joined(separator: "\n") + } + } + + struct Section { + struct Element { + public var label: String + public var abstract: String + public var discussion: String + + public init(label: String, abstract: String = "", discussion: String = "") { + self.label = label + self.abstract = abstract + self.discussion = discussion + } + + var paddedLabel: String { + String(repeating: " ", count: HelpGenerator.helpIndent) + label + } + + var rendered: String { + let paddedLabel = self.paddedLabel + let wrappedAbstract = self.abstract + .wrapped(to: HelpGenerator.screenWidth, wrappingIndent: HelpGenerator.labelColumnWidth) + let wrappedDiscussion = self.discussion.isEmpty + ? "" + : self.discussion.wrapped(to: HelpGenerator.screenWidth, wrappingIndent: HelpGenerator.helpIndent * 4) + "\n" + + if paddedLabel.count < HelpGenerator.labelColumnWidth { + return paddedLabel + + wrappedAbstract.dropFirst(paddedLabel.count) + "\n" + + wrappedDiscussion + } else { + return paddedLabel + "\n" + + wrappedAbstract + "\n" + + wrappedDiscussion + } + } + } + + enum Header: CustomStringConvertible, Equatable { + case positionalArguments + case subcommands + case options + + var description: String { + switch self { + case .positionalArguments: + return "Arguments" + case .subcommands: + return "Subcommands" + case .options: + return "Options" + } + } + } + + var header: Header + var elements: [Element] + var discussion: String + var isSubcommands: Bool + + init(header: Header, elements: [Element], discussion: String = "", isSubcommands: Bool = false) { + self.header = header + self.elements = elements + self.discussion = discussion + self.isSubcommands = isSubcommands + } + + var rendered: String { + guard !elements.isEmpty else { return "" } + + let renderedElements = elements.map { $0.rendered }.joined() + return "\(String(describing: header).uppercased()):\n" + + renderedElements + } + } + + struct DiscussionSection { + var title: String + var content: String + + init(title: String = "", content: String) { + self.title = title + self.content = content + } + } + + var abstract: String + var usage: Usage + var sections: [Section] + var discussionSections: [DiscussionSection] + + init(commandStack: [ParsableCommand.Type]) { + guard let currentCommand = commandStack.last else { + fatalError() + } + + let currentArgSet = ArgumentSet(currentCommand) + + let toolName = commandStack.map { $0._commandName }.joined(separator: " ") + var usageString = UsageGenerator(toolName: toolName, definition: [currentArgSet]).synopsis + if !currentCommand.configuration.subcommands.isEmpty { + if usageString.last != " " { usageString += " " } + usageString += "" + } + + self.abstract = currentCommand.configuration.abstract + if !currentCommand.configuration.discussion.isEmpty { + self.abstract += "\n\n\(currentCommand.configuration.discussion)" + } + + self.usage = Usage(components: [usageString]) + self.sections = HelpGenerator.generateSections(commandStack: commandStack) + self.discussionSections = [] + } + + static func generateSections(commandStack: [ParsableCommand.Type]) -> [Section] { + var positionalElements: [Section.Element] = [] + var optionElements: [Section.Element] = [] + + for commandType in commandStack { + let args = Array(ArgumentSet(commandType)) + + var i = 0 + while i < args.count { + defer { i += 1 } + let arg = args[i] + + guard arg.help.help?.shouldDisplay != false else { continue } + + let synopsis: String + let description: String + + if i < args.count - 1 && args[i + 1].help.keys == arg.help.keys { + // If the next argument has the same keys as this one, output them together + let nextArg = args[i + 1] + let defaultValue = arg.help.defaultValue.map { "(default: \($0))" } ?? "" + synopsis = "\(arg.synopsisForHelp ?? "")/\(nextArg.synopsisForHelp ?? "")" + description = [arg.help.help?.abstract ?? nextArg.help.help?.abstract ?? "", defaultValue].joined(separator: " ") + i += 1 + + } else { + let defaultValue = arg.help.defaultValue.flatMap { + return $0 == "true" || $0 == "false" + ? nil + : "(default: \($0))" + } ?? "" + synopsis = arg.synopsisForHelp ?? "" + description = [arg.help.help?.abstract ?? "", defaultValue].joined(separator: " ") + } + + let element = Section.Element(label: synopsis, abstract: description, discussion: arg.help.help?.discussion ?? "") + if case .positional = arg.kind { + positionalElements.append(element) + } else { + optionElements.append(element) + } + } + } + + let helpLabels = commandStack + .first! + .getHelpNames() + .map { $0.synopsisString } + .joined(separator: ", ") + if !helpLabels.isEmpty { + optionElements.append(.init(label: helpLabels, abstract: "Show help information.")) + } + + let subcommandElements: [Section.Element] = + commandStack.last!.configuration.subcommands.compactMap { command in + guard command.configuration.shouldDisplay else { return nil } + return Section.Element( + label: command._commandName, + abstract: command.configuration.abstract) + } + + return [ + Section(header: .positionalArguments, elements: positionalElements), + Section(header: .options, elements: optionElements), + Section(header: .subcommands, elements: subcommandElements), + ] + } + + var usageMessage: String { + "Usage: \(usage.rendered)" + } + + var rendered: String { + let renderedSections = sections + .map { $0.rendered } + .filter { !$0.isEmpty } + .joined(separator: "\n") + let renderedAbstract = abstract.isEmpty + ? "" + : "OVERVIEW: \(abstract)".wrapped(to: HelpGenerator.screenWidth) + "\n\n" + + return """ + \(renderedAbstract)\ + USAGE: \(usage.rendered) + + \(renderedSections) + """ + } +} + +internal extension ParsableCommand { + static func getHelpNames() -> [Name] { + return self.configuration + .helpNames + .makeNames(InputKey(rawValue: "help")) + .sorted(by: >) + } +} + +#if os(Linux) +import Glibc +func ioctl(_ a: Int32, _ b: Int32, _ p: UnsafeMutableRawPointer) -> Int32 { + ioctl(CInt(a), UInt(b), p) +} +#else +import Darwin +#endif + +func _terminalWidth() -> Int { + var w = winsize() + let err = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) + let result = Int(w.ws_col) + return err == 0 && result > 0 ? result : 80 +} + +func _terminalHeight() -> Int { + var w = winsize() + let err = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) + let result = Int(w.ws_row) + return err == 0 && result > 0 ? result : 25 +} diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift new file mode 100644 index 000000000..441ce61c8 --- /dev/null +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import Foundation + +enum MessageInfo { + case help(text: String) + case validation(message: String, usage: String) + case other(message: String) + + init(error: Error, type: ParsableArguments.Type) { + var commandStack: [ParsableCommand.Type] + var parserError: ParserError? = nil + + switch error { + case let e as CommandError: + commandStack = e.commandStack + parserError = e.parserError + if case .helpRequested = e.parserError { + self = .help(text: HelpGenerator(commandStack: e.commandStack).rendered) + return + } + case let e as ParserError: + commandStack = [type.asCommand] + parserError = e + if case .helpRequested = e { + self = .help(text: HelpGenerator(commandStack: [type.asCommand]).rendered) + return + } + default: + commandStack = [type.asCommand] + // if the error wasn't one of our two Error types, wrap it as a userValidationError + // to be handled appropirately below + parserError = .userValidationError(error) + } + + let usage = HelpGenerator(commandStack: commandStack).usageMessage + + // Parsing errors and user-thrown validation errors have the usage + // string attached. Other errors just get the error message. + + if case .userValidationError(let error) = parserError { + switch error { + case let error as ValidationError: + self = .validation(message: error.message, usage: usage) + case let error as CleanExit: + switch error { + case .helpRequest(let command): + if let command = command { + commandStack = CommandParser(type.asCommand).commandStack(for: command) + } + self = .help(text: HelpGenerator(commandStack: commandStack).rendered) + case .message(let message): + self = .help(text: message) + } + case let error as LocalizedError where error.errorDescription != nil: + self = .other(message: error.errorDescription!) + default: + self = .other(message: String(describing: error)) + } + } else if let parserError = parserError { + let message = ArgumentSet(commandStack.last!).helpMessage(for: parserError) + self = .validation(message: message, usage: usage) + } else { + self = .other(message: String(describing: error)) + } + } + + var message: String { + switch self { + case .help(text: let text): + return text + case .validation(message: let message, usage: _): + return message + case .other(message: let message): + return message + } + } + + var fullText: String { + switch self { + case .help(text: let text): + return text + case .validation(message: let message, usage: let usage): + return "Error: \(message)\n\(usage)" + case .other(message: let message): + return "Error: \(message)" + } + } + + var shouldExitCleanly: Bool { + switch self { + case .help: return true + case .validation, .other: return false + } + } +} diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift new file mode 100644 index 000000000..9e7cbe665 --- /dev/null +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -0,0 +1,318 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import Foundation + +struct UsageGenerator { + var toolName: String + var definition: ArgumentSet +} + +extension UsageGenerator { + init(definition: ArgumentSet) { + let toolName = CommandLine.arguments[0].split(separator: "/").last.map(String.init) ?? "" + self.init(toolName: toolName, definition: definition) + } + + init(toolName: String, parsable: ParsableArguments) { + self.init(toolName: toolName, definition: ArgumentSet(type(of: parsable))) + } + + init(toolName: String, definition: [ArgumentSet]) { + self.init(toolName: toolName, definition: ArgumentSet(additive: definition)) + } +} + +extension UsageGenerator { + /// The tool synopsis. + /// + /// In `roff`. + var synopsis: String { + let definitionSynopsis = definition.synopsis + switch definitionSynopsis.count { + case 0: + return toolName + case let x where x > 12: + return "\(toolName) " + default: + return "\(toolName) \(definition.synopsis.joined(separator: " "))" + } + } +} + +extension ArgumentSet { + var synopsis: [String] { + return self + .compactMap { $0.synopsis } + } +} + +extension ArgumentDefinition { + var synopsisForHelp: String? { + guard help.help?.shouldDisplay != false else { + return nil + } + + switch kind { + case .named: + let joinedSynopsisString = sortedNames + .map { $0.synopsisString } + .joined(separator: ", ") + + switch update { + case .unary: + return "\(joinedSynopsisString) <\(synopsisValueName ?? "")>" + case .nullary: + return joinedSynopsisString + } + case .positional: + return "<\(valueName)>" + } + } + + var unadornedSynopsis: String? { + switch kind { + case .named: + guard let name = preferredNameForSynopsis else { return nil } + + switch update { + case .unary: + return "\(name.synopsisString) <\(synopsisValueName ?? "value")>" + case .nullary: + return name.synopsisString + } + case .positional: + return "<\(valueName)>" + } + } + + var synopsis: String? { + guard help.help?.shouldDisplay != false else { + return nil + } + + guard !help.options.contains(.isOptional) else { + var n = self + n.help.options.remove(.isOptional) + return n.synopsis.flatMap { "[\($0)]" } + } + guard !help.options.contains(.isRepeating) else { + var n = self + n.help.options.remove(.isRepeating) + return n.synopsis.flatMap { "\($0) ..." } + } + + return unadornedSynopsis + } + + var sortedNames: [Name] { + return names + .sorted { (lhs, rhs) -> Bool in + switch (lhs, rhs) { + case let (.long(l), .long(r)): + return l < r + case (_, .long): + return true + case (.long, _): + return false + case let (.short(l), .short(r)): + return l < r + case (_, .short): + return true + case (.short, _): + return false + case let (.longWithSingleDash(l), .longWithSingleDash(r)): + return l < r + } + } + } + + var preferredNameForSynopsis: Name? { + sortedNames.last + } + + var synopsisValueName: String? { + valueName + } +} + +extension ArgumentSet { + func helpMessage(for error: Swift.Error) -> String { + return errorDescription(error: error) ?? "" + } + + /// Will generate a descriptive help message if possible. + /// + /// If no decriptive help message can be generated `nil` will be returned. + /// + /// - Parameter error: the parse error that occurred. + func errorDescription(error: Swift.Error) -> String? { + switch error { + case let parserError as ParserError: + return ErrorMessageGenerator(arguments: self, error: parserError) + .makeErrorMessage() + case let commandError as CommandError: + return ErrorMessageGenerator(arguments: self, error: commandError.parserError) + .makeErrorMessage() + default: + return nil + } + } +} + +struct ErrorMessageGenerator { + var arguments: ArgumentSet + var error: ParserError +} + +extension ErrorMessageGenerator { + func makeErrorMessage() -> String? { + switch error { + case .notImplemented: + return notImplementedMessage + case .invalidState: + return invalidState + case .unknownOption(let o, let n): + return unknownOptionMessage(origin: o, name: n) + case .missingValueForOption(let o, let n): + return missingValueForOptionMessage(origin: o, name: n) + case .unexpectedValueForOption(let o, let n, let v): + return unexpectedValueForOptionMessage(origin: o, name: n, value: v) + case .unexpectedExtraValues(let v): + return unexpectedExtraValuesMessage(values: v) + case .duplicateExclusiveValues(previous: let previous, duplicate: let duplicate): + return duplicateExclusiveValues(previous: previous, duplicate: duplicate) + case .noValue(forKey: let k): + return noValueMessage(key: k) + case .unableToParseValue(let o, let n, let v, forKey: let k): + return unableToParseValueMessage(origin: o, name: n, value: v, key: k) + case .helpRequested: + return nil + case .invalidOption(let str): + return "Invalid option: \(str)" + case .nonAlphanumericShortOption(let c): + return "Invalid option: -\(c)" + case .missingSubcommand: + return "Missing required subcommand." + case .userValidationError(let error): + switch error { + case let error as LocalizedError: + return error.errorDescription + default: + return String(describing: error) + } + } + } +} + +extension ErrorMessageGenerator { + func arguments(for key: InputKey) -> [ArgumentDefinition] { + return arguments + .filter { + $0.help.keys.contains(key) + } + } + + func help(for key: InputKey) -> ArgumentDefinition.Help? { + return arguments + .first { $0.help.keys.contains(key) } + .map { $0.help } + } + + func valueName(for name: Name) -> String? { + for arg in arguments { + guard + arg.names.contains(name), + let v = arg.synopsisValueName + else { continue } + return v + } + return nil + } +} + +extension ErrorMessageGenerator { + var notImplementedMessage: String { + return "Internal error. Parsing command line arguments hit unimplemented code path." + } + var invalidState: String { + return "Internal error. Invalid state while parsing command line arguments." + } + + func unknownOptionMessage(origin: InputOrigin.Element, name: Name) -> String { + switch name { + case .long(let n): + return "Unknown option '--\(n)'" + case .short(let n): + return "Unknown option '-\(n)'" + case .longWithSingleDash(let n): + return "Unknown option '-\(n)'" + } + } + + func missingValueForOptionMessage(origin: InputOrigin, name: Name) -> String { + if let valueName = valueName(for: name) { + return "Missing value for '\(name.synopsisString) <\(valueName)>'" + } else { + return "Missing value for '\(name.synopsisString)'" + } + } + + func unexpectedValueForOptionMessage(origin: InputOrigin.Element, name: Name, value: String) -> String? { + return "The option '\(name.synopsisString)' does not take any value, but '\(value)' was specified." + } + + func unexpectedExtraValuesMessage(values: [(InputOrigin, String)]) -> String? { + switch values.count { + case 0: + return nil + case 1: + return "Unexpected argument '\(values.first!.1)'" + default: + let v = values.map { $0.1 }.joined(separator: "', '") + return "\(values.count) unexpected arguments: '\(v)'" + } + } + + func duplicateExclusiveValues(previous: InputOrigin, duplicate: InputOrigin) -> String? { + return "Value at position \(duplicate) has already been set from value at position \(previous)." + } + + func noValueMessage(key: InputKey) -> String? { + let args = arguments(for: key) + let possibilities = args.compactMap { + $0.nonOptional.synopsis + } + switch possibilities.count { + case 0: + return "Missing expected argument" + case 1: + return "Missing expected argument '\(possibilities.first!)'" + default: + let p = possibilities.joined(separator: "', '") + return "Missing one of: '\(p)'" + } + } + + func unableToParseValueMessage(origin: InputOrigin, name: Name?, value: String, key: InputKey) -> String { + let valueName = arguments(for: key).first?.valueName + switch (name, valueName) { + case let (n?, v?): + return "The value '\(value)' is invalid for '\(n.synopsisString) <\(v)>'" + case let (_, v?): + return "The value '\(value)' is invalid for '<\(v)>'" + case let (n?, _): + return "The value '\(value)' is invalid for '\(n.synopsisString)'" + case (nil, nil): + return "The value '\(value)' is invalid." + } + } +} diff --git a/Sources/ArgumentParser/Utilities/StringExtensions.swift b/Sources/ArgumentParser/Utilities/StringExtensions.swift new file mode 100644 index 000000000..b02a18803 --- /dev/null +++ b/Sources/ArgumentParser/Utilities/StringExtensions.swift @@ -0,0 +1,147 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +extension String { + func wrapped(to columns: Int, wrappingIndent: Int = 0) -> String { + let columns = columns - wrappingIndent + var result: [Substring] = [] + + var currentIndex = startIndex + + while true { + let nextChunk = self[currentIndex...].prefix(columns) + if let lastLineBreak = nextChunk.lastIndex(of: "\n") { + result.append(self[currentIndex.. String { + guard let firstChar = first else { return prefix } + return "\(prefix)\(firstChar.uppercased())\(self.dropFirst())" + } + + /// Returns this string prefixed using kebab-, snake-, or camel-case style + /// depending on what can be detected from the string. + /// + /// Examples: + /// + /// "hello".addingPrefixWithAutodetectedStyle("my") + /// // my-hello + /// "hello_there".addingPrefixWithAutodetectedStyle("my") + /// // my_hello_there + /// "hello-there".addingPrefixWithAutodetectedStyle("my") + /// // my-hello-there + /// "helloThere".addingPrefixWithAutodetectedStyle("my") + /// // myHelloThere + func addingPrefixWithAutodetectedStyle(_ prefix: String) -> String { + if contains("-") { + return "\(prefix)-\(self)" + } else if contains("_") { + return "\(prefix)_\(self)" + } else if first?.isLowercase == true && contains(where: { $0.isUppercase }) { + return addingIntercappedPrefix(prefix) + } else { + return "\(prefix)-\(self)" + } + } + + /// Returns a new string with the camel-case-based words of this string + /// split by the specified separator. + /// + /// Examples: + /// + /// "myProperty".convertedToSnakeCase() + /// // my_property + /// "myURLProperty".convertedToSnakeCase() + /// // my_url_property + /// "myURLProperty".convertedToSnakeCase(separator: "-") + /// // my-url-property + func convertedToSnakeCase(separator: Character = "_") -> String { + guard !isEmpty else { return self } + // This algorithm expects the first character of the string to be lowercase, + // per Swift API design guidelines. If it's an uppercase character instead, + // add and strip an extra character at the beginning. + // TODO: Fold this logic into the body of this method? + guard first?.isUppercase == false else { + return String( + ("z" + self).convertedToSnakeCase(separator: separator) + .dropFirst(2) + ) + } + + var words : [Range] = [] + // The general idea of this algorithm is to split words on transition from + // lower to upper case, then on transition of >1 upper case characters to + // lowercase + var cursor = startIndex + + // Find next uppercase character + while let nextUpperCase = self[cursor...].dropFirst().firstIndex(where: { $0.isUppercase }) { + words.append(cursor..1 capital letters. Turn those into a word, + // stopping at the capital before the lower case character. + let beforeLowerIndex = self.index(before: nextLowerCase) + words.append(nextUpperCase.. { + var element: Element + weak var parent: Tree? + var children: [Tree] + + var isRoot: Bool { parent == nil } + var isLeaf: Bool { children.isEmpty } + var hasChildren: Bool { !isLeaf } + + init(_ element: Element) { + self.element = element + self.parent = nil + self.children = [] + } + + func addChild(_ tree: Tree) { + children.append(tree) + tree.parent = self + } +} + +extension Tree: Hashable { + static func == (lhs: Tree, rhs: Tree) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +extension Tree { + /// Returns a path of tree nodes that traverses from this node to the first + /// node (breadth-first) that matches the given predicate. + func path(toFirstWhere predicate: (Element) -> Bool) -> [Tree] { + var visited: Set = [] + var toVisit: [Tree] = [self] + var currentIndex = 0 + + // For each node, the neighbor that is most efficiently used to reach + // that node. + var cameFrom: [Tree: Tree] = [:] + + while let current = toVisit[currentIndex...].first { + currentIndex += 1 + if predicate(current.element) { + // Reconstruct the path from `self` to `current`. + return sequence(first: current, next: { cameFrom[$0] }).reversed() + } + visited.insert(current) + + for child in current.children where !visited.contains(child) { + if !toVisit.contains(child) { + toVisit.append(child) + } + + // Coming from `current` is the best path to `neighbor`. + cameFrom[child] = current + } + } + + // Didn't find a path! + return [] + } +} + +extension Tree where Element == ParsableCommand.Type { + func path(to element: Element) -> [Element] { + path(toFirstWhere: { $0 == element }).map { $0.element } + } + + func firstChild(equalTo element: Element) -> Tree? { + children.first(where: { $0.element == element }) + } + + func firstChild(withName name: String) -> Tree? { + children.first(where: { $0.element._commandName == name }) + } + + convenience init(root command: ParsableCommand.Type) { + self.init(command) + for subcommand in command.configuration.subcommands { + addChild(Tree(root: subcommand)) + } + } +} diff --git a/Sources/TestHelpers/StringHelpers.swift b/Sources/TestHelpers/StringHelpers.swift new file mode 100644 index 000000000..f014c539a --- /dev/null +++ b/Sources/TestHelpers/StringHelpers.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +extension Substring { + func trimmed() -> Substring { + guard let i = lastIndex(where: { $0 != " "}) else { + return "" + } + return self[...i] + } +} + +extension String { + public func trimmingLines() -> String { + return self + .split(separator: "\n", omittingEmptySubsequences: false) + .map { $0.trimmed() } + .joined(separator: "\n") + } +} diff --git a/Sources/TestHelpers/TestHelpers.swift b/Sources/TestHelpers/TestHelpers.swift new file mode 100644 index 000000000..1d0b7f85b --- /dev/null +++ b/Sources/TestHelpers/TestHelpers.swift @@ -0,0 +1,180 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +// extentions to the ParsableArguments protocol to facilitate XCTestExpectation support +public protocol TestableParsableArguments: ParsableArguments { + var didValidateExpectation: XCTestExpectation { get } +} + +public extension TestableParsableArguments { + mutating func validate() throws { + didValidateExpectation.fulfill() + } +} + +// extentions to the ParsableCommand protocol to facilitate XCTestExpectation support +public protocol TestableParsableCommand: ParsableCommand, TestableParsableArguments { + var didRunExpectation: XCTestExpectation { get } +} + +public extension TestableParsableCommand { + func run() throws { + didRunExpectation.fulfill() + } +} + +extension XCTestExpectation { + public convenience init(singleExpectation description: String) { + self.init(description: description) + expectedFulfillmentCount = 1 + assertForOverFulfill = true + } +} + +public func AssertResultFailure( + _ expression: @autoclosure () -> Result, + _ message: @autoclosure () -> String = "", + file: StaticString = #file, + line: UInt = #line) +{ + switch expression() { + case .success: + let msg = message() + XCTFail(msg.isEmpty ? "Incorrectly succeeded" : msg, file: file, line: line) + case .failure: + break + } +} + +public func AssertErrorMessage(_ type: A.Type, _ arguments: [String], _ errorMessage: String, file: StaticString = #file, line: UInt = #line) where A: ParsableArguments { + do { + _ = try A.parse(arguments) + XCTFail("Parsing should have failed.", file: file, line: line) + } catch { + // We expect to hit this path, i.e. getting an error: + XCTAssertEqual(A.message(for: error), errorMessage, file: file, line: line) + } +} + +public func AssertFullErrorMessage(_ type: A.Type, _ arguments: [String], _ errorMessage: String, file: StaticString = #file, line: UInt = #line) where A: ParsableArguments { + do { + _ = try A.parse(arguments) + XCTFail("Parsing should have failed.", file: file, line: line) + } catch { + // We expect to hit this path, i.e. getting an error: + XCTAssertEqual(A.fullMessage(for: error), errorMessage, file: file, line: line) + } +} + +public func AssertParse(_ type: A.Type, _ arguments: [String], file: StaticString = #file, line: UInt = #line, closure: (A) throws -> Void) where A: ParsableArguments { + do { + let parsed = try type.parse(arguments) + try closure(parsed) + } catch { + let message = type.message(for: error) + XCTFail("\"\(message)\" — \(error)", file: file, line: line) + } +} + +public func AssertParseCommand(_ rootCommand: ParsableCommand.Type, _ type: A.Type, _ arguments: [String], file: StaticString = #file, line: UInt = #line, closure: (A) throws -> Void) { + do { + let command = try rootCommand.parseAsRoot(arguments) + guard let aCommand = command as? A else { + XCTFail("Command is of unexpected type: \(command)", file: file, line: line) + return + } + try closure(aCommand) + } catch { + let message = rootCommand.message(for: error) + XCTFail("\"\(message)\" — \(error)", file: file, line: line) + } +} + +public func AssertEqualStringsIgnoringTrailingWhitespace(_ string1: String, _ string2: String, file: StaticString = #file, line: UInt = #line) { + let lines1 = string1.split(separator: "\n", omittingEmptySubsequences: false) + let lines2 = string2.split(separator: "\n", omittingEmptySubsequences: false) + + XCTAssertEqual(lines1.count, lines2.count, "Strings have different numbers of lines.", file: file, line: line) + for (line1, line2) in zip(lines1, lines2) { + XCTAssertEqual(line1.trimmed(), line2.trimmed(), file: file, line: line) + } +} + +public func AssertHelp( + for _: T.Type, equals expected: String, + file: StaticString = #file, line: UInt = #line +) { + do { + _ = try T.parse(["-h"]) + XCTFail() + } catch { + let helpString = T.fullMessage(for: error) + AssertEqualStringsIgnoringTrailingWhitespace( + helpString, expected, file: file, line: line) + } +} + +extension XCTest { + public var debugURL: URL { + let bundleURL = Bundle(for: type(of: self)).bundleURL + return bundleURL.lastPathComponent.hasSuffix("xctest") + ? bundleURL.deletingLastPathComponent() + : bundleURL + } + + public func AssertExecuteCommand( + command: String, + expected: String? = nil, + shouldError: Bool = false, + file: StaticString = #file, line: UInt = #line) + { + let splitCommand = command.split(separator: " ") + let arguments = splitCommand.dropFirst().map(String.init) + + let commandName = String(splitCommand.first!) + let commandURL = debugURL.appendingPathComponent(commandName) + guard (try? commandURL.checkResourceIsReachable()) ?? false else { + XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.", + file: file, line: line); + return + } + + let process = Process() + process.launchPath = commandURL.path + process.arguments = arguments + + let output = Pipe() + process.standardOutput = output + let error = Pipe() + process.standardError = error + + process.launch() + process.waitUntilExit() + + let outputData = output.fileHandleForReading.readDataToEndOfFile() + let outputActual = String(data: outputData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + + let errorData = error.fileHandleForReading.readDataToEndOfFile() + let errorActual = String(data: errorData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + + if let expected = expected { + AssertEqualStringsIgnoringTrailingWhitespace(expected, errorActual + outputActual, file: file, line: line) + } + if shouldError { + XCTAssertNotEqual(process.terminationStatus, 0, file: file, line: line) + } else { + XCTAssertEqual(process.terminationStatus, 0, file: file, line: line) + } + } +} diff --git a/Tests/EndToEndTests/CustomParsingEndToEndTests.swift b/Tests/EndToEndTests/CustomParsingEndToEndTests.swift new file mode 100644 index 000000000..c2e00b60f --- /dev/null +++ b/Tests/EndToEndTests/CustomParsingEndToEndTests.swift @@ -0,0 +1,168 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class ParsingEndToEndTests: XCTestCase { +} + +struct Name { + var rawValue: String + + init(rawValue: String) throws { + if rawValue == "bad" { + throw ValidationError("Bad input for name") + } + self.rawValue = rawValue + } +} + +extension Array where Element == Name { + var rawValues: [String] { + map { $0.rawValue } + } +} + +// MARK: - + +fileprivate struct Foo: ParsableCommand { + enum Subgroup: Equatable { + case first(Int) + case second(Int) + + static func makeFirst(_ str: String) throws -> Subgroup { + guard let value = Int(str) else { + throw ValidationError("Not a valid integer for 'first'") + } + return .first(value) + } + + static func makeSecond(_ str: String) throws -> Subgroup { + guard let value = Int(str) else { + throw ValidationError("Not a valid integer for 'second'") + } + return .second(value) + } + } + + @Option(transform: Subgroup.makeFirst) + var first: Subgroup + + @Argument(transform: Subgroup.makeSecond) + var second: Subgroup +} + +extension ParsingEndToEndTests { + func testParsing() throws { + AssertParse(Foo.self, ["--first", "1", "2"]) { foo in + XCTAssertEqual(foo.first, .first(1)) + XCTAssertEqual(foo.second, .second(2)) + } + } + + func testParsing_Fails() throws { + // Failure inside custom parser + XCTAssertThrowsError(try Foo.parse(["--first", "1", "bad"])) + XCTAssertThrowsError(try Foo.parse(["--first", "bad", "2"])) + XCTAssertThrowsError(try Foo.parse(["--first", "bad", "bad"])) + + // Missing argument failures + XCTAssertThrowsError(try Foo.parse(["--first", "1"])) + XCTAssertThrowsError(try Foo.parse(["5"])) + XCTAssertThrowsError(try Foo.parse([])) + } +} + +// MARK: - + +fileprivate struct Bar: ParsableCommand { + @Option(default: try! Name(rawValue: "none"), transform: { try Name(rawValue: $0) }) + var firstName: Name + + @Argument(transform: { try Name(rawValue: $0) }) + var lastName: Name? +} + +extension ParsingEndToEndTests { + func testParsing_Defaults() throws { + AssertParse(Bar.self, ["--first-name", "A", "B"]) { bar in + XCTAssertEqual(bar.firstName.rawValue, "A") + XCTAssertEqual(bar.lastName?.rawValue, "B") + } + + AssertParse(Bar.self, ["B"]) { bar in + XCTAssertEqual(bar.firstName.rawValue, "none") + XCTAssertEqual(bar.lastName?.rawValue, "B") + } + + AssertParse(Bar.self, ["--first-name", "A"]) { bar in + XCTAssertEqual(bar.firstName.rawValue, "A") + XCTAssertNil(bar.lastName) + } + + AssertParse(Bar.self, []) { bar in + XCTAssertEqual(bar.firstName.rawValue, "none") + XCTAssertNil(bar.lastName) + } + } + + func testParsing_Defaults_Fails() throws { + XCTAssertThrowsError(try Bar.parse(["--first-name", "bad"])) + XCTAssertThrowsError(try Bar.parse(["bad"])) + } +} + +// MARK: - + +fileprivate struct Qux: ParsableCommand { + @Option(transform: { try Name(rawValue: $0) }) + var firstName: [Name] + + @Argument(transform: { try Name(rawValue: $0) }) + var lastName: [Name] +} + +extension ParsingEndToEndTests { + func testParsing_Array() throws { + AssertParse(Qux.self, ["--first-name", "A", "B"]) { qux in + XCTAssertEqual(qux.firstName.rawValues, ["A"]) + XCTAssertEqual(qux.lastName.rawValues, ["B"]) + } + + AssertParse(Qux.self, ["--first-name", "A", "--first-name", "B", "C", "D"]) { qux in + XCTAssertEqual(qux.firstName.rawValues, ["A", "B"]) + XCTAssertEqual(qux.lastName.rawValues, ["C", "D"]) + } + + AssertParse(Qux.self, ["--first-name", "A", "--first-name", "B"]) { qux in + XCTAssertEqual(qux.firstName.rawValues, ["A", "B"]) + XCTAssertEqual(qux.lastName.rawValues, []) + } + + AssertParse(Qux.self, ["C", "D"]) { qux in + XCTAssertEqual(qux.firstName.rawValues, []) + XCTAssertEqual(qux.lastName.rawValues, ["C", "D"]) + } + + AssertParse(Qux.self, []) { qux in + XCTAssertEqual(qux.firstName.rawValues, []) + XCTAssertEqual(qux.lastName.rawValues, []) + } + } + + func testParsing_Array_Fails() { + XCTAssertThrowsError(try Qux.parse(["--first-name", "A", "--first-name", "B", "C", "D", "bad"])) + XCTAssertThrowsError(try Qux.parse(["--first-name", "A", "--first-name", "B", "--first-name", "bad", "C", "D"])) + } +} + diff --git a/Tests/EndToEndTests/DefaultsEndToEndTests.swift b/Tests/EndToEndTests/DefaultsEndToEndTests.swift new file mode 100644 index 000000000..ea09091bd --- /dev/null +++ b/Tests/EndToEndTests/DefaultsEndToEndTests.swift @@ -0,0 +1,338 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class DefaultsEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Foo: ParsableArguments { + struct Name: RawRepresentable, ExpressibleByArgument { + var rawValue: String + } + @Option(default: Name(rawValue: "A")) + var name: Name + @Option(default: 3) + var max: Int +} + +extension DefaultsEndToEndTests { + func testParsing_Defaults() throws { + AssertParse(Foo.self, []) { foo in + XCTAssertEqual(foo.name.rawValue, "A") + XCTAssertEqual(foo.max, 3) + } + + AssertParse(Foo.self, ["--name", "B"]) { foo in + XCTAssertEqual(foo.name.rawValue, "B") + XCTAssertEqual(foo.max, 3) + } + + AssertParse(Foo.self, ["--max", "5"]) { foo in + XCTAssertEqual(foo.name.rawValue, "A") + XCTAssertEqual(foo.max, 5) + } + + AssertParse(Foo.self, ["--max", "5", "--name", "B"]) { foo in + XCTAssertEqual(foo.name.rawValue, "B") + XCTAssertEqual(foo.max, 5) + } + } +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + enum Format: String, ExpressibleByArgument { + case A + case B + case C + } + @Option(default: "N") + var name: String + @Option(default: .A) + var format: Format + @Option() + var foo: String + @Argument() + var bar: String? +} + +extension DefaultsEndToEndTests { + func testParsing_Optional_WithAllValues_1() { + AssertParse(Bar.self, ["--name", "A", "--format", "B", "--foo", "C", "D"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithAllValues_2() { + AssertParse(Bar.self, ["D", "--format", "B", "--foo", "C", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithAllValues_3() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "D", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithMissingValues_1() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "D"]) { bar in + XCTAssertEqual(bar.name, "N") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithMissingValues_2() { + AssertParse(Bar.self, ["D", "--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, "N") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithMissingValues_3() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "D"]) { bar in + XCTAssertEqual(bar.name, "N") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithMissingValues_4() { + AssertParse(Bar.self, ["--name", "A", "--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_5() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format,.B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_6() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_7() { + AssertParse(Bar.self, ["--foo", "C"]) { bar in + XCTAssertEqual(bar.name, "N") + XCTAssertEqual(bar.format, .A) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_8() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, "N") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_9() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, "N") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_10() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, "N") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_Fails() throws { + XCTAssertThrowsError(try Bar.parse([])) + XCTAssertThrowsError(try Bar.parse(["--fooz", "C"])) + XCTAssertThrowsError(try Bar.parse(["--nam", "A", "--foo", "C"])) + XCTAssertThrowsError(try Bar.parse(["--name"])) + XCTAssertThrowsError(try Bar.parse(["A"])) + XCTAssertThrowsError(try Bar.parse(["--name", "A", "D"])) + XCTAssertThrowsError(try Bar.parse(["--name", "A", "--foo"])) + XCTAssertThrowsError(try Bar.parse(["--name", "A", "--format", "B"])) + XCTAssertThrowsError(try Bar.parse(["--name", "A", "-f"])) + XCTAssertThrowsError(try Bar.parse(["D", "--name", "A"])) + XCTAssertThrowsError(try Bar.parse(["-f", "--name", "A"])) + XCTAssertThrowsError(try Bar.parse(["--foo", "--name", "A"])) + XCTAssertThrowsError(try Bar.parse(["--foo", "--name", "AA", "BB"])) + } +} + +fileprivate struct Bar_NextInput: ParsableArguments { + enum Format: String, ExpressibleByArgument { + case A + case B + case C + case D = "-d" + } + @Option(default: "N", parsing: .unconditional) + var name: String + @Option(default: .A, parsing: .unconditional) + var format: Format + @Option(parsing: .unconditional) + var foo: String + @Argument() + var bar: String? +} + +extension DefaultsEndToEndTests { + func testParsing_Optional_WithOverlappingValues_1() { + AssertParse(Bar_NextInput.self, ["--format", "B", "--name", "--foo", "--foo", "--name"]) { bar in + XCTAssertEqual(bar.name, "--foo") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "--name") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithOverlappingValues_2() { + AssertParse(Bar_NextInput.self, ["--format", "-d", "--foo", "--name", "--name", "--foo"]) { bar in + XCTAssertEqual(bar.name, "--foo") + XCTAssertEqual(bar.format, .D) + XCTAssertEqual(bar.foo, "--name") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithOverlappingValues_3() { + AssertParse(Bar_NextInput.self, ["--format", "-d", "--name", "--foo", "--foo", "--name", "bar"]) { bar in + XCTAssertEqual(bar.name, "--foo") + XCTAssertEqual(bar.format, .D) + XCTAssertEqual(bar.foo, "--name") + XCTAssertEqual(bar.bar, "bar") + } + } +} + +// MARK: - + +fileprivate struct Baz: ParsableArguments { + @Option(default: 0, parsing: .unconditional) var int: Int + @Option(default: 0, parsing: .unconditional) var int8: Int8 + @Option(default: 0, parsing: .unconditional) var int16: Int16 + @Option(default: 0, parsing: .unconditional) var int32: Int32 + @Option(default: 0, parsing: .unconditional) var int64: Int64 + @Option(default: 0) var uint: UInt + @Option(default: 0) var uint8: UInt8 + @Option(default: 0) var uint16: UInt16 + @Option(default: 0) var uint32: UInt32 + @Option(default: 0) var uint64: UInt64 + + @Option(default: 0, parsing: .unconditional) var float: Float + @Option(default: 0, parsing: .unconditional) var double: Double + + @Option(default: false) var bool: Bool +} + +extension DefaultsEndToEndTests { + func testParsing_AllTypes_1() { + AssertParse(Baz.self, []) { baz in + XCTAssertEqual(baz.int, 0) + XCTAssertEqual(baz.int8, 0) + XCTAssertEqual(baz.int16, 0) + XCTAssertEqual(baz.int32, 0) + XCTAssertEqual(baz.int64, 0) + XCTAssertEqual(baz.uint, 0) + XCTAssertEqual(baz.uint8, 0) + XCTAssertEqual(baz.uint16, 0) + XCTAssertEqual(baz.uint32, 0) + XCTAssertEqual(baz.uint64, 0) + XCTAssertEqual(baz.float, 0) + XCTAssertEqual(baz.double, 0) + XCTAssertEqual(baz.bool, false) + } + } + + func testParsing_AllTypes_2() { + AssertParse(Baz.self, [ + "--int", "-1", "--int8", "-2", "--int16", "-3", "--int32", "-4", "--int64", "-5", + "--uint", "1", "--uint8", "2", "--uint16", "3", "--uint32", "4", "--uint64", "5", + "--float", "1.25", "--double", "2.5", "--bool", "true" + ]) { baz in + XCTAssertEqual(baz.int, -1) + XCTAssertEqual(baz.int8, -2) + XCTAssertEqual(baz.int16, -3) + XCTAssertEqual(baz.int32, -4) + XCTAssertEqual(baz.int64, -5) + XCTAssertEqual(baz.uint, 1) + XCTAssertEqual(baz.uint8, 2) + XCTAssertEqual(baz.uint16, 3) + XCTAssertEqual(baz.uint32, 4) + XCTAssertEqual(baz.uint64, 5) + XCTAssertEqual(baz.float, 1.25) + XCTAssertEqual(baz.double, 2.5) + XCTAssertEqual(baz.bool, true) + } + } + + func testParsing_AllTypes_Fails() throws { + XCTAssertThrowsError(try Baz.parse(["--int8", "256"])) + XCTAssertThrowsError(try Baz.parse(["--int16", "32768"])) + XCTAssertThrowsError(try Baz.parse(["--int32", "2147483648"])) + XCTAssertThrowsError(try Baz.parse(["--int64", "9223372036854775808"])) + XCTAssertThrowsError(try Baz.parse(["--int", "9223372036854775808"])) + + XCTAssertThrowsError(try Baz.parse(["--uint8", "512"])) + XCTAssertThrowsError(try Baz.parse(["--uint16", "65536"])) + XCTAssertThrowsError(try Baz.parse(["--uint32", "4294967296"])) + XCTAssertThrowsError(try Baz.parse(["--uint64", "18446744073709551616"])) + XCTAssertThrowsError(try Baz.parse(["--uint", "18446744073709551616"])) + + XCTAssertThrowsError(try Baz.parse(["--uint8", "-1"])) + XCTAssertThrowsError(try Baz.parse(["--uint16", "-1"])) + XCTAssertThrowsError(try Baz.parse(["--uint32", "-1"])) + XCTAssertThrowsError(try Baz.parse(["--uint64", "-1"])) + XCTAssertThrowsError(try Baz.parse(["--uint", "-1"])) + + XCTAssertThrowsError(try Baz.parse(["--float", "zzz"])) + XCTAssertThrowsError(try Baz.parse(["--double", "zzz"])) + XCTAssertThrowsError(try Baz.parse(["--bool", "truthy"])) + } +} diff --git a/Tests/EndToEndTests/EnumEndToEndTests.swift b/Tests/EndToEndTests/EnumEndToEndTests.swift new file mode 100644 index 000000000..a3b236adb --- /dev/null +++ b/Tests/EndToEndTests/EnumEndToEndTests.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class EnumEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + enum Index: String, Equatable, ExpressibleByArgument { + case hello + case goodbye + } + + @Option() + var index: Index +} + +extension EnumEndToEndTests { + func testParsing_SingleOption() throws { + AssertParse(Bar.self, ["--index", "hello"]) { bar in + XCTAssertEqual(bar.index, Bar.Index.hello) + } + AssertParse(Bar.self, ["--index", "goodbye"]) { bar in + XCTAssertEqual(bar.index, Bar.Index.goodbye) + } + } + + func testParsing_SingleOptionMultipleTimes() throws { + AssertParse(Bar.self, ["--index", "hello", "--index", "goodbye"]) { bar in + XCTAssertEqual(bar.index, Bar.Index.goodbye) + } + } + + func testParsing_SingleOption_Fails() throws { + XCTAssertThrowsError(try Bar.parse([])) + XCTAssertThrowsError(try Bar.parse(["--index"])) + XCTAssertThrowsError(try Bar.parse(["--index", "hell"])) + XCTAssertThrowsError(try Bar.parse(["--index", "helloo"])) + } +} diff --git a/Tests/EndToEndTests/FlagsEndToEndTests.swift b/Tests/EndToEndTests/FlagsEndToEndTests.swift new file mode 100644 index 000000000..b62da7e40 --- /dev/null +++ b/Tests/EndToEndTests/FlagsEndToEndTests.swift @@ -0,0 +1,243 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class FlagsEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Flag() + var verbose: Bool + + @Flag(inversion: .prefixedNo) + var extattr: Bool +} + +extension FlagsEndToEndTests { + func testParsing_defaultValue() throws { + AssertParse(Bar.self, []) { options in + XCTAssertEqual(options.verbose, false) + XCTAssertEqual(options.extattr, false) + } + } + + func testParsing_settingValue() throws { + AssertParse(Bar.self, ["--verbose"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertEqual(options.extattr, false) + } + + AssertParse(Bar.self, ["--extattr"]) { options in + XCTAssertEqual(options.verbose, false) + XCTAssertEqual(options.extattr, true) + } + } + + func testParsing_invert() throws { + AssertParse(Bar.self, ["--no-extattr"]) { options in + XCTAssertEqual(options.extattr, false) + } + AssertParse(Bar.self, ["--extattr", "--no-extattr"]) { options in + XCTAssertEqual(options.extattr, false) + } + AssertParse(Bar.self, ["--extattr", "--no-extattr", "--no-extattr"]) { options in + XCTAssertEqual(options.extattr, false) + } + AssertParse(Bar.self, ["--no-extattr", "--no-extattr", "--extattr"]) { options in + XCTAssertEqual(options.extattr, true) + } + AssertParse(Bar.self, ["--extattr", "--no-extattr", "--extattr"]) { options in + XCTAssertEqual(options.extattr, true) + } + } +} + +fileprivate struct Foo: ParsableArguments { + @Flag(default: false, inversion: .prefixedEnableDisable) + var index: Bool + @Flag(default: true, inversion: .prefixedEnableDisable) + var sandbox: Bool + @Flag(default: nil, inversion: .prefixedEnableDisable) + var requiredElement: Bool +} + +extension FlagsEndToEndTests { + func testParsingEnableDisable_defaultValue() throws { + AssertParse(Foo.self, ["--enable-required-element"]) { options in + XCTAssertEqual(options.index, false) + XCTAssertEqual(options.sandbox, true) + XCTAssertEqual(options.requiredElement, true) + } + } + + func testParsingEnableDisable_disableAll() throws { + AssertParse(Foo.self, ["--disable-index", "--disable-sandbox", "--disable-required-element"]) { options in + XCTAssertEqual(options.index, false) + XCTAssertEqual(options.sandbox, false) + XCTAssertEqual(options.requiredElement, false) + } + } + + func testParsingEnableDisable_enableAll() throws { + AssertParse(Foo.self, ["--enable-index", "--enable-sandbox", "--enable-required-element"]) { options in + XCTAssertEqual(options.index, true) + XCTAssertEqual(options.sandbox, true) + XCTAssertEqual(options.requiredElement, true) + } + } + + func testParsingEnableDisable_Fails() throws { + XCTAssertThrowsError(try Foo.parse([])) + XCTAssertThrowsError(try Foo.parse(["--disable-index"])) + XCTAssertThrowsError(try Foo.parse(["--disable-sandbox"])) + } +} + +enum Color: String, CaseIterable { + case pink + case purple + case silver +} + +enum Size: String, CaseIterable { + case small + case medium + case large + case extraLarge + case humongous = "huge" +} + +enum Shape: String, CaseIterable { + case round + case square + case oblong +} + +fileprivate struct Baz: ParsableArguments { + @Flag() + var color: Color + + @Flag(default: .small) + var size: Size + + @Flag() + var shape: Shape? +} + +extension FlagsEndToEndTests { + func testParsingCaseIterable_defaultValues() throws { + AssertParse(Baz.self, ["--pink"]) { options in + XCTAssertEqual(options.color, .pink) + XCTAssertEqual(options.size, .small) + XCTAssertEqual(options.shape, nil) + } + + AssertParse(Baz.self, ["--pink", "--medium"]) { options in + XCTAssertEqual(options.color, .pink) + XCTAssertEqual(options.size, .medium) + XCTAssertEqual(options.shape, nil) + } + + AssertParse(Baz.self, ["--pink", "--round"]) { options in + XCTAssertEqual(options.color, .pink) + XCTAssertEqual(options.size, .small) + XCTAssertEqual(options.shape, .round) + } + } + + func testParsingCaseIterable_AllValues() throws { + AssertParse(Baz.self, ["--pink", "--small", "--round"]) { options in + XCTAssertEqual(options.color, .pink) + XCTAssertEqual(options.size, .small) + XCTAssertEqual(options.shape, .round) + } + + AssertParse(Baz.self, ["--purple", "--medium", "--square"]) { options in + XCTAssertEqual(options.color, .purple) + XCTAssertEqual(options.size, .medium) + XCTAssertEqual(options.shape, .square) + } + + AssertParse(Baz.self, ["--silver", "--large", "--oblong"]) { options in + XCTAssertEqual(options.color, .silver) + XCTAssertEqual(options.size, .large) + XCTAssertEqual(options.shape, .oblong) + } + } + + func testParsingCaseIterable_CustomName() throws { + AssertParse(Baz.self, ["--pink", "--extra-large"]) { options in + XCTAssertEqual(options.color, .pink) + XCTAssertEqual(options.size, .extraLarge) + XCTAssertEqual(options.shape, nil) + } + + AssertParse(Baz.self, ["--pink", "--huge"]) { options in + XCTAssertEqual(options.color, .pink) + XCTAssertEqual(options.size, .humongous) + XCTAssertEqual(options.shape, nil) + } + } + + func testParsingCaseIterable_Fails() throws { + // Missing color + XCTAssertThrowsError(try Baz.parse([])) + XCTAssertThrowsError(try Baz.parse(["--large", "--square"])) + // Repeating flags + XCTAssertThrowsError(try Baz.parse(["--pink", "--purple"])) + XCTAssertThrowsError(try Baz.parse(["--pink", "--small", "--large"])) + XCTAssertThrowsError(try Baz.parse(["--pink", "--round", "--square"])) + // Case name instead of raw value + XCTAssertThrowsError(try Baz.parse(["--pink", "--extraLarge"])) + } +} + +fileprivate struct Qux: ParsableArguments { + @Flag() + var color: [Color] + + @Flag() + var size: [Size] +} + +extension FlagsEndToEndTests { + func testParsingCaseIterableArray_Values() throws { + AssertParse(Qux.self, []) { options in + XCTAssertEqual(options.color, []) + XCTAssertEqual(options.size, []) + } + AssertParse(Qux.self, ["--pink"]) { options in + XCTAssertEqual(options.color, [.pink]) + XCTAssertEqual(options.size, []) + } + AssertParse(Qux.self, ["--pink", "--purple", "--small"]) { options in + XCTAssertEqual(options.color, [.pink, .purple]) + XCTAssertEqual(options.size, [.small]) + } + AssertParse(Qux.self, ["--pink", "--small", "--purple", "--medium"]) { options in + XCTAssertEqual(options.color, [.pink, .purple]) + XCTAssertEqual(options.size, [.small, .medium]) + } + AssertParse(Qux.self, ["--pink", "--pink", "--purple", "--pink"]) { options in + XCTAssertEqual(options.color, [.pink, .pink, .purple, .pink]) + XCTAssertEqual(options.size, []) + } + } + + func testParsingCaseIterableArray_Fails() throws { + XCTAssertThrowsError(try Qux.parse(["--pink", "--small", "--bloop"])) + } +} diff --git a/Tests/EndToEndTests/LongNameWithShortDashEndToEndTests.swift b/Tests/EndToEndTests/LongNameWithShortDashEndToEndTests.swift new file mode 100644 index 000000000..f5dc933d7 --- /dev/null +++ b/Tests/EndToEndTests/LongNameWithShortDashEndToEndTests.swift @@ -0,0 +1,109 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class LongNameWithSingleDashEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Flag(name: .customLong("file", withSingleDash: true)) + var file: Bool + + @Flag(name: .short) + var force: Bool + + @Flag(name: .short) + var input: Bool +} + +extension LongNameWithSingleDashEndToEndTests { + func testParsing_empty() throws { + AssertParse(Bar.self, []) { options in + XCTAssertEqual(options.file, false) + XCTAssertEqual(options.force, false) + XCTAssertEqual(options.input, false) + } + } + + func testParsing_singleOption_1() { + AssertParse(Bar.self, ["-file"]) { options in + XCTAssertEqual(options.file, true) + XCTAssertEqual(options.force, false) + XCTAssertEqual(options.input, false) + } + } + + func testParsing_singleOption_2() { + AssertParse(Bar.self, ["-f"]) { options in + XCTAssertEqual(options.file, false) + XCTAssertEqual(options.force, true) + XCTAssertEqual(options.input, false) + } + } + + func testParsing_singleOption_3() { + AssertParse(Bar.self, ["-i"]) { options in + XCTAssertEqual(options.file, false) + XCTAssertEqual(options.force, false) + XCTAssertEqual(options.input, true) + } + } + + func testParsing_combined_1() { + AssertParse(Bar.self, ["-f", "-i"]) { options in + XCTAssertEqual(options.file, false) + XCTAssertEqual(options.force, true) + XCTAssertEqual(options.input, true) + } + } + + func testParsing_combined_2() { + AssertParse(Bar.self, ["-fi"]) { options in + XCTAssertEqual(options.file, false) + XCTAssertEqual(options.force, true) + XCTAssertEqual(options.input, true) + } + } + + func testParsing_combined_3() { + AssertParse(Bar.self, ["-file", "-f"]) { options in + XCTAssertEqual(options.file, true) + XCTAssertEqual(options.force, true) + XCTAssertEqual(options.input, false) + } + } + + func testParsing_combined_4() { + AssertParse(Bar.self, ["-file", "-i"]) { options in + XCTAssertEqual(options.file, true) + XCTAssertEqual(options.force, false) + XCTAssertEqual(options.input, true) + } + } + + func testParsing_combined_5() { + AssertParse(Bar.self, ["-file", "-fi"]) { options in + XCTAssertEqual(options.file, true) + XCTAssertEqual(options.force, true) + XCTAssertEqual(options.input, true) + } + } + + func testParsing_invalid() throws { + //XCTAssertThrowsError(try Bar.parse(["-fil"])) + XCTAssertThrowsError(try Bar.parse(["--file"])) + } +} diff --git a/Tests/EndToEndTests/NestedCommandEndToEndTests.swift b/Tests/EndToEndTests/NestedCommandEndToEndTests.swift new file mode 100644 index 000000000..af114e42a --- /dev/null +++ b/Tests/EndToEndTests/NestedCommandEndToEndTests.swift @@ -0,0 +1,149 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class NestedCommandEndToEndTests: XCTestCase { +} + +// MARK: Single value String + +fileprivate struct Foo: ParsableCommand { + static var configuration = + CommandConfiguration(subcommands: [Build.self, Package.self]) + + @Flag(name: .short) + var verbose: Bool + + struct Build: ParsableCommand { + @OptionGroup() var foo: Foo + + @Argument() + var input: String + } + + struct Package: ParsableCommand { + static var configuration = + CommandConfiguration(subcommands: [Clean.self, Config.self]) + + @Flag(name: .short) + var force: Bool + + struct Clean: ParsableCommand { + @OptionGroup() var foo: Foo + @OptionGroup() var package: Package + } + + struct Config: ParsableCommand { + @OptionGroup() var foo: Foo + @OptionGroup() var package: Package + } + } +} + +fileprivate func AssertParseFooCommand(_ type: A.Type, _ arguments: [String], file: StaticString = #file, line: UInt = #line, closure: (A) throws -> Void) where A: ParsableCommand { + AssertParseCommand(Foo.self, type, arguments, file: file, line: line, closure: closure) +} + + +extension NestedCommandEndToEndTests { + func testParsing_package() throws { + AssertParseFooCommand(Foo.Package.Clean.self, ["package", "clean"]) { clean in + XCTAssertEqual(clean.foo.verbose, false) + XCTAssertEqual(clean.package.force, false) + } + + AssertParseFooCommand(Foo.Package.Clean.self, ["-f", "package", "clean"]) { clean in + XCTAssertEqual(clean.foo.verbose, false) + XCTAssertEqual(clean.package.force, true) + } + + AssertParseFooCommand(Foo.Package.Clean.self, ["package", "-f", "clean"]) { clean in + XCTAssertEqual(clean.foo.verbose, false) + XCTAssertEqual(clean.package.force, true) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["package", "-v", "config"]) { config in + XCTAssertEqual(config.foo.verbose, true) + XCTAssertEqual(config.package.force, false) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["package", "config", "-v"]) { config in + XCTAssertEqual(config.foo.verbose, true) + XCTAssertEqual(config.package.force, false) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["-v", "package", "config"]) { config in + XCTAssertEqual(config.foo.verbose, true) + XCTAssertEqual(config.package.force, false) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["package", "-f", "config"]) { config in + XCTAssertEqual(config.foo.verbose, false) + XCTAssertEqual(config.package.force, true) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["package", "config", "-f"]) { config in + XCTAssertEqual(config.foo.verbose, false) + XCTAssertEqual(config.package.force, true) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["-f", "package", "config"]) { config in + XCTAssertEqual(config.foo.verbose, false) + XCTAssertEqual(config.package.force, true) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["package", "-v", "config", "-f"]) { config in + XCTAssertEqual(config.foo.verbose, true) + XCTAssertEqual(config.package.force, true) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["package", "-f", "config", "-v"]) { config in + XCTAssertEqual(config.foo.verbose, true) + XCTAssertEqual(config.package.force, true) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["package", "-vf", "config"]) { config in + XCTAssertEqual(config.foo.verbose, true) + XCTAssertEqual(config.package.force, true) + } + + AssertParseFooCommand(Foo.Package.Config.self, ["package", "-fv", "config"]) { config in + XCTAssertEqual(config.foo.verbose, true) + XCTAssertEqual(config.package.force, true) + } + } + + func testParsing_build() throws { + AssertParseFooCommand(Foo.Build.self, ["build", "file"]) { build in + XCTAssertEqual(build.foo.verbose, false) + XCTAssertEqual(build.input, "file") + } + } + + func testParsing_fails() throws { + XCTAssertThrowsError(try Foo.parse(["package"])) + XCTAssertThrowsError(try Foo.parse(["clean", "package"])) + XCTAssertThrowsError(try Foo.parse(["config", "package"])) + XCTAssertThrowsError(try Foo.parse(["package", "c"])) + XCTAssertThrowsError(try Foo.parse(["package", "build"])) + XCTAssertThrowsError(try Foo.parse(["package", "build", "clean"])) + XCTAssertThrowsError(try Foo.parse(["package", "clean", "foo"])) + XCTAssertThrowsError(try Foo.parse(["package", "config", "bar"])) + XCTAssertThrowsError(try Foo.parse(["package", "clean", "build"])) + XCTAssertThrowsError(try Foo.parse(["build"])) + XCTAssertThrowsError(try Foo.parse(["build", "-f"])) + XCTAssertThrowsError(try Foo.parse(["build", "--build"])) + XCTAssertThrowsError(try Foo.parse(["build", "--build", "12"])) + } +} diff --git a/Tests/EndToEndTests/OptionGroupEndToEndTests.swift b/Tests/EndToEndTests/OptionGroupEndToEndTests.swift new file mode 100644 index 000000000..04137597d --- /dev/null +++ b/Tests/EndToEndTests/OptionGroupEndToEndTests.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class OptionGroupEndToEndTests: XCTestCase { +} + +struct Inner: TestableParsableArguments { + @Flag(name: [.short, .long]) + var extraVerbiage: Bool + @Option(default: 0) + var size: Int + @Argument() + var name: String + + let didValidateExpectation = XCTestExpectation(singleExpectation: "inner validated") + + private enum CodingKeys: CodingKey { + case extraVerbiage + case size + case name + } +} + +struct Outer: TestableParsableArguments { + @Flag() + var verbose: Bool + @Argument() + var before: String + @OptionGroup() + var inner: Inner + @Argument() + var after: String + + let didValidateExpectation = XCTestExpectation(singleExpectation: "outer validated") + + private enum CodingKeys: CodingKey { + case verbose + case before + case inner + case after + } +} + +struct Command: TestableParsableCommand { + static let configuration = CommandConfiguration(commandName: "testCommand") + + @OptionGroup() + var outer: Outer + + let didValidateExpectation = XCTestExpectation(singleExpectation: "Command validated") + let didRunExpectation = XCTestExpectation(singleExpectation: "Command ran") + + private enum CodingKeys: CodingKey { + case outer + } +} + +extension OptionGroupEndToEndTests { + func testOptionGroup_Defaults() throws { + AssertParse(Outer.self, ["prefix", "name", "postfix"]) { options in + XCTAssertEqual(options.verbose, false) + XCTAssertEqual(options.before, "prefix") + XCTAssertEqual(options.after, "postfix") + + XCTAssertEqual(options.inner.extraVerbiage, false) + XCTAssertEqual(options.inner.size, 0) + XCTAssertEqual(options.inner.name, "name") + } + + AssertParse(Outer.self, ["prefix", "--extra-verbiage", "name", "postfix", "--verbose", "--size", "5"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertEqual(options.before, "prefix") + XCTAssertEqual(options.after, "postfix") + + XCTAssertEqual(options.inner.extraVerbiage, true) + XCTAssertEqual(options.inner.size, 5) + XCTAssertEqual(options.inner.name, "name") + } + } + + func testOptionGroup_isValidated() { + // Parse the command, this should cause validation to be once each on + // - command.outer.inner + // - command.outer + // - command + AssertParseCommand(Command.self, Command.self, ["prefix", "name", "postfix"]) { command in + wait(for: [command.didValidateExpectation, command.outer.didValidateExpectation, command.outer.inner.didValidateExpectation], timeout: 0.1) + } + } + + func testOptionGroup_Fails() throws { + XCTAssertThrowsError(try Outer.parse([])) + XCTAssertThrowsError(try Outer.parse(["prefix"])) + XCTAssertThrowsError(try Outer.parse(["prefix", "name"])) + XCTAssertThrowsError(try Outer.parse(["prefix", "name", "postfix", "extra"])) + XCTAssertThrowsError(try Outer.parse(["prefix", "name", "postfix", "--size", "a"])) + } +} diff --git a/Tests/EndToEndTests/OptionalEndToEndTests.swift b/Tests/EndToEndTests/OptionalEndToEndTests.swift new file mode 100644 index 000000000..039cf0c0f --- /dev/null +++ b/Tests/EndToEndTests/OptionalEndToEndTests.swift @@ -0,0 +1,208 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class OptionalEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Foo: ParsableArguments { + struct Name: RawRepresentable, ExpressibleByArgument { + var rawValue: String + } + @Option() var name: Name? + @Option() var max: Int? +} + +extension OptionalEndToEndTests { + func testParsing_Optional() throws { + AssertParse(Foo.self, []) { foo in + XCTAssertNil(foo.name) + XCTAssertNil(foo.max) + } + + AssertParse(Foo.self, ["--name", "A"]) { foo in + XCTAssertEqual(foo.name?.rawValue, "A") + XCTAssertNil(foo.max) + } + + AssertParse(Foo.self, ["--max", "3"]) { foo in + XCTAssertNil(foo.name) + XCTAssertEqual(foo.max, 3) + } + + AssertParse(Foo.self, ["--max", "3", "--name", "A"]) { foo in + XCTAssertEqual(foo.name?.rawValue, "A") + XCTAssertEqual(foo.max, 3) + } + } +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + enum Format: String, ExpressibleByArgument { + case A + case B + case C + } + @Option() var name: String? + @Option() var format: Format? + @Option() var foo: String + @Argument() var bar: String? +} + +extension OptionalEndToEndTests { + func testParsing_Optional_WithAllValues_1() { + AssertParse(Bar.self, ["--name", "A", "--format", "B", "--foo", "C", "D"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithAllValues_2() { + AssertParse(Bar.self, ["D", "--format", "B", "--foo", "C", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithAllValues_3() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "D", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithMissingValues_1() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "D"]) { bar in + XCTAssertEqual(bar.name, nil) + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithMissingValues_2() { + AssertParse(Bar.self, ["D", "--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, nil) + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithMissingValues_3() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "D"]) { bar in + XCTAssertEqual(bar.name, nil) + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, "D") + } + } + + func testParsing_Optional_WithMissingValues_4() { + AssertParse(Bar.self, ["--name", "A", "--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_5() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format,.B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_6() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_7() { + AssertParse(Bar.self, ["--foo", "C"]) { bar in + XCTAssertEqual(bar.name, nil) + XCTAssertEqual(bar.format, nil) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_8() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, nil) + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_9() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, nil) + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_10() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C"]) { bar in + XCTAssertEqual(bar.name, nil) + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_WithMissingValues_11() { + AssertParse(Bar.self, ["--format", "B", "--foo", "C", "--name", "A"]) { bar in + XCTAssertEqual(bar.name, "A") + XCTAssertEqual(bar.format, .B) + XCTAssertEqual(bar.foo, "C") + XCTAssertEqual(bar.bar, nil) + } + } + + func testParsing_Optional_Fails() throws { + XCTAssertThrowsError(try Bar.parse([])) + XCTAssertThrowsError(try Bar.parse(["--format", "ZZ", "--foo", "C"])) + XCTAssertThrowsError(try Bar.parse(["--fooz", "C"])) + XCTAssertThrowsError(try Bar.parse(["--nam", "A", "--foo", "C"])) + XCTAssertThrowsError(try Bar.parse(["--name"])) + XCTAssertThrowsError(try Bar.parse(["A"])) + XCTAssertThrowsError(try Bar.parse(["--name", "A", "D"])) + XCTAssertThrowsError(try Bar.parse(["--name", "A", "--foo"])) + XCTAssertThrowsError(try Bar.parse(["--name", "A", "--format", "B"])) + XCTAssertThrowsError(try Bar.parse(["--name", "A", "-f"])) + XCTAssertThrowsError(try Bar.parse(["D", "--name", "A"])) + XCTAssertThrowsError(try Bar.parse(["-f", "--name", "A"])) + } +} diff --git a/Tests/EndToEndTests/PositionalEndToEndTests.swift b/Tests/EndToEndTests/PositionalEndToEndTests.swift new file mode 100644 index 000000000..012143005 --- /dev/null +++ b/Tests/EndToEndTests/PositionalEndToEndTests.swift @@ -0,0 +1,231 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class PositionalEndToEndTests: XCTestCase { +} + +// MARK: Single value String + +fileprivate struct Bar: ParsableArguments { + @Argument() var name: String +} + +extension PositionalEndToEndTests { + func testParsing_SinglePositional() throws { + AssertParse(Bar.self, ["Bar"]) { bar in + XCTAssertEqual(bar.name, "Bar") + } + AssertParse(Bar.self, ["Bar-"]) { bar in + XCTAssertEqual(bar.name, "Bar-") + } + AssertParse(Bar.self, ["Bar--"]) { bar in + XCTAssertEqual(bar.name, "Bar--") + } + AssertParse(Bar.self, ["--", "-Bar"]) { bar in + XCTAssertEqual(bar.name, "-Bar") + } + AssertParse(Bar.self, ["--", "--Bar"]) { bar in + XCTAssertEqual(bar.name, "--Bar") + } + AssertParse(Bar.self, ["--", "--"]) { bar in + XCTAssertEqual(bar.name, "--") + } + } + + func testParsing_SinglePositional_Fails() throws { + XCTAssertThrowsError(try Bar.parse([])) + XCTAssertThrowsError(try Bar.parse(["--name"])) + XCTAssertThrowsError(try Bar.parse(["Foo", "Bar"])) + } +} + +// MARK: Two values + +fileprivate struct Baz: ParsableArguments { + @Argument() var name: String + @Argument() var format: String +} + +extension PositionalEndToEndTests { + func testParsing_TwoPositional() throws { + AssertParse(Baz.self, ["Bar", "Foo"]) { baz in + XCTAssertEqual(baz.name, "Bar") + XCTAssertEqual(baz.format, "Foo") + } + AssertParse(Baz.self, ["", "Foo"]) { baz in + XCTAssertEqual(baz.name, "") + XCTAssertEqual(baz.format, "Foo") + } + AssertParse(Baz.self, ["Bar", ""]) { baz in + XCTAssertEqual(baz.name, "Bar") + XCTAssertEqual(baz.format, "") + } + AssertParse(Baz.self, ["--", "--b", "--f"]) { baz in + XCTAssertEqual(baz.name, "--b") + XCTAssertEqual(baz.format, "--f") + } + AssertParse(Baz.self, ["b", "--", "--f"]) { baz in + XCTAssertEqual(baz.name, "b") + XCTAssertEqual(baz.format, "--f") + } + } + + func testParsing_TwoPositional_Fails() throws { + XCTAssertThrowsError(try Baz.parse(["Bar", "Foo", "Baz"])) + XCTAssertThrowsError(try Baz.parse(["Bar"])) + XCTAssertThrowsError(try Baz.parse([])) + XCTAssertThrowsError(try Baz.parse(["--name", "Bar", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["Bar", "--name", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["Bar", "Foo", "--name"])) + } +} + +// MARK: Multiple values + +fileprivate struct Qux: ParsableArguments { + @Argument() var names: [String] +} + +extension PositionalEndToEndTests { + func testParsing_MultiplePositional() throws { + AssertParse(Qux.self, []) { qux in + XCTAssertEqual(qux.names, []) + } + AssertParse(Qux.self, ["Bar"]) { qux in + XCTAssertEqual(qux.names, ["Bar"]) + } + AssertParse(Qux.self, ["Bar", "Foo"]) { qux in + XCTAssertEqual(qux.names, ["Bar", "Foo"]) + } + AssertParse(Qux.self, ["Bar", "Foo", "Baz"]) { qux in + XCTAssertEqual(qux.names, ["Bar", "Foo", "Baz"]) + } + + AssertParse(Qux.self, ["--", "--b", "--f"]) { qux in + XCTAssertEqual(qux.names, ["--b", "--f"]) + } + AssertParse(Qux.self, ["b", "--", "--f"]) { qux in + XCTAssertEqual(qux.names, ["b", "--f"]) + } + } + + func testParsing_MultiplePositional_Fails() throws { + // TODO: Allow zero-argument arrays? + XCTAssertThrowsError(try Qux.parse(["--name", "Bar", "Foo"])) + XCTAssertThrowsError(try Qux.parse(["Bar", "--name", "Foo"])) + XCTAssertThrowsError(try Qux.parse(["Bar", "Foo", "--name"])) + } +} + +// MARK: Single value plus multiple values + +fileprivate struct Wobble: ParsableArguments { + @Argument() var count: Int + @Argument() var names: [String] +} + +extension PositionalEndToEndTests { + func testParsing_SingleAndMultiplePositional() throws { + AssertParse(Wobble.self, ["5"]) { wobble in + XCTAssertEqual(wobble.count, 5) + XCTAssertEqual(wobble.names, []) + } + AssertParse(Wobble.self, ["5", "Bar"]) { wobble in + XCTAssertEqual(wobble.count, 5) + XCTAssertEqual(wobble.names, ["Bar"]) + } + AssertParse(Wobble.self, ["5", "Bar", "Foo"]) { wobble in + XCTAssertEqual(wobble.count, 5) + XCTAssertEqual(wobble.names, ["Bar", "Foo"]) + } + AssertParse(Wobble.self, ["5", "Bar", "Foo", "Baz"]) { wobble in + XCTAssertEqual(wobble.count, 5) + XCTAssertEqual(wobble.names, ["Bar", "Foo", "Baz"]) + } + + AssertParse(Wobble.self, ["5", "--", "--b", "--f"]) { wobble in + XCTAssertEqual(wobble.count, 5) + XCTAssertEqual(wobble.names, ["--b", "--f"]) + } + AssertParse(Wobble.self, ["--", "5", "--b", "--f"]) { wobble in + XCTAssertEqual(wobble.count, 5) + XCTAssertEqual(wobble.names, ["--b", "--f"]) + } + AssertParse(Wobble.self, ["5", "b", "--", "--f"]) { wobble in + XCTAssertEqual(wobble.count, 5) + XCTAssertEqual(wobble.names, ["b", "--f"]) + } + } + + func testParsing_SingleAndMultiplePositional_Fails() throws { + XCTAssertThrowsError(try Wobble.parse([])) + XCTAssertThrowsError(try Wobble.parse(["--name", "Bar", "Foo"])) + XCTAssertThrowsError(try Wobble.parse(["Bar", "--name", "Foo"])) + XCTAssertThrowsError(try Wobble.parse(["Bar", "Foo", "--name"])) + } +} + +// MARK: Multiple parsed values + +fileprivate struct Flob: ParsableArguments { + @Argument() var counts: [Int] +} + +extension PositionalEndToEndTests { + func testParsing_MultipleParsedPositional() throws { + AssertParse(Flob.self, []) { flob in + XCTAssertEqual(flob.counts, []) + } + AssertParse(Flob.self, ["5"]) { flob in + XCTAssertEqual(flob.counts, [5]) + } + AssertParse(Flob.self, ["5", "6"]) { flob in + XCTAssertEqual(flob.counts, [5, 6]) + } + + AssertParse(Flob.self, ["5", "--", "6"]) { flob in + XCTAssertEqual(flob.counts, [5, 6]) + } + AssertParse(Flob.self, ["--", "5", "6"]) { flob in + XCTAssertEqual(flob.counts, [5, 6]) + } + AssertParse(Flob.self, ["5", "6", "--"]) { flob in + XCTAssertEqual(flob.counts, [5, 6]) + } + } + + func testParsing_MultipleParsedPositional_Fails() throws { + XCTAssertThrowsError(try Flob.parse(["a"])) + XCTAssertThrowsError(try Flob.parse(["5", "6", "a"])) + } +} + +// MARK: Multiple parsed values + +fileprivate struct BadlyFormed: ParsableArguments { + @Argument() var numbers: [Int] + @Argument() var name: String +} + +extension PositionalEndToEndTests { + // This test results in a fatal error when run, so it can't be enabled + // or CI will prevent integration. Delete `disabled_` to verify the trap + // locally. + func disabled_testParsing_BadlyFormedPositional() throws { + AssertParse(BadlyFormed.self, []) { _ in + XCTFail("This should never execute") + } + } +} diff --git a/Tests/EndToEndTests/RawRepresentableEndToEndTests.swift b/Tests/EndToEndTests/RawRepresentableEndToEndTests.swift new file mode 100644 index 000000000..912671363 --- /dev/null +++ b/Tests/EndToEndTests/RawRepresentableEndToEndTests.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class RawRepresentableEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + struct Identifier: RawRepresentable, Equatable, ExpressibleByArgument { + var rawValue: Int + } + + @Option() var identifier: Identifier +} + +extension RawRepresentableEndToEndTests { + func testParsing_SingleOption() throws { + AssertParse(Bar.self, ["--identifier", "123"]) { bar in + XCTAssertEqual(bar.identifier, Bar.Identifier(rawValue: 123)) + } + } + + func testParsing_SingleOptionMultipleTimes() throws { + AssertParse(Bar.self, ["--identifier", "123", "--identifier", "456"]) { bar in + XCTAssertEqual(bar.identifier, Bar.Identifier(rawValue: 456)) + } + } + + func testParsing_SingleOption_Fails() throws { + XCTAssertThrowsError(try Bar.parse([])) + XCTAssertThrowsError(try Bar.parse(["--identifier"])) + XCTAssertThrowsError(try Bar.parse(["--identifier", "not a number"])) + XCTAssertThrowsError(try Bar.parse(["--identifier", "123.456"])) + } +} diff --git a/Tests/EndToEndTests/RepeatingEndToEndTests.swift b/Tests/EndToEndTests/RepeatingEndToEndTests.swift new file mode 100644 index 000000000..52c70b2ee --- /dev/null +++ b/Tests/EndToEndTests/RepeatingEndToEndTests.swift @@ -0,0 +1,335 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class RepeatingEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Option() var name: [String] +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingString() throws { + AssertParse(Bar.self, []) { bar in + XCTAssertTrue(bar.name.isEmpty) + } + + AssertParse(Bar.self, ["--name", "Bar"]) { bar in + XCTAssertEqual(bar.name.count, 1) + XCTAssertEqual(bar.name.first, "Bar") + } + + AssertParse(Bar.self, ["--name", "Bar", "--name", "Foo"]) { bar in + XCTAssertEqual(bar.name.count, 2) + XCTAssertEqual(bar.name.first, "Bar") + XCTAssertEqual(bar.name.last, "Foo") + } + } +} + +// MARK: - + +fileprivate struct Foo: ParsableArguments { + @Flag() + var verbose: Int +} + +extension RepeatingEndToEndTests { + func testParsing_incrementInteger() throws { + AssertParse(Foo.self, []) { options in + XCTAssertEqual(options.verbose, 0) + } + AssertParse(Foo.self, ["--verbose"]) { options in + XCTAssertEqual(options.verbose, 1) + } + AssertParse(Foo.self, ["--verbose", "--verbose"]) { options in + XCTAssertEqual(options.verbose, 2) + } + } +} + +// MARK: - + +fileprivate struct Baz: ParsableArguments { + @Flag() var verbose: Bool + @Option(parsing: .remaining) var names: [String] +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingStringRemaining_1() { + AssertParse(Baz.self, []) { baz in + XCTAssertFalse(baz.verbose) + XCTAssertTrue(baz.names.isEmpty) + } + } + + func testParsing_repeatingStringRemaining_2() { + AssertParse(Baz.self, ["--names"]) { baz in + XCTAssertFalse(baz.verbose) + XCTAssertTrue(baz.names.isEmpty) + } + } + + func testParsing_repeatingStringRemaining_3() { + AssertParse(Baz.self, ["--names", "one"]) { baz in + XCTAssertFalse(baz.verbose) + XCTAssertEqual(baz.names, ["one"]) + } + } + + func testParsing_repeatingStringRemaining_4() { + AssertParse(Baz.self, ["--names", "one", "two"]) { baz in + XCTAssertFalse(baz.verbose) + XCTAssertEqual(baz.names, ["one", "two"]) + } + } + + func testParsing_repeatingStringRemaining_5() { + AssertParse(Baz.self, ["--verbose", "--names", "one", "two"]) { baz in + XCTAssertTrue(baz.verbose) + XCTAssertEqual(baz.names, ["one", "two"]) + } + } + + func testParsing_repeatingStringRemaining_6() { + AssertParse(Baz.self, ["--names", "one", "two", "--verbose"]) { baz in + XCTAssertFalse(baz.verbose) + XCTAssertEqual(baz.names, ["one", "two", "--verbose"]) + } + } + + func testParsing_repeatingStringRemaining_7() { + AssertParse(Baz.self, ["--verbose", "--names", "one", "two", "--verbose"]) { baz in + XCTAssertTrue(baz.verbose) + XCTAssertEqual(baz.names, ["one", "two", "--verbose"]) + } + } + + func testParsing_repeatingStringRemaining_8() { + AssertParse(Baz.self, ["--verbose", "--names", "one", "two", "--verbose", "--other", "three"]) { baz in + XCTAssertTrue(baz.verbose) + XCTAssertEqual(baz.names, ["one", "two", "--verbose", "--other", "three"]) + } + } +} + +// MARK: - + +fileprivate struct Qux: ParsableArguments { + @Option(parsing: .upToNextOption) var names: [String] + @Flag() var verbose: Bool + @Argument() var extra: String? +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingStringUpToNext() throws { + AssertParse(Qux.self, []) { qux in + XCTAssertFalse(qux.verbose) + XCTAssertTrue(qux.names.isEmpty) + XCTAssertNil(qux.extra) + } + + AssertParse(Qux.self, ["--names", "one"]) { qux in + XCTAssertFalse(qux.verbose) + XCTAssertEqual(qux.names, ["one"]) + XCTAssertNil(qux.extra) + } + + // TODO: Is this the right behavior? Or should an option always consume + // _at least one_ value even if it's set to `upToNextOption`. + AssertParse(Qux.self, ["--names", "--verbose"]) { qux in + XCTAssertTrue(qux.verbose) + XCTAssertTrue(qux.names.isEmpty) + XCTAssertNil(qux.extra) + } + + AssertParse(Qux.self, ["--names", "--verbose", "three"]) { qux in + XCTAssertTrue(qux.verbose) + XCTAssertTrue(qux.names.isEmpty) + XCTAssertEqual(qux.extra, "three") + } + + AssertParse(Qux.self, ["--names", "one", "two"]) { qux in + XCTAssertFalse(qux.verbose) + XCTAssertEqual(qux.names, ["one", "two"]) + XCTAssertNil(qux.extra) + } + + AssertParse(Qux.self, ["--names", "one", "two", "--verbose"]) { qux in + XCTAssertTrue(qux.verbose) + XCTAssertEqual(qux.names, ["one", "two"]) + XCTAssertNil(qux.extra) + } + + AssertParse(Qux.self, ["--names", "one", "two", "--verbose", "three"]) { qux in + XCTAssertTrue(qux.verbose) + XCTAssertEqual(qux.names, ["one", "two"]) + XCTAssertEqual(qux.extra, "three") + } + + AssertParse(Qux.self, ["--verbose", "--names", "one", "two"]) { qux in + XCTAssertTrue(qux.verbose) + XCTAssertEqual(qux.names, ["one", "two"]) + XCTAssertNil(qux.extra) + } + } + + func testParsing_repeatingStringUpToNext_Fails() throws { + XCTAssertThrowsError(try Qux.parse(["--names", "one", "--other"])) + XCTAssertThrowsError(try Qux.parse(["--names", "one", "two", "--other"])) + // TODO: See above + XCTAssertThrowsError(try Qux.parse(["--names", "--other"])) + } +} + +// MARK: - + +fileprivate struct Wobble: ParsableArguments { + struct WobbleError: Error {} + struct Name: Equatable { + var value: String + + init(_ value: String) throws { + if value == "bad" { throw WobbleError() } + self.value = value + } + } + @Option(transform: Name.init) var names: [Name] + @Option(parsing: .upToNextOption, transform: Name.init) var moreNames: [Name] + @Option(parsing: .remaining, transform: Name.init) var evenMoreNames: [Name] +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingWithTransform() throws { + let names = ["--names", "one", "--names", "two"] + let moreNames = ["--more-names", "three", "four", "five"] + let evenMoreNames = ["--even-more-names", "six", "--seven", "--eight"] + + AssertParse(Wobble.self, []) { wobble in + XCTAssertTrue(wobble.names.isEmpty) + XCTAssertTrue(wobble.moreNames.isEmpty) + XCTAssertTrue(wobble.evenMoreNames.isEmpty) + } + + AssertParse(Wobble.self, names) { wobble in + XCTAssertEqual(wobble.names.map { $0.value }, ["one", "two"]) + XCTAssertTrue(wobble.moreNames.isEmpty) + XCTAssertTrue(wobble.evenMoreNames.isEmpty) + } + + AssertParse(Wobble.self, moreNames) { wobble in + XCTAssertTrue(wobble.names.isEmpty) + XCTAssertEqual(wobble.moreNames.map { $0.value }, ["three", "four", "five"]) + XCTAssertTrue(wobble.evenMoreNames.isEmpty) + } + + AssertParse(Wobble.self, evenMoreNames) { wobble in + XCTAssertTrue(wobble.names.isEmpty) + XCTAssertTrue(wobble.moreNames.isEmpty) + XCTAssertEqual(wobble.evenMoreNames.map { $0.value }, ["six", "--seven", "--eight"]) + } + + AssertParse(Wobble.self, Array([names, moreNames, evenMoreNames].joined())) { wobble in + XCTAssertEqual(wobble.names.map { $0.value }, ["one", "two"]) + XCTAssertEqual(wobble.moreNames.map { $0.value }, ["three", "four", "five"]) + XCTAssertEqual(wobble.evenMoreNames.map { $0.value }, ["six", "--seven", "--eight"]) + } + + AssertParse(Wobble.self, Array([moreNames, names, evenMoreNames].joined())) { wobble in + XCTAssertEqual(wobble.names.map { $0.value }, ["one", "two"]) + XCTAssertEqual(wobble.moreNames.map { $0.value }, ["three", "four", "five"]) + XCTAssertEqual(wobble.evenMoreNames.map { $0.value }, ["six", "--seven", "--eight"]) + } + + AssertParse(Wobble.self, Array([moreNames, evenMoreNames, names].joined())) { wobble in + XCTAssertTrue(wobble.names.isEmpty) + XCTAssertEqual(wobble.moreNames.map { $0.value }, ["three", "four", "five"]) + XCTAssertEqual(wobble.evenMoreNames.map { $0.value }, ["six", "--seven", "--eight", "--names", "one", "--names", "two"]) + } + } + + func testParsing_repeatingWithTransform_Fails() throws { + XCTAssertThrowsError(try Wobble.parse(["--names", "one", "--other"])) + XCTAssertThrowsError(try Wobble.parse(["--more-names", "one", "--other"])) + + XCTAssertThrowsError(try Wobble.parse(["--names", "one", "--names", "bad"])) + XCTAssertThrowsError(try Wobble.parse(["--more-names", "one", "two", "bad", "--names", "one"])) + XCTAssertThrowsError(try Wobble.parse(["--even-more-names", "one", "two", "--names", "one", "bad"])) + } +} + +// MARK: - + +fileprivate struct Weazle: ParsableArguments { + @Flag() var verbose: Bool + @Argument() var names: [String] +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingArgument() throws { + AssertParse(Weazle.self, ["one", "two", "three", "--verbose"]) { weazle in + XCTAssertTrue(weazle.verbose) + XCTAssertEqual(weazle.names, ["one", "two", "three"]) + } + + AssertParse(Weazle.self, ["--verbose", "one", "two", "three"]) { weazle in + XCTAssertTrue(weazle.verbose) + XCTAssertEqual(weazle.names, ["one", "two", "three"]) + } + + AssertParse(Weazle.self, ["one", "two", "three", "--", "--other", "--verbose"]) { weazle in + XCTAssertFalse(weazle.verbose) + XCTAssertEqual(weazle.names, ["one", "two", "three", "--other", "--verbose"]) + } + } +} + +// MARK: - + +struct PerformanceTest: ParsableCommand { + @Option(name: .short) var bundleIdentifiers: [String] + + func run() throws { print(bundleIdentifiers) } +} + +fileprivate func argumentGenerator(_ count: Int) -> [String] { + Array((1...count).map { ["-b", "bundle-id\($0)"] }.joined()) +} + +fileprivate func time(_ body: () -> Void) -> TimeInterval { + let start = Date() + body() + return Date().timeIntervalSince(start) +} + +extension RepeatingEndToEndTests { + // A regression test against array parsing performance going non-linear. + func testParsing_repeatingPerformance() throws { + let timeFor20 = time { + AssertParse(PerformanceTest.self, argumentGenerator(100)) { test in + XCTAssertEqual(100, test.bundleIdentifiers.count) + } + } + let timeFor40 = time { + AssertParse(PerformanceTest.self, argumentGenerator(200)) { test in + XCTAssertEqual(200, test.bundleIdentifiers.count) + } + } + + XCTAssertLessThan(timeFor40, timeFor20 * 10) + } +} diff --git a/Tests/EndToEndTests/ShortNameEndToEndTests.swift b/Tests/EndToEndTests/ShortNameEndToEndTests.swift new file mode 100644 index 000000000..8ed6b04fe --- /dev/null +++ b/Tests/EndToEndTests/ShortNameEndToEndTests.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class ShortNameEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Flag(name: [.long, .short]) + var verbose: Bool + + @Option(name: [.long, .short]) + var file: String? + + @Argument() + var name: String +} + +extension ShortNameEndToEndTests { + func testParsing_withLongNames() throws { + AssertParse(Bar.self, ["foo"]) { options in + XCTAssertEqual(options.verbose, false) + XCTAssertNil(options.file) + XCTAssertEqual(options.name, "foo") + } + + AssertParse(Bar.self, ["--verbose", "--file", "myfile", "foo"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertEqual(options.file, "myfile") + XCTAssertEqual(options.name, "foo") + } + } + + func testParsing_simple() throws { + AssertParse(Bar.self, ["-v", "foo"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertNil(options.file) + XCTAssertEqual(options.name, "foo") + } + + AssertParse(Bar.self, ["-f", "myfile", "foo"]) { options in + XCTAssertEqual(options.verbose, false) + XCTAssertEqual(options.file, "myfile") + XCTAssertEqual(options.name, "foo") + } + + AssertParse(Bar.self, ["-v", "-f", "myfile", "foo"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertEqual(options.file, "myfile") + XCTAssertEqual(options.name, "foo") + } + } + + func testParsing_combined() throws { + AssertParse(Bar.self, ["-vf", "myfile", "foo"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertEqual(options.file, "myfile") + XCTAssertEqual(options.name, "foo") + } + + AssertParse(Bar.self, ["-fv", "myfile", "foo"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertEqual(options.file, "myfile") + XCTAssertEqual(options.name, "foo") + } + + AssertParse(Bar.self, ["foo", "-fv", "myfile"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertEqual(options.file, "myfile") + XCTAssertEqual(options.name, "foo") + } + } +} + +// MARK: - + +fileprivate struct Foo: ParsableArguments { + @Option(name: [.long, .short]) + var name: String + + @Option(name: [.long, .short]) + var file: String + + @Option(name: [.long, .short]) + var city: String +} + +extension ShortNameEndToEndTests { + func testParsing_combinedShortNames() throws { + AssertParse(Foo.self, ["-nfc", "name", "file", "city"]) { options in + XCTAssertEqual(options.name, "name") + XCTAssertEqual(options.file, "file") + XCTAssertEqual(options.city, "city") + } + + AssertParse(Foo.self, ["-ncf", "name", "city", "file"]) { options in + XCTAssertEqual(options.name, "name") + XCTAssertEqual(options.file, "file") + XCTAssertEqual(options.city, "city") + } + + AssertParse(Foo.self, ["-fnc", "file", "name", "city"]) { options in + XCTAssertEqual(options.name, "name") + XCTAssertEqual(options.file, "file") + XCTAssertEqual(options.city, "city") + } + + AssertParse(Foo.self, ["-fcn", "file", "city", "name"]) { options in + XCTAssertEqual(options.name, "name") + XCTAssertEqual(options.file, "file") + XCTAssertEqual(options.city, "city") + } + + AssertParse(Foo.self, ["-cnf", "city", "name", "file"]) { options in + XCTAssertEqual(options.name, "name") + XCTAssertEqual(options.file, "file") + XCTAssertEqual(options.city, "city") + } + + AssertParse(Foo.self, ["-cfn", "city", "file", "name"]) { options in + XCTAssertEqual(options.name, "name") + XCTAssertEqual(options.file, "file") + XCTAssertEqual(options.city, "city") + } + } +} diff --git a/Tests/EndToEndTests/SimpleEndToEndTests.swift b/Tests/EndToEndTests/SimpleEndToEndTests.swift new file mode 100644 index 000000000..d107c1478 --- /dev/null +++ b/Tests/EndToEndTests/SimpleEndToEndTests.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class SimpleEndToEndTests: XCTestCase { +} + +// MARK: Single value String + +fileprivate struct Bar: ParsableArguments { + @Option() var name: String +} + +extension SimpleEndToEndTests { + func testParsing_SingleOption() throws { + AssertParse(Bar.self, ["--name", "Bar"]) { bar in + XCTAssertEqual(bar.name, "Bar") + } + AssertParse(Bar.self, ["--name", " foo "]) { bar in + XCTAssertEqual(bar.name, " foo ") + } + } + + func testParsing_SingleOption_Fails() throws { + XCTAssertThrowsError(try Bar.parse([])) + XCTAssertThrowsError(try Bar.parse(["--name"])) + XCTAssertThrowsError(try Bar.parse(["--name", "--foo"])) + XCTAssertThrowsError(try Bar.parse(["Bar"])) + XCTAssertThrowsError(try Bar.parse(["--name", "Bar", "Baz"])) + XCTAssertThrowsError(try Bar.parse(["--name", "Bar", "--foo"])) + XCTAssertThrowsError(try Bar.parse(["--name", "Bar", "--foo", "Foo"])) + XCTAssertThrowsError(try Bar.parse(["--name", "Bar", "-f"])) + XCTAssertThrowsError(try Bar.parse(["--foo", "--name", "Bar"])) + XCTAssertThrowsError(try Bar.parse(["--foo", "Foo", "--name", "Bar"])) + XCTAssertThrowsError(try Bar.parse(["-f", "--name", "Bar"])) + } +} + +// MARK: Single value Int + +fileprivate struct Foo: ParsableArguments { + @Option() var count: Int +} + +extension SimpleEndToEndTests { + func testParsing_SingleOption_Int() throws { + AssertParse(Foo.self, ["--count", "42"]) { foo in + XCTAssertEqual(foo.count, 42) + } + } + + func testParsing_SingleOption_Int_Fails() throws { + XCTAssertThrowsError(try Foo.parse([])) + XCTAssertThrowsError(try Foo.parse(["--count"])) + XCTAssertThrowsError(try Foo.parse(["--count", "a"])) + XCTAssertThrowsError(try Foo.parse(["Bar"])) + XCTAssertThrowsError(try Foo.parse(["--count", "42", "Baz"])) + XCTAssertThrowsError(try Foo.parse(["--count", "42", "--foo"])) + XCTAssertThrowsError(try Foo.parse(["--count", "42", "--foo", "Foo"])) + XCTAssertThrowsError(try Foo.parse(["--count", "42", "-f"])) + XCTAssertThrowsError(try Foo.parse(["--foo", "--count", "42"])) + XCTAssertThrowsError(try Foo.parse(["--foo", "Foo", "--count", "42"])) + XCTAssertThrowsError(try Foo.parse(["-f", "--count", "42"])) + } +} + +// MARK: Two values + +fileprivate struct Baz: ParsableArguments { + @Option() var name: String + @Option() var format: String +} + +extension SimpleEndToEndTests { + func testParsing_TwoOptions_1() throws { + AssertParse(Baz.self, ["--name", "Bar", "--format", "Foo"]) { baz in + XCTAssertEqual(baz.name, "Bar") + XCTAssertEqual(baz.format, "Foo") + } + } + + func testParsing_TwoOptions_2() throws { + AssertParse(Baz.self, ["--format", "Foo", "--name", "Bar"]) { baz in + XCTAssertEqual(baz.name, "Bar") + XCTAssertEqual(baz.format, "Foo") + } + } + + func testParsing_TwoOptions_Fails() throws { + XCTAssertThrowsError(try Baz.parse(["--nam", "Bar", "--format", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["--name", "Bar", "--forma", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["--name", "Bar"])) + XCTAssertThrowsError(try Baz.parse(["--format", "Foo"])) + + XCTAssertThrowsError(try Baz.parse(["--name", "--format", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["--name", "Bar", "--format"])) + XCTAssertThrowsError(try Baz.parse(["--name", "Bar", "--format", "Foo", "Baz"])) + XCTAssertThrowsError(try Baz.parse(["Bar", "--name", "--format", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["Bar", "--name", "Foo", "--format"])) + XCTAssertThrowsError(try Baz.parse(["Bar", "Foo", "--name", "--format"])) + XCTAssertThrowsError(try Baz.parse(["--name", "--name", "Bar", "--format", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["--name", "Bar", "--format", "--format", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["--format", "--name", "Bar", "Foo"])) + XCTAssertThrowsError(try Baz.parse(["--name", "--format", "Bar", "Foo"])) + } +} diff --git a/Tests/EndToEndTests/SingleValueParsingStrategyTests.swift b/Tests/EndToEndTests/SingleValueParsingStrategyTests.swift new file mode 100644 index 000000000..444dc40a9 --- /dev/null +++ b/Tests/EndToEndTests/SingleValueParsingStrategyTests.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class SingleValueParsingStrategyTests: XCTestCase { +} + +// MARK: Scanning for Value + +fileprivate struct Bar: ParsableArguments { + @Option(parsing: .scanningForValue) var name: String + @Option(parsing: .scanningForValue) var format: String + @Option(parsing: .scanningForValue) var input: String +} + +extension SingleValueParsingStrategyTests { + func testParsing_scanningForValue_1() throws { + AssertParse(Bar.self, ["--name", "Foo", "--format", "Bar", "--input", "Baz"]) { bar in + XCTAssertEqual(bar.name, "Foo") + XCTAssertEqual(bar.format, "Bar") + XCTAssertEqual(bar.input, "Baz") + } + } + + func testParsing_scanningForValue_2() throws { + AssertParse(Bar.self, ["--name", "--format", "Foo", "Bar", "--input", "Baz"]) { bar in + XCTAssertEqual(bar.name, "Foo") + XCTAssertEqual(bar.format, "Bar") + XCTAssertEqual(bar.input, "Baz") + } + } + + func testParsing_scanningForValue_3() throws { + AssertParse(Bar.self, ["--name", "--format", "--input", "Foo", "Bar", "Baz"]) { bar in + XCTAssertEqual(bar.name, "Foo") + XCTAssertEqual(bar.format, "Bar") + XCTAssertEqual(bar.input, "Baz") + } + } +} + +// MARK: Unconditional + +fileprivate struct Baz: ParsableArguments { + @Option(parsing: .unconditional) var name: String + @Option(parsing: .unconditional) var format: String + @Option(parsing: .unconditional) var input: String +} + +extension SingleValueParsingStrategyTests { + func testParsing_unconditional_1() throws { + AssertParse(Baz.self, ["--name", "Foo", "--format", "Bar", "--input", "Baz"]) { bar in + XCTAssertEqual(bar.name, "Foo") + XCTAssertEqual(bar.format, "Bar") + XCTAssertEqual(bar.input, "Baz") + } + } + + func testParsing_unconditional_2() throws { + AssertParse(Baz.self, ["--name", "--name", "--format", "--format", "--input", "--input"]) { bar in + XCTAssertEqual(bar.name, "--name") + XCTAssertEqual(bar.format, "--format") + XCTAssertEqual(bar.input, "--input") + } + } + + func testParsing_unconditional_3() throws { + AssertParse(Baz.self, ["--name", "-Foo", "--format", "-Bar", "--input", "-Baz"]) { bar in + XCTAssertEqual(bar.name, "-Foo") + XCTAssertEqual(bar.format, "-Bar") + XCTAssertEqual(bar.input, "-Baz") + } + } +} diff --git a/Tests/EndToEndTests/SubcommandEndToEndTests.swift b/Tests/EndToEndTests/SubcommandEndToEndTests.swift new file mode 100644 index 000000000..ad8f883bb --- /dev/null +++ b/Tests/EndToEndTests/SubcommandEndToEndTests.swift @@ -0,0 +1,147 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class SubcommandEndToEndTests: XCTestCase { +} + +// MARK: Single value String + +fileprivate struct Foo: ParsableCommand { + static var configuration = + CommandConfiguration(subcommands: [CommandA.self, CommandB.self]) + + @Option() var name: String +} + +fileprivate struct CommandA: ParsableCommand { + static var configuration = CommandConfiguration(commandName: "a") + + @OptionGroup() var foo: Foo + + @Option() var bar: Int +} + +fileprivate struct CommandB: ParsableCommand { + static var configuration = CommandConfiguration(commandName: "b") + + @OptionGroup() var foo: Foo + + @Option() var baz: String +} + +extension SubcommandEndToEndTests { + func testParsing_SubCommand() throws { + AssertParseCommand(Foo.self, CommandA.self, ["--name", "Foo", "a", "--bar", "42"]) { a in + XCTAssertEqual(a.bar, 42) + XCTAssertEqual(a.foo.name, "Foo") + } + + AssertParseCommand(Foo.self, CommandB.self, ["--name", "A", "b", "--baz", "abc"]) { b in + XCTAssertEqual(b.baz, "abc") + XCTAssertEqual(b.foo.name, "A") + } + } + + func testParsing_SubCommand_manual() throws { + AssertParseCommand(Foo.self, CommandA.self, ["--name", "Foo", "a", "--bar", "42"]) { a in + XCTAssertEqual(a.bar, 42) + XCTAssertEqual(a.foo.name, "Foo") + } + + AssertParseCommand(Foo.self, Foo.self, ["--name", "Foo"]) { foo in + XCTAssertEqual(foo.name, "Foo") + } + } + + func testParsing_SubCommand_help() throws { + let helpFoo = Foo.message(for: CleanExit.helpRequest()) + let helpA = Foo.message(for: CleanExit.helpRequest(CommandA.self)) + let helpB = Foo.message(for: CleanExit.helpRequest(CommandB.self)) + + AssertEqualStringsIgnoringTrailingWhitespace(""" + USAGE: foo --name + + OPTIONS: + --name + -h, --help Show help information. + + SUBCOMMANDS: + a + b + + """, helpFoo) + AssertEqualStringsIgnoringTrailingWhitespace(""" + USAGE: foo a --name --bar + + OPTIONS: + --name + --name + --bar + -h, --help Show help information. + + """, helpA) + AssertEqualStringsIgnoringTrailingWhitespace(""" + USAGE: foo b --name --baz + + OPTIONS: + --name + --name + --baz + -h, --help Show help information. + + """, helpB) + } + + + func testParsing_SubCommand_fails() throws { + XCTAssertThrowsError(try Foo.parse(["--name", "Foo", "a", "--baz", "42"]), "'baz' is not an option for the 'a' subcommand.") + XCTAssertThrowsError(try Foo.parse(["--name", "Foo", "b", "--bar", "42"]), "'bar' is not an option for the 'b' subcommand.") + } +} + +fileprivate var mathDidRun = false + +fileprivate struct Math: ParsableCommand { + enum Operation: String, ExpressibleByArgument { + case add + case multiply + } + + @Option(default: .add, help: "The operation to perform") + var operation: Operation + + @Flag(name: [.short, .long]) + var verbose: Bool + + @Argument(help: "The first operand") + var operands: [Int] + + func run() { + XCTAssertEqual(operation, .multiply) + XCTAssertTrue(verbose) + XCTAssertEqual(operands, [5, 11]) + mathDidRun = true + } +} + +extension SubcommandEndToEndTests { + func testParsing_SingleCommand() throws { + let mathCommand = try Math.parseAsRoot(["--operation", "multiply", "-v", "5", "11"]) + XCTAssertFalse(mathDidRun) + try mathCommand.run() + XCTAssertTrue(mathDidRun) + } +} + diff --git a/Tests/EndToEndTests/ValidationEndToEndTests.swift b/Tests/EndToEndTests/ValidationEndToEndTests.swift new file mode 100644 index 000000000..473fb9a59 --- /dev/null +++ b/Tests/EndToEndTests/ValidationEndToEndTests.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------*- 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 TestHelpers +import ArgumentParser + +final class ValidationEndToEndTests: XCTestCase { +} + +fileprivate enum UserValidationError: LocalizedError { + case userValidationError + + var errorDescription: String? { + switch self { + case .userValidationError: + return "UserValidationError" + } + } +} + +fileprivate struct Foo: ParsableArguments { + @Option() + var count: Int? + + @Argument() + var names: [String] + + @Flag() + var version: Bool + + @Flag(name: [.customLong("throw")]) + var throwCustomError: Bool + + mutating func validate() throws { + if version { + throw CleanExit.message("0.0.1") + } + + if names.isEmpty { + throw ValidationError("Must specify at least one name.") + } + + if let count = count, names.count != count { + throw ValidationError("Number of names (\(names.count)) doesn't match count (\(count)).") + } + + if throwCustomError { + throw UserValidationError.userValidationError + } + } +} + +extension ValidationEndToEndTests { + func testValidation() throws { + AssertParse(Foo.self, ["Joe"]) { foo in + XCTAssertEqual(foo.names, ["Joe"]) + XCTAssertNil(foo.count) + } + + AssertParse(Foo.self, ["Joe", "Moe", "--count", "2"]) { foo in + XCTAssertEqual(foo.names, ["Joe", "Moe"]) + XCTAssertEqual(foo.count, 2) + } + } + + func testValidation_Version() throws { + AssertErrorMessage(Foo.self, ["--version"], "0.0.1") + AssertFullErrorMessage(Foo.self, ["--version"], "0.0.1") + } + + func testValidation_Fails() throws { + AssertErrorMessage(Foo.self, [], "Must specify at least one name.") + AssertFullErrorMessage(Foo.self, [], """ + Error: Must specify at least one name. + Usage: foo [--count ] [ ...] [--version] [--throw] + """) + + AssertErrorMessage(Foo.self, ["--count", "3", "Joe"], """ + Number of names (1) doesn't match count (3). + """) + AssertFullErrorMessage(Foo.self, ["--count", "3", "Joe"], """ + Error: Number of names (1) doesn't match count (3). + Usage: foo [--count ] [ ...] [--version] [--throw] + """) + } + + func testCustomErrorValidation() { + // verify that error description is printed if avaiable via LocalizedError + AssertErrorMessage(Foo.self, ["--throw", "Joe"], UserValidationError.userValidationError.errorDescription!) + } +} diff --git a/Tests/ExampleTests/MathExampleTests.swift b/Tests/ExampleTests/MathExampleTests.swift new file mode 100644 index 000000000..bd94166be --- /dev/null +++ b/Tests/ExampleTests/MathExampleTests.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------*- 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 TestHelpers + +final class MathExampleTests: XCTestCase { + func testMath_Simple() throws { + AssertExecuteCommand(command: "math 1 2 3 4 5", expected: "15") + AssertExecuteCommand(command: "math multiply 1 2 3 4 5", expected: "120") + } + + func testMath_Help() throws { + let helpText = """ + OVERVIEW: A utility for performing maths. + + USAGE: math + + OPTIONS: + -h, --help Show help information. + + SUBCOMMANDS: + add Print the sum of the values. + multiply Print the product of the values. + stats Calculate descriptive statistics. + """ + + AssertExecuteCommand(command: "math -h", expected: helpText) + AssertExecuteCommand(command: "math --help", expected: helpText) + AssertExecuteCommand(command: "math help", expected: helpText) + } + + func testMath_AddHelp() throws { + let helpText = """ + OVERVIEW: Print the sum of the values. + + USAGE: math add [--hex-output] [ ...] + + ARGUMENTS: + A group of integers to operate on. + + OPTIONS: + -x, --hex-output Use hexadecimal notation for the result. + -h, --help Show help information. + """ + + AssertExecuteCommand(command: "math add -h", expected: helpText) + AssertExecuteCommand(command: "math add --help", expected: helpText) + AssertExecuteCommand(command: "math help add", expected: helpText) + } + + func testMath_StatsMeanHelp() throws { + let helpText = """ + OVERVIEW: Print the average of the values. + + USAGE: math stats average [--kind ] [ ...] + + ARGUMENTS: + A group of floating-point values to operate on. + + OPTIONS: + --kind The kind of average to provide. (default: mean) + -h, --help Show help information. + """ + + AssertExecuteCommand(command: "math stats average -h", expected: helpText) + AssertExecuteCommand(command: "math stats average --help", expected: helpText) + AssertExecuteCommand(command: "math help stats average", expected: helpText) + } + + func testMath_StatsQuantilesHelp() throws { + let helpText = """ + OVERVIEW: Print the quantiles of the values (TBD). + + USAGE: math stats quantiles [ ...] + + ARGUMENTS: + A group of floating-point values to operate on. + + OPTIONS: + -h, --help Show help information. + """ + + // The "quantiles" subcommand's run() method is unimplemented, so it + // just generates the help text. + AssertExecuteCommand(command: "math stats quantiles", expected: helpText) + + AssertExecuteCommand(command: "math stats quantiles -h", expected: helpText) + AssertExecuteCommand(command: "math stats quantiles --help", expected: helpText) + AssertExecuteCommand(command: "math help stats quantiles", expected: helpText) + } + + func testMath_Fail() throws { + AssertExecuteCommand( + command: "math --foo", + expected: """ + Error: Unexpected argument '--foo' + Usage: math add [--hex-output] [ ...] + """, + shouldError: true) + + AssertExecuteCommand( + command: "math ZZZ", + expected: """ + Error: The value 'ZZZ' is invalid for '' + Usage: math add [--hex-output] [ ...] + """, + shouldError: true) + } +} diff --git a/Tests/ExampleTests/RepeatExampleTests.swift b/Tests/ExampleTests/RepeatExampleTests.swift new file mode 100644 index 000000000..6da5b2e04 --- /dev/null +++ b/Tests/ExampleTests/RepeatExampleTests.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------*- 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 TestHelpers + +final class RepeatExampleTests: XCTestCase { + func testRepeat() throws { + AssertExecuteCommand(command: "repeat hello --count 6", expected: """ + hello + hello + hello + hello + hello + hello + """) + } + + func testRepeat_Help() throws { + let helpText = """ + USAGE: repeat [--count ] [--include-counter] + + ARGUMENTS: + The phrase to repeat. + + OPTIONS: + --count The number of times to repeat 'phrase'. + --include-counter Include a counter with each repetition. + -h, --help Show help information. + """ + + AssertExecuteCommand(command: "repeat -h", expected: helpText) + AssertExecuteCommand(command: "repeat --help", expected: helpText) + } + + func testRepeat_Fail() throws { + AssertExecuteCommand( + command: "repeat", + expected: """ + Error: Missing expected argument '' + Usage: repeat [--count ] [--include-counter] + """, + shouldError: true) + + AssertExecuteCommand( + command: "repeat hello --count", + expected: """ + Error: Missing value for '--count ' + Usage: repeat [--count ] [--include-counter] + """, + shouldError: true) + + AssertExecuteCommand( + command: "repeat hello --count ZZZ", + expected: """ + Error: The value 'ZZZ' is invalid for '--count ' + Usage: repeat [--count ] [--include-counter] + """, + shouldError: true) + } +} diff --git a/Tests/ExampleTests/RollDiceExampleTests.swift b/Tests/ExampleTests/RollDiceExampleTests.swift new file mode 100644 index 000000000..aa80d3155 --- /dev/null +++ b/Tests/ExampleTests/RollDiceExampleTests.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------*- 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 TestHelpers + +final class RollDiceExampleTests: XCTestCase { + func testRollDice() throws { + AssertExecuteCommand(command: "roll --times 6") + } + + func testRollDice_Help() throws { + let helpText = """ + USAGE: roll [--times ] [--sides ] [--seed ] [--verbose] + + OPTIONS: + --times Rolls the dice times. (default: 1) + --sides Rolls an -sided dice. (default: 6) + Use this option to override the default value of a six-sided die. + --seed A seed to use for repeatable random generation. + -v, --verbose Show all roll results. + -h, --help Show help information. + """ + + AssertExecuteCommand(command: "roll -h", expected: helpText) + AssertExecuteCommand(command: "roll --help", expected: helpText) + } + + func testRollDice_Fail() throws { + AssertExecuteCommand( + command: "roll --times", + expected: """ + Error: Missing value for '--times ' + Usage: roll [--times ] [--sides ] [--seed ] [--verbose] + """, + shouldError: true) + + AssertExecuteCommand( + command: "roll --times ZZZ", + expected: """ + Error: The value 'ZZZ' is invalid for '--times ' + Usage: roll [--times ] [--sides ] [--seed ] [--verbose] + """, + shouldError: true) + } +} diff --git a/Tests/PackageManagerTests/HelpTests.swift b/Tests/PackageManagerTests/HelpTests.swift new file mode 100644 index 000000000..6cb470d15 --- /dev/null +++ b/Tests/PackageManagerTests/HelpTests.swift @@ -0,0 +1,224 @@ +//===----------------------------------------------------------*- 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 +@testable import ArgumentParser +import TestHelpers + +final class HelpTests: XCTestCase { +} + +func getErrorText(_: T.Type, _ arguments: [String]) -> String { + do { + _ = try T.parse(arguments) + XCTFail("Didn't generate a help error") + return "" + } catch { + return T.message(for: error) + } +} + +func getErrorText(_: T.Type, _ arguments: [String]) -> String { + do { + let command = try T.parseAsRoot(arguments) + if let helpCommand = command as? HelpCommand { + return helpCommand.generateHelp() + } else { + XCTFail("Didn't generate a help error") + return "" + } + } catch { + return T.message(for: error) + } +} + +extension HelpTests { + func testGlobalHelp() throws { + XCTAssertEqual( + getErrorText(Package.self, ["help"]).trimmingLines(), + """ + USAGE: package + + OPTIONS: + -h, --help Show help information. + + SUBCOMMANDS: + clean + config + describe + generate-xcodeproj + + """.trimmingLines()) + } + + func testGlobalHelp_messageForCleanExit_helpRequest() throws { + XCTAssertEqual( + Package.message(for: CleanExit.helpRequest()).trimmingLines(), + """ + USAGE: package + + OPTIONS: + -h, --help Show help information. + + SUBCOMMANDS: + clean + config + describe + generate-xcodeproj + + """.trimmingLines() + ) + } + + func testGlobalHelp_messageForCleanExit_message() throws { + let expectedMessage = "Failure" + XCTAssertEqual( + Package.message(for: CleanExit.message(expectedMessage)).trimmingLines(), + expectedMessage + ) + } + + func testConfigHelp() throws { + XCTAssertEqual( + getErrorText(Package.self, ["help", "config"]).trimmingLines(), + """ + USAGE: package config + + OPTIONS: + -h, --help Show help information. + + SUBCOMMANDS: + get-mirror + set-mirror + unset-mirror + + """.trimmingLines()) + } + + func testGetMirrorHelp() throws { + HelpGenerator._screenWidthOverride = 80 + defer { HelpGenerator._screenWidthOverride = nil } + + XCTAssertEqual( + getErrorText(Package.self, ["help", "config", "get-mirror"]).trimmingLines(), + """ + USAGE: package config get-mirror + + OPTIONS: + --build-path + Specify build/cache directory (default: ./.build) + -c, --configuration + Build with configuration (default: debug) + --enable-automatic-resolution/--disable-automatic-resolution + Use automatic resolution if Package.resolved file is + out-of-date (default: true) + --enable-index-store/--disable-index-store + Use indexing-while-building feature (default: true) + --enable-package-manifest-caching/--disable-package-manifest-caching + Cache Package.swift manifests (default: true) + --enable-prefetching/--disable-prefetching + (default: true) + --enable-sandbox/--disable-sandbox + Use sandbox when executing subprocesses (default: + true) + --enable-pubgrub-resolver/--disable-pubgrub-resolver + [Experimental] Enable the new Pubgrub dependency + resolver (default: false) + --static-swift-stdlib/--no-static-swift-stdlib + Link Swift stdlib statically (default: false) + --package-path + Change working directory before any other operation + (default: .) + --sanitize Turn on runtime checks for erroneous behavior + --skip-update Skip updating dependencies from their remote during a + resolution + -v, --verbose Increase verbosity of informational output + -Xcc Pass flag through to all C compiler invocations + -Xcxx + Pass flag through to all C++ compiler invocations + -Xlinker Pass flag through to all linker invocations + -Xswiftc + Pass flag through to all Swift compiler invocations + --package-url + The package dependency URL + -h, --help Show help information. + + """.trimmingLines()) + } +} + +struct Simple: ParsableArguments { + @Flag() var verbose: Bool + @Option() var min: Int? + @Argument() var max: Int + + static var helpText = """ + USAGE: simple [--verbose] [--min ] + + ARGUMENTS: + + + OPTIONS: + --verbose + --min + -h, --help Show help information. + + """.trimmingLines() +} + +extension HelpTests { + func testSimpleHelp() throws { + XCTAssertEqual( + getErrorText(Simple.self, ["--help"]).trimmingLines(), + Simple.helpText) + XCTAssertEqual( + getErrorText(Simple.self, ["-h"]).trimmingLines(), + Simple.helpText) + } +} + +struct CustomHelp: ParsableCommand { + static let configuration = CommandConfiguration( + helpNames: [.customShort("?"), .customLong("show-help")] + ) +} + +extension HelpTests { + func testCustomHelpNames() { + let names = CustomHelp.getHelpNames() + XCTAssertEqual(names, [.short("?"), .long("show-help")]) + } +} + +struct NoHelp: ParsableCommand { + static let configuration = CommandConfiguration( + helpNames: [] + ) + + @Option(help: "How many florps?") var count: Int +} + +extension HelpTests { + func testNoHelpNames() { + let names = NoHelp.getHelpNames() + XCTAssertEqual(names, []) + + XCTAssertEqual( + NoHelp.message(for: CleanExit.helpRequest()).trimmingLines(), + """ + USAGE: no-help --count + + OPTIONS: + --count How many florps? + + """) + } +} diff --git a/Tests/PackageManagerTests/PackageManager/Clean.swift b/Tests/PackageManagerTests/PackageManager/Clean.swift new file mode 100644 index 000000000..f787b5ffe --- /dev/null +++ b/Tests/PackageManagerTests/PackageManager/Clean.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +extension Package { + struct Clean: ParsableCommand { + @OptionGroup() + var options: Options + } +} diff --git a/Tests/PackageManagerTests/PackageManager/Config.swift b/Tests/PackageManagerTests/PackageManager/Config.swift new file mode 100644 index 000000000..8d19a6d67 --- /dev/null +++ b/Tests/PackageManagerTests/PackageManager/Config.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +extension Package { + /// Manipulate configuration of the package + struct Config: ParsableCommand {} +} + +extension Package.Config { + public static var configuration = CommandConfiguration( + subcommands: [GetMirror.self, SetMirror.self, UnsetMirror.self]) + + /// Print mirror configuration for the given package dependency + struct GetMirror: ParsableCommand { + @OptionGroup() + var options: Options + + @Option(name: .customLong("package-url"), help: "The package dependency URL") + var packageURL: String + } + + /// Set a mirror for a dependency + struct SetMirror: ParsableCommand { + @OptionGroup() + var options: Options + + @Option(name: .customLong("mirror-url"), help: "The mirror URL") + var mirrorURL: String + + @Option(name: .customLong("package-url"), help: "The package dependency URL") + var packageURL: String + } + + /// Remove an existing mirror + struct UnsetMirror: ParsableCommand { + @OptionGroup() + var options: Options + + @Option(name: .customLong("mirror-url"), help: "The mirror URL") + var mirrorURL: String + + @Option(name: .customLong("package-url"), help: "The package dependency URL") + var packageURL: String + } +} diff --git a/Tests/PackageManagerTests/PackageManager/Describe.swift b/Tests/PackageManagerTests/PackageManager/Describe.swift new file mode 100644 index 000000000..844ee5e0b --- /dev/null +++ b/Tests/PackageManagerTests/PackageManager/Describe.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +extension Package { + /// Describe the current package + struct Describe: ParsableCommand { + @OptionGroup() + var options: Options + + @Option(help: "Output format") + var type: OutputType + + enum OutputType: String, ExpressibleByArgument, Decodable { + case json + case text + } + } +} diff --git a/Tests/PackageManagerTests/PackageManager/GenerateXcodeProject.swift b/Tests/PackageManagerTests/PackageManager/GenerateXcodeProject.swift new file mode 100644 index 000000000..662e7f048 --- /dev/null +++ b/Tests/PackageManagerTests/PackageManager/GenerateXcodeProject.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +extension Package { + /// Generates an Xcode project + struct GenerateXcodeProject: ParsableCommand { + static var configuration = + CommandConfiguration(commandName: "generate-xcodeproj") + + @OptionGroup() + var options: Options + + @Flag(help: "Enable code coverage in the generated project") + var enableCodeCoverage: Bool + + @Flag(help: "Use the legacy scheme generator") + var legacySchemeGenerator: Bool + + @Option(help: "Path where the Xcode project should be generated") + var output: String? + + @Flag(help: "Do not add file references for extra files to the generated Xcode project") + var skipExtraFiles: Bool + + @Flag(help: "Watch for changes to the Package manifest to regenerate the Xcode project") + var watch: Bool + + @Option(help: "Path to xcconfig file") + var xcconfigOverrides: String? + + func run() { + print("Generating Xcode Project.......") + } + } +} diff --git a/Tests/PackageManagerTests/PackageManager/Options.swift b/Tests/PackageManagerTests/PackageManager/Options.swift new file mode 100644 index 000000000..31b02e64c --- /dev/null +++ b/Tests/PackageManagerTests/PackageManager/Options.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------*- 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 ArgumentParser + +struct Options: ParsableArguments { + @Option(default: "./.build", help: "Specify build/cache directory") + var buildPath: String + + enum Configuration: String, ExpressibleByArgument, Decodable { + case debug + case release + } + + @Option(name: .shortAndLong, default: .debug, + help: "Build with configuration") + var configuration: Configuration + + @Flag(default: true, inversion: .prefixedEnableDisable, + help: "Use automatic resolution if Package.resolved file is out-of-date") + var automaticResolution: Bool + + @Flag(default: true, inversion: .prefixedEnableDisable, + help: "Use indexing-while-building feature") + var indexStore: Bool + + @Flag(default: true, inversion: .prefixedEnableDisable, + help: "Cache Package.swift manifests") + var packageManifestCaching: Bool + + @Flag(default: true, inversion: .prefixedEnableDisable) + var prefetching: Bool + + @Flag(default: true, inversion: .prefixedEnableDisable, + help: "Use sandbox when executing subprocesses") + var sandbox: Bool + + @Flag(inversion: .prefixedEnableDisable, + help: "[Experimental] Enable the new Pubgrub dependency resolver") + var pubgrubResolver: Bool + + @Flag(inversion: .prefixedNo, + help: "Link Swift stdlib statically") + var staticSwiftStdlib: Bool + + @Option(default: ".", + help: "Change working directory before any other operation") + var packagePath: String + + @Flag(help: "Turn on runtime checks for erroneous behavior") + var sanitize: Bool + + @Flag(help: "Skip updating dependencies from their remote during a resolution") + var skipUpdate: Bool + + @Flag(name: .shortAndLong, + help: "Increase verbosity of informational output") + var verbose: Bool + + @Option(name: .customLong("Xcc", withSingleDash: true), + parsing: .unconditionalSingleValue, + help: ArgumentHelp("Pass flag through to all C compiler invocations", + valueName: "c-compiler-flag")) + var cCompilerFlags: [String] + + @Option(name: .customLong("Xcxx", withSingleDash: true), + parsing: .unconditionalSingleValue, + help: ArgumentHelp("Pass flag through to all C++ compiler invocations", + valueName: "cxx-compiler-flag")) + var cxxCompilerFlags: [String] + + @Option(name: .customLong("Xlinker", withSingleDash: true), + parsing: .unconditionalSingleValue, + help: ArgumentHelp("Pass flag through to all linker invocations", + valueName: "linker-flag")) + var linkerFlags: [String] + + @Option(name: .customLong("Xswiftc", withSingleDash: true), + parsing: .unconditionalSingleValue, + help: ArgumentHelp("Pass flag through to all Swift compiler invocations", + valueName: "swift-compiler-flag")) + var swiftCompilerFlags: [String] +} + +struct Package: ParsableCommand { + static var configuration = CommandConfiguration( + subcommands: [Clean.self, Config.self, Describe.self, GenerateXcodeProject.self, Hidden.self]) +} + +extension Package { + struct Hidden: ParsableCommand { + static var configuration = CommandConfiguration(shouldDisplay: false) + } +} diff --git a/Tests/PackageManagerTests/Tests.swift b/Tests/PackageManagerTests/Tests.swift new file mode 100644 index 000000000..3e7de3738 --- /dev/null +++ b/Tests/PackageManagerTests/Tests.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import TestHelpers + +final class Tests: XCTestCase { +} + +extension Tests { + func testParsing() throws { + AssertParseCommand(Package.self, Package.Clean.self, ["clean"]) { clean in + let options = clean.options + XCTAssertEqual(options.buildPath, "./.build") + XCTAssertEqual(options.configuration, .debug) + XCTAssertEqual(options.automaticResolution, true) + XCTAssertEqual(options.indexStore, true) + XCTAssertEqual(options.packageManifestCaching, true) + XCTAssertEqual(options.prefetching, true) + XCTAssertEqual(options.sandbox, true) + XCTAssertEqual(options.pubgrubResolver, false) + XCTAssertEqual(options.staticSwiftStdlib, false) + XCTAssertEqual(options.packagePath, ".") + XCTAssertEqual(options.sanitize, false) + XCTAssertEqual(options.skipUpdate, false) + XCTAssertEqual(options.verbose, false) + XCTAssertEqual(options.cCompilerFlags, []) + XCTAssertEqual(options.cxxCompilerFlags, []) + XCTAssertEqual(options.linkerFlags, []) + XCTAssertEqual(options.swiftCompilerFlags, []) + } + } + + func testParsingWithGlobalOption_1() { + AssertParseCommand(Package.self, Package.GenerateXcodeProject.self, ["generate-xcodeproj", "--watch", "--output", "Foo", "--enable-automatic-resolution"]) { generate in + XCTAssertEqual(generate.output, "Foo") + XCTAssertFalse(generate.enableCodeCoverage) + XCTAssertTrue(generate.watch) + + let options = generate.options + // Default global option + XCTAssertEqual(options.configuration, .debug) + // Customized global option + XCTAssertEqual(options.automaticResolution, true) + } + } + + func testParsingWithGlobalOption_2() { + AssertParseCommand(Package.self, Package.GenerateXcodeProject.self, ["generate-xcodeproj", "--watch", "--output", "Foo", "--enable-automatic-resolution", "-Xcc", "-Ddebug"]) { generate in + XCTAssertEqual(generate.output, "Foo") + XCTAssertFalse(generate.enableCodeCoverage) + XCTAssertTrue(generate.watch) + + let options = generate.options + // Default global option + XCTAssertEqual(options.configuration, .debug) + // Customized global option + XCTAssertEqual(options.automaticResolution, true) + XCTAssertEqual(options.cCompilerFlags, ["-Ddebug"]) + } + } + + func testParsingWithGlobalOption_3() { + AssertParseCommand(Package.self, Package.GenerateXcodeProject.self, ["generate-xcodeproj", "--watch", "--output=Foo", "--enable-automatic-resolution", "-Xcc=-Ddebug"]) { generate in + XCTAssertEqual(generate.output, "Foo") + XCTAssertFalse(generate.enableCodeCoverage) + XCTAssertTrue(generate.watch) + + let options = generate.options + // Default global option + XCTAssertEqual(options.configuration, .debug) + // Customized global option + XCTAssertEqual(options.automaticResolution, true) + XCTAssertEqual(options.cCompilerFlags, ["-Ddebug"]) + } + } +} diff --git a/Tests/UnitTests/ErrorMessageTests.swift b/Tests/UnitTests/ErrorMessageTests.swift new file mode 100644 index 000000000..613d8dc6b --- /dev/null +++ b/Tests/UnitTests/ErrorMessageTests.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------*- 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 TestHelpers +@testable import ArgumentParser + +final class ErrorMessageTests: XCTestCase {} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Option() var name: String + @Option(name: [.short, .long]) var format: String +} + +extension ErrorMessageTests { + func testMissing_1() { + AssertErrorMessage(Bar.self, [], "Missing expected argument '--name '") + } + + func testMissing_2() { + AssertErrorMessage(Bar.self, ["--name", "a"], "Missing expected argument '--format '") + } + + func testUnknownOption_1() { + AssertErrorMessage(Bar.self, ["--name", "a", "--format", "b", "--verbose"], "Unexpected argument '--verbose'") + } + + func testUnknownOption_2() { + AssertErrorMessage(Bar.self, ["--name", "a", "--format", "b", "-q"], "Unexpected argument '-q'") + } + + func testUnknownOption_3() { + AssertErrorMessage(Bar.self, ["--name", "a", "--format", "b", "-bar"], "Unexpected argument '-bar'") + } + + func testUnknownOption_4() { + AssertErrorMessage(Bar.self, ["--name", "a", "-foz", "b"], "2 unexpected arguments: '-o', '-z'") + } + + func testMissingValue_1() { + AssertErrorMessage(Bar.self, ["--name", "a", "--format"], "Missing value for '--format '") + } + + func testMissingValue_2() { + AssertErrorMessage(Bar.self, ["--name", "a", "-f"], "Missing value for '-f '") + } + + func testUnusedValue_1() { + AssertErrorMessage(Bar.self, ["--name", "a", "--format", "f", "b"], "Unexpected argument 'b'") + } + + func testUnusedValue_2() { + AssertErrorMessage(Bar.self, ["--name", "a", "--format", "f", "b", "baz"], "2 unexpected arguments: 'b', 'baz'") + } +} + +fileprivate struct Foo: ParsableArguments { + enum Format: String, Equatable, Decodable, ExpressibleByArgument { + case text + case json + } + @Option(name: [.short, .long]) + var format: Format +} + +extension ErrorMessageTests { + func testWrongEnumValue() { + AssertErrorMessage(Foo.self, ["--format", "png"], "The value 'png' is invalid for '--format '") + AssertErrorMessage(Foo.self, ["-f", "png"], "The value 'png' is invalid for '-f '") + } +} + +fileprivate struct Baz: ParsableArguments { + @Flag() + var verbose: Bool +} + +extension ErrorMessageTests { + func testUnexpectedValue() { + AssertErrorMessage(Baz.self, ["--verbose=foo"], "The option '--verbose' does not take any value, but 'foo' was specified.") + } +} + +fileprivate struct Qux: ParsableArguments { + @Argument() + var firstNumber: Int + + @Option(name: .customLong("number-two")) + var secondNumber: Int +} + +extension ErrorMessageTests { + func testMissingArgument() { + AssertErrorMessage(Qux.self, ["--number-two", "2"], "Missing expected argument ''") + } + + func testInvalidNumber() { + AssertErrorMessage(Qux.self, ["--number-two", "2", "a"], "The value 'a' is invalid for ''") + AssertErrorMessage(Qux.self, ["--number-two", "a", "1"], "The value 'a' is invalid for '--number-two '") + } +} diff --git a/Tests/UnitTests/HelpGenerationTests.swift b/Tests/UnitTests/HelpGenerationTests.swift new file mode 100644 index 000000000..2618de717 --- /dev/null +++ b/Tests/UnitTests/HelpGenerationTests.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------*- 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 TestHelpers +@testable import ArgumentParser + +final class HelpGenerationTests: XCTestCase { +} + +// MARK: - + +extension HelpGenerationTests { + struct A: ParsableArguments { + @Option(help: "Your name") var name: String + @Option(help: "Your title") var title: String? + } + + func testHelp() { + AssertHelp(for: A.self, equals: """ + USAGE: a --name [--title ] + + OPTIONS: + --name <name> Your name + --title <title> Your title + -h, --help Show help information. + + """) + } + + struct B: ParsableArguments { + @Option(help: "Your name") var name: String + @Option(help: "Your title") var title: String? + + @Argument(help: .hidden) var hiddenName: String? + @Option(help: .hidden) var hiddenTitle: String? + @Flag(help: .hidden) var hiddenFlag: Bool + } + + func testHelpWithHidden() { + AssertHelp(for: B.self, equals: """ + USAGE: b --name <name> [--title <title>] + + OPTIONS: + --name <name> Your name + --title <title> Your title + -h, --help Show help information. + + """) + } + + struct C: ParsableArguments { + @Option(help: ArgumentHelp("Your name.", + discussion: "Your name is used to greet you and say hello.")) + var name: String + } + + func testHelpWithDiscussion() { + AssertHelp(for: C.self, equals: """ + USAGE: c --name <name> + + OPTIONS: + --name <name> Your name. + Your name is used to greet you and say hello. + -h, --help Show help information. + + """) + } +} diff --git a/Tests/UnitTests/NameSpecificationTests.swift b/Tests/UnitTests/NameSpecificationTests.swift new file mode 100644 index 000000000..db2ee1a79 --- /dev/null +++ b/Tests/UnitTests/NameSpecificationTests.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------*- 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 +@testable import ArgumentParser + +final class NameSpecificationTests: XCTestCase { +} + +extension NameSpecificationTests { + func testFlagNames_withNoPrefix() { + let key = InputKey(rawValue: "index") + + XCTAssertEqual(FlagInversion.prefixedNo.enableDisableNamePair(for: key, name: .customLong("foo")).1, [.long("no-foo")]) + XCTAssertEqual(FlagInversion.prefixedNo.enableDisableNamePair(for: key, name: .customLong("foo-bar-baz")).1, [.long("no-foo-bar-baz")]) + XCTAssertEqual(FlagInversion.prefixedNo.enableDisableNamePair(for: key, name: .customLong("foo_bar_baz")).1, [.long("no_foo_bar_baz")]) + XCTAssertEqual(FlagInversion.prefixedNo.enableDisableNamePair(for: key, name: .customLong("fooBarBaz")).1, [.long("noFooBarBaz")]) + } + + func testFlagNames_withEnableDisablePrefix() { + let key = InputKey(rawValue: "index") + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .long).0, [.long("enable-index")]) + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .long).1, [.long("disable-index")]) + + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .customLong("foo")).0, [.long("enable-foo")]) + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .customLong("foo")).1, [.long("disable-foo")]) + + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .customLong("foo-bar-baz")).0, [.long("enable-foo-bar-baz")]) + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .customLong("foo-bar-baz")).1, [.long("disable-foo-bar-baz")]) + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .customLong("foo_bar_baz")).0, [.long("enable_foo_bar_baz")]) + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .customLong("foo_bar_baz")).1, [.long("disable_foo_bar_baz")]) + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .customLong("fooBarBaz")).0, [.long("enableFooBarBaz")]) + XCTAssertEqual(FlagInversion.prefixedEnableDisable.enableDisableNamePair(for: key, name: .customLong("fooBarBaz")).1, [.long("disableFooBarBaz")]) + } +} + +fileprivate func Assert(nameSpecification: NameSpecification, key: String, makeNames expected: [Name], file: StaticString = #file, line: UInt = #line) { + let names = nameSpecification.makeNames(InputKey(rawValue: key)) + Assert(names: names, expected: expected, file: file, line: line) +} + +fileprivate func Assert<N>(names: [N], expected: [N], file: StaticString = #file, line: UInt = #line) where N: Equatable { + names.forEach { + XCTAssert(expected.contains($0), "Unexpected name '\($0)'.", file: file, line: line) + } + expected.forEach { + XCTAssert(names.contains($0), "Missing name '\($0)'.", file: file, line: line) + } +} + +extension NameSpecificationTests { + func testMakeNames_short() { + Assert(nameSpecification: .short, key: "foo", makeNames: [.short("f")]) + } + + func testMakeNames_Long() { + Assert(nameSpecification: .long, key: "fooBarBaz", makeNames: [.long("foo-bar-baz")]) + Assert(nameSpecification: .long, key: "fooURLForBarBaz", makeNames: [.long("foo-url-for-bar-baz")]) + } + + func testMakeNames_customLong() { + Assert(nameSpecification: .customLong("bar"), key: "foo", makeNames: [.long("bar")]) + } + + func testMakeNames_customShort() { + Assert(nameSpecification: .customShort("v"), key: "foo", makeNames: [.short("v")]) + } + + func testMakeNames_customLongWithSingleDash() { + Assert(nameSpecification: .customLong("baz", withSingleDash: true), key: "foo", makeNames: [.longWithSingleDash("baz")]) + } +} diff --git a/Tests/UnitTests/SplitArgumentTests.swift b/Tests/UnitTests/SplitArgumentTests.swift new file mode 100644 index 000000000..9868f932c --- /dev/null +++ b/Tests/UnitTests/SplitArgumentTests.swift @@ -0,0 +1,574 @@ +//===----------------------------------------------------------*- 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 +@testable import ArgumentParser +import TestHelpers + +extension SplitArguments.InputIndex: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self.init(rawValue: value) + } +} + +private func AssertIndexEqual(_ sut: SplitArguments, at index: Int, inputIndex: Int, subIndex: SplitArguments.SubIndex, file: StaticString = #file, line: UInt = #line) { + guard index < sut.elements.count else { + XCTFail("Element index \(index) is out of range. sur only has \(sut.elements.count) elements.", file: file, line: line) + return + } + let splitIndex = sut.elements[index].0 + let expected = SplitArguments.Index(inputIndex: SplitArguments.InputIndex(rawValue: inputIndex), subIndex: subIndex) + if splitIndex.inputIndex != expected.inputIndex { + XCTFail("inputIndex does not match: \(splitIndex.inputIndex.rawValue) != \(expected.inputIndex.rawValue)", file: file, line: line) + } + if splitIndex.subIndex != expected.subIndex { + XCTFail("inputIndex does not match: \(splitIndex.subIndex) != \(expected.subIndex)", file: file, line: line) + } +} + +private func AssertElementEqual(_ sut: SplitArguments, at index: Int, _ element: SplitArguments.Element, file: StaticString = #file, line: UInt = #line) { + guard index < sut.elements.count else { + XCTFail("Element index \(index) is out of range. sur only has \(sut.elements.count) elements.", file: file, line: line) + return + } + XCTAssertEqual(sut.elements[index].1, element, file: file, line: line) +} + +final class SplitArgumentTests: XCTestCase { + func testEmpty() throws { + let sut = try SplitArguments(arguments: []) + XCTAssertEqual(sut.elements.count, 0) + XCTAssertEqual(sut.originalInput.count, 0) + } + + func testSingleValue() throws { + let sut = try SplitArguments(arguments: ["abc"]) + + XCTAssertEqual(sut.elements.count, 1) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .value("abc")) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["abc"]) + } + + func testSingleLongOption() throws { + let sut = try SplitArguments(arguments: ["--abc"]) + + XCTAssertEqual(sut.elements.count, 1) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("abc")))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["--abc"]) + } + + func testSingleShortOption() throws { + let sut = try SplitArguments(arguments: ["-a"]) + + XCTAssertEqual(sut.elements.count, 1) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.short("a")))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["-a"]) + } + + func testSingleLongOptionWithValue() throws { + let sut = try SplitArguments(arguments: ["--abc=def"]) + + XCTAssertEqual(sut.elements.count, 1) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.nameWithValue(.long("abc"), "def"))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["--abc=def"]) + } + + func testMultipleShortOptionsCombined() throws { + let sut = try SplitArguments(arguments: ["-abc"]) + + XCTAssertEqual(sut.elements.count, 4) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("abc")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sut, at: 1, .option(.name(.short("a")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sut, at: 2, .option(.name(.short("b")))) + + AssertIndexEqual(sut, at: 3, inputIndex: 0, subIndex: .sub(2)) + AssertElementEqual(sut, at: 3, .option(.name(.short("c")))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["-abc"]) + } + + func testSingleLongOptionWithValueAndSingleDash() throws { + let sut = try SplitArguments(arguments: ["-abc=def"]) + + XCTAssertEqual(sut.elements.count, 1) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.nameWithValue(.longWithSingleDash("abc"), "def"))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["-abc=def"]) + } +} + +extension SplitArgumentTests { + func testMultipleValues() throws { + let sut = try SplitArguments(arguments: ["abc", "x", "1234"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .value("abc")) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .value("x")) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .value("1234")) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["abc", "x", "1234"]) + } + + func testMultipleLongOptions() throws { + let sut = try SplitArguments(arguments: ["--d", "--1", "--abc-def"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("d")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .option(.name(.long("1")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .option(.name(.long("abc-def")))) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["--d", "--1", "--abc-def"]) + } + + func testMultipleShortOptions() throws { + let sut = try SplitArguments(arguments: ["-x", "-y", "-z"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.short("x")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .option(.name(.short("y")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .option(.name(.short("z")))) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["-x", "-y", "-z"]) + } + + func testMultipleShortOptionsCombined_2() throws { + let sut = try SplitArguments(arguments: ["-bc", "-fv", "-a"]) + + XCTAssertEqual(sut.elements.count, 7) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("bc")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sut, at: 1, .option(.name(.short("b")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sut, at: 2, .option(.name(.short("c")))) + + AssertIndexEqual(sut, at: 3, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 3, .option(.name(.longWithSingleDash("fv")))) + + AssertIndexEqual(sut, at: 4, inputIndex: 1, subIndex: .sub(0)) + AssertElementEqual(sut, at: 4, .option(.name(.short("f")))) + + AssertIndexEqual(sut, at: 5, inputIndex: 1, subIndex: .sub(1)) + AssertElementEqual(sut, at: 5, .option(.name(.short("v")))) + + AssertIndexEqual(sut, at: 6, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 6, .option(.name(.short("a")))) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["-bc", "-fv", "-a"]) + } +} + +extension SplitArgumentTests { + func testMixed_1() throws { + let sut = try SplitArguments(arguments: ["-x", "abc", "--foo", "1234", "-zz"]) + + XCTAssertEqual(sut.elements.count, 7) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.short("x")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .value("abc")) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 3, inputIndex: 3, subIndex: .complete) + AssertElementEqual(sut, at: 3, .value("1234")) + + AssertIndexEqual(sut, at: 4, inputIndex: 4, subIndex: .complete) + AssertElementEqual(sut, at: 4, .option(.name(.longWithSingleDash("zz")))) + + AssertIndexEqual(sut, at: 5, inputIndex: 4, subIndex: .sub(0)) + AssertElementEqual(sut, at: 5, .option(.name(.short("z")))) + + AssertIndexEqual(sut, at: 6, inputIndex: 4, subIndex: .sub(1)) + AssertElementEqual(sut, at: 6, .option(.name(.short("z")))) + + XCTAssertEqual(sut.originalInput.count, 5) + XCTAssertEqual(sut.originalInput, ["-x", "abc", "--foo", "1234", "-zz"]) + } + + func testMixed_2() throws { + let sut = try SplitArguments(arguments: ["1234", "-zz", "abc", "-x", "--foo"]) + + XCTAssertEqual(sut.elements.count, 7) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .value("1234")) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .option(.name(.longWithSingleDash("zz")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 1, subIndex: .sub(0)) + AssertElementEqual(sut, at: 2, .option(.name(.short("z")))) + + AssertIndexEqual(sut, at: 3, inputIndex: 1, subIndex: .sub(1)) + AssertElementEqual(sut, at: 3, .option(.name(.short("z")))) + + AssertIndexEqual(sut, at: 4, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 4, .value("abc")) + + AssertIndexEqual(sut, at: 5, inputIndex: 3, subIndex: .complete) + AssertElementEqual(sut, at: 5, .option(.name(.short("x")))) + + AssertIndexEqual(sut, at: 6, inputIndex: 4, subIndex: .complete) + AssertElementEqual(sut, at: 6, .option(.name(.long("foo")))) + + XCTAssertEqual(sut.originalInput.count, 5) + XCTAssertEqual(sut.originalInput, ["1234", "-zz", "abc", "-x", "--foo"]) + } + + func testTerminator_1() throws { + let sut = try SplitArguments(arguments: ["--foo", "--", "--bar"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .terminator) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .value("--bar")) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["--foo", "--", "--bar"]) + } + + func testTerminator_2() throws { + let sut = try SplitArguments(arguments: ["--foo", "--", "bar"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .terminator) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .value("bar")) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["--foo", "--", "bar"]) + } + + func testTerminator_3() throws { + let sut = try SplitArguments(arguments: ["--foo", "--", "--bar=baz"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .terminator) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .value("--bar=baz")) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["--foo", "--", "--bar=baz"]) + } + + func testTerminatorAtTheEnd() throws { + let sut = try SplitArguments(arguments: ["--foo", "--"]) + + XCTAssertEqual(sut.elements.count, 2) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .terminator) + + XCTAssertEqual(sut.originalInput.count, 2) + XCTAssertEqual(sut.originalInput, ["--foo", "--"]) + } + + func testTerminatorAtTheBeginning() throws { + let sut = try SplitArguments(arguments: ["--", "--foo"]) + + XCTAssertEqual(sut.elements.count, 2) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .terminator) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .value("--foo")) + + XCTAssertEqual(sut.originalInput.count, 2) + XCTAssertEqual(sut.originalInput, ["--", "--foo"]) + } +} + +// MARK: - Removing Entries + +extension SplitArgumentTests { + func testRemovingValuesForLongNames() throws { + var sut = try SplitArguments(arguments: ["--foo", "--bar"]) + XCTAssertEqual(sut.elements.count, 2) + sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 1) + sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 0) + } + + func testRemovingValuesForLongNamesWithValue() throws { + var sut = try SplitArguments(arguments: ["--foo=A", "--bar=B"]) + XCTAssertEqual(sut.elements.count, 2) + sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 1) + sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 0) + } + + func testRemovingValuesForShortNames() throws { + var sut = try SplitArguments(arguments: ["-f", "-b"]) + XCTAssertEqual(sut.elements.count, 2) + sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 1) + sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 0) + } + + func testRemovingValuesForCombinedShortNames() throws { + let sut = try SplitArguments(arguments: ["-fb"]) + + XCTAssertEqual(sut.elements.count, 3) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("fb")))) + AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sut, at: 1, .option(.name(.short("f")))) + AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sut, at: 2, .option(.name(.short("b")))) + + do { + var sutB = sut + sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) + + XCTAssertEqual(sutB.elements.count, 0) + } + do { + var sutB = sut + sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .sub(0))) + + XCTAssertEqual(sutB.elements.count, 1) + AssertIndexEqual(sutB, at: 0, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sutB, at: 0, .option(.name(.short("b")))) + } + do { + var sutB = sut + sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .sub(1))) + + XCTAssertEqual(sutB.elements.count, 1) + AssertIndexEqual(sutB, at: 0, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sutB, at: 0, .option(.name(.short("f")))) + } + } +} + +// MARK: - Pop & Peek + +extension SplitArgumentTests { + func testPopNext() throws { + var sut = try SplitArguments(arguments: ["--foo", "bar"]) + + let a = try XCTUnwrap(sut.popNext()) + XCTAssertEqual(a.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) + XCTAssertEqual(a.1, .option(.name(.long("foo")))) + + let b = try XCTUnwrap(sut.popNext()) + XCTAssertEqual(b.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(b.1, .value("bar")) + + XCTAssertNil(sut.popNext()) + } + + func testPeekNext() throws { + let sut = try SplitArguments(arguments: ["--foo", "bar"]) + + let a = try XCTUnwrap(sut.peekNext()) + XCTAssertEqual(a.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) + XCTAssertEqual(a.1, .option(.name(.long("foo")))) + + let b = try XCTUnwrap(sut.peekNext()) + XCTAssertEqual(b.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) + XCTAssertEqual(b.1, .option(.name(.long("foo")))) + } + + func testPeekNextWhenEmpty() throws { + let sut = try SplitArguments(arguments: []) + XCTAssertNil(sut.peekNext()) + } + + func testPopNextElementIfValueAfter_1() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let value = try XCTUnwrap(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(value.1, "bar") + } + + func testPopNextElementIfValueAfter_2() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let value = try XCTUnwrap(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) + XCTAssertEqual(value.1, "foo") + } + + func testPopNextElementIfValueAfter_3() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + XCTAssertNil(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete)))) + } + + func testPopNextValueAfter_1() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let valueA = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(valueA.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(valueA.1, "bar") + + let valueB = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(valueB.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) + XCTAssertEqual(valueB.1, "foo") + } + + func testPopNextValueAfter_2() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let value = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) + XCTAssertEqual(value.1, "foo") + + XCTAssertNil(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) + } + + func testPopNextValueAfter_3() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + XCTAssertNil(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete)))) + } + + func testPopNextElementAsValueAfter_1() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let valueA = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(valueA.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(valueA.1, "bar") + + let valueB = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(valueB.0, .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete))) + XCTAssertEqual(valueB.1, "--foo") + } + + func testPopNextElementAsValueAfter_2() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + XCTAssertNil(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete)))) + } + + func testPopNextElementAsValueAfter_3() throws { + var sut = try SplitArguments(arguments: ["--bar", "-bar"]) + + let value = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(value.1, "-bar") + } + + func testPopNextElementIfValue() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + _ = try XCTUnwrap(sut.popNext()) + + let value = try XCTUnwrap(sut.popNextElementIfValue()) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(value.1, "bar") + + XCTAssertNil(sut.popNextElementIfValue()) + } + + func testPopNextValue() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let valueA = try XCTUnwrap(sut.popNextValue()) + XCTAssertEqual(valueA.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(valueA.1, "bar") + + let valueB = try XCTUnwrap(sut.popNextValue()) + XCTAssertEqual(valueB.0, SplitArguments.Index(inputIndex: 3, subIndex: .complete)) + XCTAssertEqual(valueB.1, "foo") + + XCTAssertNil(sut.popNextElementIfValue()) + } + + func testPeekNextValue() throws { + let sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let valueA = try XCTUnwrap(sut.peekNextValue()) + XCTAssertEqual(valueA.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(valueA.1, "bar") + + let valueB = try XCTUnwrap(sut.peekNextValue()) + XCTAssertEqual(valueB.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(valueB.1, "bar") + } +} diff --git a/Tests/UnitTests/StringWrappingTests.swift b/Tests/UnitTests/StringWrappingTests.swift new file mode 100644 index 000000000..2b6f0e305 --- /dev/null +++ b/Tests/UnitTests/StringWrappingTests.swift @@ -0,0 +1,133 @@ +//===----------------------------------------------------------*- 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 +@testable import ArgumentParser + +final class StringWrappingTests: XCTestCase {} + +let shortSample = """ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lectus proin nibh nisl condimentum id. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Massa id neque aliquam vestibulum morbi blandit cursus risus at. Iaculis urna id volutpat lacus laoreet. Netus et malesuada fames ac turpis egestas sed tempus urna. +""" + +let longSample = """ +Pretium vulputate sapien nec sagittis aliquam malesuada bibendum. Ut diam quam nulla porttitor. + +Egestas egestas fringilla phasellus faucibus. Amet dictum sit amet justo donec enim diam. Consectetur adipiscing elit duis tristique. + +Enim lobortis scelerisque fermentum dui. + +Et leo duis ut diam quam. + +Integer eget aliquet nibh praesent tristique magna sit. Faucibus turpis in eu mi bibendum neque egestas congue quisque. Risus nec feugiat in fermentum posuere urna nec tincidunt. +""" + +// MARK: - + +extension StringWrappingTests { + func testShort() { + XCTAssertEqual(shortSample.wrapped(to: 40), """ + Lorem ipsum dolor sit amet, consectetur + adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna + aliqua. Lectus proin nibh nisl + condimentum id. Semper feugiat nibh sed + pulvinar proin gravida hendrerit. Massa + id neque aliquam vestibulum morbi + blandit cursus risus at. Iaculis urna + id volutpat lacus laoreet. Netus et + malesuada fames ac turpis egestas sed + tempus urna. + """) + + XCTAssertEqual(shortSample.wrapped(to: 80), """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Lectus proin nibh nisl condimentum + id. Semper feugiat nibh sed pulvinar proin gravida hendrerit. Massa id neque + aliquam vestibulum morbi blandit cursus risus at. Iaculis urna id volutpat + lacus laoreet. Netus et malesuada fames ac turpis egestas sed tempus urna. + """) + } + + func testShortWithIndent() { + XCTAssertEqual(shortSample.wrapped(to: 60, wrappingIndent: 10), """ + Lorem ipsum dolor sit amet, consectetur + adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Lectus proin + nibh nisl condimentum id. Semper feugiat nibh sed + pulvinar proin gravida hendrerit. Massa id neque + aliquam vestibulum morbi blandit cursus risus at. + Iaculis urna id volutpat lacus laoreet. Netus et + malesuada fames ac turpis egestas sed tempus urna. + """) + } + + func testLong() { + XCTAssertEqual(longSample.wrapped(to: 40), """ + Pretium vulputate sapien nec sagittis + aliquam malesuada bibendum. Ut diam + quam nulla porttitor. + + Egestas egestas fringilla phasellus + faucibus. Amet dictum sit amet justo + donec enim diam. Consectetur adipiscing + elit duis tristique. + + Enim lobortis scelerisque fermentum + dui. + + Et leo duis ut diam quam. + + Integer eget aliquet nibh praesent + tristique magna sit. Faucibus turpis in + eu mi bibendum neque egestas congue + quisque. Risus nec feugiat in + fermentum posuere urna nec tincidunt. + """) + + XCTAssertEqual(longSample.wrapped(to: 80), """ + Pretium vulputate sapien nec sagittis aliquam malesuada bibendum. Ut diam quam + nulla porttitor. + + Egestas egestas fringilla phasellus faucibus. Amet dictum sit amet justo donec + enim diam. Consectetur adipiscing elit duis tristique. + + Enim lobortis scelerisque fermentum dui. + + Et leo duis ut diam quam. + + Integer eget aliquet nibh praesent tristique magna sit. Faucibus turpis in eu + mi bibendum neque egestas congue quisque. Risus nec feugiat in fermentum + posuere urna nec tincidunt. + """) + } + + func testLongWithIndent() { + XCTAssertEqual(longSample.wrapped(to: 60, wrappingIndent: 10), """ + Pretium vulputate sapien nec sagittis aliquam + malesuada bibendum. Ut diam quam nulla porttitor. + + Egestas egestas fringilla phasellus faucibus. + Amet dictum sit amet justo donec enim diam. + Consectetur adipiscing elit duis tristique. + + Enim lobortis scelerisque fermentum dui. + + Et leo duis ut diam quam. + + Integer eget aliquet nibh praesent tristique + magna sit. Faucibus turpis in eu mi bibendum + neque egestas congue quisque. Risus nec + feugiat in fermentum posuere urna nec tincidunt. + """) + + } +} diff --git a/Tests/UnitTests/TreeTests.swift b/Tests/UnitTests/TreeTests.swift new file mode 100644 index 000000000..bb14d8c07 --- /dev/null +++ b/Tests/UnitTests/TreeTests.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------*- 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 +@testable import ArgumentParser + +final class TreeTests: XCTestCase { +} + +// MARK: - + +let tree: Tree<Int> = { + let tree = Tree(1) + for x in 11...13 { + let node = Tree(x) + tree.addChild(node) + for y in 1...3 { + let subnode = Tree(x * 10 + y) + node.addChild(subnode) + } + } + return tree +}() + +extension TreeTests { + func testHierarchy() { + XCTAssertEqual(tree.element, 1) + XCTAssertEqual(tree.children.map { $0.element }, [11, 12, 13]) + XCTAssertEqual( + tree.children.flatMap { $0.children.map { $0.element } }, + [111, 112, 113, 121, 122, 123, 131, 132, 133]) + } + + func testSearch() { + XCTAssertEqual( + tree.path(toFirstWhere: { $0 == 1 }).map { $0.element }, + [1]) + XCTAssertEqual( + tree.path(toFirstWhere: { $0 == 13 }).map { $0.element }, + [1, 13]) + XCTAssertEqual( + tree.path(toFirstWhere: { $0 == 133 }).map { $0.element }, + [1, 13, 133]) + + XCTAssertTrue(tree.path(toFirstWhere: { $0 < 0 }).isEmpty) + } +} diff --git a/Tests/UnitTests/UsageGenerationTests.swift b/Tests/UnitTests/UsageGenerationTests.swift new file mode 100644 index 000000000..821961df1 --- /dev/null +++ b/Tests/UnitTests/UsageGenerationTests.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------*- 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 +@testable import ArgumentParser + +final class UsageGenerationTests: XCTestCase { +} + +// MARK: - + +extension UsageGenerationTests { + func testNameSynopsis() { + XCTAssertEqual(Name.long("foo").synopsisString, "--foo") + XCTAssertEqual(Name.short("f").synopsisString, "-f") + XCTAssertEqual(Name.longWithSingleDash("foo").synopsisString, "-foo") + } +} + +extension UsageGenerationTests { + struct A: ParsableArguments { + @Option() var firstName: String + @Option() var title: String + } + + func testSynopsis() { + let help = UsageGenerator(toolName: "bar", parsable: A()) + XCTAssertEqual(help.synopsis, "bar --first-name <first-name> --title <title>") + } + + struct B: ParsableArguments { + @Option() var firstName: String? + @Option() var title: String? + } + + func testSynopsisWithOptional() { + let help = UsageGenerator(toolName: "bar", parsable: B()) + XCTAssertEqual(help.synopsis, "bar [--first-name <first-name>] [--title <title>]") + } + + struct C: ParsableArguments { + @Flag() var log: Bool + @Flag() var verbose: Int + } + + func testFlagSynopsis() { + let help = UsageGenerator(toolName: "bar", parsable: C()) + XCTAssertEqual(help.synopsis, "bar [--log] [--verbose ...]") + } + + struct D: ParsableArguments { + @Argument() var firstName: String + @Argument() var title: String? + } + + func testPositionalSynopsis() { + let help = UsageGenerator(toolName: "bar", parsable: D()) + XCTAssertEqual(help.synopsis, "bar <first-name> [<title>]") + } + + struct E: ParsableArguments { + @Option(default: "no-name") + var name: String + + @Option(default: 0) + var count: Int + } + + func testSynopsisWithDefaults() { + let help = UsageGenerator(toolName: "bar", parsable: E()) + XCTAssertEqual(help.synopsis, "bar [--name <name>] [--count <count>]") + } + + struct F: ParsableArguments { + @Option() var name: [String] + @Argument() var nameCounts: [Int] + } + + func testSynopsisWithRepeats() { + let help = UsageGenerator(toolName: "bar", parsable: F()) + XCTAssertEqual(help.synopsis, "bar [--name <name> ...] [<name-counts> ...]") + } + + struct G: ParsableArguments { + @Option(help: ArgumentHelp(valueName: "path")) + var filePath: String? + + @Argument(help: ArgumentHelp(valueName: "user-home-path")) + var homePath: String + } + + func testSynopsisWithCustomization() { + let help = UsageGenerator(toolName: "bar", parsable: G()) + XCTAssertEqual(help.synopsis, "bar [--file-path <path>] <user-home-path>") + } + + struct H: ParsableArguments { + @Option(help: .hidden) var firstName: String? + @Argument(help: .hidden) var title: String? + } + + func testSynopsisWithHidden() { + let help = UsageGenerator(toolName: "bar", parsable: H()) + XCTAssertEqual(help.synopsis, "bar") + } +}