From 679bf1ae3f902b0c2fb432d177497ab2faeacd2b Mon Sep 17 00:00:00 2001 From: ayadav4 Date: Thu, 22 Jun 2023 14:24:25 +0530 Subject: [PATCH] intialcommit --- CHANGELOG.md | 554 +++++++++++++ CMakeLists.txt | 37 + CODE_OF_CONDUCT.md | 55 ++ CONTRIBUTING.md | 10 + Documentation/01 Getting Started.md | 286 +++++++ .../02 Arguments, Options, and Flags.md | 468 +++++++++++ Documentation/03 Commands and Subcommands.md | 178 ++++ Documentation/04 Customizing Help.md | 209 +++++ Documentation/05 Validation and Errors.md | 157 ++++ .../06 Manual Parsing and Testing.md | 112 +++ Documentation/07 Completion Scripts.md | 118 +++ Examples/CMakeLists.txt | 16 + Examples/math/main.swift | 246 ++++++ Examples/repeat/main.swift | 37 + Examples/roll/SplitMix64.swift | 26 + Examples/roll/main.swift | 48 ++ LICENSE.txt | 211 +++++ Package.swift | 67 ++ README.md | 121 ++- Scripts/environment.sh | 37 + Sources/ArgumentParser/CMakeLists.txt | 52 ++ .../BashCompletionsGenerator.swift | 216 +++++ .../Completions/CompletionsGenerator.swift | 127 +++ .../FishCompletionsGenerator.swift | 153 ++++ .../Completions/ZshCompletionsGenerator.swift | 199 +++++ .../Parsable Properties/Argument.swift | 479 +++++++++++ .../Parsable Properties/ArgumentHelp.swift | 54 ++ .../Parsable Properties/CompletionKind.swift | 65 ++ .../Parsable Properties/Errors.swift | 115 +++ .../Parsable Properties/Flag.swift | 579 +++++++++++++ .../NameSpecification.swift | 175 ++++ .../Parsable Properties/Option.swift | 604 ++++++++++++++ .../Parsable Properties/OptionGroup.swift | 103 +++ .../Parsable Types/CommandConfiguration.swift | 114 +++ .../Parsable Types/EnumerableFlag.swift | 79 ++ .../ExpressibleByArgument.swift | 107 +++ .../Parsable Types/ParsableArguments.swift | 299 +++++++ .../ParsableArgumentsValidation.swift | 293 +++++++ .../Parsable Types/ParsableCommand.swift | 107 +++ .../Parsing/ArgumentDecoder.swift | 278 +++++++ .../Parsing/ArgumentDefinition.swift | 258 ++++++ .../ArgumentParser/Parsing/ArgumentSet.swift | 449 ++++++++++ .../Parsing/CommandParser.swift | 389 +++++++++ .../ArgumentParser/Parsing/InputOrigin.swift | 123 +++ Sources/ArgumentParser/Parsing/Name.swift | 91 ++ Sources/ArgumentParser/Parsing/Parsed.swift | 83 ++ .../ArgumentParser/Parsing/ParsedValues.swift | 91 ++ .../ArgumentParser/Parsing/ParserError.swift | 47 ++ .../Parsing/SplitArguments.swift | 650 +++++++++++++++ .../ArgumentParser/Usage/HelpCommand.swift | 48 ++ .../ArgumentParser/Usage/HelpGenerator.swift | 343 ++++++++ .../ArgumentParser/Usage/MessageInfo.swift | 159 ++++ .../ArgumentParser/Usage/UsageGenerator.swift | 428 ++++++++++ .../Utilities/SequenceExtensions.swift | 38 + .../Utilities/StringExtensions.swift | 174 ++++ Sources/ArgumentParser/Utilities/Tree.swift | 104 +++ .../ArgumentParserTestHelpers/CMakeLists.txt | 9 + .../StringHelpers.swift | 28 + .../TestHelpers.swift | 201 +++++ Sources/CMakeLists.txt | 4 + .../CMakeLists.txt | 20 + .../CustomParsingEndToEndTests.swift | 168 ++++ .../DefaultSubcommandEndToEndTests.swift | 71 ++ .../DefaultsEndToEndTests.swift | 779 ++++++++++++++++++ .../EnumEndToEndTests.swift | 96 +++ .../EqualsEndToEndTests.swift | 79 ++ .../FlagsEndToEndTests.swift | 332 ++++++++ .../JoinedEndToEndTests.swift | 193 +++++ .../LongNameWithShortDashEndToEndTests.swift | 109 +++ .../NestedCommandEndToEndTests.swift | 192 +++++ .../OptionGroupEndToEndTests.swift | 110 +++ .../OptionalEndToEndTests.swift | 208 +++++ .../PositionalEndToEndTests.swift | 231 ++++++ .../RawRepresentableEndToEndTests.swift | 48 ++ .../RepeatingEndToEndTests.swift | 433 ++++++++++ .../ShortNameEndToEndTests.swift | 139 ++++ .../SimpleEndToEndTests.swift | 117 +++ .../SingleValueParsingStrategyTests.swift | 85 ++ .../SourceCompatEndToEndTests.swift | 210 +++++ .../SubcommandEndToEndTests.swift | 263 ++++++ .../TransformEndToEndTests.swift | 163 ++++ .../UnparsedValuesEndToEndTest.swift | 258 ++++++ .../ValidationEndToEndTests.swift | 173 ++++ .../MathExampleTests.swift | 568 +++++++++++++ .../RepeatExampleTests.swift | 91 ++ .../RollDiceExampleTests.swift | 58 ++ .../CMakeLists.txt | 8 + .../HelpTests.swift | 279 +++++++ .../PackageManager/Clean.swift | 19 + .../PackageManager/Config.swift | 55 ++ .../PackageManager/Describe.swift | 28 + .../PackageManager/GenerateXcodeProject.swift | 45 + .../PackageManager/Options.swift | 99 +++ .../Tests.swift | 86 ++ Tests/ArgumentParserUnitTests/CMakeLists.txt | 13 + .../CompletionScriptTests.swift | 266 ++++++ .../ErrorMessageTests.swift | 192 +++++ .../ExitCodeTests.swift | 90 ++ .../HelpGenerationTests.swift | 508 ++++++++++++ .../InputOriginTests.swift | 33 + .../ArgumentParserUnitTests/MirrorTests.swift | 59 ++ .../NameSpecificationTests.swift | 80 ++ .../ParsableArgumentsValidationTests.swift | 464 +++++++++++ .../SequenceExtensionTests.swift | 35 + .../SplitArgumentTests.swift | 574 +++++++++++++ .../StringEditDistanceTests.swift | 27 + .../StringSnakeCaseTests.swift | 101 +++ .../StringWrappingTests.swift | 167 ++++ Tests/ArgumentParserUnitTests/TreeTests.swift | 72 ++ .../UsageGenerationTests.swift | 200 +++++ Tests/CMakeLists.txt | 2 + Tests/LinuxMain.swift | 19 + Tools/changelog-authors/main.swift | 143 ++++ cmake/modules/ArgumentParserConfig.cmake.in | 3 + cmake/modules/CMakeLists.txt | 8 + cmake/modules/SwiftSupport.cmake | 99 +++ swift-argument-parser | 0 swift-argument-parser.podspec | 16 + 118 files changed, 19890 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CMakeLists.txt 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 Documentation/07 Completion Scripts.md create mode 100644 Examples/CMakeLists.txt 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 100755 Scripts/environment.sh create mode 100644 Sources/ArgumentParser/CMakeLists.txt create mode 100644 Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift create mode 100644 Sources/ArgumentParser/Completions/CompletionsGenerator.swift create mode 100644 Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift create mode 100644 Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift 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/CompletionKind.swift create mode 100644 Sources/ArgumentParser/Parsable Properties/Errors.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 Types/CommandConfiguration.swift create mode 100644 Sources/ArgumentParser/Parsable Types/EnumerableFlag.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/ParsableArgumentsValidation.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/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/SequenceExtensions.swift create mode 100644 Sources/ArgumentParser/Utilities/StringExtensions.swift create mode 100644 Sources/ArgumentParser/Utilities/Tree.swift create mode 100644 Sources/ArgumentParserTestHelpers/CMakeLists.txt create mode 100644 Sources/ArgumentParserTestHelpers/StringHelpers.swift create mode 100644 Sources/ArgumentParserTestHelpers/TestHelpers.swift create mode 100644 Sources/CMakeLists.txt create mode 100644 Tests/ArgumentParserEndToEndTests/CMakeLists.txt create mode 100644 Tests/ArgumentParserEndToEndTests/CustomParsingEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/EnumEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/EqualsEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/LongNameWithShortDashEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/NestedCommandEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/PositionalEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/RawRepresentableEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/ShortNameEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/SimpleEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/SingleValueParsingStrategyTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift create mode 100644 Tests/ArgumentParserEndToEndTests/UnparsedValuesEndToEndTest.swift create mode 100644 Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift create mode 100644 Tests/ArgumentParserExampleTests/MathExampleTests.swift create mode 100644 Tests/ArgumentParserExampleTests/RepeatExampleTests.swift create mode 100644 Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift create mode 100644 Tests/ArgumentParserPackageManagerTests/CMakeLists.txt create mode 100644 Tests/ArgumentParserPackageManagerTests/HelpTests.swift create mode 100644 Tests/ArgumentParserPackageManagerTests/PackageManager/Clean.swift create mode 100644 Tests/ArgumentParserPackageManagerTests/PackageManager/Config.swift create mode 100644 Tests/ArgumentParserPackageManagerTests/PackageManager/Describe.swift create mode 100644 Tests/ArgumentParserPackageManagerTests/PackageManager/GenerateXcodeProject.swift create mode 100644 Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift create mode 100644 Tests/ArgumentParserPackageManagerTests/Tests.swift create mode 100644 Tests/ArgumentParserUnitTests/CMakeLists.txt create mode 100644 Tests/ArgumentParserUnitTests/CompletionScriptTests.swift create mode 100644 Tests/ArgumentParserUnitTests/ErrorMessageTests.swift create mode 100644 Tests/ArgumentParserUnitTests/ExitCodeTests.swift create mode 100644 Tests/ArgumentParserUnitTests/HelpGenerationTests.swift create mode 100644 Tests/ArgumentParserUnitTests/InputOriginTests.swift create mode 100644 Tests/ArgumentParserUnitTests/MirrorTests.swift create mode 100644 Tests/ArgumentParserUnitTests/NameSpecificationTests.swift create mode 100644 Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift create mode 100644 Tests/ArgumentParserUnitTests/SequenceExtensionTests.swift create mode 100644 Tests/ArgumentParserUnitTests/SplitArgumentTests.swift create mode 100644 Tests/ArgumentParserUnitTests/StringEditDistanceTests.swift create mode 100644 Tests/ArgumentParserUnitTests/StringSnakeCaseTests.swift create mode 100644 Tests/ArgumentParserUnitTests/StringWrappingTests.swift create mode 100644 Tests/ArgumentParserUnitTests/TreeTests.swift create mode 100644 Tests/ArgumentParserUnitTests/UsageGenerationTests.swift create mode 100644 Tests/CMakeLists.txt create mode 100644 Tests/LinuxMain.swift create mode 100644 Tools/changelog-authors/main.swift create mode 100644 cmake/modules/ArgumentParserConfig.cmake.in create mode 100644 cmake/modules/CMakeLists.txt create mode 100644 cmake/modules/SwiftSupport.cmake create mode 100644 swift-argument-parser create mode 100644 swift-argument-parser.podspec diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0a738d9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,554 @@ +# CHANGELOG + + + +This project follows semantic versioning. While still in major version `0`, +source-stability is only guaranteed within minor versions (e.g. between +`0.0.3` and `0.0.4`). If you want to guard against potentially source-breaking +package updates, you can specify your package dependency using +`.upToNextMinor(from: "0.4.0")` as the requirement. + +## [Unreleased] + +*No changes yet.* + +--- + +## [0.4.4] - 2021-07-30 + +### Fixes + +- Includes a workaround for a runtime crash with certain `OptionGroup` + configurations when a command is compiled in release mode. + +## [0.4.3] - 2021-04-28 + +### Additions + +- Experimental API for hiding `@OptionGroup`-declared properties from + the help screen. + +The 0.4.3 release includes a contribution from [miggs597]. Thank you! + +## [0.4.2] - 2021-04-21 + +### Fixes + +- Both parts of a flag with an inversion are now hidden when specified. +- Better support for building on OpenBSD. +- Optional unparsed values are now always properly decoded. ([#290]) +- Help information from super-commands is no longer unnecessarily injected + into subcommand help screens. + +The 0.4.2 release includes contributions from [3405691582], [kylemacomber], +[miggs597], [natecook1000], and [werm098]. Thank you! + +## [0.4.1] - 2021-03-08 + +### Additions + +- When a user provides an invalid value as an argument or option, the error + message now includes the help text for that argument. + +### Fixes + +- Zsh completion scripts for commands that include a hyphen no longer cause + errors. +- Optional unparsed values are now decoded correctly in `ParsableArguments` + types. + +The 0.4.1 release includes contributions from [adellibovi] and [natecook1000]. +Thank you! + +## [0.4.0] - 2021-03-04 + +### Additions + +- Short options can now support "joined option" syntax, which lets users specify + a value appended immediately after the option's short name. For example, in + addition to calling this `example` command with `-D debug` and `-D=debug`, + users can now write `-Ddebug` for the same parsed value. ([#240]) + + ```swift + @main + struct Example: ParsableCommand { + @Option(name: .customShort("D", allowingJoined: true)) + var debugValue: String + + func run() { + print(debugValue) + } + } + ``` + +### Changes + +- The `CommandConfiguration.helpNames` property is now optional, to allow the + overridden help flags of parent commands to flow down to their children. Most + existing code should not be affected, but if you've customized a command's + help flags you may see different behavior. ([#251]) +- The `errorCode` property is no longer used as a command's exit code when + `CustomNSError` types are thrown. ([#276]) + + *Migration:* Instead of throwing a `CustomNSError` type, print your error + manually and throw an `ExitCode` error to customize your command's exit code. + +### Removals + +- Old, deprecated property wrapper initializers have been removed. + +### Fixes + +- Validation errors now show the correct help flags when help flags have been + customized. +- Options, flags, and arguments that are marked as hidden from the help screen + are also suppressed from completion scripts. +- Non-parsed variable properties are now allowed in parsable types. +- Error messages produced when `NSError` types are thrown have been improved. +- The usage line for commands with a large number of options includes more + detail about required flags and positional arguments. +- Support for CMake builds on Apple Silicon is improved. + +The 0.4.0 release includes contributions from [CodaFi], [lorentey], +[natecook1000], [schlagelk], and [Zoha131]. Thank you! + +--- + +## [0.3.2] - 2021-01-15 + +### Fixes + +- Changes made to a command's properties in its `validate` method are now + persisted. +- The exit code defined by error types that conform to `CustomNSError` are now + honored. +- Improved error message when declaring a command type with an unadorned + mutable property. (See [#256] for more.) +- Migrated from `CRT` to `MSVCRT` for Windows platforms. +- Fixes and improvements for building with CMake for Windows and Apple Silicon. +- Documentation improvements. + +The 0.3.2 release includes contributions from [compnerd], [CypherPoet], +[damuellen], [drewmccormack], [elliottwilliams], [gmittert], [MaxDesiatov], +[natecook1000], [pegasuze], and [SergeyPetrachkov]. Thank you! + +## [0.3.1] - 2020-09-02 + +### Fixes + +- An option or flag can now declare a name with both single- and double- + dash prefixes, such as `-my-flag` and `--my-flag`. Specify both names in the + `name` parameter when declaring your property: + + ```swift + @Flag(name: [.long, .customLong("my-flag", withSingleDash: true)]) + var myFlag = false + ``` + +- Parsing performance improvements. + +## [0.3.0] - 2020-08-15 + +### Additions + +- Shell completions scripts are now available for Fish. + +### Changes + +- Array properties without a default value are now treated as required for the + user of a command-line tool. In previous versions of the library, these + properties defaulted to an empty array; a deprecation was introduced for this + behavior in version 0.2.0. + + *Migration:* Specify an empty array as the default value for properties that + should not require user input: + + ```swift + // old + @Option var names: [String] + // new + @Option var names: [String] = [] + ``` + +The 0.3.0 release includes contributions from [dduan], [MPLew-is], +[natecook1000], and [thomasvl]. Thank you! + +--- + +## [0.2.2] - 2020-08-05 + +### Fixes + +- Zsh completion scripts have improved documentation and better support + multi-word completion strings, escaped characters, non-standard executable + locations, and empty help strings. + +The 0.2.2 release includes contributions from [interstateone], +[miguelangel-dev], [natecook1000], [stuartcarnie], and [Wevah]. Thank you! + +## [0.2.1] - 2020-07-30 + +### Additions + +- You can now generate Bash and Zsh shell completion scripts for commands, + either by using the `--generate-completion-script` flag when running a + command, or by calling the static `completionScript(for:)` method on a root + `ParsableCommand` type. See the [guide to completion scripts][comp-guide] for + information on customizing and installing the completion script for your + command. + +### Fixes + +- Property wrappers without parameters can now be written without parentheses + — e.g. `@Flag var verbose = false`. +- When displaying default values for array properties, the help screen now + correctly uses the element type's `ExpressibleByArgument` conformance to + generate the description. +- Running a project that defines a command as its own subcommand now fails with + a useful error message. + +The 0.2.1 release includes contributions from [natecook1000], [NicFontana], +[schlagelk], [sharplet], and [Wevah]. Thank you! + +[comp-guide]: https://github.com/apple/swift-argument-parser/blob/main/Documentation/07%20Completion%20Scripts.md + +## [0.2.0] - 2020-06-23 + +### Additions + +- You can now specify default values for array properties of parsable types. + The default values are overridden if the user provides at least one value + as part of the command-line arguments. + +### Changes + +- This release of `swift-argument-parser` requires Swift 5.2. +- Default values for all properties are now written using default initialization + syntax, including some values that were previously implicit, such as empty + arrays and `false` for Boolean flags. + + *Migration:* Specify default values using typical Swift default value syntax + to remove the deprecation warnings: + + ```swift + // old + @Flag var verbose: Bool + // new + @Flag var verbose = false + ``` + + **_Important:_** There is a semantic change for flags with inversions that do + not have a default value. In previous releases, these flags had a default + value of `false`; starting in 0.2.0, these flags will have no default, and + will therefore be required by the user. Specify a default value of `false` to + retain the old behavior. + +### Fixes + +- Options with multiple names now consistently show the first-declared name + in usage and help screens. +- Default subcommands are indicated in the help screen. +- User errors with options are now shown before positional argument errors, + eliminating some false negative reports. +- CMake compatibility fixes. + +The 0.2.0 release includes contributions from [artemnovichkov], [compnerd], +[ibrahimoktay], [john-mueller], [MPLew-is], [natecook1000], and [owenv]. +Thank you! + +--- + +## [0.1.0] - 2020-06-03 + +### Additions + +- Error messages and help screens now include information about how to request + more help. +- CMake builds now support installation. + +### Changes + +- The `static func main()` method on `ParsableCommand` no longer returns + `Never`. This allows `ParsableCommand` types to be designated as the entry + point for a Swift executable by using the `@main` attribute. + + *Migration:* For most uses, this change is source compatible. If you have + used `main()` where a `() -> Never` function is explicitly required, you'll + need to change your usage or capture the method in another function. + +- `Optional` no longer conforms to `ExpressibleByArgument`, to avoid some + property declarations that don't make sense. + + *Migration:* This is source-compatible for all property declarations, with + deprecations for optional properties that define an explicit default. If + you're using optional values where an `ExpressibleByArgument` type is + expected, such as a generic function, you will need to change your usage + or provide an explicit override. + +- `ParsableCommand`'s `run()` method requirement is now a `mutating` method, + allowing mutations to a command's properties, such as sorting an array of + arguments, without additional copying. + + *Migration:* No changes are required for commands that are executed through + the `main()` method. If you manually parse a command and then call its + `run()` method, you may need to change the command from a constant to a + variable. + +### Removals + +- The `@Flag` initializers that were deprecated in version 0.0.6 are now + marked as unavailable. + +### Fixes + +- `@Option` properties of an optional type that use a `transform` closure now + correctly indicate their optionality in the usage string. +- Correct wrapping and indentation are maintained for abstracts and discussions + with short lines. +- Empty abstracts no longer add extra blank lines to the help screen. +- Help requests are still honored even when a parsed command fails validation. +- The `--` terminator isn't consumed when parsing a command, so that it can be + parsed as a value when a subcommand includes an `.unconditionalRemaining` + argument array. +- CMake builds work correctly again. + +The 0.1.0 release includes contributions from [aleksey-mashanov], [BradLarson], +[compnerd], [erica], [ibrahimoktay], and [natecook1000]. Thank you! + +--- + +## [0.0.6] - 2020-05-14 + +### Additions + +- Command definition validation now checks for name collisions between options + and flags. +- `ValidationError.message` is now publicly accessible. +- Added an `EnumerableFlag` protocol for `CaseIterable` types that are used to + provide the names for flags. When declaring conformance to `EnumerableFlag`, + you can override the name specification and help text for individual flags. + See [#65] for more detail. +- When a command that requires arguments is called with no arguments at all, + the error message includes the full help text instead of the short usage + string. This is intended to provide a better experience for first-time users. +- Added a `helpMessage()` method for generating the help text for a command + or subcommand. + +### Deprecations + +- `@Flag` properties that use `CaseIterable`/`String` types as their values + are deprecated, and the related `@Flag` initializers will be removed + in a future version. + + *Migration:* Add `EnumerableFlag` conformance to the type of these kinds of + `@Flag` properties. + +### Fixes + +- Errors thrown while parsing in a `transform` closure are printed correclty + instead of a general `Invalid state` error. +- Improvements to the guides and in the error message when attempting to access + a value from an argument/option/flag definition. +- Fixed issues in the CMake and Windows build configurations. +- You can now use an `=` to join a value with an option's short name when calling + a command. This previously only worked for long names. + +The 0.0.6 release includes contributions from [compnerd], [john-mueller], +[natecook1000], [owenv], [rjstelling], and [toddthomas]. Thank you! + +## [0.0.5] - 2020-04-15 + +### Additions + +- You can now specify a version string in a `ParsableCommand`'s configuration. + The generated tool will then automatically respond to a `--version` flag. +- Command definitions are now validated at runtime in debug mode, to check + issues that can't be detected during compilation. + +### Fixes + +- Deprecation warnings during compilation on Linux have been removed. +- The `validate()` method is now called on each command in the matched command + stack, instead of only the last command in the stack. + +The 0.0.5 release includes contributions from [kennyyork], [natecook1000], +[sgl0v], and [YuAo]. Thank you! + +## [0.0.4] - 2020-03-23 + +### Fixes + +- Removed usage of 5.2-only syntax. + +## [0.0.3] - 2020-03-22 + +### Additions + +- You can specify the `.unconditionalRemaining` parsing strategy for arrays of + positional arguments to accept dash-prefixed input, like + `example --one two -three`. +- You can now provide a default value for a positional argument. +- You can now customize the display of default values in the extended help for + an `ExpressibleByArgument` type. +- You can call the static `exitCode(for:)` method on any command to retrieve the + exit code for a given error. + +### Fixes + +- Supporting targets are now prefixed to prevent conflicts with other libraries. +- The extension providing `init?(argument:)` to `RawRepresentable` types is now + properly constrained. +- The parser no longer treats passing the same exclusive flag more than once as + an error. +- `ParsableArguments` types that are declared as `@OptionGroup` properties on + commands can now also be declared on subcommands. Previosuly, the parent + command's declaration would prevent subcommands from seeing the user-supplied + arguments. +- Default values are rendered correctly for properties with `Optional` types. +- The output of help requests is now printed during the "exit" phase of execution, + instead of during the "run" phase. +- Usage strings now correctly show that optional positional arguments aren't + required. +- Extended help now omits extra line breaks when displaying arguments or commands + with long names that don't provide help text. + +The 0.0.3 release includes contributions from [compnerd], [elliottwilliams], +[glessard], [griffin-stewie], [iainsmith], [Lantua], [miguelangel-dev], +[natecook1000], [sjavora], and [YuAo]. Thank you! + +## [0.0.2] - 2020-03-06 + +### Additions + +- The `EX_USAGE` exit code is now used for validation errors. +- The parser provides near-miss suggestions when a user provides an unknown + option. +- `ArgumentParser` now builds on Windows. +- You can throw an `ExitCode` error to exit without printing any output. +- You can now create optional Boolean flags with inversions that default to + `nil`: + ```swift + @Flag(inversion: .prefixedNo) var takeMyShot: Bool? + ``` +- You can now specify exclusivity for case-iterable flags and for Boolean flags + with inversions. + +### Fixes + +- Cleaned up a wide variety of documentation typos and shortcomings. +- Improved different kinds of error messages: + - Duplicate exclusive flags now show the duplicated arguments. + - Subcommand validation errors print the correct usage string. +- In the help screen: + - Removed the extra space before the default value for arguments without + descriptions. + - Removed the default value note when the default value is an empty string. + - Default values are now shown for Boolean options. + - Case-iterable flags are now grouped correctly. + - Case-iterable flags with default values now show the default value. + - Arguments from parent commands that are included via `@OptionGroup` in + subcommands are no longer duplicated. +- Case-iterable flags created with the `.chooseFirst` exclusivity parameter now + correctly ignore additional flags. + +The 0.0.2 release includes contributions from [AliSoftware], [buttaface], +[compnerd], [dduan], [glessard], [griffin-stewie], [IngmarStein], +[jonathanpenn], [klaaspieter], [natecook1000], [Sajjon], [sjavora], +[Wildchild9], and [zntfdr]. Thank you! + +## [0.0.1] - 2020-02-27 + +- `ArgumentParser` initial release. + +--- + +This changelog's format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + + + +[Unreleased]: https://github.com/apple/swift-argument-parser/compare/0.4.4...HEAD +[0.4.4]: https://github.com/apple/swift-argument-parser/compare/0.4.2...0.4.4 +[0.4.3]: https://github.com/apple/swift-argument-parser/compare/0.4.2...0.4.3 +[0.4.2]: https://github.com/apple/swift-argument-parser/compare/0.4.1...0.4.2 +[0.4.1]: https://github.com/apple/swift-argument-parser/compare/0.4.0...0.4.1 +[0.4.0]: https://github.com/apple/swift-argument-parser/compare/0.3.2...0.4.0 +[0.3.2]: https://github.com/apple/swift-argument-parser/compare/0.3.1...0.3.2 +[0.3.1]: https://github.com/apple/swift-argument-parser/compare/0.3.0...0.3.1 +[0.3.0]: https://github.com/apple/swift-argument-parser/compare/0.2.2...0.3.0 +[0.2.2]: https://github.com/apple/swift-argument-parser/compare/0.2.1...0.2.2 +[0.2.1]: https://github.com/apple/swift-argument-parser/compare/0.2.0...0.2.1 +[0.2.0]: https://github.com/apple/swift-argument-parser/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/apple/swift-argument-parser/compare/0.0.6...0.1.0 +[0.0.6]: https://github.com/apple/swift-argument-parser/compare/0.0.5...0.0.6 +[0.0.5]: https://github.com/apple/swift-argument-parser/compare/0.0.4...0.0.5 +[0.0.4]: https://github.com/apple/swift-argument-parser/compare/0.0.3...0.0.4 +[0.0.3]: https://github.com/apple/swift-argument-parser/compare/0.0.2...0.0.3 +[0.0.2]: https://github.com/apple/swift-argument-parser/compare/0.0.1...0.0.2 +[0.0.1]: https://github.com/apple/swift-argument-parser/releases/tag/0.0.1 + + + +[#65]: https://github.com/apple/swift-argument-parser/pull/65 +[#240]: https://github.com/apple/swift-argument-parser/pull/240 +[#251]: https://github.com/apple/swift-argument-parser/pull/251 +[#256]: https://github.com/apple/swift-argument-parser/pull/256 +[#276]: https://github.com/apple/swift-argument-parser/pull/276 +[#290]: https://github.com/apple/swift-argument-parser/pull/290 + + + +[3405691582]: https://github.com/apple/swift-argument-parser/commits?author=3405691582 +[adellibovi]: https://github.com/apple/swift-argument-parser/commits?author=adellibovi +[aleksey-mashanov]: https://github.com/apple/swift-argument-parser/commits?author=aleksey-mashanov +[AliSoftware]: https://github.com/apple/swift-argument-parser/commits?author=AliSoftware +[artemnovichkov]: https://github.com/apple/swift-argument-parser/commits?author=artemnovichkov +[BradLarson]: https://github.com/apple/swift-argument-parser/commits?author=BradLarson +[buttaface]: https://github.com/apple/swift-argument-parser/commits?author=buttaface +[CodaFi]: https://github.com/apple/swift-argument-parser/commits?author=CodaFi +[compnerd]: https://github.com/apple/swift-argument-parser/commits?author=compnerd +[CypherPoet]: https://github.com/apple/swift-argument-parser/commits?author=CypherPoet +[damuellen]: https://github.com/apple/swift-argument-parser/commits?author=damuellen +[dduan]: https://github.com/apple/swift-argument-parser/commits?author=dduan +[drewmccormack]: https://github.com/apple/swift-argument-parser/commits?author=drewmccormack +[elliottwilliams]: https://github.com/apple/swift-argument-parser/commits?author=elliottwilliams +[erica]: https://github.com/apple/swift-argument-parser/commits?author=erica +[glessard]: https://github.com/apple/swift-argument-parser/commits?author=glessard +[gmittert]: https://github.com/apple/swift-argument-parser/commits?author=gmittert +[griffin-stewie]: https://github.com/apple/swift-argument-parser/commits?author=griffin-stewie +[iainsmith]: https://github.com/apple/swift-argument-parser/commits?author=iainsmith +[ibrahimoktay]: https://github.com/apple/swift-argument-parser/commits?author=ibrahimoktay +[IngmarStein]: https://github.com/apple/swift-argument-parser/commits?author=IngmarStein +[interstateone]: https://github.com/apple/swift-argument-parser/commits?author=interstateone +[john-mueller]: https://github.com/apple/swift-argument-parser/commits?author=john-mueller +[jonathanpenn]: https://github.com/apple/swift-argument-parser/commits?author=jonathanpenn +[kennyyork]: https://github.com/apple/swift-argument-parser/commits?author=kennyyork +[klaaspieter]: https://github.com/apple/swift-argument-parser/commits?author=klaaspieter +[kylemacomber]: https://github.com/apple/swift-argument-parser/commits?author=kylemacomber +[Lantua]: https://github.com/apple/swift-argument-parser/commits?author=Lantua +[lorentey]: https://github.com/apple/swift-argument-parser/commits?author=lorentey +[MaxDesiatov]: https://github.com/apple/swift-argument-parser/commits?author=MaxDesiatov +[miggs597]: https://github.com/apple/swift-argument-parser/commits?author=miggs597 +[miguelangel-dev]: https://github.com/apple/swift-argument-parser/commits?author=miguelangel-dev +[MPLew-is]: https://github.com/apple/swift-argument-parser/commits?author=MPLew-is +[natecook1000]: https://github.com/apple/swift-argument-parser/commits?author=natecook1000 +[NicFontana]: https://github.com/apple/swift-argument-parser/commits?author=NicFontana +[owenv]: https://github.com/apple/swift-argument-parser/commits?author=owenv +[pegasuze]: https://github.com/apple/swift-argument-parser/commits?author=pegasuze +[rjstelling]: https://github.com/apple/swift-argument-parser/commits?author=rjstelling +[Sajjon]: https://github.com/apple/swift-argument-parser/commits?author=Sajjon +[schlagelk]: https://github.com/apple/swift-argument-parser/commits?author=schlagelk +[SergeyPetrachkov]: https://github.com/apple/swift-argument-parser/commits?author=SergeyPetrachkov +[sgl0v]: https://github.com/apple/swift-argument-parser/commits?author=sgl0v +[sharplet]: https://github.com/apple/swift-argument-parser/commits?author=sharplet +[sjavora]: https://github.com/apple/swift-argument-parser/commits?author=sjavora +[stuartcarnie]: https://github.com/apple/swift-argument-parser/commits?author=stuartcarnie +[thomasvl]: https://github.com/apple/swift-argument-parser/commits?author=thomasvl +[toddthomas]: https://github.com/apple/swift-argument-parser/commits?author=toddthomas +[werm098]: https://github.com/apple/swift-argument-parser/commits?author=werm098 +[Wevah]: https://github.com/apple/swift-argument-parser/commits?author=Wevah +[Wildchild9]: https://github.com/apple/swift-argument-parser/commits?author=Wildchild9 +[YuAo]: https://github.com/apple/swift-argument-parser/commits?author=YuAo +[zntfdr]: https://github.com/apple/swift-argument-parser/commits?author=zntfdr +[Zoha131]: https://github.com/apple/swift-argument-parser/commits?author=Zoha131 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..25282eb --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.15.1) + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) + +project(swift-argument-parser + LANGUAGES Swift) + +option(BUILD_EXAMPLES "Build Example Programs" TRUE) +option(BUILD_SHARED_LIBS "Build shared libraries by default" YES) + +include(CTest) +include(SwiftSupport) + +if(CMAKE_VERSION VERSION_LESS 3.16 AND CMAKE_SYSTEM_NAME STREQUAL Windows) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +else() + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +endif() + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift) + +find_package(dispatch CONFIG) +find_package(Foundation CONFIG) +find_package(XCTest CONFIG) + +add_subdirectory(Sources) +if(BUILD_EXAMPLES) + add_subdirectory(Examples) +endif() +if(BUILD_TESTING) + add_subdirectory(Tests) +endif() + +add_subdirectory(cmake/modules) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2b0a603 --- /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 0000000..e36bf96 --- /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 0000000..9fcad96 --- /dev/null +++ b/Documentation/01 Getting Started.md @@ -0,0 +1,286 @@ +# 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 + +Let's write a tool called `count` that reads an input file, counts the words, and writes the result to an output file. + +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.4.0"), + ], + targets: [ + .target( + name: "count", + dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]), + ] +) +``` + +> **Note:** To read more about creating and configuring packages using Swift Package Manager, see [Using the Package Manager](https://swift.org/getting-started/#using-the-package-manager). + +## Building Our First Command + +Once we've built the `count` tool, we'll be able to run it 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 + + mutating 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 + + mutating 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 `, 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 they have to remember whether to provide the input file or the output file 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 = false + + mutating 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 = false + + mutating func run() throws { ... } +} +``` + +The default name specification is `.long`, which uses a property's name 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 = false + + mutating 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 +import ArgumentParser +import Foundation + +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 = false + + mutating 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 0000000..886bc7f --- /dev/null +++ b/Documentation/02 Arguments, Options, and Flags.md @@ -0,0 +1,468 @@ +# 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 var index = 0 + @Flag var verbose = false + @Flag var stripWhitespace = false +} +``` + +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. +- What kinds of inputs are valid, and whether arguments are required, is based on your properties' types and default values. + +In this example, all of the properties have default values (optional properties default to `nil`). + +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 + See 'example --help' for more information. +% example --user-name kjohnson +Error: Missing '' +Usage: example --user-name + See 'example --help' for more information. +``` + +When providing a default value for an array property, any user-supplied values replace the entire default. + +```swift +struct Lucky: ParsableCommand { + @Argument var numbers = [7, 14, 21] + + mutating func run() throws { + print(""" + Your lucky numbers are: + \(numbers.map(String.init).joined(separator: " ")) + """) + } +} +``` + +``` +% lucky +Your lucky numbers are: +7 14 21 +% lucky 1 2 3 +Your lucky numbers are: +1 2 3 +``` + +## Customizing option and flag names + +By default, options and flags derive the name that you use on the command line from the name of the property, such as `--count` and `--index`. Camel-case names are converted to lowercase with hyphen-separated words, like `--strip-whitespace`. + +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 = false + + @Flag(name: .short) + var verbose = false + + @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: ExpressibleByArgument { + 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 + + mutating 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. See [Handling Transform Errors](./05%20Validation%20and%20Errors.md#handling-transform-errors) for more about customizing `transform` function errors. + +## Using flag inversions, enumerations, and counts + +Flags are most frequently used for `Bool` properties. You can generate a `true`/`false` pair of flags by specifying a flag inversion: + +```swift +struct Example: ParsableCommand { + @Flag(inversion: .prefixedNo) + var index = true + + @Flag(inversion: .prefixedEnableDisable) + var requiredElement: Bool + + mutating func run() throws { + print(index, requiredElement) + } +} +``` + +When declaring a flag with an inversion, set the default by specifying `true` or `false` as the property's initial value. If you want to require that the user specify one of the two inversions, leave off the default value. + +In the `Example` command defined above, a flag is required for the `requiredElement` property. The specified prefixes are prepended to the long names for the flags: + +``` +% example --enable-required-element +true true +% example --no-index --disable-required-element +false false +% example --index +Error: Missing one of: '--enable-required-element', '--disable-required-element' +``` + +To create a flag with custom names for a Boolean value, to provide an exclusive choice between more than two names, or for collecting multiple values from a set of defined choices, define an enumeration that conforms to the `EnumerableFlag` protocol. + +```swift +enum CacheMethod: String, EnumerableFlag { + case inMemoryCache + case persistentCache +} + +enum Color: String, EnumerableFlag { + case pink, purple, silver +} + +struct Example: ParsableCommand { + @Flag var cacheMethod: CacheMethod + @Flag var colors: [Color] = [] + + mutating func run() throws { + print(cacheMethod) + print(colors) + } +} +``` + +The flag names in this case are drawn from the raw values — for information about customizing the names and help text, see the [`EnumerableFlag` documentation](../Sources/ArgumentParser/Parsable%20Types/EnumerableFlag.swift). + +``` +% 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 + + mutating func run() throws { + print("Verbosity level: \(verbose)") + } +} +``` + +In this example, `verbose` 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 default values + +You can specify default values for almost all supported argument, option, and flag types using normal property initialization syntax: + +```swift +enum CustomFlag: String, EnumerableFlag { + case foo, bar, baz +} + +struct Example: ParsableCommand { + @Flag + var booleanFlag = false + + @Flag + var arrayFlag: [CustomFlag] = [.foo, .baz] + + @Option + var singleOption = 0 + + @Option + var arrayOption = ["bar", "qux"] + + @Argument + var singleArgument = "quux" + + @Argument + var arrayArgument = ["quux", "quuz"] +} +``` + +This includes all of the variants of the argument types above (including `@Option(transform: ...)`, etc.), with a few notable exceptions: +- `Optional`-typed values (which default to `nil` and for which a default would not make sense, as the value could never be `nil`) +- `Int` flags (which are used for counting the number of times a flag is specified and therefore default to `0`) + +If a default is not specified, the user must provide a value for that argument/option/flag or will receive an error that the value is missing. + +You must also always specify a default of `false` for a non-optional `Bool` flag, as in the example above. This makes the behavior consistent with both normal Swift properties (which either must be explicitly initialized or optional to initialize a `struct`/`class` containing them) and the other property types. + + +## 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 = false + @Option var name: String + @Argument var file: String? + + mutating 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 [] + See 'example --help' for more information. +``` + +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 = false + + mutating 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] + See 'example --help' for more information. +``` + +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"] +``` + +### Alternative positional argument parsing strategies + +The default strategy for parsing arrays of positional arguments is to ignore all dash-prefixed command-line inputs. For example, this command accepts a `--verbose` flag and a list of file names as positional arguments: + +```swift +struct Example: ParsableCommand { + @Flag var verbose = false + @Argument var files: [String] = [] + + mutating func run() throws { + print("Verbose: \(verbose), files: \(files)") + } +} +``` + +The `files` argument array uses the default `.remaining` parsing strategy, so it only picks up values that don't have a prefix: + +``` +% example --verbose file1.swift file2.swift +Verbose: true, files: ["file1.swift", "file2.swift"] +% example --verbose file1.swift file2.swift --other +Error: Unexpected argument '--other' +Usage: example [--verbose] [ ...] + See 'example --help' for more information. +``` + +Any input after the `--` terminator is automatically treated as positional input, so users can provide dash-prefixed values that way even with the default configuration: + +``` +% example --verbose -- file1.swift file2.swift --other +Verbose: true, files: ["file1.swift", "file2.swift", "--other"] +``` + +The `.unconditionalRemaining` parsing strategy uses whatever input is left after parsing known options and flags, even if that input is dash-prefixed, including the terminator itself. If `files` were defined as `@Argument(parsing: .unconditionalRemaining) var files: [String]`, then the resulting array would also include strings that look like options: + +``` +% example --verbose file1.swift file2.swift --other +Verbose: true, files: ["file1.swift", "file2.swift", "--other"] +% example -- --verbose file1.swift file2.swift --other +Verbose: false, files: ["--", "--verbose", "file1.swift", "file2.swift", "--other"] +``` diff --git a/Documentation/03 Commands and Subcommands.md b/Documentation/03 Commands and Subcommands.md new file mode 100644 index 0000000..1825af7 --- /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 on 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). + + See 'math help stats ' for detailed help. +``` + +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 `Add`, `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 `--hexadecimal-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 = false + + @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 + + mutating 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 + + mutating 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(help: "The kind of average to provide.") + var kind: Kind = .mean + + @Argument(help: "A group of floating-point values to operate on.") + var values: [Double] = [] + + func calculateMean() -> Double { ... } + func calculateMedian() -> Double { ... } + func calculateMode() -> [Double] { ... } + + mutating 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] = [] + + mutating 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/main/Examples/math/main.swift). diff --git a/Documentation/04 Customizing Help.md b/Documentation/04 Customizing Help.md new file mode 100644 index 0000000..f50546c --- /dev/null +++ b/Documentation/04 Customizing Help.md @@ -0,0 +1,209 @@ +# 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 = false + + @Option(help: "The number of extra lines to show.") + var extraLines = 0 + + @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 = false + + @Option(help: ArgumentHelp( + "The number of extra lines to show.", + valueName: "n")) + var extraLines = 0 + + @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 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 an 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 + + mutating 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 the `-h` or the `--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( + helpNames: [.long, .customShort("?")]) + + @Option(name: .shortAndLong, help: "The number of history entries to show.") + var historyDepth: Int + + mutating 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. +``` + +When not overridden, custom help names are inherited by subcommands. In this example, the parent command defines `--help` and `-?` as its help names: + +```swift +struct Parent: ParsableCommand { + static let configuration = CommandConfiguration( + subcommands: [Child.self], + helpNames: [.long, .customShort("?")]) + + struct Child: ParsableCommand { + @Option(name: .shortAndLong, help: "The host the server will run on.") + var host: String + } +} +``` + +The `child` subcommand inherits the parent's help names, allowing the user to distinguish between the host argument (`-h`) and help (`-?`). + +``` +% parent child -h 192.0.0.0 +... +% parent child -? +USAGE: parent child --host + +OPTIONS: + -h, --host The host the server will run on. + -?, --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` includes a `.hidden` static property that makes it even simpler to hide arguments: + +```swift +struct Example: ParsableCommand { + @Flag(help: .hidden) + var experimentalEnableWidgets: Bool +} +``` + +## Generating Help Text Programmatically + +The help screen is automatically shown to users when they call your command with the help flag. You can generate the same text from within your program by calling the `helpMessage()` method. + +```swift +let help = Repeat.helpMessage() +// `help` matches the output above + +let fortyColumnHelp = Repeat.helpMessage(columns: 40) +// `fortyColumnHelp` is the same help screen, but wrapped to 40 columns +``` + +When generating help text for a subcommand, call `helpMessage(for:)` on the `ParsableCommand` type that represents the root of the command tree and pass the subcommand type as a parameter to ensure the correct display. diff --git a/Documentation/05 Validation and Errors.md b/Documentation/05 Validation and Errors.md new file mode 100644 index 0000000..941df83 --- /dev/null +++ b/Documentation/05 Validation and Errors.md @@ -0,0 +1,157 @@ +# 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 var count: Int = 1 + @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.") + } + } + + mutating 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 ] [ ...] + See 'select --help' for more information. +% select --count 2 hello +Error: Please specify a 'count' less than the number of elements. +Usage: select [--count ] [ ...] + See 'select --help' for more information. +% select --count 0 hello hey hi howdy +Error: Please specify a 'count' of at least 1. +Usage: select [--count ] [ ...] + See 'select --help' for more information. +% 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. Errors that conform to `CustomStringConvertible` or `LocalizedError` provide the best experience for users. + +```swift +struct LineCount: ParsableCommand { + @Argument var file: String + + mutating 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. +``` + +If you print your error output yourself, you still need to throw an error from `validate()` or `run()`, so that your command exits with the appropriate exit code. To avoid printing an extra error message, use the `ExitCode` error, which has static properties for success, failure, and validation errors, or lets you specify a specific exit code. + +```swift +struct RuntimeError: Error, CustomStringConvertible { + var description: String +} + +struct Example: ParsableCommand { + @Argument var inputFile: String + + mutating func run() throws { + if !ExampleCore.processFile(inputFile) { + // ExampleCore.processFile(_:) prints its own errors + // and returns `false` on failure. + throw ExitCode.failure + } + } +} +``` + +## Handling Transform Errors + +During argument and option parsing, you can use a closure to transform the command line strings to custom types. If this transformation fails, you can throw a `ValidationError`; its `message` property will be displayed to the user. + +In addition, you can throw your own errors. Errors that conform to `CustomStringConvertible` or `LocalizedError` provide the best experience for users. + +```swift +struct ExampleTransformError: Error, CustomStringConvertible { + var description: String +} + +struct ExampleDataModel: Codable { + let identifier: UUID + let tokens: [String] + let tokenCount: Int + + static func dataModel(_ jsonString: String) throws -> ExampleDataModel { + guard let data = jsonString.data(using: .utf8) else { throw ValidationError("Badly encoded string, should be UTF-8") } + return try JSONDecoder().decode(ExampleDataModel.self, from: data) + } +} + +struct Example: ParsableCommand { + // Reads in the argument string and attempts to transform it to + // an `ExampleDataModel` object using the `JSONDecoder`. If the + // string is not valid JSON, `decode` will throw an error and + // parsing will halt. + @Argument(transform: ExampleDataModel.dataModel) + var inputJSON: ExampleDataModel + + // Specifiying this option will always cause the parser to exit + // and print the custom error. + @Option(transform: { throw ExampleTransformError(description: "Trying to write to failOption always produces an error. Input: \($0)") }) + var failOption: String? +} +``` + +Throwing from a transform closure benefits users by providing context and can reduce development time by pinpointing issues quickly and more precisely. + +``` +% example '{"Bad JSON"}' +Error: The value '{"Bad JSON"}' is invalid for '': dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "No value for key in object around character 11." UserInfo={NSDebugDescription=No value for key in object around character 11.}))) +Usage: example --fail-option + See 'select --help' for more information. +``` + +While throwing standard library or Foundation errors adds context, custom errors provide the best experience for users and developers. + +``` +% example '{"tokenCount":0,"tokens":[],"identifier":"F77D661C-C5B7-448E-9344-267B284F66AD"}' --fail-option="Some Text Here!" +Error: The value 'Some Text Here!' is invalid for '--fail-option ': Trying to write to failOption always produces an error. Input: Some Text Here! +Usage: example --fail-option + See 'select --help' for more information. +``` diff --git a/Documentation/06 Manual Parsing and Testing.md b/Documentation/06 Manual Parsing and Testing.md new file mode 100644 index 0000000..da70b88 --- /dev/null +++ b/Documentation/06 Manual Parsing and Testing.md @@ -0,0 +1,112 @@ +# 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 var count: Int = 1 + @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 { + var command = try Math.parseAsRoot() + + switch command { + case var 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.dropFirst().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/Documentation/07 Completion Scripts.md b/Documentation/07 Completion Scripts.md new file mode 100644 index 0000000..4cf0a41 --- /dev/null +++ b/Documentation/07 Completion Scripts.md @@ -0,0 +1,118 @@ +# Completion Scripts + +Generate customized completion scripts for your shell of choice. + +## Generating and Installing Completion Scripts + +Command-line tools that you build with `ArgumentParser` include a built-in option for generating completion scripts, with support for Bash, Z shell, and Fish. To generate completions, run your command with the `--generate-completion-script` flag to generate completions for the autodetected shell, or with a value to generate completions for a specific shell. + +``` +$ example --generate-completion-script bash +#compdef example +local context state state_descr line +_example_commandname="example" +typeset -A opt_args + +_example() { + integer ret=1 + local -a args + ... +} + +_example +``` + +The correct method of installing a completion script depends on your shell and your configuration. + +### Installing Zsh Completions + +If you have [`oh-my-zsh`](https://ohmyz.sh) installed, you already have a directory of automatically loading completion scripts — `.oh-my-zsh/completions`. Copy your new completion script to that directory. + +``` +$ example --generate-completion-script zsh > ~/.oh-my-zsh/completions/_example +``` + +> Your completion script must have the following filename format: `_example`. + +Without `oh-my-zsh`, you'll need to add a path for completion scripts to your function path, and turn on completion script autoloading. First, add these lines to `~/.zshrc`: + +``` +fpath=(~/.zsh/completion $fpath) +autoload -U compinit +compinit +``` + +Next, create a directory at `~/.zsh/completion` and copy the completion script to the new directory. + +### Installing Bash Completions + +If you have [`bash-completion`](https://github.com/scop/bash-completion) installed, you can just copy your new completion script to the `/usr/local/etc/bash_completion.d` directory. + +Without `bash-completion`, you'll need to source the completion script directly. Copy it to a directory such as `~/.bash_completions/`, and then add the following line to `~/.bash_profile` or `~/.bashrc`: + +``` +source ~/.bash_completions/example.bash +``` + +### Installing Fish Completions + +Copy the completion script to any path listed in the environment variable `$fish_completion_path`. For example, a typical location is `~/.config/fish/completions/your_script.fish`. + +## Customizing Completions + +`ArgumentParser` provides default completions for any types that it can. For example, an `@Option` property that is a `CaseIterable` type will automatically have the correct values as completion suggestions. + +When declaring an option or argument, you can customize the completions that are offered by specifying a `CompletionKind`. With this completion kind you can specify that the value should be a file, a directory, or one of a list of strings: + +```swift +struct Example: ParsableCommand { + @Option(help: "The file to read from.", completion: .file()) + var input: String + + @Option(help: "The output directory.", completion: .directory) + var outputDir: String + + @Option(help: "The preferred file format.", completion: .list(["markdown", "rst"])) + var format: String + + enum CompressionType: String, CaseIterable, ExpressibleByArgument { + case zip, gzip + } + + @Option(help: "The compression type to use.") + var compression: CompressionType +} +``` + +The generated completion script will suggest only file names for the `--input` option, only directory names for `--output-dir`, and only the strings `markdown` and `rst` for `--format`. The `--compression` option uses the default completions for a `CaseIterable` type, so the completion script will suggest `zip` and `gzip`. + +You can define the default completion kind for custom `ExpressibleByArgument` types by implementing `static var defaultCompletionKind: CompletionKind`. For example, any arguments or options with this `File` type will automatically use files for completions: + +```swift +struct File: Hashable, ExpressibleByArgument { + var path: String + + init?(argument: String) { + self.path = argument + } + + static var defaultCompletionKind: CompletionKind { + .file() + } +} +``` + +For even more control over the suggested completions, you can specify a function that will be called during completion by using the `.custom` completion kind. + +```swift +func listExecutables(_ arguments: [String]) -> [String] { + // Generate the list of executables in the current directory +} + +struct SwiftRun { + @Option(help: "The target to execute.", completion: .custom(listExecutables)) + var target: String? +} +``` + +In this example, when a user requests completions for the `--target` option, the completion script runs the `SwiftRun` command-line tool with a special syntax, calling the `listExecutables` function with an array of the arguments given so far. diff --git a/Examples/CMakeLists.txt b/Examples/CMakeLists.txt new file mode 100644 index 0000000..06c366e --- /dev/null +++ b/Examples/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(math + math/main.swift) +target_link_libraries(math PRIVATE + ArgumentParser + $<$:m>) + +add_executable(repeat + repeat/main.swift) +target_link_libraries(repeat PRIVATE + ArgumentParser) + +add_executable(roll + roll/main.swift + roll/SplitMix64.swift) +target_link_libraries(roll PRIVATE + ArgumentParser) diff --git a/Examples/math/main.swift b/Examples/math/main.swift new file mode 100644 index 0000000..5e40f68 --- /dev/null +++ b/Examples/math/main.swift @@ -0,0 +1,246 @@ +//===----------------------------------------------------------*- 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.", + + // Commands can define a version for automatic '--version' support. + version: "1.0.0", + + // 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 = false + + @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 + + mutating 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 + + mutating 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.", + version: "1.5.0-alpha") + + enum Kind: String, ExpressibleByArgument, CaseIterable { + case mean, median, mode + } + + @Option(help: "The kind of average to provide.") + var kind: Kind = .mean + + @Argument(help: "A group of floating-point values to operate on.") + var values: [Double] = [] + + func validate() throws { + if (kind == .median || kind == .mode) && values.isEmpty { + throw ValidationError("Please provide at least one value to calculate the \(kind).") + } + } + + 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 } + } + + mutating 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] = [] + + mutating 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(completion: .list(["alphabet", "alligator", "branch", "braggart"])) + var oneOfFour: String? + + @Argument(completion: .custom { _ in ["alabaster", "breakfast", "crunch", "crash"] }) + var customArg: String? + + @Argument(help: "A group of floating-point values to operate on.") + var values: [Double] = [] + + // These args and the validation method are for testing exit codes: + @Flag(help: .hidden) + var testSuccessExitCode = false + @Flag(help: .hidden) + var testFailureExitCode = false + @Flag(help: .hidden) + var testValidationExitCode = false + @Option(help: .hidden) + var testCustomExitCode: Int32? + + // These args are for testing custom completion scripts: + @Option(completion: .file(extensions: ["txt", "md"])) + var file: String? + @Option(completion: .directory) + var directory: String? + + @Option(completion: .shellCommand("head -100 /usr/share/dict/words | tail -50")) + var shell: String? + + @Option(completion: .custom(customCompletion)) + var custom: String? + + func validate() throws { + if testSuccessExitCode { + throw ExitCode.success + } + + if testFailureExitCode { + throw ExitCode.failure + } + + if testValidationExitCode { + throw ExitCode.validationFailure + } + + if let exitCode = testCustomExitCode { + throw ExitCode(exitCode) + } + } + } +} + +func customCompletion(_ s: [String]) -> [String] { + return (s.last ?? "").starts(with: "a") + ? ["aardvark", "aaaaalbert"] + : ["hello", "helicopter", "heliotrope"] +} + +Math.main() diff --git a/Examples/repeat/main.swift b/Examples/repeat/main.swift new file mode 100644 index 0000000..bc5e8b1 --- /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 = false + + @Argument(help: "The phrase to repeat.") + var phrase: String + + mutating 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 0000000..a1d7a41 --- /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 0000000..3ae5865 --- /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(help: ArgumentHelp("Rolls the dice times.", valueName: "n")) + var times = 1 + + @Option(help: ArgumentHelp( + "Rolls an -sided dice.", + discussion: "Use this option to override the default value of a six-sided die.", + valueName: "m")) + var sides = 6 + + @Option(help: "A seed to use for repeatable random generation.") + var seed: Int? + + @Flag(name: .shortAndLong, help: "Show all roll results.") + var verbose = false +} + +// 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 0000000..61b0c78 --- /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 0000000..a8caf6c --- /dev/null +++ b/Package.swift @@ -0,0 +1,67 @@ +// swift-tools-version:5.2 +//===----------------------------------------------------------*- 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 + +var package = Package( + name: "swift-argument-parser", + products: [ + .library( + name: "ArgumentParser", + targets: ["ArgumentParser"]), + ], + dependencies: [], + targets: [ + .target( + name: "ArgumentParser", + dependencies: []), + .target( + name: "ArgumentParserTestHelpers", + 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"), + + .target( + name: "changelog-authors", + dependencies: ["ArgumentParser"], + path: "Tools/changelog-authors"), + + .testTarget( + name: "ArgumentParserEndToEndTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]), + .testTarget( + name: "ArgumentParserUnitTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]), + .testTarget( + name: "ArgumentParserExampleTests", + dependencies: ["ArgumentParserTestHelpers"]), + ] +) + +#if swift(>=5.2) +// Skip if < 5.2 to avoid issue with nested type synthesized 'CodingKeys' +package.targets.append( + .testTarget( + name: "ArgumentParserPackageManagerTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"])) +#endif diff --git a/README.md b/README.md index 900ab94..18f8343 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,119 @@ -# Argument-Parser -Argument parser Cocoapods support +# Swift Argument Parser + +## Usage + +Begin by declaring a type that defines the information +that you need to collect from the command line. +Decorate each stored property with one of `ArgumentParser`'s property wrappers, +declare conformance to `ParsableCommand`, +and implement your command's logic in the `run()` method. + +```swift +import ArgumentParser + +struct Repeat: ParsableCommand { + @Flag(help: "Include a counter with each repetition.") + var includeCounter = false + + @Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.") + var count: Int? + + @Argument(help: "The phrase to repeat.") + var phrase: String + + mutating func run() throws { + let repeatCount = count ?? .max + + for i in 1...repeatCount { + if includeCounter { + print("\(i): \(phrase)") + } else { + print(phrase) + } + } + } +} + +Repeat.main() +``` + +You kick off execution by calling your type's static `main()` method. +The `ArgumentParser` library parses the command-line arguments, +instantiates your command type, and then either executes your `run()` method +or exits with a useful message. + +`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 --count 3 +Error: Missing expected argument 'phrase'. +Usage: repeat [--count ] [--include-counter] + See 'repeat --help' for more information. +$ 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. +``` + +For more information and documentation about all supported options, see [the `Documentation` folder at the root of the repository](https://github.com/apple/swift-argument-parser/tree/main/Documentation). + +## 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/pull/72) is a simple utility with two commands. +- [`swift-format`](https://github.com/apple/swift-format/pull/154) uses some advanced features, like custom option values and hidden flags. + +## Adding `ArgumentParser` as a Dependency + +To use the `ArgumentParser` library in a SwiftPM project, +add the following line to the dependencies in your `Package.swift` file: + +```swift +.package(url: "https://github.com/apple/swift-argument-parser", from: "0.4.0"), +``` + +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, +use this dependency specification instead: + +```swift +.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.4.0")), +``` + +Finally, include `"ArgumentParser"` as a dependency for your executable target: + +```swift +let package = Package( + // name, platforms, products, etc. + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser", from: "0.4.0"), + // other dependencies + ], + targets: [ + .target(name: "", dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ]), + // other targets + ] +) +``` diff --git a/Scripts/environment.sh b/Scripts/environment.sh new file mode 100755 index 0000000..812cd01 --- /dev/null +++ b/Scripts/environment.sh @@ -0,0 +1,37 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## 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 +## +##===----------------------------------------------------------------------===## + +read -p "This will replace your current pasteboard. Continue? [y/n]" -n 1 -r +echo # (optional) move to a new line +if [[ $REPLY =~ ^[Yy]$ ]] +then + swiftversion=$(swift --version) + unix_version_name=$(uname -a | tr ";" '\n') + i="${i}Swift version: ${swiftversion}\n" + i="${i}Unix version: ${unix_version_name}\n" + + # Check if OS is macOS, if so retrieve which version and Xcode version. + if [[ "$OSTYPE" == "darwin"* ]]; then + macos=$(defaults read loginwindow SystemVersionStampAsString | cat -) + xcodebuild_version=$(/usr/bin/xcodebuild -version | grep Xcode) + xcodebuild_build=$(/usr/bin/xcodebuild -version | grep Build) + xcodeselectpath=$(xcode-select -p | cat -) + + i="${i}\nmacOS version: ${macos}\n" + i="${i}Xcode-select path: '${xcodeselectpath}\n" + i="${i}Xcode: ${xcodebuild_version} (${xcodebuild_build})" + fi + + echo -e "${i}" | pbcopy + echo "Your pasteboard now contains debug info, paste it on Github" +fi + diff --git a/Sources/ArgumentParser/CMakeLists.txt b/Sources/ArgumentParser/CMakeLists.txt new file mode 100644 index 0000000..bace0ad --- /dev/null +++ b/Sources/ArgumentParser/CMakeLists.txt @@ -0,0 +1,52 @@ +add_library(ArgumentParser + Completions/BashCompletionsGenerator.swift + Completions/CompletionsGenerator.swift + Completions/FishCompletionsGenerator.swift + Completions/ZshCompletionsGenerator.swift + + "Parsable Properties/Argument.swift" + "Parsable Properties/ArgumentHelp.swift" + "Parsable Properties/CompletionKind.swift" + "Parsable Properties/Errors.swift" + "Parsable Properties/Flag.swift" + "Parsable Properties/NameSpecification.swift" + "Parsable Properties/Option.swift" + "Parsable Properties/OptionGroup.swift" + + "Parsable Types/CommandConfiguration.swift" + "Parsable Types/EnumerableFlag.swift" + "Parsable Types/ExpressibleByArgument.swift" + "Parsable Types/ParsableArguments.swift" + "Parsable Types/ParsableArgumentsValidation.swift" + "Parsable Types/ParsableCommand.swift" + + Parsing/ArgumentDecoder.swift + Parsing/ArgumentDefinition.swift + Parsing/ArgumentSet.swift + Parsing/CommandParser.swift + Parsing/InputOrigin.swift + Parsing/Name.swift + Parsing/Parsed.swift + Parsing/ParsedValues.swift + Parsing/ParserError.swift + Parsing/SplitArguments.swift + + Usage/HelpCommand.swift + Usage/HelpGenerator.swift + Usage/MessageInfo.swift + Usage/UsageGenerator.swift + + Utilities/SequenceExtensions.swift + Utilities/StringExtensions.swift + Utilities/Tree.swift) +# NOTE: workaround for CMake not setting up include flags yet +set_target_properties(ArgumentParser PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) +target_compile_options(ArgumentParser PRIVATE + $<$:-enable-testing>) +target_link_libraries(ArgumentParser PRIVATE + $<$>:Foundation>) + + +_install_target(ArgumentParser) +set_property(GLOBAL APPEND PROPERTY ArgumentParser_EXPORTS ArgumentParser) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift new file mode 100644 index 0000000..809421f --- /dev/null +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -0,0 +1,216 @@ +//===----------------------------------------------------------*- 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 BashCompletionsGenerator { + /// Generates a Bash completion script for the given command. + static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { + // TODO: Add a check to see if the command is installed where we expect? + let initialFunctionName = [type].completionFunctionName() + return """ + #!/bin/bash + + \(generateCompletionFunction([type])) + + complete -F \(initialFunctionName) \(type._commandName) + """ + } + + /// Generates a Bash completion function for the last command in the given list. + fileprivate static func generateCompletionFunction(_ commands: [ParsableCommand.Type]) -> String { + let type = commands.last! + let functionName = commands.completionFunctionName() + + // The root command gets a different treatment for the parsing index. + let isRootCommand = commands.count == 1 + let dollarOne = isRootCommand ? "1" : "$1" + let subcommandArgument = isRootCommand ? "2" : "$(($1+1))" + + // Include 'help' in the list of subcommands for the root command. + var subcommands = type.configuration.subcommands + if !subcommands.isEmpty && isRootCommand { + subcommands.append(HelpCommand.self) + } + + // Generate the words that are available at the "top level" of this + // command — these are the dash-prefixed names of options and flags as well + // as all the subcommand names. + let completionWords = generateArgumentWords(commands) + + subcommands.map { $0._commandName } + // FIXME: These shouldn't be hard-coded, since they're overridable + + ["-h", "--help"] + + // Generate additional top-level completions — these are completion lists + // or custom function-based word lists from positional arguments. + let additionalCompletions = generateArgumentCompletions(commands) + + // Start building the resulting function code. + var result = "\(functionName)() {\n" + + // The function that represents the root command has some additional setup + // that other command functions don't need. + if isRootCommand { + result += """ + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + """.indentingEachLine(by: 4) + } + + // Start by declaring a local var for the top-level completions. + // Return immediately if the completion matching hasn't moved further. + result += " opts=\"\(completionWords.joined(separator: " "))\"\n" + for line in additionalCompletions { + result += " opts=\"$opts \(line)\"\n" + } + + result += """ + if [[ $COMP_CWORD == "\(dollarOne)" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + + """ + + // Generate the case pattern-matching statements for option values. + // If there aren't any, skip the case block altogether. + let optionHandlers = generateOptionHandlers(commands) + if !optionHandlers.isEmpty { + result += """ + case $prev in + \(optionHandlers.indentingEachLine(by: 4)) + esac + """.indentingEachLine(by: 4) + "\n" + } + + // Build out completions for the subcommands. + if !subcommands.isEmpty { + // Subcommands have their own case statement that delegates out to + // the subcommand completion functions. + result += " case ${COMP_WORDS[\(dollarOne)]} in\n" + for subcommand in subcommands { + result += """ + (\(subcommand._commandName)) + \(functionName)_\(subcommand._commandName) \(subcommandArgument) + return + ;; + + """ + .indentingEachLine(by: 8) + } + result += " esac\n" + } + + // Finish off the function. + result += """ + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + } + + """ + + return result + + subcommands + .map { generateCompletionFunction(commands + [$0]) } + .joined() + } + + /// Returns the option and flag names that can be top-level completions. + fileprivate static func generateArgumentWords(_ commands: [ParsableCommand.Type]) -> [String] { + ArgumentSet(commands.last!) + .flatMap { $0.bashCompletionWords() } + } + + /// Returns additional top-level completions from positional arguments. + /// + /// These consist of completions that are defined as `.list` or `.custom`. + fileprivate static func generateArgumentCompletions(_ commands: [ParsableCommand.Type]) -> [String] { + ArgumentSet(commands.last!) + .compactMap { arg -> String? in + guard arg.isPositional else { return nil } + + switch arg.completion.kind { + case .default, .file, .directory: + return nil + case .list(let list): + return list.joined(separator: " ") + case .shellCommand(let command): + return "$(\(command))" + case .custom: + // Generate a call back into the command to retrieve a completions list + let commandName = commands.first!._commandName + let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ") + // TODO: Make this work for @Arguments + let argumentName = arg.preferredNameForSynopsis?.synopsisString + ?? arg.help.keys.first?.rawValue ?? "---" + + return """ + $(\(commandName) ---completion \(subcommandNames) -- \(argumentName) "$COMP_WORDS") + """ + } + } + } + + /// Returns the case-matching statements for supplying completions after an option or flag. + fileprivate static func generateOptionHandlers(_ commands: [ParsableCommand.Type]) -> String { + ArgumentSet(commands.last!) + .compactMap { arg -> String? in + let words = arg.bashCompletionWords() + if words.isEmpty { return nil } + + // Flags don't take a value, so we don't provide follow-on completions. + if arg.isNullary { return nil } + + return """ + \(arg.bashCompletionWords().joined(separator: "|"))) + \(arg.bashValueCompletion(commands).indentingEachLine(by: 4)) + return + ;; + """ + } + .joined(separator: "\n") + } +} + +extension ArgumentDefinition { + /// Returns the different completion names for this argument. + fileprivate func bashCompletionWords() -> [String] { + return help.help?.shouldDisplay == false + ? [] + : names.map { $0.synopsisString } + } + + /// Returns the bash completions that can follow this argument's `--name`. + fileprivate func bashValueCompletion(_ commands: [ParsableCommand.Type]) -> String { + switch completion.kind { + case .default: + return "" + + case .file(_): + // TODO: Use '_filedir' when available + // FIXME: Use the extensions array + return #"COMPREPLY=( $(compgen -f -- "$cur") )"# + + case .directory: + return #"COMPREPLY=( $(compgen -d -- "$cur") )"# + + case .list(let list): + return #"COMPREPLY=( $(compgen -W "\#(list.joined(separator: " "))" -- "$cur") )"# + + case .shellCommand(let command): + return "COMPREPLY=( $(\(command)) )" + + case .custom: + // Generate a call back into the command to retrieve a completions list + let commandName = commands.first!._commandName + return #"COMPREPLY=( $(compgen -W "$(\#(commandName) \#(customCompletionCall(commands)) "$COMP_WORDS")" -- "$cur") )"# + } + } +} diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift new file mode 100644 index 0000000..4c45f9f --- /dev/null +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -0,0 +1,127 @@ +//===----------------------------------------------------------*- 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 canImport(Glibc) +import Glibc +#elseif canImport(Darwin) +import Darwin +#elseif canImport(CRT) +import CRT +#endif + +/// A shell for which the parser can generate a completion script. +public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { + public var rawValue: String + + /// Creates a new instance from the given string. + public init?(rawValue: String) { + switch rawValue { + case "zsh", "bash", "fish": + self.rawValue = rawValue + default: + return nil + } + } + + /// An instance representing `zsh`. + public static var zsh: CompletionShell { CompletionShell(rawValue: "zsh")! } + + /// An instance representing `bash`. + public static var bash: CompletionShell { CompletionShell(rawValue: "bash")! } + + /// An instance representing `fish`. + public static var fish: CompletionShell { CompletionShell(rawValue: "fish")! } + + /// Returns an instance representing the current shell, if recognized. + public static func autodetected() -> CompletionShell? { +#if os(Windows) + return nil +#else + // FIXME: This retrieves the user's preferred shell, not necessarily the one currently in use. + guard let shellVar = getenv("SHELL") else { return nil } + let shellParts = String(cString: shellVar).split(separator: "/") + return CompletionShell(rawValue: String(shellParts.last ?? "")) +#endif + } + + /// An array of all supported shells for completion scripts. + public static var allCases: [CompletionShell] { + [.zsh, .bash, .fish] + } +} + +struct CompletionsGenerator { + var shell: CompletionShell + var command: ParsableCommand.Type + + init(command: ParsableCommand.Type, shell: CompletionShell?) throws { + guard let _shell = shell ?? .autodetected() else { + throw ParserError.unsupportedShell() + } + + self.shell = _shell + self.command = command + } + + init(command: ParsableCommand.Type, shellName: String?) throws { + if let shellName = shellName { + guard let shell = CompletionShell(rawValue: shellName) else { + throw ParserError.unsupportedShell(shellName) + } + try self.init(command: command, shell: shell) + } else { + try self.init(command: command, shell: nil) + } + } + + /// Generates a Bash completion script for this generators shell and command.. + func generateCompletionScript() -> String { + switch shell { + case .zsh: + return ZshCompletionsGenerator.generateCompletionScript(command) + case .bash: + return BashCompletionsGenerator.generateCompletionScript(command) + case .fish: + return FishCompletionsGenerator.generateCompletionScript(command) + default: + fatalError("Invalid CompletionShell: \(shell)") + } + } +} + +extension ArgumentDefinition { + /// Returns a string with the arguments for the callback to generate custom completions for + /// this argument. + func customCompletionCall(_ commands: [ParsableCommand.Type]) -> String { + let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ") + let argumentName = preferredNameForSynopsis?.synopsisString + ?? self.help.keys.first?.rawValue ?? "---" + return "---completion \(subcommandNames) -- \(argumentName)" + } +} + +extension ParsableCommand { + fileprivate static var compositeCommandName: [String] { + if let superCommandName = configuration._superCommandName { + return [superCommandName] + _commandName.split(separator: " ").map(String.init) + } else { + return _commandName.split(separator: " ").map(String.init) + } + } +} + +extension Sequence where Element == ParsableCommand.Type { + func completionFunctionName() -> String { + "_" + self.flatMap { $0.compositeCommandName } + .uniquingAdjacentElements() + .joined(separator: "_") + } +} diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift new file mode 100644 index 0000000..e3266fd --- /dev/null +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -0,0 +1,153 @@ +struct FishCompletionsGenerator { + static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { + let programName = type._commandName + let helper = """ + function __fish_\(programName)_using_command + set cmd (commandline -opc) + if [ (count $cmd) -eq (count $argv) ] + for i in (seq (count $argv)) + if [ $cmd[$i] != $argv[$i] ] + return 1 + end + end + return 0 + end + return 1 + end + + """ + + let completions = generateCompletions(commandChain: [programName], [type]) + .joined(separator: "\n") + + return helper + completions + } + + static func generateCompletions(commandChain: [String], _ commands: [ParsableCommand.Type]) + -> [String] + { + let type = commands.last! + let isRootCommand = commands.count == 1 + let programName = commandChain[0] + var subcommands = type.configuration.subcommands + + if !subcommands.isEmpty { + if isRootCommand { + subcommands.append(HelpCommand.self) + } + } + + let prefix = "complete -c \(programName) -n '__fish_\(programName)_using_command" + /// We ask each suggestion to produce 2 pieces of information + /// - Parameters + /// - ancestors: a list of "ancestor" which must be present in the current shell buffer for + /// this suggetion to be considered. This could be a combination of (nested) + /// subcommands and flags. + /// - suggestion: text for the actual suggestion + /// - Returns: A completion expression + func complete(ancestors: [String], suggestion: String) -> String { + "\(prefix) \(ancestors.joined(separator: " "))' \(suggestion)" + } + + let subcommandCompletions = subcommands.map { (subcommand: ParsableCommand.Type) -> String in + let escapedAbstract = subcommand.configuration.abstract.fishEscape() + let suggestion = "-f -a '\(subcommand._commandName)' -d '\(escapedAbstract)'" + return complete(ancestors: commandChain, suggestion: suggestion) + } + + let argumentCompletions = ArgumentSet(type) + .flatMap { $0.argumentSegments(commandChain) } + .map { complete(ancestors: $0, suggestion: $1) } + + let completionsFromSubcommands = subcommands.flatMap { subcommand in + generateCompletions(commandChain: commandChain + [subcommand._commandName], [subcommand]) + } + + return argumentCompletions + subcommandCompletions + completionsFromSubcommands + } +} + +extension String { + fileprivate func fishEscape() -> String { + self.replacingOccurrences(of: "'", with: #"\'"#) + } +} + +extension Name { + fileprivate var asFishSuggestion: String { + switch self { + case .long(let longName): + return "-l \(longName)" + case .short(let shortName, _): + return "-s \(shortName)" + case .longWithSingleDash(let dashedName): + return "-o \(dashedName)" + } + } + + fileprivate var asFormattedFlag: String { + switch self { + case .long(let longName): + return "--\(longName)" + case .short(let shortName, _): + return "-\(shortName)" + case .longWithSingleDash(let dashedName): + return "-\(dashedName)" + } + } +} + +extension ArgumentDefinition { + fileprivate func argumentSegments(_ commandChain: [String]) -> [([String], String)] { + guard help.help?.shouldDisplay != false else { return [] } + + var results = [([String], String)]() + var formattedFlags = [String]() + var flags = [String]() + switch self.kind { + case .positional, .default: + break + case .named(let names): + flags = names.map { $0.asFishSuggestion } + formattedFlags = names.map { $0.asFormattedFlag } + if !flags.isEmpty { + // add these flags to suggestions + var suggestion = "-f\(isNullary ? "" : " -r") \(flags.joined(separator: " "))" + if let abstract = help.help?.abstract, !abstract.isEmpty { + suggestion += " -d '\(abstract.fishEscape())'" + } + + results.append((commandChain, suggestion)) + } + } + + if isNullary { + return results + } + + // each flag alternative gets its own completion suggestion + for flag in formattedFlags { + let ancestors = commandChain + [flag] + switch self.completion.kind { + case .default: + break + case .list(let list): + results.append((ancestors, "-f -k -a '\(list.joined(separator: " "))'")) + case .file(let extensions): + let pattern = "*.{\(extensions.joined(separator: ","))}" + results.append((ancestors, "-f -a '(for i in \(pattern); echo $i;end)'")) + case .directory: + results.append((ancestors, "-f -a '(__fish_complete_directories)'")) + case .shellCommand(let shellCommand): + results.append((ancestors, "-f -a '(\(shellCommand))'")) + case .custom: + let program = commandChain[0] + let subcommands = commandChain.dropFirst().joined(separator: " ") + let suggestion = "-f -a '(command \(program) ---completion \(subcommands) -- --custom (commandline -opc)[1..-1])'" + results.append((ancestors, suggestion)) + } + } + + return results + } +} diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift new file mode 100644 index 0000000..e623df0 --- /dev/null +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -0,0 +1,199 @@ +//===----------------------------------------------------------*- 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 ZshCompletionsGenerator { + /// Generates a Zsh completion script for the given command. + static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { + let initialFunctionName = [type].completionFunctionName() + + return """ + #compdef \(type._commandName) + local context state state_descr line + _\(type._commandName.zshEscapingCommandName())_commandname=$words[1] + typeset -A opt_args + + \(generateCompletionFunction([type])) + _custom_completion() { + local completions=("${(@f)$($*)}") + _describe '' completions + } + + \(initialFunctionName) + """ + } + + static func generateCompletionFunction(_ commands: [ParsableCommand.Type]) -> String { + let type = commands.last! + let functionName = commands.completionFunctionName() + let isRootCommand = commands.count == 1 + + var args = generateCompletionArguments(commands) + args.append("'(-h --help)'{-h,--help}'[Print help information.]'") + + var subcommands = type.configuration.subcommands + var subcommandHandler = "" + if !subcommands.isEmpty { + args.append("'(-): :->command'") + args.append("'(-)*:: :->arg'") + + if isRootCommand { + subcommands.append(HelpCommand.self) + } + + let subcommandModes = subcommands.map { + """ + '\($0._commandName):\($0.configuration.abstract.zshEscaped())' + """ + .indentingEachLine(by: 12) + } + let subcommandArgs = subcommands.map { + """ + (\($0._commandName)) + \(functionName)_\($0._commandName) + ;; + """ + .indentingEachLine(by: 12) + } + + subcommandHandler = """ + case $state in + (command) + local subcommands + subcommands=( + \(subcommandModes.joined(separator: "\n")) + ) + _describe "subcommand" subcommands + ;; + (arg) + case ${words[1]} in + \(subcommandArgs.joined(separator: "\n")) + esac + ;; + esac + + """ + .indentingEachLine(by: 4) + } + + let functionText = """ + \(functionName)() { + integer ret=1 + local -a args + args+=( + \(args.joined(separator: "\n").indentingEachLine(by: 8)) + ) + _arguments -w -s -S $args[@] && ret=0 + \(subcommandHandler) + return ret + } + + + """ + + return functionText + + subcommands + .map { generateCompletionFunction(commands + [$0]) } + .joined() + } + + static func generateCompletionArguments(_ commands: [ParsableCommand.Type]) -> [String] { + ArgumentSet(commands.last!) + .compactMap { $0.zshCompletionString(commands) } + } +} + +extension String { + fileprivate func zshEscapingSingleQuotes() -> String { + self.replacingOccurrences(of: "'", with: #"'"'"'"#) + } + + fileprivate func zshEscapingMetacharacters() -> String { + self.replacingOccurrences(of: #"[\\\[\]]"#, with: #"\\$0"#, options: .regularExpression) + } + + fileprivate func zshEscaped() -> String { + self.zshEscapingSingleQuotes().zshEscapingMetacharacters() + } + + fileprivate func zshEscapingCommandName() -> String { + self.replacingOccurrences(of: "-", with: "_") + } +} + +extension ArgumentDefinition { + var zshCompletionAbstract: String { + guard + let abstract = help.help?.abstract, + !abstract.isEmpty + else { return "" } + return "[\(abstract.zshEscaped())]" + } + + func zshCompletionString(_ commands: [ParsableCommand.Type]) -> String? { + guard help.help?.shouldDisplay != false else { return nil } + + var inputs: String + switch update { + case .unary: + inputs = ":\(valueName):\(zshActionString(commands))" + case .nullary: + inputs = "" + } + + let line: String + switch names.count { + case 0: + line = "" + case 1: + line = """ + \(names[0].synopsisString)\(zshCompletionAbstract) + """ + default: + let synopses = names.map { $0.synopsisString } + line = """ + (\(synopses.joined(separator: " ")))'\ + {\(synopses.joined(separator: ","))}\ + '\(zshCompletionAbstract) + """ + } + + return "'\(line)\(inputs)'" + } + + /// Returns the zsh "action" for an argument completion string. + func zshActionString(_ commands: [ParsableCommand.Type]) -> String { + switch completion.kind { + case .default: + return "" + + case .file(let extensions): + let pattern = extensions.isEmpty + ? "" + : " -g '\(extensions.map { "*." + $0 }.joined(separator: " "))'" + return "_files\(pattern.zshEscaped())" + + case .directory: + return "_files -/" + + case .list(let list): + return "(" + list.joined(separator: " ") + ")" + + case .shellCommand(let command): + return "{local -a list; list=(${(f)\"$(\(command))\"}); _describe '''' list}" + + case .custom: + // Generate a call back into the command to retrieve a completions list + let commandName = commands.first!._commandName + return "{_custom_completion $_\(commandName)_commandname \(customCompletionCall(commands)) $words}" + } + } +} + diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift new file mode 100644 index 0000000..1cf8e6f --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -0,0 +1,479 @@ +//===----------------------------------------------------------*- 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 can 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) + } + + /// This initializer works around a quirk of property wrappers, where the + /// compiler will not see no-argument initializers in extensions. Explicitly + /// marking this initializer unavailable means that when `Value` conforms to + /// `ExpressibleByArgument`, that overload will be selected instead. + /// + /// ```swift + /// @Argument() var foo: String // Syntax without this initializer + /// @Argument var foo: String // Syntax with this initializer + /// ``` + @available(*, unavailable, message: "A default value must be provided unless the value type conforms to ExpressibleByArgument.") + public init() { + fatalError("unavailable") + } + + /// The value presented by this property wrapper. + public var wrappedValue: Value { + get { + switch _parsedValue { + case .value(let v): + return v + case .definition: + fatalError(directlyInitializedError) + } + } + 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 with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: Value?, + help: ArgumentHelp?, + completion: CompletionKind? + ) { + self.init(_parsedValue: .init { key in + ArgumentSet(key: key, kind: .positional, parseType: Value.self, name: NameSpecification.long, default: initial, help: help, completion: completion ?? Value.defaultCompletionKind) + }) + } + + /// Creates a property with a default value provided by standard Swift default value syntax. + /// + /// This method is called to initialize an `Argument` with a default value such as: + /// ```swift + /// @Argument var foo: String = "bar" + /// ``` + /// + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - help: Information about how to use this argument. + public init( + wrappedValue: Value, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) { + self.init( + initial: wrappedValue, + help: help, + completion: completion + ) + } + + /// Creates a property with no default value. + /// + /// This method is called to initialize an `Argument` without a default value such as: + /// ```swift + /// @Argument var foo: String + /// ``` + /// + /// - Parameters: + /// - help: Information about how to use this argument. + public init( + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) { + self.init( + initial: nil, + help: help, + completion: completion + ) + } +} + +/// The strategy to use when parsing multiple values from `@Option` arguments +/// into an array. +public enum ArgumentArrayParsingStrategy { + /// Parse only unprefixed values from the command-line input, ignoring + /// any inputs that have a dash prefix. + /// + /// For example, for a parsable type defined as following: + /// + /// struct Options: ParsableArguments { + /// @Flag var verbose: Bool + /// @Argument(parsing: .remaining) var words: [String] + /// } + /// + /// Parsing the input `--verbose one two` or `one two --verbose` would result + /// in `Options(verbose: true, words: ["one", "two"])`. Parsing the input + /// `one two --other` would result in an unknown option error for `--other`. + /// + /// This is the default strategy for parsing argument arrays. + case remaining + + /// Parse all remaining inputs after parsing any known options or flags, + /// including dash-prefixed inputs and the `--` terminator. + /// + /// For example, for a parsable type defined as following: + /// + /// struct Options: ParsableArguments { + /// @Flag var verbose: Bool + /// @Argument(parsing: .unconditionalRemaining) var words: [String] + /// } + /// + /// Parsing the input `--verbose one two --other` would include the `--other` + /// flag in `words`, resulting in + /// `Options(verbose: true, words: ["one", "two", "--other"])`. + /// + /// - Note: This parsing strategy can be surprising for users, particularly + /// when combined with options and flags. Prefer `remaining` whenever + /// possible, since users can always terminate options and flags with + /// the `--` terminator. With the `remaining` parsing strategy, the input + /// `--verbose -- one two --other` would have the same result as the above + /// example: `Options(verbose: true, words: ["one", "two", "--other"])`. + case unconditionalRemaining +} + +extension Argument { + /// Creates an optional property that reads its value from an argument. + /// + /// The argument is optional for the caller of the command and defaults to + /// `nil`. + /// + /// - Parameter help: Information about how to use this argument. + public init( + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where Value == T? { + self.init(_parsedValue: .init { key in + var arg = ArgumentDefinition( + key: key, + kind: .positional, + parsingStrategy: .nextAsValue, + parser: T.init(argument:), + default: nil, + completion: completion ?? T.defaultCompletionKind) + arg.help.help = help + return ArgumentSet(arg.optional) + }) + } + + /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: Value?, + help: ArgumentHelp?, + completion: CompletionKind?, + transform: @escaping (String) throws -> Value + ) { + self.init(_parsedValue: .init { key in + let help = ArgumentDefinition.Help(options: [], help: help, key: key) + let arg = ArgumentDefinition(kind: .positional, help: help, completion: completion ?? .default, update: .unary({ + (origin, name, valueString, parsedValues) in + do { + let transformedValue = try transform(valueString) + parsedValues.set(transformedValue, forKey: key, inputOrigin: origin) + } catch { + throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) + } + }), initial: { origin, values in + if let v = initial { + values.set(v, forKey: key, inputOrigin: origin) + } + }) + return ArgumentSet(arg) + }) + } + + /// Creates a property with a default value provided by standard Swift default value syntax, parsing with the given closure. + /// + /// This method is called to initialize an `Argument` with a default value such as: + /// ```swift + /// @Argument(transform: baz) + /// var foo: String = "bar" + /// ``` + /// + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property wrapper initialization. + /// - 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( + wrappedValue: Value, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Value + ) { + self.init( + initial: wrappedValue, + help: help, + completion: completion, + transform: transform + ) + } + + /// Creates a property with no default value, parsing with the given closure. + /// + /// This method is called to initialize an `Argument` with no default value such as: + /// ```swift + /// @Argument(transform: baz) + /// var foo: String + /// ``` + /// + /// - 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, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Value + ) { + self.init( + initial: nil, + help: help, + completion: completion, + transform: transform + ) + } + + + /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: Value?, + parsingStrategy: ArgumentArrayParsingStrategy, + help: ArgumentHelp?, + completion: CompletionKind? + ) + where Element: ExpressibleByArgument, Value == Array + { + self.init(_parsedValue: .init { key in + // Assign the initial-value setter and help text for default value based on if an initial value was provided. + let setInitialValue: ArgumentDefinition.Initial + let helpDefaultValue: String? + if let initial = initial { + setInitialValue = { origin, values in + values.set(initial, forKey: key, inputOrigin: origin) + } + helpDefaultValue = !initial.isEmpty ? initial.defaultValueDescription : nil + } else { + setInitialValue = { _, _ in } + helpDefaultValue = nil + } + + let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + var arg = ArgumentDefinition( + kind: .positional, + help: help, + completion: completion ?? Element.defaultCompletionKind, + parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput, + update: .appendToArray(forType: Element.self, key: key), + initial: setInitialValue) + arg.help.defaultValue = helpDefaultValue + return ArgumentSet(arg) + }) + } + + /// Creates a property that reads an array from zero or more arguments. + /// + /// - Parameters: + /// - initial: A default value to use for this property. + /// - parsingStrategy: The behavior to use when parsing multiple values + /// from the command-line arguments. + /// - help: Information about how to use this argument. + public init( + wrappedValue: Value, + parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) + where Element: ExpressibleByArgument, Value == Array + { + self.init( + initial: wrappedValue, + parsingStrategy: parsingStrategy, + help: help, + completion: completion + ) + } + + /// Creates a property with no default value that reads an array from zero or more arguments. + /// + /// This method is called to initialize an array `Argument` with no default value such as: + /// ```swift + /// @Argument() + /// var foo: [String] + /// ``` + /// + /// - Parameters: + /// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments. + /// - help: Information about how to use this argument. + public init( + parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) + where Element: ExpressibleByArgument, Value == Array + { + self.init( + initial: nil, + parsingStrategy: parsingStrategy, + help: help, + completion: completion + ) + } + + /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: Value?, + parsingStrategy: ArgumentArrayParsingStrategy, + help: ArgumentHelp?, + completion: CompletionKind?, + transform: @escaping (String) throws -> Element + ) + where Value == Array + { + self.init(_parsedValue: .init { key in + // Assign the initial-value setter and help text for default value based on if an initial value was provided. + let setInitialValue: ArgumentDefinition.Initial + let helpDefaultValue: String? + if let initial = initial { + setInitialValue = { origin, values in + values.set(initial, forKey: key, inputOrigin: origin) + } + helpDefaultValue = !initial.isEmpty ? "\(initial)" : nil + } else { + setInitialValue = { _, _ in } + helpDefaultValue = nil + } + + let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + var arg = ArgumentDefinition( + kind: .positional, + help: help, + completion: completion ?? .default, + parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput, + update: .unary({ + (origin, name, valueString, parsedValues) in + do { + let transformedElement = try transform(valueString) + parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { + $0.append(transformedElement) + }) + } catch { + throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) + } + }), + initial: setInitialValue) + arg.help.defaultValue = helpDefaultValue + return ArgumentSet(arg) + }) + } + + /// Creates a property that reads an array from zero or more arguments, + /// parsing each element with the given closure. + /// + /// - Parameters: + /// - initial: A default value to use for this property. + /// - parsingStrategy: The behavior to use when parsing multiple values + /// from the command-line arguments. + /// - 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( + wrappedValue: Value, + parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Element + ) + where Value == Array + { + self.init( + initial: wrappedValue, + parsingStrategy: parsingStrategy, + help: help, + completion: completion, + transform: transform + ) + } + + /// Creates a property with no default value that reads an array from zero or more arguments, parsing each element with the given closure. + /// + /// This method is called to initialize an array `Argument` with no default value such as: + /// ```swift + /// @Argument(tranform: baz) + /// var foo: [String] + /// ``` + /// + /// - Parameters: + /// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments. + /// - help: Information about how to use this argument. + /// - transform: A closure that converts a string into this property's element type or throws an error. + public init( + parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Element + ) + where Value == Array + { + self.init( + initial: nil, + parsingStrategy: parsingStrategy, + help: help, + completion: completion, + transform: transform + ) + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift b/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift new file mode 100644 index 0000000..546f5b0 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.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 +// +//===----------------------------------------------------------------------===// + +/// 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. + /// + /// - Note: This property is ignored when generating help for flags, since + /// flags don't include a value. + 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/CompletionKind.swift b/Sources/ArgumentParser/Parsable Properties/CompletionKind.swift new file mode 100644 index 0000000..f0f510f --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/CompletionKind.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------*- 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 type of completion to use for an argument or option. +public struct CompletionKind { + internal enum Kind { + /// Use the default completion kind for the value's type. + case `default` + + /// Use the specified list of completion strings. + case list([String]) + + /// Complete file names with the specified extensions. + case file(extensions: [String]) + + /// Complete directory names that match the specified pattern. + case directory + + /// Call the given shell command to generate completions. + case shellCommand(String) + + /// Generate completions using the given closure. + case custom(([String]) -> [String]) + } + + internal var kind: Kind + + /// Use the default completion kind for the value's type. + public static var `default`: CompletionKind { + CompletionKind(kind: .default) + } + + /// Use the specified list of completion strings. + public static func list(_ words: [String]) -> CompletionKind { + CompletionKind(kind: .list(words)) + } + + /// Complete file names. + public static func file(extensions: [String] = []) -> CompletionKind { + CompletionKind(kind: .file(extensions: extensions)) + } + + /// Complete directory names. + public static var directory: CompletionKind { + CompletionKind(kind: .directory) + } + + /// Call the given shell command to generate completions. + public static func shellCommand(_ command: String) -> CompletionKind { + CompletionKind(kind: .shellCommand(command)) + } + + /// Generate completions using the given closure. + public static func custom(_ completion: @escaping ([String]) -> [String]) -> CompletionKind { + CompletionKind(kind: .custom(completion)) + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/Errors.swift b/Sources/ArgumentParser/Parsable Properties/Errors.swift new file mode 100644 index 0000000..68a6de2 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/Errors.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------*- 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 canImport(Glibc) +import Glibc +#elseif canImport(Darwin) +import Darwin +#elseif canImport(CRT) +import CRT +#endif + +#if os(Windows) +import let WinSDK.ERROR_BAD_ARGUMENTS +#endif + +/// An error type that is presented to the user as an error with parsing their +/// command-line input. +public struct ValidationError: Error, CustomStringConvertible { + /// The error message represented by this instance, this string is presented to + /// the user when a `ValidationError` is thrown from either; `run()`, + /// `validate()` or a transform closure. + public internal(set) 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 only includes an exit code. +/// +/// If you're printing custom errors messages yourself, you can throw this error +/// to specify the exit code without adding any additional output to standard +/// out or standard error. +public struct ExitCode: Error, RawRepresentable, Hashable { + /// The exit code represented by this instance. + public var rawValue: Int32 + + /// Creates a new `ExitCode` with the given code. + public init(_ code: Int32) { + self.rawValue = code + } + + public init(rawValue: Int32) { + self.init(rawValue) + } + + /// An exit code that indicates successful completion of a command. + public static let success = ExitCode(EXIT_SUCCESS) + + /// An exit code that indicates that the command failed. + public static let failure = ExitCode(EXIT_FAILURE) + + /// An exit code that indicates that the user provided invalid input. +#if os(Windows) + public static let validationFailure = ExitCode(ERROR_BAD_ARGUMENTS) +#else + public static let validationFailure = ExitCode(EX_USAGE) +#endif + + /// A Boolean value indicating whether this exit code represents the + /// successful completion of a command. + public var isSuccess: Bool { + self == Self.success + } +} + +/// 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 the 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 Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift new file mode 100644 index 0000000..1873cab --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -0,0 +1,579 @@ +//===----------------------------------------------------------*- 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 on 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 `EnumerableFlag` +/// type. When using an `EnumerableFlag` type as a flag, the individual cases +/// form the flags that are used on the command line. +/// +/// struct Options { +/// enum Operation: EnumerableFlag { +/// 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) + } + + /// This initializer works around a quirk of property wrappers, where the + /// compiler will not see no-argument initializers in extensions. Explicitly + /// marking this initializer unavailable means that when `Value` is a type + /// supported by `Flag` like `Bool` or `EnumerableFlag`, the appropriate + /// overload will be selected instead. + /// + /// ```swift + /// @Flag() var flag: Bool // Syntax without this initializer + /// @Flag var flag: Bool // Syntax with this initializer + /// ``` + @available(*, unavailable, message: "A default value must be provided unless the value type is supported by Flag.") + public init() { + fatalError("unavailable") + } + + /// The value presented by this property wrapper. + public var wrappedValue: Value { + get { + switch _parsedValue { + case .value(let v): + return v + case .definition: + fatalError(directlyInitializedError) + } + } + 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 == Optional { + /// Creates a Boolean property that reads its value from the presence of + /// one or more inverted flags. + /// + /// Use this initializer to create an optional 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. If neither is specified, the resulting + /// flag value would be `nil`. + /// + /// @Flag(inversion: .prefixedNo) + /// var useHTTPS: Bool? + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - inversion: The method for converting this flags name into an on/off + /// pair. + /// - exclusivity: The behavior to use when an on/off pair of flags is + /// specified. + /// - help: Information about how to use this flag. + public init( + name: NameSpecification = .long, + inversion: FlagInversion, + exclusivity: FlagExclusivity = .chooseLast, + help: ArgumentHelp? = nil + ) { + self.init(_parsedValue: .init { key in + .flag(key: key, name: name, default: nil, inversion: inversion, exclusivity: exclusivity, help: help) + }) + } +} + +extension Flag where Value == Bool { + /// Creates a Boolean property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + name: NameSpecification, + initial: Bool?, + help: ArgumentHelp? = nil + ) { + self.init(_parsedValue: .init { key in + .flag(key: key, name: name, default: initial, help: help) + }) + } + + /// Creates a Boolean property with default value provided by standard Swift default value syntax that reads its value from the presence of a flag. + /// + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - name: A specification for what names are allowed for this flag. + /// - help: Information about how to use this flag. + public init( + wrappedValue: Bool, + name: NameSpecification = .long, + help: ArgumentHelp? = nil + ) { + self.init( + name: name, + initial: wrappedValue, + help: help + ) + } + + /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + name: NameSpecification, + initial: Bool?, + inversion: FlagInversion, + exclusivity: FlagExclusivity, + help: ArgumentHelp? + ) { + self.init(_parsedValue: .init { key in + .flag(key: key, name: name, default: initial, inversion: inversion, exclusivity: exclusivity, help: help) + }) + } + + /// Creates a Boolean property with default value provided by standard Swift default value syntax 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. + /// + /// ```swift + /// @Flag(inversion: .prefixedNo) + /// var useHTTPS: Bool = true + /// ```` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - inversion: The method for converting this flag's name into an on/off pair. + /// - exclusivity: The behavior to use when an on/off pair of flags is specified. + /// - help: Information about how to use this flag. + public init( + wrappedValue: Bool, + name: NameSpecification = .long, + inversion: FlagInversion, + exclusivity: FlagExclusivity = .chooseLast, + help: ArgumentHelp? = nil + ) { + self.init( + name: name, + initial: wrappedValue, + inversion: inversion, + exclusivity: exclusivity, + help: help + ) + } + + /// Creates a Boolean property with no default value 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. + /// + /// ```swift + /// @Flag(inversion: .prefixedNo) + /// var useHTTPS: Bool + /// ```` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - inversion: The method for converting this flag's name into an on/off pair. + /// - exclusivity: The behavior to use when an on/off pair of flags is specified. + /// - help: Information about how to use this flag. + public init( + name: NameSpecification = .long, + inversion: FlagInversion, + exclusivity: FlagExclusivity = .chooseLast, + help: ArgumentHelp? = nil + ) { + self.init( + name: name, + initial: nil, + inversion: inversion, + exclusivity: exclusivity, + help: help + ) + } +} + +extension Flag where Value == Int { + /// Creates an 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) + }) + } +} + +// - MARK: EnumerableFlag + +extension Flag where Value: EnumerableFlag { + /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: Value?, + exclusivity: FlagExclusivity, + help: ArgumentHelp? + ) { + self.init(_parsedValue: .init { key in + // This gets flipped to `true` the first time one of these flags is + // encountered. + var hasUpdated = false + let defaultValue = initial.map(String.init(describing:)) + + let caseHelps = Value.allCases.map { Value.help(for: $0) } + let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil }) + + let args = Value.allCases.enumerated().map { (i, value) -> ArgumentDefinition in + let caseKey = InputKey(rawValue: String(describing: value)) + let name = Value.name(for: value) + let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help + let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: helpForCase, defaultValue: defaultValue, key: key, isComposite: !hasCustomCaseHelp) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in + hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) + })) + } + return ArgumentSet(args) + }) + } + + /// Creates a property with a default value provided by standard Swift default value syntax that gets its value from the presence of a flag. + /// + /// Use this initializer to customize the name and number of states further than using a `Bool`. + /// To use, define an `EnumerableFlag` 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. + /// + /// ```swift + /// enum ServerChoice: EnumerableFlag { + /// case useProductionServer + /// case useDevelopmentServer + /// } + /// + /// @Flag var serverChoice: ServerChoice = .useProductionServer + /// ``` + /// + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - exclusivity: The behavior to use when multiple flags are specified. + /// - help: Information about how to use this flag. + public init( + wrappedValue: Value, + exclusivity: FlagExclusivity = .exclusive, + help: ArgumentHelp? = nil + ) { + self.init( + initial: wrappedValue, + exclusivity: exclusivity, + help: help + ) + } + + /// Creates a property with no default value that gets its value from the presence of a flag. + /// + /// Use this initializer to customize the name and number of states further than using a `Bool`. + /// To use, define an `EnumerableFlag` 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. + /// + /// ```swift + /// enum ServerChoice: EnumerableFlag { + /// case useProductionServer + /// case useDevelopmentServer + /// } + /// + /// @Flag var serverChoice: ServerChoice + /// ``` + /// + /// - Parameters: + /// - exclusivity: The behavior to use when multiple flags are specified. + /// - help: Information about how to use this flag. + public init( + exclusivity: FlagExclusivity = .exclusive, + help: ArgumentHelp? = nil + ) { + self.init( + initial: nil, + exclusivity: exclusivity, + help: help + ) + } +} + +extension Flag { + /// Creates a property that gets its value from the presence of a flag, + /// where the allowed flags are defined by an `EnumerableFlag` type. + public init( + exclusivity: FlagExclusivity = .exclusive, + help: ArgumentHelp? = nil + ) where Value == Element?, Element: EnumerableFlag { + self.init(_parsedValue: .init { key in + // This gets flipped to `true` the first time one of these flags is + // encountered. + var hasUpdated = false + + let caseHelps = Element.allCases.map { Element.help(for: $0) } + let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil }) + + let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in + let caseKey = InputKey(rawValue: String(describing: value)) + let name = Element.name(for: value) + let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help + let help = ArgumentDefinition.Help(options: .isOptional, help: helpForCase, key: key, isComposite: !hasCustomCaseHelp) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in + hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) + })) + + } + return ArgumentSet(args) + }) + } + + /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: [Element]?, + help: ArgumentHelp? = nil + ) where Value == Array, Element: EnumerableFlag { + self.init(_parsedValue: .init { key in + let caseHelps = Element.allCases.map { Element.help(for: $0) } + let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil }) + + let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in + let caseKey = InputKey(rawValue: String(describing: value)) + let name = Element.name(for: value) + let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help + let help = ArgumentDefinition.Help(options: .isOptional, help: helpForCase, key: key, isComposite: !hasCustomCaseHelp) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in + values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { + $0.append(value) + }) + })) + } + return ArgumentSet(args) + }) + } + + /// Creates an array property that gets its values from the presence of + /// zero or more flags, where the allowed flags are defined by an + /// `EnumerableFlag` 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( + wrappedValue: [Element], + help: ArgumentHelp? = nil + ) where Value == Array, Element: EnumerableFlag { + self.init( + initial: wrappedValue, + help: help + ) + } + + /// Creates an array property with no default value that gets its values from the presence of zero or more flags, where the allowed flags are defined by an `EnumerableFlag` type. + /// + /// This method is called to initialize an array `Flag` with no default value such as: + /// ```swift + /// @Flag + /// var foo: [CustomFlagType] + /// ``` + /// + /// - Parameters: + /// - help: Information about how to use this flag. + public init( + help: ArgumentHelp? = nil + ) where Value == Array, Element: EnumerableFlag { + self.init( + initial: nil, + help: help + ) + } +} + +// - MARK: Unavailable CaseIterable/RawValue == String + +extension Flag where Value: CaseIterable, Value: RawRepresentable, Value: Equatable, 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 + /// `nil`, this flag is required. + /// - exclusivity: The behavior to use when multiple flags are specified. + /// - help: Information about how to use this flag. + @available(*, unavailable, message: "Add 'EnumerableFlag' conformance to your value type and, if needed, specify the 'name' of each case there.") + 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 defaultValue = initial.map(String.init(describing:)) + + let args = Value.allCases.map { value -> ArgumentDefinition in + let caseKey = InputKey(rawValue: value.rawValue) + let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: help, defaultValue: defaultValue, key: key, isComposite: true) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in + hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) + })) + } + return ArgumentSet(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. + @available(*, unavailable, message: "Add 'EnumerableFlag' conformance to your value type and, if needed, specify the 'name' of each case there.") + public init( + name: NameSpecification = .long, + exclusivity: FlagExclusivity = .exclusive, + help: ArgumentHelp? = nil + ) where Value == Element?, Element: CaseIterable, Element: Equatable, 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, isComposite: true) + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in + hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) + })) + } + return ArgumentSet(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 + /// `CaseIterable` 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. + @available(*, unavailable, message: "Add 'EnumerableFlag' conformance to your value type and, if needed, specify the 'name' of each case there.") + 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, isComposite: true) + 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(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, completion: .default, 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 0000000..15414fa --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift @@ -0,0 +1,175 @@ +//===----------------------------------------------------------*- 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. + /// + /// - Parameters: + /// - name: The name of the option or flag. + /// - withSingleDash: A Boolean value indicating whether to use a single + /// dash as the prefix. If `false`, the name has a double-dash prefix. + 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. + /// + /// When passing `true` as `allowingJoined` in an `@Option` declaration, + /// the user can join a value with the option name. For example, if an + /// option is declared as `-D`, allowing joined values, a user could pass + /// `-Ddebug` to specify `debug` as the value for that option. + /// + /// - Parameters: + /// - char: The name of the option or flag. + /// - allowingJoined: A Boolean value indicating whether this short name + /// allows a joined value. + case customShort(_ char: Character, allowingJoined: Bool = false) + } + var elements: [Element] + + public init(_ sequence: S) where S : Sequence, Element == S.Element { + self.elements = sequence.uniquing() + } + + 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. + /// + /// - Parameters: + /// - name: The name of the option or flag. + /// - withSingleDash: A Boolean value indicating whether to use a single + /// dash as the prefix. If `false`, the name has a double-dash prefix. + 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. + /// + /// When passing `true` as `allowingJoined` in an `@Option` declaration, + /// the user can join a value with the option name. For example, if an + /// option is declared as `-D`, allowing joined values, a user could pass + /// `-Ddebug` to specify `debug` as the value for that option. + /// + /// - Parameters: + /// - char: The name of the option or flag. + /// - allowingJoined: A Boolean value indicating whether this short name + /// allows a joined value. + public static func customShort(_ char: Character, allowingJoined: Bool = false) -> NameSpecification { + [.customShort(char, allowingJoined: allowingJoined)] + } + + /// 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, let allowingJoined): + return .short(name, allowingJoined: allowingJoined) + } + } +} + +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 0000000..72f0153 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -0,0 +1,604 @@ +//===----------------------------------------------------------*- 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 implicitly 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) + } + + /// This initializer works around a quirk of property wrappers, where the + /// compiler will not see no-argument initializers in extensions. Explicitly + /// marking this initializer unavailable means that when `Value` conforms to + /// `ExpressibleByArgument`, that overload will be selected instead. + /// + /// ```swift + /// @Option() var foo: String // Syntax without this initializer + /// @Option var foo: String // Syntax with this initializer + /// ``` + @available(*, unavailable, message: "A default value must be provided unless the value type conforms to ExpressibleByArgument.") + public init() { + fatalError("unavailable") + } + + /// The value presented by this property wrapper. + public var wrappedValue: Value { + get { + switch _parsedValue { + case .value(let v): + return v + case .definition: + fatalError(directlyInitializedError) + } + } + 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 with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + name: NameSpecification, + initial: Value?, + parsingStrategy: SingleValueParsingStrategy, + help: ArgumentHelp?, + completion: CompletionKind? + ) { + self.init(_parsedValue: .init { key in + ArgumentSet( + key: key, + kind: .name(key: key, specification: name), + parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + parseType: Value.self, + name: name, + default: initial, help: help, completion: completion ?? Value.defaultCompletionKind) + } + ) + } + + /// Creates a property with a default value provided by standard Swift default value syntax. + /// + /// This method is called to initialize an `Option` with a default value such as: + /// ```swift + /// @Option var foo: String = "bar" + /// ``` + /// + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + public init( + wrappedValue: Value, + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + completion: CompletionKind? = nil, + help: ArgumentHelp? = nil + ) { + self.init( + name: name, + initial: wrappedValue, + parsingStrategy: parsingStrategy, + help: help, + completion: completion) + } + + /// Creates a property with no default value. + /// + /// This method is called to initialize an `Option` without a default value such as: + /// ```swift + /// @Option var foo: String + /// ``` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) { + self.init( + name: name, + initial: nil, + parsingStrategy: parsingStrategy, + help: help, + completion: completion) + } +} + +/// The strategy to use when parsing a single value from `@Option` arguments. +/// +/// - SeeAlso: `ArrayParsingStrategy`` +public enum SingleValueParsingStrategy { + /// Parse the input after the option. Expect it to be a value. + /// + /// For inputs such as `--foo foo`, this would parse `foo` as the + /// value. However, the input `--foo --bar foo bar` would + /// result in an error. Even though two values are provided, they don’t + /// succeed each option. Parsing would result in an error such as the following: + /// + /// 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 inputs 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 a value, 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 immediately 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 immediately after the option, even if 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 inputs 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 parse the input `--name Foo -- Bar --baz` such that the `remainder` + /// would hold the value `["Bar", "--baz"]`. + case remaining +} + +extension Option { + /// Creates a property that reads its value from a labeled option. + /// + /// If the property has an `Optional` type, or you provide a non-`nil` + /// value for the `initial` parameter, specifying this option is not + /// required. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. + /// - help: Information about how to use this option. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where Value == T? { + self.init(_parsedValue: .init { key in + var arg = ArgumentDefinition( + key: key, + kind: .name(key: key, specification: name), + parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + parser: T.init(argument:), + default: nil, + completion: completion ?? T.defaultCompletionKind) + arg.help.help = help + return ArgumentSet(arg.optional) + }) + } + + /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + name: NameSpecification, + initial: Value?, + parsingStrategy: SingleValueParsingStrategy, + help: ArgumentHelp?, + completion: CompletionKind?, + 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: initial != nil ? .isOptional : [], help: help, key: key) + var arg = ArgumentDefinition(kind: kind, help: help, completion: completion ?? .default, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .unary({ + (origin, name, valueString, parsedValues) in + do { + let transformedValue = try transform(valueString) + parsedValues.set(transformedValue, forKey: key, inputOrigin: origin) + } catch { + throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) + } + }), initial: { origin, values in + if let v = initial { + values.set(v, forKey: key, inputOrigin: origin) + } + }) + arg.help.options.formUnion(ArgumentDefinition.Help.Options(type: Value.self)) + arg.help.defaultValue = initial.map { "\($0)" } + return ArgumentSet(arg) + }) + } + + /// Creates a property with a default value provided by standard Swift default value syntax, parsing with the given closure. + /// + /// This method is called to initialize an `Option` with a default value such as: + /// ```swift + /// @Option(transform: baz) + /// var foo: String = "bar" + /// ``` + /// - Parameters: + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property wrapper initialization. + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - transform: A closure that converts a string into this property's type or throws an error. + public init( + wrappedValue: Value, + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Value + ) { + self.init( + name: name, + initial: wrappedValue, + parsingStrategy: parsingStrategy, + help: help, + completion: completion, + transform: transform + ) + } + + /// Creates a property with no default value, parsing with the given closure. + /// + /// This method is called to initialize an `Option` with no default value such as: + /// ```swift + /// @Option(transform: baz) + /// var foo: String + /// ``` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when looking for this option's value. + /// - help: Information about how to use this option. + /// - transform: A closure that converts a string into this property's type or throws an error. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Value + ) { + self.init( + name: name, + initial: nil, + parsingStrategy: parsingStrategy, + help: help, + completion: completion, + transform: transform + ) + } + + + /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: [Element]?, + name: NameSpecification, + parsingStrategy: ArrayParsingStrategy, + help: ArgumentHelp?, + completion: CompletionKind? + ) where Element: ExpressibleByArgument, Value == Array { + self.init(_parsedValue: .init { key in + // Assign the initial-value setter and help text for default value based on if an initial value was provided. + let setInitialValue: ArgumentDefinition.Initial + let helpDefaultValue: String? + if let initial = initial { + setInitialValue = { origin, values in + values.set(initial, forKey: key, inputOrigin: origin) + } + helpDefaultValue = !initial.isEmpty ? initial.defaultValueDescription : nil + } else { + setInitialValue = { _, _ in } + helpDefaultValue = nil + } + + let kind = ArgumentDefinition.Kind.name(key: key, specification: name) + let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + var arg = ArgumentDefinition( + kind: kind, + help: help, + completion: completion ?? Element.defaultCompletionKind, + parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + update: .appendToArray(forType: Element.self, key: key), + initial: setInitialValue + ) + arg.help.defaultValue = helpDefaultValue + return ArgumentSet(arg) + }) + } + + /// Creates an array property that reads its values from zero or more + /// labeled options. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. + /// - parsingStrategy: The behavior to use when parsing multiple values + /// from the command-line arguments. + /// - help: Information about how to use this option. + public init( + wrappedValue: [Element], + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where Element: ExpressibleByArgument, Value == Array { + self.init( + initial: wrappedValue, + name: name, + parsingStrategy: parsingStrategy, + help: help, + completion: completion + ) + } + + /// Creates an array property with no default value that reads its values from zero or more labeled options. + /// + /// This method is called to initialize an array `Option` with no default value such as: + /// ```swift + /// @Option() + /// var foo: [String] + /// ``` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - parsingStrategy: The behavior to use when 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, + completion: CompletionKind? = nil + ) where Element: ExpressibleByArgument, Value == Array { + self.init( + initial: nil, + name: name, + parsingStrategy: parsingStrategy, + help: help, + completion: completion + ) + } + + + /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: [Element]?, + name: NameSpecification, + parsingStrategy: ArrayParsingStrategy, + help: ArgumentHelp?, + completion: CompletionKind?, + transform: @escaping (String) throws -> Element + ) where Value == Array { + self.init(_parsedValue: .init { key in + // Assign the initial-value setter and help text for default value based on if an initial value was provided. + let setInitialValue: ArgumentDefinition.Initial + let helpDefaultValue: String? + if let initial = initial { + setInitialValue = { origin, values in + values.set(initial, forKey: key, inputOrigin: origin) + } + helpDefaultValue = !initial.isEmpty ? "\(initial)" : nil + } else { + setInitialValue = { _, _ in } + helpDefaultValue = nil + } + + let kind = ArgumentDefinition.Kind.name(key: key, specification: name) + let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + var arg = ArgumentDefinition( + kind: kind, + help: help, + completion: completion ?? .default, + parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + update: .unary({ (origin, name, valueString, parsedValues) in + do { + let transformedElement = try transform(valueString) + parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { + $0.append(transformedElement) + }) + } catch { + throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) + } + }), + initial: setInitialValue + ) + arg.help.defaultValue = helpDefaultValue + return ArgumentSet(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 if the `initial` parameter + /// is not specified. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. If `initial` is + /// `nil`, this option defaults to an empty array. + /// - parsingStrategy: The behavior to use when parsing multiple values + /// from the command-line arguments. + /// - help: Information about how to use this option. + /// - transform: A closure that converts a string into this property's + /// element type or throws an error. + public init( + wrappedValue: [Element], + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Element + ) where Value == Array { + self.init( + initial: wrappedValue, + name: name, + parsingStrategy: parsingStrategy, + help: help, + completion: completion, + transform: transform + ) + } + + /// Creates an array property with no default value that reads its values from zero or more labeled options, parsing each element with the given closure. + /// + /// This method is called to initialize an array `Option` with no default value such as: + /// ```swift + /// @Option(transform: baz) + /// var foo: [String] + /// ``` + /// + /// - 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, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Element + ) where Value == Array { + self.init( + initial: nil, + name: name, + parsingStrategy: parsingStrategy, + help: help, + completion: completion, + transform: transform + ) + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift new file mode 100644 index 0000000..b0d816a --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------*- 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 var _hiddenFromHelp: Bool = false + + // FIXME: Adding this property works around the crasher described in + // https://github.com/apple/swift-argument-parser/issues/338 + internal var _dummy: Bool = false + + 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) + if let d = decoder as? SingleValueDecoder { + d.saveValue(wrappedValue) + } + } + + do { + try wrappedValue.validate() + } catch { + throw ParserError.userValidationError(error) + } + } + + /// Creates a property that represents another parsable type. + public init() { + self.init(_parsedValue: .init { _ in + ArgumentSet(Value.self) + }) + } + + /// The value presented by this property wrapper. + public var wrappedValue: Value { + get { + switch _parsedValue { + case .value(let v): + return v + case .definition: + fatalError(directlyInitializedError) + } + } + 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*)" + } + } +} + +// Experimental use with caution +extension OptionGroup { + public init(_hiddenFromHelp: Bool) { + self.init() + self._hiddenFromHelp = _hiddenFromHelp + } +} diff --git a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift new file mode 100644 index 0000000..ea3b8b3 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.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 +// +//===----------------------------------------------------------------------===// + +/// 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 words. + public var commandName: String? + + /// The name of this command's "super-command". (experimental) + /// + /// Use this when a command is part of a group of commands that are installed + /// with a common dash-prefix, like `git`'s and `swift`'s constellation of + /// independent commands. + public var _superCommandName: 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 + + /// Version information for this command. + public var version: 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 words. + /// - abstract: A one-line description of the command. + /// - discussion: A longer description of the command. + /// - version: The version number for this command. When you provide a + /// non-empty string, the argument parser prints it if the user provides + /// a `--version` flag. + /// - 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, when combined + /// with a simulated Boolean property named `help`. If `helpNames` is + /// `nil`, the names are inherited from the parent command, if any, or + /// `-h` and `--help`. + public init( + commandName: String? = nil, + abstract: String = "", + discussion: String = "", + version: String = "", + shouldDisplay: Bool = true, + subcommands: [ParsableCommand.Type] = [], + defaultSubcommand: ParsableCommand.Type? = nil, + helpNames: NameSpecification? = nil + ) { + self.commandName = commandName + self.abstract = abstract + self.discussion = discussion + self.version = version + self.shouldDisplay = shouldDisplay + self.subcommands = subcommands + self.defaultSubcommand = defaultSubcommand + self.helpNames = helpNames + } + + /// Creates the configuration for a command with a "super-command". + /// (experimental) + public init( + commandName: String? = nil, + _superCommandName: String, + abstract: String = "", + discussion: String = "", + version: String = "", + shouldDisplay: Bool = true, + subcommands: [ParsableCommand.Type] = [], + defaultSubcommand: ParsableCommand.Type? = nil, + helpNames: NameSpecification? = nil + ) { + self.commandName = commandName + self._superCommandName = _superCommandName + self.abstract = abstract + self.discussion = discussion + self.version = version + self.shouldDisplay = shouldDisplay + self.subcommands = subcommands + self.defaultSubcommand = defaultSubcommand + self.helpNames = helpNames + } +} diff --git a/Sources/ArgumentParser/Parsable Types/EnumerableFlag.swift b/Sources/ArgumentParser/Parsable Types/EnumerableFlag.swift new file mode 100644 index 0000000..891fe23 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/EnumerableFlag.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------*- 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 represents the different possible flags to be used by a +/// `@Flag` property. +/// +/// For example, the `Size` enumeration declared here can be used as the type of +/// a `@Flag` property: +/// +/// enum Size: String, EnumerableFlag { +/// case small, medium, large, extraLarge +/// } +/// +/// struct Example: ParsableCommand { +/// @Flag var sizes: [Size] +/// +/// mutating func run() { +/// print(sizes) +/// } +/// } +/// +/// By default, each case name is converted to a flag by using the `.long` name +/// specification, so a user can call `example` like this: +/// +/// $ example --small --large +/// [.small, .large] +/// +/// Provide alternative or additional name specifications for each case by +/// implementing the `name(for:)` static method on your `EnumerableFlag` type. +/// +/// extension Size { +/// static func name(for value: Self) -> NameSpecification { +/// switch value { +/// case .extraLarge: +/// return [.customShort("x"), .long] +/// default: +/// return .shortAndLong +/// } +/// } +/// } +/// +/// With this extension, a user can use short or long versions of the flags: +/// +/// $ example -s -l -x --medium +/// [.small, .large, .extraLarge, .medium] +public protocol EnumerableFlag: CaseIterable, Equatable { + /// Returns the name specification to use for the given flag. + /// + /// The default implementation for this method always returns `.long`. + /// Implement this method for your custom `EnumerableFlag` type to provide + /// different name specifications for different cases. + static func name(for value: Self) -> NameSpecification + + /// Returns the help information to show for the given flag. + /// + /// The default implementation for this method always returns `nil`, which + /// groups the flags together with the help provided in the `@Flag` + /// declaration. Implement this method for your custom type to provide + /// different help information for each flag. + static func help(for value: Self) -> ArgumentHelp? +} + +extension EnumerableFlag { + public static func name(for value: Self) -> NameSpecification { + .long + } + + public static func help(for value: Self) -> ArgumentHelp? { + nil + } +} diff --git a/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift b/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift new file mode 100644 index 0000000..eb45908 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/ExpressibleByArgument.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------*- 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) + + /// The description of this instance to show as a default value in a + /// command-line tool's help screen. + var defaultValueDescription: String { get } + + /// An array of all possible strings to that can convert to value of this + /// type. + /// + /// The default implementation of this property returns an empty array. + static var allValueStrings: [String] { get } + + /// The completion kind to use for options or arguments of this type that + /// don't explicitly declare a completion kind. + /// + /// The default implementation of this property returns `.default`. + static var defaultCompletionKind: CompletionKind { get } +} + +extension ExpressibleByArgument { + public var defaultValueDescription: String { + "\(self)" + } + + public static var allValueStrings: [String] { [] } + + public static var defaultCompletionKind: CompletionKind { + .default + } +} + +extension ExpressibleByArgument where Self: CaseIterable { + public static var allValueStrings: [String] { + self.allCases.map { String(describing: $0) } + } + + public static var defaultCompletionKind: CompletionKind { + .list(allValueStrings) + } +} + +extension ExpressibleByArgument where Self: CaseIterable, Self: RawRepresentable, RawValue == String { + public static var allValueStrings: [String] { + self.allCases.map { $0.rawValue } + } +} + +extension String: ExpressibleByArgument { + public init?(argument: String) { + self = argument + } +} + +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 where Self: ExpressibleByArgument { + 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 {} + +extension Array where Element: ExpressibleByArgument { + var defaultValueDescription: String { + map { $0.defaultValueDescription }.joined(separator: ", ") + } +} diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift new file mode 100644 index 0000000..f1dd1a1 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -0,0 +1,299 @@ +//===----------------------------------------------------------*- 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 canImport(Glibc) +import Glibc +let _exit: (Int32) -> Never = Glibc.exit +#elseif canImport(Darwin) +import Darwin +let _exit: (Int32) -> Never = Darwin.exit +#elseif canImport(CRT) +import CRT +let _exit: (Int32) -> Never = ucrt._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 + + /// The label to use for "Error: ..." messages from this type. (experimental) + static var _errorLabel: String { get } +} + +/// 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 + } + + public static var _errorLabel: String { + "Error" + } +} + +// 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(for: self) + } + + /// Returns the text of the help screen for this type. + /// + /// - Parameter columns: The column width to use when wrapping long lines in + /// the help screen. If `columns` is `nil`, uses the current terminal width, + /// or a default value of `80` if the terminal width is not available. + /// - Returns: The full help screen for this type. + public static func helpMessage(columns: Int? = nil) -> String { + HelpGenerator(self).rendered(screenWidth: columns) + } + + /// Returns the exit code for the given error. + /// + /// The returned code is the same exit code that is used if `error` is passed + /// to `exit(withError:)`. + /// + /// - Parameter error: An error to generate an exit code for. + /// - Returns: The exit code for `error`. + public static func exitCode( + for error: Error + ) -> ExitCode { + MessageInfo(error: error, type: self).exitCode + } + + /// Returns a shell completion script for the specified shell. + /// + /// - Parameter shell: The shell to generate a completion script for. + /// - Returns: The completion script for `shell`. + public static func completionScript(for shell: CompletionShell) -> String { + let completionsGenerator = try! CompletionsGenerator(command: self.asCommand, shell: shell) + return completionsGenerator.generateCompletionScript() + } + + /// 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 `EX_USAGE` or `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(ExitCode.success.rawValue) + } + + let messageInfo = MessageInfo(error: error, type: self) + let fullText = messageInfo.fullText(for: self) + if !fullText.isEmpty { + if messageInfo.shouldExitCleanly { + print(fullText) + } else { + print(fullText, to: &standardError) + } + } + _exit(messageInfo.exitCode.rawValue) + } + + /// 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) + } + } +} + +/// Unboxes the given value if it is a `nil` value. +/// +/// If the value passed is the `.none` case of any optional type, this function +/// returns `nil`. +/// +/// let intAsAny = (1 as Int?) as Any +/// let nilAsAny = (nil as Int?) as Any +/// nilOrValue(intAsAny) // Optional(1) as Any? +/// nilOrValue(nilAsAny) // nil as Any? +func nilOrValue(_ value: Any) -> Any? { + if case Optional.none = value { + return nil + } else { + return value + } +} + +/// Existential protocol for property wrappers, so that they can provide +/// the argument set that they define. +protocol ArgumentSetProvider { + func argumentSet(for key: InputKey) -> ArgumentSet + + var _hiddenFromHelp: Bool { get } +} + +extension ArgumentSetProvider { + var _hiddenFromHelp: Bool { false } +} + +extension ArgumentSet { + init(_ type: ParsableArguments.Type, creatingHelp: Bool = false) { + + #if DEBUG + do { + try type._validate() + } catch { + assertionFailure("\(error)") + } + #endif + + let a: [ArgumentSet] = Mirror(reflecting: type.init()) + .children + .compactMap { child in + guard var codingKey = child.label else { return nil } + + if let parsed = child.value as? ArgumentSetProvider { + if creatingHelp { + guard !parsed._hiddenFromHelp 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) + } else { + // Save a non-wrapped property as is + var definition = ArgumentDefinition( + key: InputKey(rawValue: codingKey), + kind: .default, + parser: { _ in nil }, + default: nilOrValue(child.value), + completion: .default) + definition.help.help = .hidden + return ArgumentSet(definition) + } + } + self.init(sets: a) + } +} + +/// The fatal error message to display when someone accesses a +/// `ParsableArguments` type after initializing it directly. +internal let directlyInitializedError = """ + + -------------------------------------------------------------------- + Can't read a value from a parsable argument definition. + + This error indicates that a property declared with an `@Argument`, + `@Option`, `@Flag`, or `@OptionGroup` property wrapper was neither + initialized to a value nor decoded from command-line arguments. + + To get a valid value, either call one of the static parsing methods + (`parse`, `parseAsRoot`, or `main`) or define an initializer that + initializes _every_ property of your parsable type. + -------------------------------------------------------------------- + + """ diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift new file mode 100644 index 0000000..bbbb9db --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift @@ -0,0 +1,293 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +fileprivate protocol ParsableArgumentsValidator { + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? +} + +enum ValidatorErrorKind { + case warning + case failure +} + +protocol ParsableArgumentsValidatorError: Error { + var kind: ValidatorErrorKind { get } +} + +struct ParsableArgumentsValidationError: Error, CustomStringConvertible { + let parsableArgumentsType: ParsableArguments.Type + let underlayingErrors: [Error] + var description: String { + """ + Validation failed for `\(parsableArgumentsType)`: + + \(underlayingErrors.map({"- \($0)"}).joined(separator: "\n")) + + + """ + } +} + +extension ParsableArguments { + static func _validate() throws { + let validators: [ParsableArgumentsValidator.Type] = [ + PositionalArgumentsValidator.self, + ParsableArgumentsCodingKeyValidator.self, + ParsableArgumentsUniqueNamesValidator.self, + NonsenseFlagsValidator.self, + ] + let errors = validators.compactMap { validator in + validator.validate(self) + } + if errors.count > 0 { + throw ParsableArgumentsValidationError(parsableArgumentsType: self, underlayingErrors: errors) + } + } +} + +fileprivate extension ArgumentSet { + var firstPositionalArgument: ArgumentDefinition? { + content.first(where: { $0.isPositional }) + } + + var firstRepeatedPositionalArgument: ArgumentDefinition? { + content.first(where: { $0.isRepeatingPositional }) + } +} + +/// 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. +struct PositionalArgumentsValidator: ParsableArgumentsValidator { + + struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { + let repeatedPositionalArgument: String + + let positionalArgumentFollowingRepeated: String + + var description: String { + "Can't have a positional argument `\(positionalArgumentFollowingRepeated)` following an array of positional arguments `\(repeatedPositionalArgument)`." + } + + var kind: ValidatorErrorKind { .failure } + } + + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { + let sets: [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) + } + + guard let repeatedPositional = sets.firstIndex(where: { $0.firstRepeatedPositionalArgument != nil }) + else { return nil } + guard let positionalFollowingRepeated = sets[repeatedPositional...] + .dropFirst() + .first(where: { $0.firstPositionalArgument != nil }) + else { return nil } + + let firstRepeatedPositionalArgument: ArgumentDefinition = sets[repeatedPositional].firstRepeatedPositionalArgument! + let positionalFollowingRepeatedArgument: ArgumentDefinition = positionalFollowingRepeated.firstPositionalArgument! + return Error( + repeatedPositionalArgument: firstRepeatedPositionalArgument.help.keys.first!.rawValue, + positionalArgumentFollowingRepeated: positionalFollowingRepeatedArgument.help.keys.first!.rawValue) + } +} + +/// Ensure that all arguments have corresponding coding keys +struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { + + private struct Validator: Decoder { + let argumentKeys: [String] + + enum ValidationResult: Swift.Error { + case success + case missingCodingKeys([String]) + } + + let codingPath: [CodingKey] = [] + let userInfo: [CodingUserInfoKey : Any] = [:] + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + fatalError() + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + fatalError() + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + let missingKeys = argumentKeys.filter { Key(stringValue: $0) == nil } + if missingKeys.isEmpty { + throw ValidationResult.success + } else { + throw ValidationResult.missingCodingKeys(missingKeys) + } + } + } + + /// This error indicates that an option, a flag, or an argument of + /// a `ParsableArguments` is defined without a corresponding `CodingKey`. + struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { + let missingCodingKeys: [String] + + var description: String { + if missingCodingKeys.count > 1 { + return "Arguments \(missingCodingKeys.map({ "`\($0)`" }).joined(separator: ",")) are defined without corresponding `CodingKey`s." + } else { + return "Argument `\(missingCodingKeys[0])` is defined without a corresponding `CodingKey`." + } + } + + var kind: ValidatorErrorKind { + .failure + } + } + + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { + let argumentKeys: [String] = Mirror(reflecting: type.init()) + .children + .compactMap { child in + guard + let codingKey = child.label, + let _ = child.value as? ArgumentSetProvider + else { return nil } + + // Property wrappers have underscore-prefixed names + return String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) + } + guard argumentKeys.count > 0 else { + return nil + } + do { + let _ = try type.init(from: Validator(argumentKeys: argumentKeys)) + fatalError("The validator should always throw.") + } catch let result as Validator.ValidationResult { + switch result { + case .missingCodingKeys(let keys): + return Error(missingCodingKeys: keys) + case .success: + return nil + } + } catch { + fatalError("Unexpected validation error: \(error)") + } + } +} + +/// Ensure argument names are unique within a `ParsableArguments` or `ParsableCommand`. +struct ParsableArgumentsUniqueNamesValidator: ParsableArgumentsValidator { + struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { + var duplicateNames: [String: Int] = [:] + + var description: String { + duplicateNames.map { entry in + "Multiple (\(entry.value)) `Option` or `Flag` arguments are named \"\(entry.key)\"." + }.joined(separator: "\n") + } + + var kind: ValidatorErrorKind { .failure } + } + + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { + let argSets: [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) + } + + let countedNames: [String: Int] = argSets.reduce(into: [:]) { countedNames, args in + for name in args.content.flatMap({ $0.names }) { + countedNames[name.synopsisString, default: 0] += 1 + } + } + + let duplicateNames = countedNames.filter { $0.value > 1 } + return duplicateNames.isEmpty + ? nil + : Error(duplicateNames: duplicateNames) + } +} + +struct NonsenseFlagsValidator: ParsableArgumentsValidator { + struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { + var names: [String] + + var description: String { + """ + One or more Boolean flags is declared with an initial value of `true`. + This results in the flag always being `true`, no matter whether the user + specifies the flag or not. To resolve this error, change the default to + `false`, provide a value for the `inversion:` parameter, or remove the + `@Flag` property wrapper altogether. + + Affected flag(s): + \(names.joined(separator: "\n")) + """ + } + + var kind: ValidatorErrorKind { .warning } + } + + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { + let argSets: [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) + } + + let nonsenseFlags: [String] = argSets.flatMap { args -> [String] in + args.compactMap { def in + if case .nullary = def.update, + !def.help.isComposite, + def.help.options.contains(.isOptional), + def.help.defaultValue == "true" + { + return def.unadornedSynopsis + } else { + return nil + } + } + } + + return nonsenseFlags.isEmpty + ? nil + : Error(names: nonsenseFlags) + } +} diff --git a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift new file mode 100644 index 0000000..1042105 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------*- 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. + mutating 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 mutating 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 a + /// 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()) + return try parser.parse(arguments: arguments).get() + } + + /// Returns the text of the help screen for the given subcommand of this + /// command. + /// + /// - Parameters: + /// - subcommand: The subcommand to generate the help screen for. + /// `subcommand` must be declared in the subcommand tree of this + /// command. + /// - columns: The column width to use when wrapping long line in the + /// help screen. If `columns` is `nil`, uses the current terminal + /// width, or a default value of `80` if the terminal width is not + /// available. + public static func helpMessage( + for subcommand: ParsableCommand.Type, + columns: Int? = nil + ) -> String { + let stack = CommandParser(self).commandStack(for: subcommand) + return HelpGenerator(commandStack: stack).rendered(screenWidth: columns) + } + + /// Parses an instance of this type, or one of its subcommands, from + /// the given arguments and calls its `run()` method, exiting with a + /// relevant error message if necessary. + /// + /// - 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]?) { + do { + var command = try parseAsRoot(arguments) + try command.run() + } catch { + exit(withError: error) + } + } + + /// Parses an instance of this type, or one of its subcommands, from + /// command-line arguments and calls its `run()` method, exiting with a + /// relevant error message if necessary. + public static func main() { + self.main(nil) + } +} diff --git a/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift new file mode 100644 index 0000000..8784adb --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift @@ -0,0 +1,278 @@ +//===----------------------------------------------------------*- 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 previously decoded parsable arguments type. +/// +/// Because arguments are consumed and decoded the first time they're +/// encountered, we save the decoded instances for using later in the +/// command/subcommand hierarchy. +struct DecodedArguments { + var type: ParsableArguments.Type + var value: ParsableArguments + + var commandType: ParsableCommand.Type? { + type as? ParsableCommand.Type + } + + var command: ParsableCommand? { + value as? ParsableCommand + } +} + +/// A decoder that decodes from parsed command-line arguments. +final class ArgumentDecoder: Decoder { + init(values: ParsedValues, previouslyDecoded: [DecodedArguments] = []) { + self.values = values + self.previouslyDecoded = previouslyDecoded + self.usedOrigins = InputOrigin() + + // Mark the terminator position(s) as used: + values.elements.values.filter { $0.key == .terminator }.forEach { + usedOrigins.formUnion($0.inputOrigin) + } + } + + let values: ParsedValues + var usedOrigins: InputOrigin + var nextCommandIndex = 0 + var previouslyDecoded: [DecodedArguments] = [] + + 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 element(forKey: key)?.value == nil + } + + 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 decodeIfPresent(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T? where T : Decodable { + let parsedElement = element(forKey: key) + if let parsedElement = parsedElement, parsedElement.inputOrigin.isDefaultValue { + return parsedElement.value as? T + } + let subDecoder = SingleValueDecoder(userInfo: decoder.userInfo, underlying: decoder, codingPath: codingPath + [key], key: InputKey(key), parsedElement: parsedElement) + do { + return try type.init(from: subDecoder) + } catch let error as ParserError { + if case .noValue = error { + return nil + } else { + throw error + } + } + } + + 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 { + guard let previous = underlying.previouslyDecoded.first(where: { type == $0.type }) + else { throw ParserError.invalidState } + return previous.value as! T + } + + func saveValue(_ value: T, type: T.Type = T.self) { + underlying.previouslyDecoded.append(DecodedArguments(type: type, value: value)) + } + + 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 0000000..f544df4 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -0,0 +1,258 @@ +//===----------------------------------------------------------*- 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 { + /// A closure that modifies a `ParsedValues` instance to include this + /// argument's value. + enum Update { + typealias Nullary = (InputOrigin, Name?, inout ParsedValues) throws -> Void + typealias Unary = (InputOrigin, Name?, String, inout ParsedValues) throws -> Void + + /// An argument that gets its value solely from its presence. + case nullary(Nullary) + + /// An argument that takes a string as its value. + case unary(Unary) + } + + typealias Initial = (InputOrigin, inout ParsedValues) throws -> Void + + enum Kind { + /// An option or flag, with a name and an optional value. + case named([Name]) + + /// A positional argument. + case positional + + /// A pseudo-argument that takes its value from a property's default value + /// instead of from command-line arguments. + case `default` + } + + struct Help { + var options: Options + var help: ArgumentHelp? + var discussion: String? + var defaultValue: String? + var keys: [InputKey] + var allValues: [String] = [] + var isComposite: Bool = false + + 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, isComposite: Bool = false) { + self.options = options + self.help = help + self.defaultValue = defaultValue + self.keys = [key] + self.isComposite = isComposite + } + + init(type: T.Type, options: Options = [], help: ArgumentHelp? = nil, defaultValue: String? = nil, key: InputKey) { + self.options = options + self.help = help + self.defaultValue = defaultValue + self.keys = [key] + self.allValues = type.allValueStrings + } + } + + /// 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 values, regardless of its type. + case allRemainingInput + } + + var kind: Kind + var help: Help + var completion: CompletionKind + var parsingStrategy: ParsingStrategy + var update: Update + var initial: Initial + + var names: [Name] { + switch kind { + case .named(let n): return n + case .positional, .default: return [] + } + } + + var valueName: String { + return help.help?.valueName + ?? preferredNameForSynopsis?.valueString + ?? help.keys.first?.rawValue.convertedToSnakeCase(separator: "-") + ?? "value" + } + + init( + kind: Kind, + help: Help, + completion: CompletionKind, + 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.completion = completion + 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)>" + case (.default, _): + return "" + } + } +} + +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) + } + + var isNullary: Bool { + if case .nullary = update { + return true + } else { + return false + } + } + + var allowsJoinedValue: Bool { + names.contains(where: { $0.allowsJoined }) + } +} + +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 0000000..69f1323 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -0,0 +1,449 @@ +//===----------------------------------------------------------*- 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 { + var content: [ArgumentDefinition] = [] + var namePositions: [Name: Int] = [:] + + init(_ arguments: S) where S.Element == ArgumentDefinition { + self.content = Array(arguments) + self.namePositions = Dictionary( + content.enumerated().flatMap { i, arg in arg.names.map { ($0.nameToMatch, i) } }, + uniquingKeysWith: { first, _ in first }) + } + + init() {} + + init(_ arg: ArgumentDefinition) { + self.init([arg]) + } + + init(sets: [ArgumentSet]) { + self.init(sets.joined()) + } +} + +extension ArgumentSet: CustomDebugStringConvertible { + var debugDescription: String { + content + .map { $0.debugDescription } + .joined(separator: " / ") + } +} + +extension ArgumentSet: Sequence { + func makeIterator() -> Array.Iterator { + return content.makeIterator() + } +} + +// MARK: Flag + +extension ArgumentSet { + /// Creates an argument set for a single Boolean flag. + static func flag(key: InputKey, name: NameSpecification, default initialValue: Bool?, help: ArgumentHelp?) -> ArgumentSet { + // The flag is required if initialValue is `nil`, otherwise it's optional + let helpOptions: ArgumentDefinition.Help.Options = initialValue != nil ? .isOptional : [] + let defaultValueString = initialValue == true ? "true" : nil + + let help = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: defaultValueString, key: key) + let arg = ArgumentDefinition(kind: .name(key: key, specification: name), help: help, completion: .default, update: .nullary({ (origin, name, values) in + values.set(true, forKey: key, inputOrigin: origin) + }), initial: { origin, values in + if let initialValue = initialValue { + values.set(initialValue, forKey: key, inputOrigin: origin) + } + }) + return ArgumentSet(arg) + } + + static func updateFlag(key: InputKey, value: Value, origin: InputOrigin, values: inout ParsedValues, hasUpdated: Bool, exclusivity: FlagExclusivity) throws -> Bool { + switch (hasUpdated, exclusivity) { + case (true, .exclusive): + // This value has already been set. + if let previous = values.element(forKey: key) { + if (previous.value as? Value) == value { + // setting the value again will consume the argument + values.set(value, forKey: key, inputOrigin: origin) + } + else { + throw ParserError.duplicateExclusiveValues(previous: previous.inputOrigin, duplicate: origin, originalInput: values.originalInput) + } + } + case (true, .chooseFirst): + values.update(forKey: key, inputOrigin: origin, initial: value, closure: { _ in }) + case (false, _), (_, .chooseLast): + values.set(value, forKey: key, inputOrigin: origin) + } + return true + } + + /// Creates an argument set for a pair of inverted Boolean flags. + static func flag(key: InputKey, name: NameSpecification, default initialValue: Bool?, inversion: FlagInversion, exclusivity: FlagExclusivity, help: ArgumentHelp?) -> ArgumentSet { + // The flag is required if initialValue is `nil`, otherwise it's optional + let helpOptions: ArgumentDefinition.Help.Options = initialValue != nil ? .isOptional : [] + + let enableHelp = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: initialValue.map(String.init), key: key, isComposite: true) + let disableHelp = ArgumentDefinition.Help(options: [.isOptional], help: help, key: key) + + let (enableNames, disableNames) = inversion.enableDisableNamePair(for: key, name: name) + + var hasUpdated = false + let enableArg = ArgumentDefinition(kind: .named(enableNames), help: enableHelp, completion: .default, update: .nullary({ (origin, name, values) in + hasUpdated = try ArgumentSet.updateFlag(key: key, value: true, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) + }), initial: { origin, values in + if let initialValue = initialValue { + values.set(initialValue, forKey: key, inputOrigin: origin) + } + }) + let disableArg = ArgumentDefinition(kind: .named(disableNames), help: disableHelp, completion: .default, update: .nullary({ (origin, name, values) in + hasUpdated = try ArgumentSet.updateFlag(key: key, value: false, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) + }), initial: { _, _ in }) + return ArgumentSet([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, completion: .default, update: .nullary({ (origin, name, values) in + guard let a = values.element(forKey: key)?.value, let b = a as? Int else { + throw ParserError.invalidState + } + 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?, completion: CompletionKind) { + var arg = ArgumentDefinition(key: key, kind: kind, parsingStrategy: parsingStrategy, parser: A.init(argument:), default: initial, completion: completion) + arg.help.help = help + arg.help.defaultValue = initial.map { "\($0.defaultValueDescription)" } + self.init(arg) + } +} + +extension ArgumentDefinition { + /// Create a unary / argument that parses using the given closure. + init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .nextAsValue, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind) { + self.init(kind: kind, help: ArgumentDefinition.Help(key: key), completion: completion, parsingStrategy: parsingStrategy, update: .unary({ (origin, name, value, values) in + guard let v = parser(value) else { + throw ParserError.unableToParseValue(origin, name, value, forKey: key) + } + values.set(v, forKey: key, inputOrigin: origin) + }), initial: { origin, values in + switch kind { + case .default: + values.set(initial, forKey: key, inputOrigin: InputOrigin(element: .defaultValue)) + case .named, .positional: + values.set(initial, forKey: key, inputOrigin: origin) + } + }) + + 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 commands. + /// + /// 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 -> ParsedValues { + // Create a local, mutable copy of the arguments: + var inputArguments = 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 argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) + { + // Found a joined argument + let origins = origin.inserting(origin2) + try update(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) + } else if let (origin2, value) = inputArguments.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 argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { + // Found a joined argument + let origins = origin.inserting(origin2) + try update(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) + } else if let (origin2, value) = inputArguments.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 if argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { + // Found a joined argument + let origins = origin.inserting(origin2) + try update(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) + } else { + guard let (origin2, value) = inputArguments.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) + } else if argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { + // Found a joined argument + let origins = origin.inserting(origin2) + try update(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) + inputArguments.removeAll(in: usedOrigins) + } + + // ...and then consume the rest of the arguments + while let (origin2, value) = inputArguments.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) + } else if argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { + // Found a joined argument + let origins = origin.inserting(origin2) + try update(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) + inputArguments.removeAll(in: usedOrigins) + } + + // ...and then consume the arguments until hitting an option + while let (origin2, value) = inputArguments.popNextElementIfValue() { + let origins = origin.inserting(origin2) + try update(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) + } + } + } + + var result = ParsedValues(elements: [:], originalInput: all.originalInput) + var allUsedOrigins = InputOrigin() + + try setInitialValues(into: &result) + + // Loop over all arguments: + while let (origin, next) = inputArguments.popNext() { + var usedOrigins = InputOrigin() + defer { + inputArguments.removeAll(in: usedOrigins) + allUsedOrigins.formUnion(usedOrigins) + } + + switch next.value { + case .value: + // We'll parse positional values later. + break + case let .option(parsed): + // Look for an argument that matches this `--option` or `-o`-style + // input. If we can't find one, just move on to the next input. We + // defer catching leftover arguments until we've fully extracted all + // the information for the selected command. + guard let argument = first(matching: parsed) + else { continue } + + switch argument.update { + case let .nullary(update): + // We 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: + // Ignore the terminator, it might get picked up as a positional value later. + break + } + } + + // We have parsed all non-positional values at this point. + // Next: parse / consume the positional values. + var unusedArguments = all + unusedArguments.removeAll(in: allUsedOrigins) + try parsePositionalValues(from: unusedArguments, into: &result) + + return 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: Where `parsed` came from. + /// - Returns: The matching definition. + func first( + matching parsed: ParsedArgument + ) -> ArgumentDefinition? { + namePositions[parsed.name].map { content[$0] } + } + + func firstPositional( + named name: String + ) -> ArgumentDefinition? { + let key = InputKey(rawValue: name) + return first(where: { $0.help.keys.contains(key) }) + } + + func parsePositionalValues( + from unusedInput: SplitArguments, + into result: inout ParsedValues + ) throws { + // Filter out the inputs that aren't "whole" arguments, like `-h` and `-i` + // from the input `-hi`. + var argumentStack = unusedInput.elements.filter { + $0.index.subIndex == .complete + }.map { + (InputOrigin.Element.argumentIndex($0.index), $0) + }[...] + + guard !argumentStack.isEmpty else { return } + + /// Pops arguments until reaching one that is a value (i.e., isn't dash- + /// prefixed). + func skipNonValues() { + while argumentStack.first?.1.isValue == false { + _ = argumentStack.popFirst() + } + } + + /// Pops the origin of the next argument to use. + /// + /// If `unconditional` is false, this skips over any non-"value" input. + func next(unconditional: Bool) -> InputOrigin.Element? { + if !unconditional { + skipNonValues() + } + return argumentStack.popFirst()?.0 + } + + ArgumentLoop: + for argumentDefinition in self { + guard case .positional = argumentDefinition.kind else { continue } + guard case let .unary(update) = argumentDefinition.update else { + preconditionFailure("Shouldn't see a nullary positional argument.") + } + let allowOptionsAsInput = argumentDefinition.parsingStrategy == .allRemainingInput + + repeat { + guard let origin = next(unconditional: allowOptionsAsInput) else { + break ArgumentLoop + } + let value = unusedInput.originalInput(at: origin)! + try update([origin], nil, value, &result) + } while argumentDefinition.isRepeatingPositional + } + } +} diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift new file mode 100644 index 0000000..6aa81e2 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -0,0 +1,389 @@ +//===----------------------------------------------------------*- 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 decodedArguments: [DecodedArguments] = [] + + var rootCommand: ParsableCommand.Type { + commandTree.element + } + + var commandStack: [ParsableCommand.Type] { + let result = decodedArguments.compactMap { $0.commandType } + if currentNode.element == result.last { + return result + } else { + return result + [currentNode.element] + } + } + + init(_ rootCommand: ParsableCommand.Type) { + do { + self.commandTree = try Tree(root: rootCommand) + } catch Tree.InitializationError.recursiveSubcommand(let command) { + fatalError("The ParsableCommand \"\(command)\" can't have itself as its own subcommand.") + } catch { + fatalError("Unexpected error: \(error).") + } + 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? { + guard let (origin, element) = split.peekNext(), + element.isValue, + let value = split.originalInput(at: origin), + let subcommandNode = currentNode.firstChild(withName: value) + else { return nil } + _ = split.popNextValue() + return subcommandNode + } + + /// Throws a `HelpRequested` error if the user has specified either of the + /// built in help flags. + func checkForBuiltInFlags(_ split: SplitArguments) throws { + // Look for help flags + guard !split.contains(anyOf: self.commandStack.getHelpNames()) else { + throw HelpRequested() + } + + // Look for --version if any commands in the stack define a version + if commandStack.contains(where: { !$0.configuration.version.isEmpty }) { + guard !split.contains(Name.long("version")) else { + throw CommandError(commandStack: commandStack, parserError: .versionRequested) + } + } + } + + /// 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 checkForBuiltInFlags(split) + + // We should have used up all arguments at this point: + guard !split.containsNonTerminatorArguments else { + // Check if one of the arguments is an unknown option + for element in split.elements { + if case .option(let argument) = element.value { + throw ParserError.unknownOption(InputOrigin.Element.argumentIndex(element.index), argument.name) + } + } + + let extra = split.coalescedExtraElements() + throw ParserError.unexpectedExtraValues(extra) + } + + guard let lastCommand = decodedArguments.lazy.compactMap({ $0.command }).last else { + throw ParserError.invalidState + } + + return lastCommand + } + + /// Extracts the current command from `split`, throwing if decoding isn't + /// possible. + fileprivate mutating func parseCurrent(_ split: inout SplitArguments) throws -> ParsableCommand { + // Build the argument set (i.e. information on how to parse): + let commandArguments = ArgumentSet(currentNode.element) + + // Parse the arguments, ignoring anything unexpected + let values = try commandArguments.lenientParse(split) + + // Decode the values from ParsedValues into the ParsableCommand: + let decoder = ArgumentDecoder(values: values, previouslyDecoded: decodedArguments) + 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 checkForBuiltInFlags(split) + throw error + } + + // Decoding was successful, so remove the arguments that were used + // by the decoder. + split.removeAll(in: decoder.usedOrigins) + + // Save the decoded results to add to the next command. + let newDecodedValues = decoder.previouslyDecoded + .filter { prev in !decodedArguments.contains(where: { $0.type == prev.type })} + decodedArguments.append(contentsOf: newDecodedValues) + decodedArguments.append(DecodedArguments(type: currentNode.element, value: decodedResult)) + + return decodedResult + } + + /// Starting with the current node, extracts commands out of `split` and + /// descends into subcommands as far as possible. + internal mutating func descendingParse(_ split: inout SplitArguments) throws { + while true { + var parsedCommand = try parseCurrent(&split) + + // after decoding a command, make sure to validate it + do { + try parsedCommand.validate() + var lastArgument = decodedArguments.removeLast() + lastArgument.value = parsedCommand + decodedArguments.append(lastArgument) + } catch { + try checkForBuiltInFlags(split) + throw CommandError(commandStack: commandStack, parserError: ParserError.userValidationError(error)) + } + + // 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 checkForBuiltInFlags(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 { + do { + try handleCustomCompletion(arguments) + } catch { + return .failure(CommandError(commandStack: [commandTree.element], parserError: error as! ParserError)) + } + + 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 checkForCompletionScriptRequest(&split) + 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 { + let error = arguments.isEmpty ? ParserError.noArguments(error) : error + return .failure(CommandError(commandStack: commandStack, parserError: error)) + } catch is HelpRequested { + return .success(HelpCommand(commandStack: commandStack)) + } catch { + return .failure(CommandError(commandStack: commandStack, parserError: .invalidState)) + } + } +} + +// MARK: Completion Script Support + +struct GenerateCompletions: ParsableCommand { + @Option() var generateCompletionScript: String +} + +struct AutodetectedGenerateCompletions: ParsableCommand { + @Flag() var generateCompletionScript = false +} + +extension CommandParser { + func checkForCompletionScriptRequest(_ split: inout SplitArguments) throws { + // Pseudo-commands don't support `--generate-completion-script` flag + guard rootCommand.configuration._superCommandName == nil else { + return + } + + // We don't have the ability to check for `--name [value]`-style args yet, + // so we need to try parsing two different commands. + + // First look for `--generate-completion-script ` + var completionsParser = CommandParser(GenerateCompletions.self) + if let result = try? completionsParser.parseCurrent(&split) as? GenerateCompletions { + throw CommandError(commandStack: commandStack, parserError: .completionScriptRequested(shell: result.generateCompletionScript)) + } + + // Check for for `--generate-completion-script` without a value + var autodetectedParser = CommandParser(AutodetectedGenerateCompletions.self) + if let result = try? autodetectedParser.parseCurrent(&split) as? AutodetectedGenerateCompletions, + result.generateCompletionScript + { + throw CommandError(commandStack: commandStack, parserError: .completionScriptRequested(shell: nil)) + } + } + + func handleCustomCompletion(_ arguments: [String]) throws { + // Completion functions use a custom format: + // + // ---completion [ ...] -- [] + // + // The triple-dash prefix makes '---completion' invalid syntax for regular + // arguments, so it's safe to use for this internal purpose. + guard arguments.first == "---completion" + else { return } + + var args = arguments.dropFirst() + var current = commandTree + while let subcommandName = args.popFirst() { + // A double dash separates the subcommands from the argument information + if subcommandName == "--" { break } + + guard let nextCommandNode = current.firstChild(withName: subcommandName) + else { throw ParserError.invalidState } + current = nextCommandNode + } + + // Some kind of argument name is the next required element + guard let argToMatch = args.popFirst() else { + throw ParserError.invalidState + } + // Completion text is optional here + let completionValues = Array(args) + + // Generate the argument set and parse the argument to find in the set + let argset = ArgumentSet(current.element) + let parsedArgument = try! parseIndividualArg(argToMatch, at: 0).first! + + // Look up the specified argument and retrieve its custom completion function + let completionFunction: ([String]) -> [String] + + switch parsedArgument.value { + case .option(let parsed): + guard let matchedArgument = argset.first(matching: parsed), + case .custom(let f) = matchedArgument.completion.kind + else { throw ParserError.invalidState } + completionFunction = f + + case .value(let str): + guard let matchedArgument = argset.firstPositional(named: str), + case .custom(let f) = matchedArgument.completion.kind + else { throw ParserError.invalidState } + completionFunction = f + + case .terminator: + throw ParserError.invalidState + } + + // Parsing and retrieval successful! We don't want to continue with any + // other parsing here, so after printing the result of the completion + // function, exit with a success code. + let output = completionFunction(completionValues).joined(separator: "\n") + throw ParserError.completionScriptCustomResponse(output) + } +} + +// MARK: Building Command Stacks + +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(_ needle: Name) -> Bool { + self.elements.contains { + switch $0.value { + case .option(.name(let name)), + .option(.nameWithValue(let name, _)): + return name == needle + default: + return false + } + } + } + + func contains(anyOf names: [Name]) -> Bool { + self.elements.contains { + switch $0.value { + 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 0000000..be50c02 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/InputOrigin.swift @@ -0,0 +1,123 @@ +//===----------------------------------------------------------*- 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 a sinlge +/// index, multiple indices, or from part of an index. For this command: +/// +/// struct Example: ParsableCommand { +/// @Flag(name: .short) var verbose = false +/// @Flag(name: .short) var expert = false +/// +/// @Option var count: Int +/// } +/// +/// ...with this usage: +/// +/// $ example -ve --count 5 +/// +/// The parsed value for the `count` property will come from indices `1` and +/// `2`, while the value for `verbose` will come from index `1`, sub-index `0`. +struct InputOrigin: Equatable, ExpressibleByArrayLiteral { + enum Element: Comparable, Hashable { + /// The input value came from a property's default value, not from a + /// command line argument. + case defaultValue + + /// The input value came from the specified index in the argument string. + case argumentIndex(SplitArguments.Index) + + var baseIndex: Int? { + switch self { + case .defaultValue: + return nil + case .argumentIndex(let i): + return i.inputIndex.rawValue + } + } + + var subIndex: Int? { + switch self { + case .defaultValue: + return nil + case .argumentIndex(let i): + switch i.subIndex { + case .complete: return nil + case .sub(let n): return n + } + } + } + } + + private var _elements: Set = [] + var elements: [Element] { + Array(_elements).sorted() + } + + init() { + } + + init(elements: [Element]) { + _elements = Set(elements) + } + + init(element: Element) { + _elements = Set([element]) + } + + init(arrayLiteral elements: Element...) { + self.init(elements: elements) + } + + init(argumentIndex: SplitArguments.Index) { + self.init(element: .argumentIndex(argumentIndex)) + } + + mutating func insert(_ other: Element) { + guard !_elements.contains(other) else { return } + _elements.insert(other) + } + + func inserting(_ other: Element) -> Self { + 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 forEach(_ closure: (Element) -> Void) { + _elements.forEach(closure) + } +} + +extension InputOrigin { + var isDefaultValue: Bool { + return _elements.count == 1 && _elements.first == .defaultValue + } +} + +extension InputOrigin.Element { + static func < (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.argumentIndex(let l), .argumentIndex(let r)): + return l < r + case (.argumentIndex, .defaultValue): + return true + case (.defaultValue, _): + return false + } + } +} diff --git a/Sources/ArgumentParser/Parsing/Name.swift b/Sources/ArgumentParser/Parsing/Name.swift new file mode 100644 index 0000000..1859c57 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/Name.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------*- 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: Hashable { + /// 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, allowingJoined: Bool = false) + /// 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())) + } + } +} + +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 + } + } + + var isShort: Bool { + switch self { + case .short: + return true + default: + return false + } + } + + var allowsJoined: Bool { + switch self { + case .short(_, let allowingJoined): + return allowingJoined + default: + return false + } + } + + /// The instance to match against user input -- this always has + /// `allowingJoined` as `false`, since that's the way input is parsed. + var nameToMatch: Name { + switch self { + case .long, .longWithSingleDash: return self + case .short(let c, _): return .short(c) + } + } +} + +// 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 0000000..df85cd4 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/Parsed.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------*- 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. + case value(Value) + case definition((InputKey) -> ArgumentSet) + + internal init(_ makeSet: @escaping (InputKey) -> ArgumentSet) { + self = .definition(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(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 0000000..69ef15c --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ParsedValues.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------*- 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, Hashable { + 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 + fileprivate var shouldClearArrayIfParsed = true + } + + /// These are the parsed key-value pairs. + var elements: [InputKey: Element] = [:] + + /// This is the *original* array of arguments that this was parsed from. + /// + /// This is used for error output generation. + var originalInput: [String] +} + +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 e = elements[element.key] { + // Merge the source values. We need to keep track + // of any previous source indexes we have used for + // this key. + var element = element + element.inputOrigin.formUnion(e.inputOrigin) + elements[element.key] = element + } else { + elements[element.key] = element + } + } + + func element(forKey key: InputKey) -> Element? { + elements[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) + } + + 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 + // The first time a value is parsed from command line, empty array of any default values. + if e.shouldClearArrayIfParsed { + v.removeAll() + e.shouldClearArrayIfParsed = false + } + closure(&v) + e.value = v + e.inputOrigin.formUnion(inputOrigin) + set(e) + } +} diff --git a/Sources/ArgumentParser/Parsing/ParserError.swift b/Sources/ArgumentParser/Parsing/ParserError.swift new file mode 100644 index 0000000..d0b12bc --- /dev/null +++ b/Sources/ArgumentParser/Parsing/ParserError.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 +// +//===----------------------------------------------------------------------===// + +/// Gets thrown while parsing and will be handled by the error output generation. +enum ParserError: Error { + case helpRequested + case versionRequested + + case completionScriptRequested(shell: String?) + case completionScriptCustomResponse(String) + case unsupportedShell(String? = nil) + + 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, originalInput: [String]) + /// We need a value for the given key, but it’s not there. Some non-optional option or argument is missing. + case noValue(forKey: InputKey) + case unableToParseValue(InputOrigin, Name?, String, forKey: InputKey, originalError: Error? = nil) + case missingSubcommand + case userValidationError(Error) + case noArguments(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 0000000..94a6541 --- /dev/null +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -0,0 +1,650 @@ +//===----------------------------------------------------------*- 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[.. Element { + Element(value: .option(arg), index: index) + } + + static func value(_ str: String, index: Index) -> Element { + Element(value: .value(str), index: index) + } + + static func terminator(index: Index) -> Element { + Element(value: .terminator, index: index) + } + } + + /// The position of the original input string for an element. + /// + /// For example, if `originalInput` is `["--foo", "-vh"]`, there are index + /// positions 0 (`--foo`) and 1 (`-vh`). + struct InputIndex: RawRepresentable, Hashable, Comparable { + var rawValue: Int + + static func <(lhs: InputIndex, rhs: InputIndex) -> Bool { + lhs.rawValue < rhs.rawValue + } + } + + /// The position within an option for an element. + /// + /// Single-dash prefixed options can be treated as a whole option or as a + /// group of individual short options. For example, the input `-vh` is split + /// into three elements, with distinct sub-indexes: + /// + /// - `-vh`: `.complete` + /// - `-v`: `.sub(0)` + /// - `-h`: `.sub(1)` + 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 + } + } + } + + /// An index into the original input and the sub-index of an element. + 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 = .complete + + var completeIndex: Index { + return Index(inputIndex: inputIndex) + } + } + + /// The parsed arguments. Onl + var _elements: [Element] = [] + var firstUnused: Int = 0 + + /// The original array of arguments that was used to generate this instance. + var originalInput: [String] + + /// The unused arguments represented by this instance. + var elements: ArraySlice { + _elements[firstUnused...] + } +} + +extension SplitArguments.Element: CustomDebugStringConvertible { + var debugDescription: String { + switch value { + 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 { element -> String in + switch element.value { + case .option(.name(let name)): + return "[\(element.index)] \(name.synopsisString)" + case .option(.nameWithValue(let name, let value)): + return "[\(element.index)] \(name.synopsisString)='\(value)'" + case .value(let value): + return "[\(element.index)] '\(value)'" + case .terminator: + return "[\(element.index)] --" + } + } + .joined(separator: " ") + } +} + +extension SplitArguments.Element { + var isValue: Bool { + switch value { + case .value: return true + case .option, .terminator: return false + } + } + + var isTerminator: Bool { + switch value { + case .terminator: return true + case .option, .value: return false + } + } +} + +extension SplitArguments { + /// `true` if the arguments are empty. + var isEmpty: Bool { + elements.isEmpty + } + + /// `false` if the arguments are empty, or if the only remaining argument is + /// the `--` terminator. + var containsNonTerminatorArguments: Bool { + if elements.isEmpty { return false } + if elements.count > 1 { return true } + + if elements.first?.isTerminator == true { return false } + else { return true } + } + + /// Returns the original input string at the given origin, or `nil` if + /// `origin` is a sub-index. + func originalInput(at origin: InputOrigin.Element) -> String? { + guard case let .argumentIndex(index) = origin else { + return nil + } + return originalInput[index.inputIndex.rawValue] + } + + /// Returns the position in `elements` of the given input origin. + mutating func position(of origin: InputOrigin.Element) -> Int? { + guard case let .argumentIndex(index) = origin else { return nil } + return elements.firstIndex(where: { $0.index == index }) + } + + /// Returns the position in `elements` of the first element after the given + /// input origin. + mutating func position(after origin: InputOrigin.Element) -> Int? { + guard case let .argumentIndex(index) = origin else { return nil } + return elements.firstIndex(where: { $0.index > index }) + } + + mutating func popNext() -> (InputOrigin.Element, Element)? { + guard let element = elements.first else { return nil } + removeFirst() + return (.argumentIndex(element.index), element) + } + + func peekNext() -> (InputOrigin.Element, Element)? { + guard let element = elements.first else { return nil } + return (.argumentIndex(element.index), element) + } + + mutating func extractJoinedElement(at origin: InputOrigin.Element) -> (InputOrigin.Element, String)? { + guard case let .argumentIndex(index) = origin else { return nil } + + // Joined arguments only apply when parsing the first sub-element of a + // larger input argument. + guard index.subIndex == .sub(0) else { return nil } + + // Rebuild the origin position for the full argument string, e.g. `-Ddebug` + // instead of just the `-D` portion. + let completeOrigin = InputOrigin.Element.argumentIndex(index.completeIndex) + + // Get the value from the original string, following the dash and short + // option name. For example, for `-Ddebug`, drop the `-D`, leaving `debug` + // as the value. + let value = String(originalInput(at: completeOrigin)!.dropFirst(2)) + + return (completeOrigin, 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, an 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 let start = position(after: origin), + let elementIndex = elements[start...].firstIndex(where: { $0.index.subIndex == .complete }) + else { return nil } + + // Only succeed if the element is a value (not prefixed with a dash) + guard case .value(let value) = elements[elementIndex].value + else { return nil } + + defer { remove(at: elementIndex) } + let matchedArgumentIndex = elements[elementIndex].index + 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 let start = position(after: origin) else { return nil } + guard let resultIndex = elements[start...].firstIndex(where: { $0.isValue }) else { return nil } + + defer { remove(at: resultIndex) } + return (.argumentIndex(elements[resultIndex].index), elements[resultIndex].value.valueString!) + } + + /// 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 let start = position(after: origin) else { return nil } + // Elements are sorted by their `InputIndex`. Find the first `InputIndex` + // after `origin`: + guard let nextIndex = elements[start...].first(where: { $0.index.subIndex == .complete })?.index else { return nil } + // Remove all elements with this `InputIndex`: + remove(at: nextIndex) + // Return the original input + return (.argumentIndex(nextIndex), originalInput[nextIndex.inputIndex.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 element = elements.first, element.isValue else { return nil } + removeFirst() + return (.argumentIndex(element.index), element.value.valueString!) + } + + /// 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: { $0.isValue }) + else { return nil } + let e = elements[idx] + remove(at: idx) + return (e.index, e.value.valueString!) + } + + /// Finds and returns the next element that is a value. + func peekNextValue() -> (Index, String)? { + guard let idx = elements.firstIndex(where: { $0.isValue }) + else { return nil } + let e = elements[idx] + return (e.index, e.value.valueString!) + } + + /// Removes the first element in `elements`. + mutating func removeFirst() { + firstUnused += 1 + } + + /// Removes the element at the given position. + mutating func remove(at position: Int) { + guard position >= firstUnused else { + return + } + + // This leaves duplicates of still to-be-used arguments in the unused + // portion of the _elements array. + for i in (firstUnused..) { + var lo = subrange.startIndex + var hi = subrange.endIndex + + // This leaves duplicates of still to-be-used arguments in the unused + // portion of the _elements array. + while lo > firstUnused { + hi -= 1 + lo -= 1 + _elements[hi] = _elements[lo] + } + firstUnused += subrange.count + } + + /// 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) { + guard !isEmpty else { return } + + // Find the first element at the given input index. Since `elements` is + // always sorted by input index, we can leave this method if we see a + // higher value than `position`. + var start = elements.startIndex + while start < elements.endIndex { + if elements[start].index.inputIndex == position.inputIndex { break } + if elements[start].index.inputIndex > position.inputIndex { return } + start += 1 + } + guard start < elements.endIndex else { return } + + if case .complete = position.subIndex { + // When removing a `.complete` position, we need to remove both the + // complete element and any sub-elements with the same input index. + + // Remove up to the first element where the input index doesn't match. + let end = elements[start...].firstIndex(where: { $0.index.inputIndex != position.inputIndex }) + ?? elements.endIndex + + remove(subrange: start.. [(InputOrigin, String)] { + let completeIndexes: [InputIndex] = elements + .compactMap { + guard case .complete = $0.index.subIndex else { return nil } + return $0.index.inputIndex + } + + // Now return all elements that are either: + // 1) `.complete` + // 2) `.sub` but not in `completeIndexes` + + let extraElements = elements.filter { + switch $0.index.subIndex { + case .complete: + return true + case .sub: + return !completeIndexes.contains($0.index.inputIndex) + } + } + return extraElements.map { element -> (InputOrigin, String) in + let input: String + switch element.index.subIndex { + case .complete: + input = originalInput[element.index.inputIndex.rawValue] + case .sub: + if case .option(let option) = element.value { + input = String(describing: option) + } else { + // Odd case. Fall back to entire input at that index: + input = originalInput[element.index.inputIndex.rawValue] + } + } + return (.init(argumentIndex: element.index), input) + } + } +} + +func parseIndividualArg(_ arg: String, at position: Int) throws -> [SplitArguments.Element] { + let index = SplitArguments.Index(inputIndex: .init(rawValue: position)) + if let nonDashIdx = arg.firstIndex(where: { $0 != "-" }) { + let dashCount = arg.distance(from: arg.startIndex, to: nonDashIdx) + let remainder = arg[nonDashIdx.. `Name.short("c")` + /// Otherwise, treat it as a long name with single dash. + /// `-count=1` -> `Name.longWithSingleDash("count")` + $0.count == 1 ? Name.short($0.first!) : Name.longWithSingleDash(String($0)) + }) + } + + init(longArgRemainder remainder: Substring, makeName: (Substring) -> 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 0000000..81739e2 --- /dev/null +++ b/Sources/ArgumentParser/Usage/HelpCommand.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 +// +//===----------------------------------------------------------------------===// + +struct HelpCommand: ParsableCommand { + static var configuration = CommandConfiguration( + commandName: "help", + abstract: "Show subcommand help information.") + + @Argument var subcommands: [String] = [] + + private(set) var commandStack: [ParsableCommand.Type] = [] + + init() {} + + mutating func run() throws { + throw CommandError(commandStack: commandStack, parserError: .helpRequested) + } + + 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 0000000..5984a99 --- /dev/null +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -0,0 +1,343 @@ +//===----------------------------------------------------------*- 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 systemScreenWidth: Int { + _screenWidthOverride ?? _terminalSize().width + } + + internal static var _screenWidthOverride: Int? = nil + + struct Usage { + var components: [String] + + func rendered(screenWidth: Int) -> String { + components + .joined(separator: "\n") + } + } + + struct Section { + struct Element: Hashable { + var label: String + var abstract: String = "" + var discussion: String = "" + + var paddedLabel: String { + String(repeating: " ", count: HelpGenerator.helpIndent) + label + } + + func rendered(screenWidth: Int) -> String { + let paddedLabel = self.paddedLabel + let wrappedAbstract = self.abstract + .wrapped(to: screenWidth, wrappingIndent: HelpGenerator.labelColumnWidth) + let wrappedDiscussion = self.discussion.isEmpty + ? "" + : self.discussion.wrapped(to: screenWidth, wrappingIndent: HelpGenerator.helpIndent * 4) + "\n" + let renderedAbstract: String = { + guard !abstract.isEmpty else { return "" } + if paddedLabel.count < HelpGenerator.labelColumnWidth { + // Render after padded label. + return String(wrappedAbstract.dropFirst(paddedLabel.count)) + } else { + // Render in a new line. + return "\n" + wrappedAbstract + } + }() + return paddedLabel + + renderedAbstract + "\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 = false + + func rendered(screenWidth: Int) -> String { + guard !elements.isEmpty else { return "" } + + let renderedElements = elements.map { $0.rendered(screenWidth: screenWidth) }.joined() + return "\(String(describing: header).uppercased()):\n" + + renderedElements + } + } + + struct DiscussionSection { + var title: String = "" + var content: String + } + + var commandStack: [ParsableCommand.Type] + 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) + self.commandStack = commandStack + + // Build the tool name and subcommand name from the command configuration + var toolName = commandStack.map { $0._commandName }.joined(separator: " ") + if let superName = commandStack.first!.configuration._superCommandName { + toolName = "\(superName) \(toolName)" + } + + 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 { + if !self.abstract.isEmpty { + self.abstract += "\n" + } + self.abstract += "\n\(currentCommand.configuration.discussion)" + } + + self.usage = Usage(components: [usageString]) + self.sections = HelpGenerator.generateSections(commandStack: commandStack) + self.discussionSections = [] + } + + init(_ type: ParsableArguments.Type) { + self.init(commandStack: [type.asCommand]) + } + + static func generateSections(commandStack: [ParsableCommand.Type]) -> [Section] { + var positionalElements: [Section.Element] = [] + var optionElements: [Section.Element] = [] + /// Used to keep track of elements already seen from parent commands. + var alreadySeenElements = Set() + + guard let commandType = commandStack.last else { + return [] + } + + let args = Array(ArgumentSet(commandType, creatingHelp: true)) + + 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 args[i].help.isComposite { + // If this argument is composite, we have a group of arguments to + // output together. + var groupedArgs = [arg] + let defaultValue = arg.help.defaultValue.map { "(default: \($0))" } ?? "" + while i < args.count - 1 && args[i + 1].help.keys == arg.help.keys { + groupedArgs.append(args[i + 1]) + i += 1 + } + + var synopsisString = "" + for arg in groupedArgs { + if !synopsisString.isEmpty { synopsisString.append("/") } + synopsisString.append("\(arg.synopsisForHelp ?? "")") + } + synopsis = synopsisString + + var descriptionString: String? + for arg in groupedArgs { + if let desc = arg.help.help?.abstract { + descriptionString = desc + break + } + } + description = [descriptionString, defaultValue] + .compactMap { $0 } + .joined(separator: " ") + } else { + let defaultValue = arg.help.defaultValue.flatMap { $0.isEmpty ? nil : "(default: \($0))" } ?? "" + synopsis = arg.synopsisForHelp ?? "" + description = [arg.help.help?.abstract, defaultValue] + .compactMap { $0 } + .joined(separator: " ") + } + + let element = Section.Element(label: synopsis, abstract: description, discussion: arg.help.help?.discussion ?? "") + if !alreadySeenElements.contains(element) { + alreadySeenElements.insert(element) + if case .positional = arg.kind { + positionalElements.append(element) + } else { + optionElements.append(element) + } + } + } + + if commandStack.contains(where: { !$0.configuration.version.isEmpty }) { + optionElements.append(.init(label: "--version", abstract: "Show the version.")) + } + + let helpLabels = commandStack + .getHelpNames() + .map { $0.synopsisString } + .joined(separator: ", ") + if !helpLabels.isEmpty { + optionElements.append(.init(label: helpLabels, abstract: "Show help information.")) + } + + let configuration = commandStack.last!.configuration + let subcommandElements: [Section.Element] = + configuration.subcommands.compactMap { command in + guard command.configuration.shouldDisplay else { return nil } + var label = command._commandName + if command == configuration.defaultSubcommand { + label += " (default)" + } + return Section.Element( + label: label, + abstract: command.configuration.abstract) + } + + return [ + Section(header: .positionalArguments, elements: positionalElements), + Section(header: .options, elements: optionElements), + Section(header: .subcommands, elements: subcommandElements), + ] + } + + func usageMessage(screenWidth: Int? = nil) -> String { + let screenWidth = screenWidth ?? HelpGenerator.systemScreenWidth + return "Usage: \(usage.rendered(screenWidth: screenWidth))" + } + + var includesSubcommands: Bool { + guard let subcommandSection = sections.first(where: { $0.header == .subcommands }) + else { return false } + return !subcommandSection.elements.isEmpty + } + + func rendered(screenWidth: Int? = nil) -> String { + let screenWidth = screenWidth ?? HelpGenerator.systemScreenWidth + let renderedSections = sections + .map { $0.rendered(screenWidth: screenWidth) } + .filter { !$0.isEmpty } + .joined(separator: "\n") + let renderedAbstract = abstract.isEmpty + ? "" + : "OVERVIEW: \(abstract)".wrapped(to: screenWidth) + "\n\n" + + var helpSubcommandMessage: String = "" + if includesSubcommands { + var names = commandStack.map { $0._commandName } + if let superName = commandStack.first!.configuration._superCommandName { + names.insert(superName, at: 0) + } + names.insert("help", at: 1) + + helpSubcommandMessage = """ + + See '\(names.joined(separator: " ")) ' for detailed help. + """ + } + + return """ + \(renderedAbstract)\ + USAGE: \(usage.rendered(screenWidth: screenWidth)) + + \(renderedSections)\(helpSubcommandMessage) + """ + } +} + +fileprivate extension CommandConfiguration { + static var defaultHelpNames: NameSpecification { [.short, .long] } +} + +fileprivate extension NameSpecification { + func generateHelpNames() -> [Name] { + return self.makeNames(InputKey(rawValue: "help")).sorted(by: >) + } +} + +internal extension BidirectionalCollection where Element == ParsableCommand.Type { + func getHelpNames() -> [Name] { + return self.last(where: { $0.configuration.helpNames != nil }) + .map { $0.configuration.helpNames!.generateHelpNames() } + ?? CommandConfiguration.defaultHelpNames.generateHelpNames() + } + + func getPrimaryHelpName() -> Name? { + let names = getHelpNames() + return names.first(where: { !$0.isShort }) ?? names.first + } +} + +#if canImport(Glibc) +import Glibc +func ioctl(_ a: Int32, _ b: Int32, _ p: UnsafeMutableRawPointer) -> Int32 { + ioctl(CInt(a), UInt(b), p) +} +#elseif canImport(Darwin) +import Darwin +#elseif canImport(CRT) +import CRT +import WinSDK +#endif + +func _terminalSize() -> (width: Int, height: Int) { +#if os(Windows) + var csbi: CONSOLE_SCREEN_BUFFER_INFO = CONSOLE_SCREEN_BUFFER_INFO() + + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) + return (width: Int(csbi.srWindow.Right - csbi.srWindow.Left) + 1, + height: Int(csbi.srWindow.Bottom - csbi.srWindow.Top) + 1) +#else + var w = winsize() +#if os(OpenBSD) + // TIOCGWINSZ is a complex macro, so we need the flattened value. + let tiocgwinsz = Int32(0x40087468) + let err = ioctl(STDOUT_FILENO, tiocgwinsz, &w) +#else + let err = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) +#endif + let width = Int(w.ws_col) + let height = Int(w.ws_row) + guard err == 0 else { return (80, 25) } + return (width: width > 0 ? width : 80, + height: height > 0 ? height : 25) +#endif +} diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift new file mode 100644 index 0000000..85c764f --- /dev/null +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------*- 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, help: String) + case other(message: String, exitCode: Int32) + + 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 + + // Exit early on built-in requests + switch e.parserError { + case .helpRequested: + self = .help(text: HelpGenerator(commandStack: e.commandStack).rendered()) + return + + case .versionRequested: + let versionString = commandStack + .map { $0.configuration.version } + .last(where: { !$0.isEmpty }) + ?? "Unspecified version" + self = .help(text: versionString) + return + + case .completionScriptRequested(let shell): + do { + let completionsGenerator = try CompletionsGenerator(command: type.asCommand, shellName: shell) + self = .help(text: completionsGenerator.generateCompletionScript()) + return + } catch { + self.init(error: error, type: type) + return + } + + case .completionScriptCustomResponse(let output): + self = .help(text: output) + return + + default: + break + } + + case let e as ParserError: + // Send ParserErrors back through the CommandError path + self.init(error: CommandError(commandStack: [type.asCommand], parserError: e), type: type) + return + + default: + commandStack = [type.asCommand] + // if the error wasn't one of our two Error types, wrap it as a userValidationError + // to be handled appropriately below + parserError = .userValidationError(error) + } + + var usage = HelpGenerator(commandStack: commandStack).usageMessage() + + let commandNames = commandStack.map { $0._commandName }.joined(separator: " ") + if let helpName = commandStack.getPrimaryHelpName() { + usage += "\n See '\(commandNames) \(helpName.synopsisString)' for more information." + } + + // 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, help: "") + 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 ExitCode: + self = .other(message: "", exitCode: error.rawValue) + case let error as LocalizedError where error.errorDescription != nil: + self = .other(message: error.errorDescription!, exitCode: EXIT_FAILURE) + default: + if Swift.type(of: error) is NSError.Type { + self = .other(message: error.localizedDescription, exitCode: EXIT_FAILURE) + } else { + self = .other(message: String(describing: error), exitCode: EXIT_FAILURE) + } + } + } else if let parserError = parserError { + let usage: String = { + guard case ParserError.noArguments = parserError else { return usage } + return "\n" + HelpGenerator(commandStack: [type.asCommand]).rendered() + }() + let argumentSet = ArgumentSet(commandStack.last!) + let message = argumentSet.errorDescription(error: parserError) ?? "" + let helpAbstract = argumentSet.helpDescription(error: parserError) ?? "" + self = .validation(message: message, usage: usage, help: helpAbstract) + } else { + self = .other(message: String(describing: error), exitCode: EXIT_FAILURE) + } + } + + var message: String { + switch self { + case .help(text: let text): + return text + case .validation(message: let message, usage: _, help: _): + return message + case .other(let message, _): + return message + } + } + + func fullText(for args: ParsableArguments.Type) -> String { + switch self { + case .help(text: let text): + return text + case .validation(message: let message, usage: let usage, help: let help): + let helpMessage = help.isEmpty ? "" : "Help: \(help)\n" + let errorMessage = message.isEmpty ? "" : "\(args._errorLabel): \(message)\n" + return errorMessage + helpMessage + usage + case .other(let message, _): + return message.isEmpty ? "" : "\(args._errorLabel): \(message)" + } + } + + var shouldExitCleanly: Bool { + switch self { + case .help: return true + case .validation, .other: return false + } + } + + var exitCode: ExitCode { + switch self { + case .help: return ExitCode.success + case .validation: return ExitCode.validationFailure + case .other(_, let code): return ExitCode(code) + } + } +} diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift new file mode 100644 index 0000000..1232c2c --- /dev/null +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -0,0 +1,428 @@ +//===----------------------------------------------------------*- 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(sets: 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: + // When we have too many options, keep required and positional arguments, + // but discard the rest. + let synopsis: [String] = definition.compactMap { argument in + guard argument.isPositional || !argument.help.options.contains(.isOptional) else { + return nil + } + return argument.synopsis + } + if !synopsis.isEmpty, synopsis.count <= 12 { + return "\(toolName) [] \(synopsis.joined(separator: " "))" + } + 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 = partitionedNames + .map { $0.synopsisString } + .joined(separator: ", ") + + switch update { + case .unary: + return "\(joinedSynopsisString) <\(synopsisValueName ?? "")>" + case .nullary: + return joinedSynopsisString + } + case .positional: + return "<\(valueName)>" + case .default: + return "" + } + } + + 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)>" + case .default: + return "" + } + } + + 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 partitionedNames: [Name] { + return names.filter{ $0.isShort } + names.filter{ !$0.isShort } + } + + var preferredNameForSynopsis: Name? { + names.first { !$0.isShort } ?? names.first + } + + var synopsisValueName: String? { + valueName + } +} + +extension ArgumentSet { + /// Will generate a descriptive help message if possible. + /// + /// If no descriptive 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 + } + } + + func helpDescription(error: Swift.Error) -> String? { + switch error { + case let parserError as ParserError: + return ErrorMessageGenerator(arguments: self, error: parserError) + .makeHelpMessage() + case let commandError as CommandError: + return ErrorMessageGenerator(arguments: self, error: commandError.parserError) + .makeHelpMessage() + default: + return nil + } + } +} + +struct ErrorMessageGenerator { + var arguments: ArgumentSet + var error: ParserError +} + +extension ErrorMessageGenerator { + func makeErrorMessage() -> String? { + switch error { + case .helpRequested, .versionRequested, .completionScriptRequested, .completionScriptCustomResponse: + return nil + + case .unsupportedShell(let shell?): + return unsupportedShell(shell) + case .unsupportedShell: + return unsupportedAutodetectedShell + + 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, originalInput: let arguments): + return duplicateExclusiveValues(previous: previous, duplicate: duplicate, arguments: arguments) + case .noValue(forKey: let k): + return noValueMessage(key: k) + case .unableToParseValue(let o, let n, let v, forKey: let k, originalError: let e): + return unableToParseValueMessage(origin: o, name: n, value: v, key: k, error: e) + 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) + } + case .noArguments(let error): + switch error { + case let error as ParserError: + return ErrorMessageGenerator(arguments: self.arguments, error: error).makeErrorMessage() + case let error as LocalizedError: + return error.errorDescription + default: + return String(describing: error) + } + } + } + + func makeHelpMessage() -> String? { + switch error { + case .unableToParseValue(let o, let n, let v, forKey: let k, originalError: let e): + return unableToParseHelpMessage(origin: o, name: n, value: v, key: k, error: e) + default: + return nil + } + } +} + +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." + } + + var unsupportedAutodetectedShell: String { + """ + Can't autodetect a supported shell. + Please use --generate-completion-script= with one of: + \(CompletionShell.allCases.map { $0.rawValue }.joined(separator: " ")) + """ + } + + func unsupportedShell(_ shell: String) -> String { + """ + Can't generate completion scripts for '\(shell)'. + Please use --generate-completion-script= with one of: + \(CompletionShell.allCases.map { $0.rawValue }.joined(separator: " ")) + """ + } + + func unknownOptionMessage(origin: InputOrigin.Element, name: Name) -> String { + if case .short = name { + return "Unknown option '\(name.synopsisString)'" + } + + // An empirically derived magic number + let SIMILARITY_FLOOR = 4 + + let notShort: (Name) -> Bool = { (name: Name) in + switch name { + case .short: return false + case .long: return true + case .longWithSingleDash: return true + } + } + let suggestion = arguments + .flatMap({ $0.names }) + .filter({ $0.synopsisString.editDistance(to: name.synopsisString) < SIMILARITY_FLOOR }) // only include close enough suggestion + .filter(notShort) // exclude short option suggestions + .min(by: { lhs, rhs in // find the suggestion closest to the argument + lhs.synopsisString.editDistance(to: name.synopsisString) < rhs.synopsisString.editDistance(to: name.synopsisString) + }) + + if let suggestion = suggestion { + return "Unknown option '\(name.synopsisString)'. Did you mean '\(suggestion.synopsisString)'?" + } + return "Unknown option '\(name.synopsisString)'" + } + + 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, arguments: [String]) -> String? { + func elementString(_ origin: InputOrigin, _ arguments: [String]) -> String? { + guard case .argumentIndex(let split) = origin.elements.first else { return nil } + var argument = "\'\(arguments[split.inputIndex.rawValue])\'" + if case let .sub(offsetIndex) = split.subIndex { + let stringIndex = argument.index(argument.startIndex, offsetBy: offsetIndex+2) + argument = "\'\(argument[stringIndex])\' in \(argument)" + } + return "flag \(argument)" + } + + // Note that the RHS of these coalescing operators cannot be reached at this time. + let dupeString = elementString(duplicate, arguments) ?? "position \(duplicate)" + let origString = elementString(previous, arguments) ?? "position \(previous)" + + //TODO: review this message once environment values are supported. + return "Value to be set with \(dupeString) had already been set with \(origString)" + } + + func noValueMessage(key: InputKey) -> String? { + let args = arguments(for: key) + let possibilities = args.compactMap { + $0.nonOptional.synopsis + } + switch possibilities.count { + case 0: + return "No value set for non-argument var \(key). Replace with a static variable, or let constant." + case 1: + return "Missing expected argument '\(possibilities.first!)'" + default: + let p = possibilities.joined(separator: "', '") + return "Missing one of: '\(p)'" + } + } + + func unableToParseHelpMessage(origin: InputOrigin, name: Name?, value: String, key: InputKey, error: Error?) -> String { + guard let abstract = help(for: key)?.help?.abstract else { return "" } + + let valueName = arguments(for: key).first?.valueName + + switch (name, valueName) { + case let (n?, v?): + return "\(n.synopsisString) <\(v)> \(abstract)" + case let (_, v?): + return "<\(v)> \(abstract)" + case (_, _): + return "" + } + } + + func unableToParseValueMessage(origin: InputOrigin, name: Name?, value: String, key: InputKey, error: Error?) -> String { + let valueName = arguments(for: key).first?.valueName + + // We want to make the "best effort" in producing a custom error message. + // We favour `LocalizedError.errorDescription` and fall back to + // `CustomStringConvertible`. To opt in, return your custom error message + // as the `description` property of `CustomStringConvertible`. + let customErrorMessage: String = { + switch error { + case let err as LocalizedError where err.errorDescription != nil: + return ": " + err.errorDescription! // !!! Checked above that this will not be nil + case let err?: + return ": " + String(describing: err) + default: + return "" + } + }() + + switch (name, valueName) { + case let (n?, v?): + return "The value '\(value)' is invalid for '\(n.synopsisString) <\(v)>'\(customErrorMessage)" + case let (_, v?): + return "The value '\(value)' is invalid for '<\(v)>'\(customErrorMessage)" + case let (n?, _): + return "The value '\(value)' is invalid for '\(n.synopsisString)'\(customErrorMessage)" + case (nil, nil): + return "The value '\(value)' is invalid.\(customErrorMessage)" + } + } +} diff --git a/Sources/ArgumentParser/Utilities/SequenceExtensions.swift b/Sources/ArgumentParser/Utilities/SequenceExtensions.swift new file mode 100644 index 0000000..f36c124 --- /dev/null +++ b/Sources/ArgumentParser/Utilities/SequenceExtensions.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------*- 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 Sequence where Element: Hashable { + /// Returns an array with only the unique elements of this sequence, in the + /// order of the first occurence of each unique element. + func uniquing() -> [Element] { + var seen = Set() + return self.filter { seen.insert($0).0 } + } + + /// Returns an array, collapsing runs of consecutive equal elements into + /// the first element of each run. + /// + /// [1, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1].uniquingAdjacentElements() + /// // [1, 2, 3, 2, 1] + func uniquingAdjacentElements() -> [Element] { + var iterator = makeIterator() + guard let first = iterator.next() + else { return [] } + + var result = [first] + while let element = iterator.next() { + if result.last != element { + result.append(element) + } + } + return result + } +} diff --git a/Sources/ArgumentParser/Utilities/StringExtensions.swift b/Sources/ArgumentParser/Utilities/StringExtensions.swift new file mode 100644 index 0000000..c402d3e --- /dev/null +++ b/Sources/ArgumentParser/Utilities/StringExtensions.swift @@ -0,0 +1,174 @@ +//===----------------------------------------------------------*- 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(contentsOf: 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 } + var result = "" + // Whether we should append a separator when we see a uppercase character. + var separateOnUppercase = true + for index in indices { + let nextIndex = self.index(after: index) + let character = self[index] + if character.isUppercase { + if separateOnUppercase && !result.isEmpty { + // Append the separator. + result += "\(separator)" + } + // If the next character is uppercase and the next-next character is lowercase, like "L" in "URLSession", we should separate words. + separateOnUppercase = nextIndex < endIndex && self[nextIndex].isUppercase && self.index(after: nextIndex) < endIndex && self[self.index(after: nextIndex)].isLowercase + } else { + // If the character is `separator`, we do not want to append another separator when we see the next uppercase character. + separateOnUppercase = character != separator + } + // Append the lowercased character. + result += character.lowercased() + } + return result + } + + /// Returns the edit distance between this string and the provided target string. + /// + /// Uses the Levenshtein distance algorithm internally. + /// + /// See: https://en.wikipedia.org/wiki/Levenshtein_distance + /// + /// Examples: + /// + /// "kitten".editDistance(to: "sitting") + /// // 3 + /// "bar".editDistance(to: "baz") + /// // 1 + + func editDistance(to target: String) -> Int { + let rows = self.count + let columns = target.count + + if rows <= 0 || columns <= 0 { + return max(rows, columns) + } + + var matrix = Array(repeating: Array(repeating: 0, count: columns + 1), count: rows + 1) + + for row in 1...rows { + matrix[row][0] = row + } + for column in 1...columns { + matrix[0][column] = column + } + + for row in 1...rows { + for column in 1...columns { + let source = self[self.index(self.startIndex, offsetBy: row - 1)] + let target = target[target.index(target.startIndex, offsetBy: column - 1)] + let cost = source == target ? 0 : 1 + + matrix[row][column] = Swift.min( + matrix[row - 1][column] + 1, + matrix[row][column - 1] + 1, + matrix[row - 1][column - 1] + cost + ) + } + } + + return matrix.last!.last! + } + + func indentingEachLine(by n: Int) -> String { + let hasTrailingNewline = self.last == "\n" + let lines = self.split(separator: "\n", omittingEmptySubsequences: false) + if hasTrailingNewline && lines.last == "" { + return lines.dropLast().map { String(repeating: " ", count: n) + $0 } + .joined(separator: "\n") + "\n" + } else { + return lines.map { String(repeating: " ", count: n) + $0 } + .joined(separator: "\n") + } + } +} diff --git a/Sources/ArgumentParser/Utilities/Tree.swift b/Sources/ArgumentParser/Utilities/Tree.swift new file mode 100644 index 0000000..fe40fc0 --- /dev/null +++ b/Sources/ArgumentParser/Utilities/Tree.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +final class Tree { + 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) throws { + self.init(command) + for subcommand in command.configuration.subcommands { + if subcommand == command { + throw InitializationError.recursiveSubcommand(subcommand) + } + try addChild(Tree(root: subcommand)) + } + } + + enum InitializationError: Error { + case recursiveSubcommand(ParsableCommand.Type) + } +} diff --git a/Sources/ArgumentParserTestHelpers/CMakeLists.txt b/Sources/ArgumentParserTestHelpers/CMakeLists.txt new file mode 100644 index 0000000..ae4e69d --- /dev/null +++ b/Sources/ArgumentParserTestHelpers/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(ArgumentParserTestHelpers + StringHelpers.swift + TestHelpers.swift) +set_target_properties(ArgumentParserTestHelpers PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) +target_link_libraries(ArgumentParserTestHelpers PUBLIC + ArgumentParser + XCTest + Foundation) diff --git a/Sources/ArgumentParserTestHelpers/StringHelpers.swift b/Sources/ArgumentParserTestHelpers/StringHelpers.swift new file mode 100644 index 0000000..f014c53 --- /dev/null +++ b/Sources/ArgumentParserTestHelpers/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/ArgumentParserTestHelpers/TestHelpers.swift b/Sources/ArgumentParserTestHelpers/TestHelpers.swift new file mode 100644 index 0000000..57a3bd0 --- /dev/null +++ b/Sources/ArgumentParserTestHelpers/TestHelpers.swift @@ -0,0 +1,201 @@ +//===----------------------------------------------------------*- 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 + +// extensions 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() + } +} + +// extensions to the ParsableCommand protocol to facilitate XCTestExpectation support +public protocol TestableParsableCommand: ParsableCommand, TestableParsableArguments { + var didRunExpectation: XCTestExpectation { get } +} + +public extension TestableParsableCommand { + mutating 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(file: (file), line: line) + } catch { + let helpString = T.fullMessage(for: error) + AssertEqualStringsIgnoringTrailingWhitespace( + helpString, expected, file: file, line: line) + } + + let helpString = T.helpMessage() + AssertEqualStringsIgnoringTrailingWhitespace( + helpString, expected, file: file, line: line) +} + +public func AssertHelp( + for _: T.Type, root _: U.Type, equals expected: String, + file: StaticString = #file, line: UInt = #line +) { + let helpString = U.helpMessage(for: T.self) + 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, + exitCode: ExitCode = .success, + 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() + if #available(macOS 10.13, *) { + process.executableURL = commandURL + } else { + process.launchPath = commandURL.path + } + process.arguments = arguments + + let output = Pipe() + process.standardOutput = output + let error = Pipe() + process.standardError = error + + if #available(macOS 10.13, *) { + guard (try? process.run()) != nil else { + XCTFail("Couldn't run command process.", file: (file), line: line) + return + } + } else { + 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) + } + + XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: (file), line: line) + } +} diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt new file mode 100644 index 0000000..a98ace3 --- /dev/null +++ b/Sources/CMakeLists.txt @@ -0,0 +1,4 @@ +add_subdirectory(ArgumentParser) +if(BUILD_TESTING) + add_subdirectory(ArgumentParserTestHelpers) +endif() diff --git a/Tests/ArgumentParserEndToEndTests/CMakeLists.txt b/Tests/ArgumentParserEndToEndTests/CMakeLists.txt new file mode 100644 index 0000000..d89093b --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(EndToEndTests + CustomParsingEndToEndTests.swift + DefaultsEndToEndTests.swift + EnumEndToEndTests.swift + FlagsEndToEndTests.swift + JoinedEndToEndTests.swift + LongNameWithShortDashEndToEndTests.swift + NestedCommandEndToEndTests.swift + OptionalEndToEndTests.swift + OptionGroupEndToEndTests.swift + PositionalEndToEndTests.swift + RawRepresentableEndToEndTests.swift + RepeatingEndToEndTests.swift + ShortNameEndToEndTests.swift + SimpleEndToEndTests.swift + SingleValueParsingStrategyTests.swift + SubcommandEndToEndTests.swift + ValidationEndToEndTests.swift) +target_link_libraries(EndToEndTests PUBLIC + ArgumentParserTestHelpers) diff --git a/Tests/ArgumentParserEndToEndTests/CustomParsingEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/CustomParsingEndToEndTests.swift new file mode 100644 index 0000000..57d95f4 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +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(transform: { try Name(rawValue: $0) }) + var firstName: Name = try! Name(rawValue: "none") + + @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/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift new file mode 100644 index 0000000..a603d60 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +final class DefaultSubcommandEndToEndTests: XCTestCase { +} + +// MARK: - + +private struct Main: ParsableCommand { + static var configuration = CommandConfiguration( + subcommands: [Default.self, Foo.self, Bar.self], + defaultSubcommand: Default.self + ) +} + +private struct Default: ParsableCommand { + enum Mode: String, CaseIterable, ExpressibleByArgument { + case foo, bar, baz + } + + @Option var mode: Mode = .foo +} + +private struct Foo: ParsableCommand {} +private struct Bar: ParsableCommand {} + +extension DefaultSubcommandEndToEndTests { + func testDefaultSubcommand() { + AssertParseCommand(Main.self, Default.self, []) { def in + XCTAssertEqual(.foo, def.mode) + } + + AssertParseCommand(Main.self, Default.self, ["--mode=bar"]) { def in + XCTAssertEqual(.bar, def.mode) + } + + AssertParseCommand(Main.self, Default.self, ["--mode", "bar"]) { def in + XCTAssertEqual(.bar, def.mode) + } + + AssertParseCommand(Main.self, Default.self, ["--mode", "baz"]) { def in + XCTAssertEqual(.baz, def.mode) + } + } + + func testNonDefaultSubcommand() { + AssertParseCommand(Main.self, Foo.self, ["foo"]) { _ in } + AssertParseCommand(Main.self, Bar.self, ["bar"]) { _ in } + + AssertParseCommand(Main.self, Default.self, ["default", "--mode", "bar"]) { def in + XCTAssertEqual(.bar, def.mode) + } + } + + func testParsingFailure() { + XCTAssertThrowsError(try Main.parseAsRoot(["--mode", "qux"])) + XCTAssertThrowsError(try Main.parseAsRoot(["qux"])) + } +} diff --git a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift new file mode 100644 index 0000000..dd85025 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift @@ -0,0 +1,779 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +final class DefaultsEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Foo: ParsableArguments { + struct Name: RawRepresentable, ExpressibleByArgument { + var rawValue: String + } + @Option + var name: Name = Name(rawValue: "A") + @Option + var max: Int = 3 +} + +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 + var name: String = "N" + @Option + var format: Format = .A + @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(parsing: .unconditional) + var name: String = "N" + @Option(parsing: .unconditional) + var format: Format = .A + @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(parsing: .unconditional) var int: Int = 0 + @Option(parsing: .unconditional) var int8: Int8 = 0 + @Option(parsing: .unconditional) var int16: Int16 = 0 + @Option(parsing: .unconditional) var int32: Int32 = 0 + @Option(parsing: .unconditional) var int64: Int64 = 0 + @Option var uint: UInt = 0 + @Option var uint8: UInt8 = 0 + @Option var uint16: UInt16 = 0 + @Option var uint32: UInt32 = 0 + @Option var uint64: UInt64 = 0 + + @Option(parsing: .unconditional) var float: Float = 0 + @Option(parsing: .unconditional) var double: Double = 0 + + @Option var bool: Bool = false +} + +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"])) + } +} + +fileprivate struct Qux: ParsableArguments { + @Argument + var name: String = "quux" +} + +extension DefaultsEndToEndTests { + func testParsing_ArgumentDefaults() throws { + AssertParse(Qux.self, []) { qux in + XCTAssertEqual(qux.name, "quux") + } + AssertParse(Qux.self, ["Bar"]) { qux in + XCTAssertEqual(qux.name, "Bar") + } + AssertParse(Qux.self, ["Bar-"]) { qux in + XCTAssertEqual(qux.name, "Bar-") + } + AssertParse(Qux.self, ["Bar--"]) { qux in + XCTAssertEqual(qux.name, "Bar--") + } + AssertParse(Qux.self, ["--", "-Bar"]) { qux in + XCTAssertEqual(qux.name, "-Bar") + } + AssertParse(Qux.self, ["--", "--Bar"]) { qux in + XCTAssertEqual(qux.name, "--Bar") + } + AssertParse(Qux.self, ["--", "--"]) { qux in + XCTAssertEqual(qux.name, "--") + } + } + + func testParsing_ArgumentDefaults_Fails() throws { + XCTAssertThrowsError(try Qux.parse(["--name"])) + XCTAssertThrowsError(try Qux.parse(["Foo", "Bar"])) + } +} + +fileprivate func exclaim(_ input: String) throws -> String { + return input + "!" +} + +fileprivate struct OptionPropertyInitArguments_Default: ParsableArguments { + @Option + var data: String = "test" + + @Option(transform: exclaim) + var transformedData: String = "test" +} + +fileprivate struct OptionPropertyInitArguments_NoDefault_NoTransform: ParsableArguments { + @Option() + var data: String +} + +fileprivate struct OptionPropertyInitArguments_NoDefault_Transform: ParsableArguments { + @Option(transform: exclaim) + var transformedData: String +} + +extension DefaultsEndToEndTests { + /// Tests that using default property initialization syntax parses the default value for the argument when nothing is provided from the command-line. + func testParsing_OptionPropertyInit_Default_NoTransform_UseDefault() throws { + AssertParse(OptionPropertyInitArguments_Default.self, []) { arguments in + XCTAssertEqual(arguments.data, "test") + } + } + + /// Tests that using default property initialization syntax parses the command-line-provided value for the argument when provided. + func testParsing_OptionPropertyInit_Default_NoTransform_OverrideDefault() throws { + AssertParse(OptionPropertyInitArguments_Default.self, ["--data", "test2"]) { arguments in + XCTAssertEqual(arguments.data, "test2") + } + } + + /// Tests that *not* providing a default value still parses the argument correctly from the command-line. + /// This test is almost certainly duplicated by others in the repository, but allows for quick use of test filtering during development on the initialization functionality. + func testParsing_OptionPropertyInit_NoDefault_NoTransform() throws { + AssertParse(OptionPropertyInitArguments_NoDefault_NoTransform.self, ["--data", "test"]) { arguments in + XCTAssertEqual(arguments.data, "test") + } + } + + /// Tests that using default property initialization syntax on a property with a `transform` function provided parses the default value for the argument when nothing is provided from the command-line. + func testParsing_OptionPropertyInit_Default_Transform_UseDefault() throws { + AssertParse(OptionPropertyInitArguments_Default.self, []) { arguments in + XCTAssertEqual(arguments.transformedData, "test") + } + } + + /// Tests that using default property initialization syntax on a property with a `transform` function provided parses and transforms the command-line-provided value for the argument when provided. + func testParsing_OptionPropertyInit_Default_Transform_OverrideDefault() throws { + AssertParse(OptionPropertyInitArguments_Default.self, ["--transformed-data", "test2"]) { arguments in + XCTAssertEqual(arguments.transformedData, "test2!") + } + } + + /// Tests that *not* providing a default value for a property with a `transform` function still parses the argument correctly from the command-line. + /// This test is almost certainly duplicated by others in the repository, but allows for quick use of test filtering during development on the initialization functionality. + func testParsing_OptionPropertyInit_NoDefault_Transform() throws { + AssertParse(OptionPropertyInitArguments_NoDefault_Transform.self, ["--transformed-data", "test"]) { arguments in + XCTAssertEqual(arguments.transformedData, "test!") + } + } +} + + +fileprivate struct ArgumentPropertyInitArguments_Default_NoTransform: ParsableArguments { + @Argument + var data: String = "test" +} + +fileprivate struct ArgumentPropertyInitArguments_NoDefault_NoTransform: ParsableArguments { + @Argument() + var data: String +} + +fileprivate struct ArgumentPropertyInitArguments_Default_Transform: ParsableArguments { + @Argument(transform: exclaim) + var transformedData: String = "test" +} + +fileprivate struct ArgumentPropertyInitArguments_NoDefault_Transform: ParsableArguments { + @Argument(transform: exclaim) + var transformedData: String +} + +extension DefaultsEndToEndTests { + /// Tests that using default property initialization syntax parses the default value for the argument when nothing is provided from the command-line. + func testParsing_ArgumentPropertyInit_Default_NoTransform_UseDefault() throws { + AssertParse(ArgumentPropertyInitArguments_Default_NoTransform.self, []) { arguments in + XCTAssertEqual(arguments.data, "test") + } + } + + /// Tests that using default property initialization syntax parses the command-line-provided value for the argument when provided. + func testParsing_ArgumentPropertyInit_Default_NoTransform_OverrideDefault() throws { + AssertParse(ArgumentPropertyInitArguments_Default_NoTransform.self, ["test2"]) { arguments in + XCTAssertEqual(arguments.data, "test2") + } + } + + /// Tests that *not* providing a default value still parses the argument correctly from the command-line. + /// This test is almost certainly duplicated by others in the repository, but allows for quick use of test filtering during development on the initialization functionality. + func testParsing_ArgumentPropertyInit_NoDefault_NoTransform() throws { + AssertParse(ArgumentPropertyInitArguments_NoDefault_NoTransform.self, ["test"]) { arguments in + XCTAssertEqual(arguments.data, "test") + } + } + + /// Tests that using default property initialization syntax on a property with a `transform` function provided parses the default value for the argument when nothing is provided from the command-line. + func testParsing_ArgumentPropertyInit_Default_Transform_UseDefault() throws { + AssertParse(ArgumentPropertyInitArguments_Default_Transform.self, []) { arguments in + XCTAssertEqual(arguments.transformedData, "test") + } + } + + /// Tests that using default property initialization syntax on a property with a `transform` function provided parses and transforms the command-line-provided value for the argument when provided. + func testParsing_ArgumentPropertyInit_Default_Transform_OverrideDefault() throws { + AssertParse(ArgumentPropertyInitArguments_Default_Transform.self, ["test2"]) { arguments in + XCTAssertEqual(arguments.transformedData, "test2!") + } + } + + /// Tests that *not* providing a default value for a property with a `transform` function still parses the argument correctly from the command-line. + /// This test is almost certainly duplicated by others in the repository, but allows for quick use of test filtering during development on the initialization functionality. + func testParsing_ArgumentPropertyInit_NoDefault_Transform() throws { + AssertParse(ArgumentPropertyInitArguments_NoDefault_Transform.self, ["test"]) { arguments in + XCTAssertEqual(arguments.transformedData, "test!") + } + } +} + +fileprivate struct Quux: ParsableArguments { + @Option(parsing: .upToNextOption) + var letters: [String] = ["A", "B"] + + @Argument() + var numbers: [Int] = [1, 2] +} + +extension DefaultsEndToEndTests { + func testParsing_ArrayDefaults() throws { + AssertParse(Quux.self, []) { qux in + XCTAssertEqual(qux.letters, ["A", "B"]) + XCTAssertEqual(qux.numbers, [1, 2]) + } + AssertParse(Quux.self, ["--letters", "C", "D"]) { qux in + XCTAssertEqual(qux.letters, ["C", "D"]) + XCTAssertEqual(qux.numbers, [1, 2]) + } + AssertParse(Quux.self, ["3", "4"]) { qux in + XCTAssertEqual(qux.letters, ["A", "B"]) + XCTAssertEqual(qux.numbers, [3, 4]) + } + AssertParse(Quux.self, ["3", "4", "--letters", "C", "D"]) { qux in + XCTAssertEqual(qux.letters, ["C", "D"]) + XCTAssertEqual(qux.numbers, [3, 4]) + } + } +} + +fileprivate struct FlagPropertyInitArguments_Bool_Default: ParsableArguments { + @Flag(inversion: .prefixedNo) + var data: Bool = false +} + +fileprivate struct FlagPropertyInitArguments_Bool_NoDefault: ParsableArguments { + @Flag(inversion: .prefixedNo) + var data: Bool +} + +extension DefaultsEndToEndTests { + /// Tests that using default property initialization syntax parses the default value for the argument when nothing is provided from the command-line. + func testParsing_FlagPropertyInit_Bool_Default_UseDefault() throws { + AssertParse(FlagPropertyInitArguments_Bool_Default.self, []) { arguments in + XCTAssertEqual(arguments.data, false) + } + } + + /// Tests that using default property initialization syntax parses the command-line-provided value for the argument when provided. + func testParsing_FlagPropertyInit_Bool_Default_OverrideDefault() throws { + AssertParse(FlagPropertyInitArguments_Bool_Default.self, ["--data"]) { arguments in + XCTAssertEqual(arguments.data, true) + } + } + + /// Tests that *not* providing a default value still parses the argument correctly from the command-line. + /// This test is almost certainly duplicated by others in the repository, but allows for quick use of test filtering during development on the initialization functionality. + func testParsing_FlagPropertyInit_Bool_NoDefault() throws { + AssertParse(FlagPropertyInitArguments_Bool_NoDefault.self, ["--data"]) { arguments in + XCTAssertEqual(arguments.data, true) + } + } +} + + +fileprivate enum HasData: EnumerableFlag { + case noData + case data +} + +fileprivate struct FlagPropertyInitArguments_EnumerableFlag_Default: ParsableArguments { + @Flag + var data: HasData = .noData +} + +fileprivate struct FlagPropertyInitArguments_EnumerableFlag_NoDefault: ParsableArguments { + @Flag() + var data: HasData +} + + +extension DefaultsEndToEndTests { + /// Tests that using default property initialization syntax parses the default value for the argument when nothing is provided from the command-line. + func testParsing_FlagPropertyInit_EnumerableFlag_Default_UseDefault() throws { + AssertParse(FlagPropertyInitArguments_EnumerableFlag_Default.self, []) { arguments in + XCTAssertEqual(arguments.data, .noData) + } + } + + /// Tests that using default property initialization syntax parses the command-line-provided value for the argument when provided. + func testParsing_FlagPropertyInit_EnumerableFlag_Default_OverrideDefault() throws { + AssertParse(FlagPropertyInitArguments_EnumerableFlag_Default.self, ["--data"]) { arguments in + XCTAssertEqual(arguments.data, .data) + } + } + + /// Tests that *not* providing a default value still parses the argument correctly from the command-line. + /// This test is almost certainly duplicated by others in the repository, but allows for quick use of test filtering during development on the initialization functionality. + func testParsing_FlagPropertyInit_EnumerableFlag_NoDefault() throws { + AssertParse(FlagPropertyInitArguments_EnumerableFlag_NoDefault.self, ["--data"]) { arguments in + XCTAssertEqual(arguments.data, .data) + } + } +} + +fileprivate struct Main: ParsableCommand { + static var configuration = CommandConfiguration( + subcommands: [Sub.self], + defaultSubcommand: Sub.self + ) + + struct Options: ParsableArguments { + @Option(parsing: .upToNextOption) + var letters: [String] = ["A", "B"] + } + + struct Sub: ParsableCommand { + @Argument() + var numbers: [Int] = [1, 2] + + @OptionGroup() + var options: Main.Options + } +} + +extension DefaultsEndToEndTests { + func testParsing_ArrayDefaults_Subcommands() { + AssertParseCommand(Main.self, Main.Sub.self, []) { sub in + XCTAssertEqual(sub.options.letters, ["A", "B"]) + XCTAssertEqual(sub.numbers, [1, 2]) + } + AssertParseCommand(Main.self, Main.Sub.self, ["--letters", "C", "D"]) { sub in + XCTAssertEqual(sub.options.letters, ["C", "D"]) + XCTAssertEqual(sub.numbers, [1, 2]) + } + AssertParseCommand(Main.self, Main.Sub.self, ["3", "4"]) { sub in + XCTAssertEqual(sub.options.letters, ["A", "B"]) + XCTAssertEqual(sub.numbers, [3, 4]) + } + AssertParseCommand(Main.self, Main.Sub.self, ["3", "4", "--letters", "C", "D"]) { sub in + XCTAssertEqual(sub.options.letters, ["C", "D"]) + XCTAssertEqual(sub.numbers, [3, 4]) + } + } +} + + +fileprivate struct RequiredArray_Option_NoTransform: ParsableArguments { + @Option(parsing: .remaining) + var array: [String] +} + +fileprivate struct RequiredArray_Option_Transform: ParsableArguments { + @Option(parsing: .remaining, transform: exclaim) + var array: [String] +} + +fileprivate struct RequiredArray_Argument_NoTransform: ParsableArguments { + @Argument() + var array: [String] +} + +fileprivate struct RequiredArray_Argument_Transform: ParsableArguments { + @Argument(transform: exclaim) + var array: [String] +} + +fileprivate struct RequiredArray_Flag: ParsableArguments { + @Flag + var array: [HasData] +} + +extension DefaultsEndToEndTests { + /// Tests that not providing an argument for a required array option produces an error. + func testParsing_RequiredArray_Option_NoTransform_NoInput() { + XCTAssertThrowsError(try RequiredArray_Option_NoTransform.parse([])) + } + + /// Tests that providing a single argument for a required array option parses that value correctly. + func testParsing_RequiredArray_Option_NoTransform_SingleInput() { + AssertParse(RequiredArray_Option_NoTransform.self, ["--array", "1"]) { arguments in + XCTAssertEqual(arguments.array, ["1"]) + } + } + + /// Tests that providing multiple arguments for a required array option parses those values correctly. + func testParsing_RequiredArray_Option_NoTransform_MultipleInput() { + AssertParse(RequiredArray_Option_NoTransform.self, ["--array", "2", "3"]) { arguments in + XCTAssertEqual(arguments.array, ["2", "3"]) + } + } + + /// Tests that not providing an argument for a required array option with a transform produces an error. + func testParsing_RequiredArray_Option_Transform_NoInput() { + XCTAssertThrowsError(try RequiredArray_Option_Transform.parse([])) + } + + /// Tests that providing a single argument for a required array option with a transform parses that value correctly. + func testParsing_RequiredArray_Option_Transform_SingleInput() { + AssertParse(RequiredArray_Option_Transform.self, ["--array", "1"]) { arguments in + XCTAssertEqual(arguments.array, ["1!"]) + } + } + + /// Tests that providing multiple arguments for a required array option with a transform parses those values correctly. + func testParsing_RequiredArray_Option_Transform_MultipleInput() { + AssertParse(RequiredArray_Option_Transform.self, ["--array", "2", "3"]) { arguments in + XCTAssertEqual(arguments.array, ["2!", "3!"]) + } + } + + + /// Tests that not providing an argument for a required array argument produces an error. + func testParsing_RequiredArray_Argument_NoTransform_NoInput() { + XCTAssertThrowsError(try RequiredArray_Argument_NoTransform.parse([])) + } + + /// Tests that providing a single argument for a required array argument parses that value correctly. + func testParsing_RequiredArray_Argument_NoTransform_SingleInput() { + AssertParse(RequiredArray_Argument_NoTransform.self, ["1"]) { arguments in + XCTAssertEqual(arguments.array, ["1"]) + } + } + + /// Tests that providing multiple arguments for a required array argument parses those values correctly. + func testParsing_RequiredArray_Argument_NoTransform_MultipleInput() { + AssertParse(RequiredArray_Argument_NoTransform.self, ["2", "3"]) { arguments in + XCTAssertEqual(arguments.array, ["2", "3"]) + } + } + + /// Tests that not providing an argument for a required array argument with a transform produces an error. + func testParsing_RequiredArray_Argument_Transform_NoInput() { + XCTAssertThrowsError(try RequiredArray_Argument_Transform.parse([])) + } + + /// Tests that providing a single argument for a required array argument with a transform parses that value correctly. + func testParsing_RequiredArray_Argument_Transform_SingleInput() { + AssertParse(RequiredArray_Argument_Transform.self, ["1"]) { arguments in + XCTAssertEqual(arguments.array, ["1!"]) + } + } + + /// Tests that providing multiple arguments for a required array argument with a transform parses those values correctly. + func testParsing_RequiredArray_Argument_Transform_MultipleInput() { + AssertParse(RequiredArray_Argument_Transform.self, ["2", "3"]) { arguments in + XCTAssertEqual(arguments.array, ["2!", "3!"]) + } + } + + + /// Tests that not providing an argument for a required array flag produces an error. + func testParsing_RequiredArray_Flag_NoInput() { + XCTAssertThrowsError(try RequiredArray_Flag.parse([])) + } + + /// Tests that providing a single argument for a required array flag parses that value correctly. + func testParsing_RequiredArray_Flag_SingleInput() { + AssertParse(RequiredArray_Flag.self, ["--data"]) { arguments in + XCTAssertEqual(arguments.array, [.data]) + } + } + + /// Tests that providing multiple arguments for a required array flag parses those values correctly. + func testParsing_RequiredArray_Flag_MultipleInput() { + AssertParse(RequiredArray_Flag.self, ["--data", "--no-data"]) { arguments in + XCTAssertEqual(arguments.array, [.data, .noData]) + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/EnumEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/EnumEndToEndTests.swift new file mode 100644 index 0000000..283cc90 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/EnumEndToEndTests.swift @@ -0,0 +1,96 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +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"])) + } +} + +// MARK: - + +fileprivate struct Baz: ParsableArguments { + enum Mode: String, CaseIterable, ExpressibleByArgument { + case generateBashScript = "generate-bash-script" + case generateZshScript + } + + @Option(name: .customLong("mode")) var modeOption: Mode? + @Argument() var modeArg: Mode? +} + +extension EnumEndToEndTests { + func test_ParsingRawValue_Option() throws { + AssertParse(Baz.self, ["--mode", "generate-bash-script"]) { baz in + XCTAssertEqual(baz.modeOption, .generateBashScript) + XCTAssertNil(baz.modeArg) + } + AssertParse(Baz.self, ["--mode", "generateZshScript"]) { baz in + XCTAssertEqual(baz.modeOption, .generateZshScript) + XCTAssertNil(baz.modeArg) + } + } + + func test_ParsingRawValue_Argument() throws { + AssertParse(Baz.self, ["generate-bash-script"]) { baz in + XCTAssertEqual(baz.modeArg, .generateBashScript) + XCTAssertNil(baz.modeOption) + } + AssertParse(Baz.self, ["generateZshScript"]) { baz in + XCTAssertEqual(baz.modeArg, .generateZshScript) + XCTAssertNil(baz.modeOption) + } + } + + func test_ParsingRawValue_Fails() throws { + XCTAssertThrowsError(try Baz.parse(["generateBashScript"])) + XCTAssertThrowsError(try Baz.parse(["--mode generateBashScript"])) + XCTAssertThrowsError(try Baz.parse(["generate-zsh-script"])) + XCTAssertThrowsError(try Baz.parse(["--mode generate-zsh-script"])) + } +} diff --git a/Tests/ArgumentParserEndToEndTests/EqualsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/EqualsEndToEndTests.swift new file mode 100644 index 0000000..5530775 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/EqualsEndToEndTests.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +final class EqualsEndToEndTests: XCTestCase { +} + +// MARK: .short name + +fileprivate struct Foo: ParsableArguments { + @Flag(name: .short) var toggle: Bool = false + @Option(name: .short) var name: String? + @Option(name: .short) var format: String +} + +extension EqualsEndToEndTests { + func testEquals_withShortName() throws { + AssertParse(Foo.self, ["-n=Name", "-f=Format"]) { foo in + XCTAssertEqual(foo.toggle, false) + XCTAssertEqual(foo.name, "Name") + XCTAssertEqual(foo.format, "Format") + } + } + + func testEquals_withCombinedShortName_1() throws { + AssertParse(Foo.self, ["-tf", "Format"]) { foo in + XCTAssertEqual(foo.toggle, true) + XCTAssertEqual(foo.name, nil) + XCTAssertEqual(foo.format, "Format") + } + } + + func testEquals_withCombinedShortName_2() throws { + XCTAssertThrowsError(try Foo.parse(["-tf=Format"])) + } +} + +// MARK: .shortAndLong name + +fileprivate struct Bar: ParsableArguments { + @Option(name: .shortAndLong) var name: String + @Option(name: .shortAndLong) var format: String +} + +extension EqualsEndToEndTests { + func testEquals_withShortAndLongName() throws { + AssertParse(Bar.self, ["-n=Name", "-f=Format"]) { bar in + XCTAssertEqual(bar.name, "Name") + XCTAssertEqual(bar.format, "Format") + } + } +} + +// MARK: .customShort name + +fileprivate struct Baz: ParsableArguments { + @Option(name: .customShort("i")) var name: String + @Option(name: .customShort("t")) var format: String +} + +extension EqualsEndToEndTests { + func testEquals_withCustomShortName() throws { + AssertParse(Baz.self, ["-i=Name", "-t=Format"]) { baz in + XCTAssertEqual(baz.name, "Name") + XCTAssertEqual(baz.format, "Format") + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift new file mode 100644 index 0000000..2ff0983 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift @@ -0,0 +1,332 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +final class FlagsEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Flag + var verbose: Bool = false + + @Flag(inversion: .prefixedNo) + var extattr: Bool = false + + @Flag(inversion: .prefixedNo, exclusivity: .exclusive) + var extattr2: Bool? + + @Flag(inversion: .prefixedEnableDisable, exclusivity: .chooseFirst) + var logging: Bool = false +} + +extension FlagsEndToEndTests { + func testParsing_defaultValue() throws { + AssertParse(Bar.self, []) { options in + XCTAssertEqual(options.verbose, false) + XCTAssertEqual(options.extattr, false) + XCTAssertEqual(options.extattr2, nil) + } + } + + func testParsing_settingValue() throws { + AssertParse(Bar.self, ["--verbose"]) { options in + XCTAssertEqual(options.verbose, true) + XCTAssertEqual(options.extattr, false) + XCTAssertEqual(options.extattr2, nil) + } + + AssertParse(Bar.self, ["--extattr"]) { options in + XCTAssertEqual(options.verbose, false) + XCTAssertEqual(options.extattr, true) + XCTAssertEqual(options.extattr2, nil) + } + + AssertParse(Bar.self, ["--extattr2"]) { options in + XCTAssertEqual(options.verbose, false) + XCTAssertEqual(options.extattr, false) + XCTAssertEqual(options.extattr2, .some(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) + } + AssertParse(Bar.self, ["--enable-logging"]) { options in + XCTAssertEqual(options.logging, true) + } + AssertParse(Bar.self, ["--no-extattr2", "--no-extattr2"]) { options in + XCTAssertEqual(options.extattr2, false) + } + AssertParse(Bar.self, ["--disable-logging", "--enable-logging"]) { options in + XCTAssertEqual(options.logging, false) + } + } +} + +fileprivate struct Foo: ParsableArguments { + @Flag(inversion: .prefixedEnableDisable) + var index: Bool = false + @Flag(inversion: .prefixedEnableDisable) + var sandbox: Bool = true + @Flag(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, EnumerableFlag { + case pink + case purple + case silver +} + +enum Size: String, EnumerableFlag { + case small + case medium + case large + case extraLarge + case humongous + + static func name(for value: Size) -> NameSpecification { + switch value { + case .small, .medium, .large: + return .shortAndLong + case .humongous: + return [.long, .customLong("huge")] + default: + return .long + } + } + + static func help(for value: Size) -> ArgumentHelp? { + switch value { + case .small: + return "A smallish size" + case .medium: + return "Not too big, not too small" + case .humongous: + return "Roughly the size of a barge" + case .large, .extraLarge: + return nil + } + } +} + +enum Shape: String, EnumerableFlag { + case round + case square + case oblong +} + +fileprivate struct Baz: ParsableArguments { + @Flag() + var color: Color + + @Flag + var size: Size = .small + + @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) + } + + AssertParse(Baz.self, ["--pink", "--humongous"]) { options in + XCTAssertEqual(options.color, .pink) + XCTAssertEqual(options.size, .humongous) + XCTAssertEqual(options.shape, nil) + } + + AssertParse(Baz.self, ["--pink", "--huge", "--humongous"]) { 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] = [.small, .medium] +} + +extension FlagsEndToEndTests { + func testParsingCaseIterableArray_Values() throws { + AssertParse(Qux.self, []) { options in + XCTAssertEqual(options.color, []) + XCTAssertEqual(options.size, [.small, .medium]) + } + AssertParse(Qux.self, ["--pink"]) { options in + XCTAssertEqual(options.color, [.pink]) + XCTAssertEqual(options.size, [.small, .medium]) + } + 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, [.small, .medium]) + } + } + + func testParsingCaseIterableArray_Fails() throws { + XCTAssertThrowsError(try Qux.parse(["--pink", "--small", "--bloop"])) + } +} + +fileprivate struct RepeatOK: ParsableArguments { + @Flag(exclusivity: .chooseFirst) + var color: Color + + @Flag(exclusivity: .chooseLast) + var shape: Shape + + @Flag(exclusivity: .exclusive) + var size: Size = .small +} + +extension FlagsEndToEndTests { + func testParsingCaseIterable_RepeatableFlags() throws { + AssertParse(RepeatOK.self, ["--pink", "--purple", "--square"]) { options in + XCTAssertEqual(options.color, .pink) + XCTAssertEqual(options.shape, .square) + } + + AssertParse(RepeatOK.self, ["--round", "--oblong", "--silver"]) { options in + XCTAssertEqual(options.color, .silver) + XCTAssertEqual(options.shape, .oblong) + } + + AssertParse(RepeatOK.self, ["--large", "--pink", "--round", "-l"]) { options in + XCTAssertEqual(options.size, .large) + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift new file mode 100644 index 0000000..09a7861 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift @@ -0,0 +1,193 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +final class JoinedEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Foo: ParsableArguments { + @Option(name: .customShort("f")) + var file = "" + + @Option(name: .customShort("d", allowingJoined: true)) + var debug = "" + + @Flag(name: .customLong("fdi", withSingleDash: true)) + var fdi = false +} + +extension JoinedEndToEndTests { + func testSingleValueParsing() throws { + AssertParse(Foo.self, []) { foo in + XCTAssertEqual(foo.file, "") + XCTAssertEqual(foo.debug, "") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-f", "file", "-d=Debug"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-f", "file", "-d", "Debug"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-f", "file", "-dDebug"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-dDebug", "-f", "file"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-dDebug"]) { foo in + XCTAssertEqual(foo.file, "") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-fd", "file", "Debug"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-fd", "file", "Debug", "-fdi"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, true) + } + + AssertParse(Foo.self, ["-fdi"]) { foo in + XCTAssertEqual(foo.file, "") + XCTAssertEqual(foo.debug, "") + XCTAssertEqual(foo.fdi, true) + } + } + + func testSingleValueParsing_Fails() throws { + XCTAssertThrowsError(try Foo.parse(["-f", "-d"])) + XCTAssertThrowsError(try Foo.parse(["-f", "file", "-d"])) + XCTAssertThrowsError(try Foo.parse(["-fd", "file"])) + XCTAssertThrowsError(try Foo.parse(["-fdDebug", "file"])) + XCTAssertThrowsError(try Foo.parse(["-fFile"])) + } +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Option(name: .customShort("D", allowingJoined: true)) + var debug: [String] = [] +} + +extension JoinedEndToEndTests { + func testArrayValueParsing() throws { + AssertParse(Bar.self, []) { bar in + XCTAssertEqual(bar.debug, []) + } + + AssertParse(Bar.self, ["-Ddebug1"]) { bar in + XCTAssertEqual(bar.debug, ["debug1"]) + } + + AssertParse(Bar.self, ["-Ddebug1", "-Ddebug2", "-Ddebug3"]) { bar in + XCTAssertEqual(bar.debug, ["debug1", "debug2", "debug3"]) + } + + AssertParse(Bar.self, ["-D", "debug1", "-Ddebug2", "-D", "debug3"]) { bar in + XCTAssertEqual(bar.debug, ["debug1", "debug2", "debug3"]) + } + } + + func testArrayValueParsing_Fails() throws { + XCTAssertThrowsError(try Bar.parse(["-D"])) + XCTAssertThrowsError(try Bar.parse(["-Ddebug1", "debug2"])) + } +} + +// MARK: - + +fileprivate struct Baz: ParsableArguments { + @Option(name: .customShort("D", allowingJoined: true), parsing: .upToNextOption) + var debug: [String] = [] + + @Flag var verbose = false +} + +extension JoinedEndToEndTests { + func testArrayUpToNextParsing() throws { + AssertParse(Baz.self, []) { baz in + XCTAssertEqual(baz.debug, []) + } + + AssertParse(Baz.self, ["-Ddebug1", "debug2"]) { baz in + XCTAssertEqual(baz.debug, ["debug1", "debug2"]) + XCTAssertEqual(baz.verbose, false) + } + + AssertParse(Baz.self, ["-Ddebug1", "debug2", "--verbose"]) { baz in + XCTAssertEqual(baz.debug, ["debug1", "debug2"]) + XCTAssertEqual(baz.verbose, true) + } + + AssertParse(Baz.self, ["-Ddebug1", "debug2", "-Ddebug3", "debug4"]) { baz in + XCTAssertEqual(baz.debug, ["debug3", "debug4"]) + } + } + + func testArrayUpToNextParsing_Fails() throws { + XCTAssertThrowsError(try Baz.parse(["-D", "--other"])) + XCTAssertThrowsError(try Baz.parse(["-Ddebug", "--other"])) + XCTAssertThrowsError(try Baz.parse(["-Ddebug", "--other"])) + XCTAssertThrowsError(try Baz.parse(["-Ddebug", "debug", "--other"])) + } +} + +// MARK: - + +fileprivate struct Qux: ParsableArguments { + @Option(name: .customShort("D", allowingJoined: true), parsing: .remaining) + var debug: [String] = [] +} + +extension JoinedEndToEndTests { + func testArrayRemainingParsing() throws { + AssertParse(Qux.self, []) { qux in + XCTAssertEqual(qux.debug, []) + } + + AssertParse(Qux.self, ["-Ddebug1", "debug2"]) { qux in + XCTAssertEqual(qux.debug, ["debug1", "debug2"]) + } + + AssertParse(Qux.self, ["-Ddebug1", "debug2", "-Ddebug3", "debug4", "--other"]) { qux in + XCTAssertEqual(qux.debug, ["debug1", "debug2", "-Ddebug3", "debug4", "--other"]) + } + } + + func testArrayRemainingParsing_Fails() throws { + XCTAssertThrowsError(try Baz.parse(["--other", "-Ddebug", "debug"])) + } +} diff --git a/Tests/ArgumentParserEndToEndTests/LongNameWithShortDashEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/LongNameWithShortDashEndToEndTests.swift new file mode 100644 index 0000000..940d205 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +import ArgumentParser + +final class LongNameWithSingleDashEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Flag(name: .customLong("file", withSingleDash: true)) + var file: Bool = false + + @Flag(name: .short) + var force: Bool = false + + @Flag(name: .short) + var input: Bool = false +} + +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/ArgumentParserEndToEndTests/NestedCommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/NestedCommandEndToEndTests.swift new file mode 100644 index 0000000..1c5c520 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/NestedCommandEndToEndTests.swift @@ -0,0 +1,192 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +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 = false + + 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 = false + + 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.self, ["package"]) { package in + XCTAssertFalse(package.force) + } + + AssertParseFooCommand(Foo.Package.Clean.self, ["package", "clean"]) { clean in + XCTAssertEqual(clean.foo.verbose, false) + XCTAssertEqual(clean.package.force, false) + } + + 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, ["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.parseAsRoot(["clean", "package"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["config", "package"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["package", "c"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["package", "build"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["package", "build", "clean"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["package", "clean", "foo"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["package", "config", "bar"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["package", "clean", "build"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["build"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["build", "-f"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["build", "--build"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["build", "--build", "12"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["-f", "package", "clean"])) + XCTAssertThrowsError(try Foo.parseAsRoot(["-f", "package", "config"])) + } +} + +private struct Options: ParsableArguments { + @Option() var firstName: String? +} + +private struct UniqueOptions: ParsableArguments { + @Option() var lastName: String? +} + +private struct Super: ParsableCommand { + static var configuration: CommandConfiguration { + .init(subcommands: [Sub1.self, Sub2.self]) + } + + @OptionGroup() var options: Options + + struct Sub1: ParsableCommand { + @OptionGroup() var options: Options + } + + struct Sub2: ParsableCommand { + @OptionGroup() var options: UniqueOptions + } +} + +extension NestedCommandEndToEndTests { + func testParsing_SharedOptions() throws { + AssertParseCommand(Super.self, Super.self, []) { sup in + XCTAssertNil(sup.options.firstName) + } + + AssertParseCommand(Super.self, Super.self, ["--first-name", "Foo"]) { sup in + XCTAssertEqual("Foo", sup.options.firstName) + } + + AssertParseCommand(Super.self, Super.Sub1.self, ["sub1"]) { sub1 in + XCTAssertNil(sub1.options.firstName) + } + + AssertParseCommand(Super.self, Super.Sub1.self, ["sub1", "--first-name", "Foo"]) { sub1 in + XCTAssertEqual("Foo", sub1.options.firstName) + } + + AssertParseCommand(Super.self, Super.Sub2.self, ["sub2", "--last-name", "Foo"]) { sub2 in + XCTAssertEqual("Foo", sub2.options.lastName) + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/OptionGroupEndToEndTests.swift new file mode 100644 index 0000000..20b1d9d --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +import ArgumentParser + +final class OptionGroupEndToEndTests: XCTestCase { +} + +fileprivate struct Inner: TestableParsableArguments { + @Flag(name: [.short, .long]) + var extraVerbiage: Bool = false + @Option + var size: Int = 0 + @Argument() + var name: String + + let didValidateExpectation = XCTestExpectation(singleExpectation: "inner validated") + + private enum CodingKeys: CodingKey { + case extraVerbiage + case size + case name + } +} + +fileprivate struct Outer: TestableParsableArguments { + @Flag + var verbose: Bool = false + @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 + } +} + +fileprivate 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/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift new file mode 100644 index 0000000..9148b3c --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +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/ArgumentParserEndToEndTests/PositionalEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/PositionalEndToEndTests.swift new file mode 100644 index 0000000..c1e3f1b --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +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/ArgumentParserEndToEndTests/RawRepresentableEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/RawRepresentableEndToEndTests.swift new file mode 100644 index 0000000..4ab344b --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +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/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift new file mode 100644 index 0000000..38d85de --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift @@ -0,0 +1,433 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +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 = false + @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 Outer: ParsableCommand { + static let configuration = CommandConfiguration(subcommands: [Inner.self]) +} + +fileprivate struct Inner: ParsableCommand { + @Flag + var verbose: Bool = false + + @Argument(parsing: .unconditionalRemaining) + var files: [String] = [] +} + +extension RepeatingEndToEndTests { + func testParsing_subcommandRemaining() { + AssertParseCommand( + Outer.self, Inner.self, + ["inner", "--verbose", "one", "two", "--", "three", "--other"]) + { inner in + XCTAssertTrue(inner.verbose) + XCTAssertEqual(inner.files, ["one", "two", "--", "three", "--other"]) + } + } +} + +// MARK: - + +fileprivate struct Qux: ParsableArguments { + @Option(parsing: .upToNextOption) var names: [String] = [] + @Flag var verbose: Bool = false + @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 = false + @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: - + +fileprivate struct Foozle: ParsableArguments { + @Flag var verbose: Bool = false + @Flag(name: .customShort("f")) var useFiles: Bool = false + @Flag(name: .customShort("i")) var useStandardInput: Bool = false + @Argument(parsing: .unconditionalRemaining) var names: [String] = [] +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingUnconditionalArgument() throws { + AssertParse(Foozle.self, []) { foozle in + XCTAssertFalse(foozle.verbose) + XCTAssertEqual(foozle.names, []) + } + + AssertParse(Foozle.self, ["--other"]) { foozle in + XCTAssertFalse(foozle.verbose) + XCTAssertEqual(foozle.names, ["--other"]) + } + + AssertParse(Foozle.self, ["--verbose", "one", "two", "three"]) { foozle in + XCTAssertTrue(foozle.verbose) + XCTAssertEqual(foozle.names, ["one", "two", "three"]) + } + + AssertParse(Foozle.self, ["one", "two", "three", "--other", "--verbose"]) { foozle in + XCTAssertTrue(foozle.verbose) + XCTAssertEqual(foozle.names, ["one", "two", "three", "--other"]) + } + + AssertParse(Foozle.self, ["--verbose", "--other", "one", "two", "three"]) { foozle in + XCTAssertTrue(foozle.verbose) + XCTAssertEqual(foozle.names, ["--other", "one", "two", "three"]) + } + + AssertParse(Foozle.self, ["--verbose", "--other", "one", "--", "two", "three"]) { foozle in + XCTAssertTrue(foozle.verbose) + XCTAssertEqual(foozle.names, ["--other", "one", "--", "two", "three"]) + } + + AssertParse(Foozle.self, ["--other", "one", "--", "two", "three", "--verbose"]) { foozle in + XCTAssertFalse(foozle.verbose) + XCTAssertEqual(foozle.names, ["--other", "one", "--", "two", "three", "--verbose"]) + } + + AssertParse(Foozle.self, ["--", "--verbose", "--other", "one", "two", "three"]) { foozle in + XCTAssertFalse(foozle.verbose) + XCTAssertEqual(foozle.names, ["--", "--verbose", "--other", "one", "two", "three"]) + } + + AssertParse(Foozle.self, ["-one", "-two", "three"]) { foozle in + XCTAssertFalse(foozle.verbose) + XCTAssertFalse(foozle.useFiles) + XCTAssertFalse(foozle.useStandardInput) + XCTAssertEqual(foozle.names, ["-one", "-two", "three"]) + } + + AssertParse(Foozle.self, ["-one", "-two", "three", "-if"]) { foozle in + XCTAssertFalse(foozle.verbose) + XCTAssertTrue(foozle.useFiles) + XCTAssertTrue(foozle.useStandardInput) + XCTAssertEqual(foozle.names, ["-one", "-two", "three"]) + } + } + + func testParsing_repeatingUnconditionalArgument_Fails() throws { + // Only partially matches the `-fob` argument + XCTAssertThrowsError(try Foozle.parse(["-fib"])) + } +} + +// MARK: - + +struct PerformanceTest: ParsableCommand { + @Option(name: .short) var bundleIdentifiers: [String] = [] + + mutating 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/ArgumentParserEndToEndTests/ShortNameEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/ShortNameEndToEndTests.swift new file mode 100644 index 0000000..2f3a31d --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +import ArgumentParser + +final class ShortNameEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Flag(name: [.long, .short]) + var verbose: Bool = false + + @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/ArgumentParserEndToEndTests/SimpleEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SimpleEndToEndTests.swift new file mode 100644 index 0000000..c8fd2ef --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +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/ArgumentParserEndToEndTests/SingleValueParsingStrategyTests.swift b/Tests/ArgumentParserEndToEndTests/SingleValueParsingStrategyTests.swift new file mode 100644 index 0000000..8014a73 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/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 ArgumentParserTestHelpers +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/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift new file mode 100644 index 0000000..9280f88 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift @@ -0,0 +1,210 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +/// The goal of this test class is to validate source compatibility. By running +/// this class's tests, all property wrapper initializers should be called. +final class SourceCompatEndToEndTests: XCTestCase {} + +// MARK: - Property Wrapper Initializers + +fileprivate struct AlmostAllArguments: ParsableArguments { + @Argument(help: "") var a_newDefaultSyntax: Int = 0 + @Argument() var a0: Int + @Argument(help: "") var a1: Int + @Argument var a2_newDefaultSyntax: Int = 0 + + @Argument(help: "", transform: { _ in 0 }) var b_newDefaultSyntax: Int = 0 + @Argument var b1_newDefaultSyntax: Int = 0 + @Argument(help: "") var b2: Int + @Argument(transform: { _ in 0 }) var b3: Int + @Argument(help: "", transform: { _ in 0 }) var b4: Int + @Argument(transform: { _ in 0 }) var b5_newDefaultSyntax: Int = 0 + @Argument(help: "") var b6_newDefaultSyntax: Int = 0 + + @Argument() var c0: Int? + @Argument(help: "") var c1: Int? + + @Argument(help: "") var d2: Int? + @Argument(transform: { _ in 0 }) var d3: Int? + @Argument(help: "", transform: { _ in 0 }) var d4: Int? + + @Argument(parsing: .remaining, help: "") var e: [Int] = [1, 2] + @Argument(parsing: .remaining, help: "") var e1: [Int] + @Argument(parsing: .remaining) var e2: [Int] = [1, 2] + @Argument(help: "") var e3: [Int] = [1, 2] + @Argument() var e4: [Int] + @Argument(help: "") var e5: [Int] + @Argument(parsing: .remaining) var e6: [Int] + @Argument() var e7: [Int] = [1, 2] + @Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e8: [Int] = [1, 2] + @Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e9: [Int] + @Argument(parsing: .remaining, transform: { _ in 0 }) var e10: [Int] = [1, 2] + @Argument(help: "", transform: { _ in 0 }) var e11: [Int] = [1, 2] + @Argument(transform: { _ in 0 }) var e12: [Int] + @Argument(help: "", transform: { _ in 0 }) var e13: [Int] + @Argument(parsing: .remaining, transform: { _ in 0 }) var e14: [Int] + @Argument(transform: { _ in 0 }) var e15: [Int] = [1, 2] +} + +fileprivate struct AllOptions: ParsableArguments { + @Option(name: .long, parsing: .next, help: "") var a_newDefaultSyntax: Int = 0 + @Option(parsing: .next, help: "") var a1_newDefaultSyntax: Int = 0 + @Option(name: .long, parsing: .next, help: "") var a2: Int + @Option(name: .long, help: "") var a3_newDefaultSyntax: Int = 0 + @Option(parsing: .next, help: "") var a4: Int + @Option(help: "") var a5_newDefaultSyntax: Int = 0 + @Option(parsing: .next) var a6_newDefaultSyntax: Int = 0 + @Option(name: .long, help: "") var a7: Int + @Option(name: .long, parsing: .next) var a8: Int + @Option(name: .long) var a9_newDefaultSyntax: Int = 0 + @Option(name: .long) var a10: Int + @Option var a11_newDefaultSyntax: Int = 0 + @Option(parsing: .next) var a12: Int + @Option(help: "") var a13: Int + + @Option(name: .long, parsing: .next, help: "") var b2: Int? + @Option(parsing: .next, help: "") var b4: Int? + @Option(name: .long, help: "") var b7: Int? + @Option(name: .long, parsing: .next) var b8: Int? + @Option(name: .long) var b10: Int? + @Option(parsing: .next) var b12: Int? + @Option(help: "") var b13: Int? + + @Option(name: .long, parsing: .next, help: "", transform: { _ in 0 }) var c_newDefaultSyntax: Int = 0 + @Option(parsing: .next, help: "", transform: { _ in 0 }) var c1_newDefaultSyntax: Int = 0 + @Option(name: .long, parsing: .next, help: "", transform: { _ in 0 }) var c2: Int + @Option(name: .long, help: "", transform: { _ in 0 }) var c3_newDefaultSyntax: Int = 0 + @Option(parsing: .next, help: "", transform: { _ in 0 }) var c4: Int + @Option(help: "", transform: { _ in 0 }) var c5_newDefaultSyntax: Int = 0 + @Option(parsing: .next, transform: { _ in 0 }) var c6_newDefaultSyntax: Int = 0 + @Option(name: .long, help: "", transform: { _ in 0 }) var c7: Int + @Option(name: .long, parsing: .next, transform: { _ in 0 }) var c8: Int + @Option(name: .long, transform: { _ in 0 }) var c9_newDefaultSyntax: Int = 0 + @Option(name: .long, transform: { _ in 0 }) var c10: Int + @Option(transform: { _ in 0 }) var c11_newDefaultSyntax: Int = 0 + @Option(parsing: .next, transform: { _ in 0 }) var c12: Int + @Option(help: "", transform: { _ in 0 }) var c13: Int + + @Option(name: .long, parsing: .next, help: "", transform: { _ in 0 }) var d2: Int? + @Option(parsing: .next, help: "", transform: { _ in 0 }) var d4: Int? + @Option(name: .long, help: "", transform: { _ in 0 }) var d7: Int? + @Option(name: .long, parsing: .next, transform: { _ in 0 }) var d8: Int? + @Option(name: .long, transform: { _ in 0 }) var d10: Int? + @Option(parsing: .next, transform: { _ in 0 }) var d12: Int? + @Option(help: "", transform: { _ in 0 }) var d13: Int? + + @Option(name: .long, parsing: .singleValue, help: "") var e: [Int] = [1, 2] + @Option(parsing: .singleValue, help: "") var e1: [Int] = [1, 2] + @Option(name: .long, parsing: .singleValue, help: "") var e2: [Int] + @Option(name: .long, help: "") var e3: [Int] = [1, 2] + @Option(parsing: .singleValue, help: "") var e4: [Int] + @Option(help: "") var e5: [Int] = [1, 2] + @Option(parsing: .singleValue) var e6: [Int] = [1, 2] + @Option(name: .long, help: "") var e7: [Int] + @Option(name: .long, parsing: .singleValue) var e8: [Int] + @Option(name: .long) var e9: [Int] = [1, 2] + @Option(name: .long) var e10: [Int] + @Option() var e11: [Int] = [1, 2] + @Option(parsing: .singleValue) var e12: [Int] + @Option(help: "") var e13: [Int] + + @Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int] = [1, 2] + @Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int] = [1, 2] + @Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f2: [Int] + @Option(name: .long, help: "", transform: { _ in 0 }) var f3: [Int] = [1, 2] + @Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f4: [Int] + @Option(help: "", transform: { _ in 0 }) var f5: [Int] = [1, 2] + @Option(parsing: .singleValue, transform: { _ in 0 }) var f6: [Int] = [1, 2] + @Option(name: .long, help: "", transform: { _ in 0 }) var f7: [Int] + @Option(name: .long, parsing: .singleValue, transform: { _ in 0 }) var f8: [Int] + @Option(name: .long, transform: { _ in 0 }) var f9: [Int] = [1, 2] + @Option(name: .long, transform: { _ in 0 }) var f10: [Int] + @Option(transform: { _ in 0 }) var f11: [Int] = [1, 2] + @Option(parsing: .singleValue, transform: { _ in 0 }) var f12: [Int] + @Option(help: "", transform: { _ in 0 }) var f13: [Int] +} + +struct AllFlags: ParsableArguments { + enum E: String, EnumerableFlag { + case one, two, three + } + + @Flag(name: .long, help: "") var a_explicitFalse: Bool = false + @Flag() var a0_explicitFalse: Bool = false + @Flag(name: .long) var a1_explicitFalse: Bool = false + @Flag(help: "") var a2_explicitFalse: Bool = false + + @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var b: Bool + @Flag(inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var b1: Bool + @Flag(name: .long, inversion: .prefixedNo, help: "") var b2: Bool + @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast) var b3: Bool + @Flag(inversion: .prefixedNo, help: "") var b4: Bool + @Flag(inversion: .prefixedNo, exclusivity: .chooseLast) var b5: Bool + @Flag(name: .long, inversion: .prefixedNo) var b6: Bool + @Flag(inversion: .prefixedNo) var b7: Bool + + @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var c_newDefaultSyntax: Bool = false + @Flag(inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var c1_newDefaultSyntax: Bool = false + @Flag(name: .long, inversion: .prefixedNo, help: "") var c2_newDefaultSyntax: Bool = false + @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast) var c3_newDefaultSyntax: Bool = false + @Flag(inversion: .prefixedNo, help: "") var c4_newDefaultSyntax: Bool = false + @Flag(inversion: .prefixedNo, exclusivity: .chooseLast) var c5_newDefaultSyntax: Bool = false + @Flag(name: .long, inversion: .prefixedNo) var c6_newDefaultSyntax: Bool = false + @Flag(inversion: .prefixedNo) var c7_newDefaultSyntax: Bool = false + + @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var d_implicitNil: Bool + @Flag(inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var d1_implicitNil: Bool + @Flag(name: .long, inversion: .prefixedNo, help: "") var d2_implicitNil: Bool + @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast) var d3_implicitNil: Bool + @Flag(inversion: .prefixedNo, help: "") var d4_implicitNil: Bool + @Flag(inversion: .prefixedNo, exclusivity: .chooseLast) var d5_implicitNil: Bool + @Flag(name: .long, inversion: .prefixedNo) var d6_implicitNil: Bool + @Flag(inversion: .prefixedNo) var d7_implicitNil: Bool + + @Flag(name: .long, help: "") var e: Int + @Flag() var e0: Int + @Flag(name: .long) var e1: Int + @Flag(help: "") var e2: Int + + @Flag(exclusivity: .chooseLast, help: "") var f_newDefaultSyntax: E = .one + @Flag() var f1: E + @Flag(exclusivity: .chooseLast, help: "") var f2: E + @Flag(help: "") var f3_newDefaultSyntax: E = .one + @Flag(exclusivity: .chooseLast) var f4_newDefaultSyntax: E = .one + @Flag(help: "") var f5: E + @Flag(exclusivity: .chooseLast) var f6: E + @Flag var f7_newDefaultSyntax: E = .one + + @Flag(exclusivity: .chooseLast, help: "") var g: E? + @Flag() var g1: E? + @Flag(help: "") var g2: E? + @Flag(exclusivity: .chooseLast) var g3: E? + + @Flag(help: "") var h: [E] = [] + @Flag() var h1: [E] = [] + @Flag(help: "") var h2: [E] + @Flag() var h3: [E] +} + +extension SourceCompatEndToEndTests { + func testParsingAll() throws { + // This is just checking building the argument definitions, not the actual + // validation or usage of these definitions, which would fail. + _ = AlmostAllArguments() + _ = AllOptions() + _ = AllFlags() + } +} + diff --git a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift new file mode 100644 index 0000000..ed89a44 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift @@ -0,0 +1,263 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +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 + + See 'foo help ' for detailed help. + """, helpFoo) + AssertEqualStringsIgnoringTrailingWhitespace(""" + USAGE: foo a --name --bar + + OPTIONS: + --name + --bar + -h, --help Show help information. + + """, helpA) + AssertEqualStringsIgnoringTrailingWhitespace(""" + USAGE: foo b --name --baz + + OPTIONS: + --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(help: "The operation to perform") + var operation: Operation = .add + + @Flag(name: [.short, .long]) + var verbose: Bool = false + + @Argument(help: "The first operand") + var operands: [Int] = [] + + mutating func run() { + XCTAssertEqual(operation, .multiply) + XCTAssertTrue(verbose) + XCTAssertEqual(operands, [5, 11]) + mathDidRun = true + } +} + +extension SubcommandEndToEndTests { + func testParsing_SingleCommand() throws { + var mathCommand = try Math.parseAsRoot(["--operation", "multiply", "-v", "5", "11"]) + XCTAssertFalse(mathDidRun) + try mathCommand.run() + XCTAssertTrue(mathDidRun) + } +} + +// MARK: Nested Command Arguments Validated + +struct BaseCommand: ParsableCommand { + enum BaseCommandError: Error { + case baseCommandFailure + case subCommandFailure + } + + static let baseFlagValue = "base" + + static var configuration = CommandConfiguration( + commandName: "base", + subcommands: [SubCommand.self] + ) + + @Option() + var baseFlag: String + + mutating func validate() throws { + guard baseFlag == BaseCommand.baseFlagValue else { + throw BaseCommandError.baseCommandFailure + } + } +} + +extension BaseCommand { + struct SubCommand : ParsableCommand { + static let subFlagValue = "sub" + + static var configuration = CommandConfiguration( + commandName: "sub", + subcommands: [SubSubCommand.self] + ) + + @Option() + var subFlag: String + + mutating func validate() throws { + guard subFlag == SubCommand.subFlagValue else { + throw BaseCommandError.subCommandFailure + } + } + } +} + +extension BaseCommand.SubCommand { + struct SubSubCommand : ParsableCommand, TestableParsableArguments { + let didValidateExpectation = XCTestExpectation(singleExpectation: "did validate subcommand") + + static var configuration = CommandConfiguration( + commandName: "subsub" + ) + + @Flag + var subSubFlag: Bool = false + + private enum CodingKeys: CodingKey { + case subSubFlag + } + } +} + +extension SubcommandEndToEndTests { + func testValidate_subcommands() { + // provide a value to base-flag that will throw + AssertErrorMessage( + BaseCommand.self, + ["--base-flag", "foo", "sub", "--sub-flag", "foo", "subsub"], + "baseCommandFailure" + ) + + // provide a value to sub-flag that will throw + AssertErrorMessage( + BaseCommand.self, + ["--base-flag", BaseCommand.baseFlagValue, "sub", "--sub-flag", "foo", "subsub"], + "subCommandFailure" + ) + + // provide a valid command and make sure both validates succeed + AssertParseCommand(BaseCommand.self, + BaseCommand.SubCommand.SubSubCommand.self, + ["--base-flag", BaseCommand.baseFlagValue, "sub", "--sub-flag", BaseCommand.SubCommand.subFlagValue, "subsub", "--sub-sub-flag"]) { cmd in + XCTAssertTrue(cmd.subSubFlag) + + // make sure that the instance of SubSubCommand provided + // had its validate method called, not just that any instance of SubSubCommand was validated + wait(for: [cmd.didValidateExpectation], timeout: 0.1) + } + } +} + +// MARK: Version flags + +private struct A: ParsableCommand { + static var configuration = CommandConfiguration( + version: "1.0.0", + subcommands: [HasVersionFlag.self, NoVersionFlag.self]) + + struct HasVersionFlag: ParsableCommand { + @Flag var version: Bool = false + } + + struct NoVersionFlag: ParsableCommand { + @Flag var hello: Bool = false + } +} + +extension SubcommandEndToEndTests { + func testParsingVersionFlags() throws { + AssertErrorMessage(A.self, ["--version"], "1.0.0") + AssertErrorMessage(A.self, ["no-version-flag", "--version"], "1.0.0") + + AssertParseCommand(A.self, A.HasVersionFlag.self, ["has-version-flag", "--version"]) { cmd in + XCTAssertTrue(cmd.version) + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift new file mode 100644 index 0000000..957eae5 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift @@ -0,0 +1,163 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +final class TransformEndToEndTests: XCTestCase { +} + +fileprivate enum FooBarError: Error { + case outOfBounds +} + +fileprivate protocol Convert { + static func convert(_ str: String) throws -> Int +} + +extension Convert { + static func convert(_ str: String) throws -> Int { + guard let converted = Int(argument: str) else { throw ValidationError("Could not transform to an Int.") } + guard converted < 1000 else { throw FooBarError.outOfBounds } + return converted + } +} + +// MARK: - Options + +fileprivate struct FooOption: Convert, ParsableArguments { + + static var usageString: String = """ + Usage: foo_option --string + See 'foo_option --help' for more information. + """ + static var help: String = "Help: --string Convert string to integer\n" + + @Option(help: ArgumentHelp("Convert string to integer", valueName: "int_str"), + transform: { try convert($0) }) + var string: Int +} + +fileprivate struct BarOption: Convert, ParsableCommand { + + static var usageString: String = """ + Usage: bar-option [--strings ...] + See 'bar-option --help' for more information. + """ + static var help: String = "Help: --strings Convert a list of strings to an array of integers\n" + + @Option(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"), + transform: { try convert($0) }) + var strings: [Int] = [] +} + +extension TransformEndToEndTests { + + // MARK: Single Values + + func testSingleOptionTransform() throws { + AssertParse(FooOption.self, ["--string", "42"]) { foo in + XCTAssertEqual(foo.string, 42) + } + } + + func testSingleOptionValidation_Fail_CustomErrorMessage() throws { + AssertFullErrorMessage(FooOption.self, ["--string", "Forty Two"], "Error: The value 'Forty Two' is invalid for '--string ': Could not transform to an Int.\n" + FooOption.help + FooOption.usageString) + } + + func testSingleOptionValidation_Fail_DefaultErrorMessage() throws { + AssertFullErrorMessage(FooOption.self, ["--string", "4827"], "Error: The value '4827' is invalid for '--string ': outOfBounds\n" + FooOption.help + FooOption.usageString) + } + + // MARK: Arrays + + func testOptionArrayTransform() throws { + AssertParse(BarOption.self, ["--strings", "42", "--strings", "72", "--strings", "99"]) { bar in + XCTAssertEqual(bar.strings, [42, 72, 99]) + } + } + + func testOptionArrayValidation_Fail_CustomErrorMessage() throws { + AssertFullErrorMessage(BarOption.self, ["--strings", "Forty Two", "--strings", "72", "--strings", "99"], "Error: The value 'Forty Two' is invalid for '--strings ': Could not transform to an Int.\n" + BarOption.help + BarOption.usageString) + } + + func testOptionArrayValidation_Fail_DefaultErrorMessage() throws { + AssertFullErrorMessage(BarOption.self, ["--strings", "4827", "--strings", "72", "--strings", "99"], "Error: The value '4827' is invalid for '--strings ': outOfBounds\n" + BarOption.help + BarOption.usageString) + } +} + +// MARK: - Arguments + +fileprivate struct FooArgument: Convert, ParsableArguments { + + static var usageString: String = """ + Usage: foo_argument + See 'foo_argument --help' for more information. + """ + static var help: String = "Help: Convert string to integer\n" + + enum FooError: Error { + case outOfBounds + } + + @Argument(help: ArgumentHelp("Convert string to integer", valueName: "int_str"), + transform: { try convert($0) }) + var string: Int +} + +fileprivate struct BarArgument: Convert, ParsableCommand { + + static var usageString: String = """ + Usage: bar-argument [ ...] + See 'bar-argument --help' for more information. + """ + static var help: String = "Help: Convert a list of strings to an array of integers\n" + + @Argument(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"), + transform: { try convert($0) }) + var strings: [Int] = [] +} + +extension TransformEndToEndTests { + + // MARK: Single Values + + func testArgumentTransform() throws { + AssertParse(FooArgument.self, ["42"]) { foo in + XCTAssertEqual(foo.string, 42) + } + } + + func testArgumentValidation_Fail_CustomErrorMessage() throws { + AssertFullErrorMessage(FooArgument.self, ["Forty Two"], "Error: The value 'Forty Two' is invalid for '': Could not transform to an Int.\n" + FooArgument.help + FooArgument.usageString) + } + + func testArgumentValidation_Fail_DefaultErrorMessage() throws { + AssertFullErrorMessage(FooArgument.self, ["4827"], "Error: The value '4827' is invalid for '': outOfBounds\n" + FooArgument.help + FooArgument.usageString) + } + + // MARK: Arrays + + func testArgumentArrayTransform() throws { + AssertParse(BarArgument.self, ["42", "72", "99"]) { bar in + XCTAssertEqual(bar.strings, [42, 72, 99]) + } + } + + func testArgumentArrayValidation_Fail_CustomErrorMessage() throws { + AssertFullErrorMessage(BarArgument.self, ["Forty Two", "72", "99"], "Error: The value 'Forty Two' is invalid for '': Could not transform to an Int.\n" + BarArgument.help + BarArgument.usageString) + } + + func testArgumentArrayValidation_Fail_DefaultErrorMessage() throws { + AssertFullErrorMessage(BarArgument.self, ["4827", "72", "99"], "Error: The value '4827' is invalid for '': outOfBounds\n" + BarArgument.help + BarArgument.usageString) + } +} diff --git a/Tests/ArgumentParserEndToEndTests/UnparsedValuesEndToEndTest.swift b/Tests/ArgumentParserEndToEndTests/UnparsedValuesEndToEndTest.swift new file mode 100644 index 0000000..ccf2e6c --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/UnparsedValuesEndToEndTest.swift @@ -0,0 +1,258 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +final class UnparsedValuesEndToEndTests: XCTestCase {} + +// MARK: Two values + unparsed variable + +fileprivate struct Qux: ParsableArguments { + @Option() var name: String + @Flag() var verbose = false + var count = 0 +} + +fileprivate struct Quizzo: ParsableArguments { + @Option() var name: String + @Flag() var verbose = false + let count = 0 +} + +extension UnparsedValuesEndToEndTests { + func testParsing_TwoPlusUnparsed() throws { + AssertParse(Qux.self, ["--name", "Qux"]) { qux in + XCTAssertEqual(qux.name, "Qux") + XCTAssertFalse(qux.verbose) + XCTAssertEqual(qux.count, 0) + } + AssertParse(Qux.self, ["--name", "Qux", "--verbose"]) { qux in + XCTAssertEqual(qux.name, "Qux") + XCTAssertTrue(qux.verbose) + XCTAssertEqual(qux.count, 0) + } + + AssertParse(Quizzo.self, ["--name", "Qux", "--verbose"]) { quizzo in + XCTAssertEqual(quizzo.name, "Qux") + XCTAssertTrue(quizzo.verbose) + XCTAssertEqual(quizzo.count, 0) + } + } + + func testParsing_TwoPlusUnparsed_Fails() throws { + XCTAssertThrowsError(try Qux.parse([])) + XCTAssertThrowsError(try Qux.parse(["--name"])) + XCTAssertThrowsError(try Qux.parse(["--name", "Qux", "--count"])) + XCTAssertThrowsError(try Qux.parse(["--name", "Qux", "--count", "2"])) + } +} + +// MARK: Two value + unparsed optional variable + +fileprivate struct Hogeraa: ParsableArguments { + var fullName: String? = "Full Name" +} + +fileprivate struct Hogera: ParsableArguments { + @Option() var firstName: String + @Flag() var hasLastName = false + var fullName: String? + mutating func validate() throws { + if hasLastName { fullName = "\(firstName) LastName" } + } +} + +fileprivate struct Piyo: ParsableArguments { + @Option() var firstName: String + @Flag() var hasLastName = false + var fullName: String! + mutating func validate() throws { + fullName = firstName + (hasLastName ? " LastName" : "") + } +} + +extension UnparsedValuesEndToEndTests { + func testParsing_TwoPlusOptionalUnparsed() throws { + AssertParse(Hogeraa.self, []) { hogeraa in + XCTAssertEqual(hogeraa.fullName, "Full Name") + } + + AssertParse(Hogera.self, ["--first-name", "Hogera"]) { hogera in + XCTAssertEqual(hogera.firstName, "Hogera") + XCTAssertFalse(hogera.hasLastName) + XCTAssertNil(hogera.fullName) + } + AssertParse(Hogera.self, ["--first-name", "Hogera", "--has-last-name"]) { hogera in + XCTAssertEqual(hogera.firstName, "Hogera") + XCTAssertTrue(hogera.hasLastName) + XCTAssertEqual(hogera.fullName, "Hogera LastName") + } + + AssertParse(Piyo.self, ["--first-name", "Hogera"]) { piyo in + XCTAssertEqual(piyo.firstName, "Hogera") + XCTAssertFalse(piyo.hasLastName) + XCTAssertEqual(piyo.fullName, "Hogera") + } + AssertParse(Piyo.self, ["--first-name", "Hogera", "--has-last-name"]) { piyo in + XCTAssertEqual(piyo.firstName, "Hogera") + XCTAssertTrue(piyo.hasLastName) + XCTAssertEqual(piyo.fullName, "Hogera LastName") + } + } + + func testParsing_TwoPlusOptionalUnparsed_Fails() throws { + XCTAssertThrowsError(try Hogeraa.parse(["--full-name"])) + XCTAssertThrowsError(try Hogeraa.parse(["--full-name", "Hogera Piyo"])) + XCTAssertThrowsError(try Hogera.parse([])) + XCTAssertThrowsError(try Hogera.parse(["--first-name"])) + XCTAssertThrowsError(try Hogera.parse(["--first-name", "Hogera", "--full-name"])) + XCTAssertThrowsError(try Hogera.parse(["--first-name", "Hogera", "--full-name", "Hogera Piyo"])) + XCTAssertThrowsError(try Piyo.parse([])) + XCTAssertThrowsError(try Piyo.parse(["--first-name"])) + XCTAssertThrowsError(try Piyo.parse(["--first-name", "Hogera", "--full-name"])) + XCTAssertThrowsError(try Piyo.parse(["--first-name", "Hogera", "--full-name", "Hogera Piyo"])) + } +} + +// MARK: Nested unparsed decodable type + + +fileprivate struct Foo: ParsableCommand { + @Flag var foo: Bool = false + var config: Config? + @OptionGroup var opt: OptionalArguments + @OptionGroup var def: DefaultedArguments +} + +fileprivate struct Config: Decodable { + var name: String + var age: Int +} + +fileprivate struct OptionalArguments: ParsableArguments { + @Argument var title: String? + @Option var edition: Int? +} + +fileprivate struct DefaultedArguments: ParsableArguments { + @Option var one = 1 + @Option var two = 2 +} + +extension UnparsedValuesEndToEndTests { + func testUnparsedNestedValues() { + AssertParse(Foo.self, []) { foo in + XCTAssertFalse(foo.foo) + XCTAssertNil(foo.opt.title) + XCTAssertNil(foo.opt.edition) + XCTAssertEqual(1, foo.def.one) + XCTAssertEqual(2, foo.def.two) + } + + AssertParse(Foo.self, ["--foo", "--edition", "5", "Hello", "--one", "2", "--two", "1"]) { foo in + XCTAssertTrue(foo.foo) + XCTAssertEqual("Hello", foo.opt.title) + XCTAssertEqual(5, foo.opt.edition) + XCTAssertEqual(2, foo.def.one) + XCTAssertEqual(1, foo.def.two) + } + } + + func testUnparsedNestedValues_Fails() { + XCTAssertThrowsError(try Foo.parse(["--edition", "aaa"])) + XCTAssertThrowsError(try Foo.parse(["--one", "aaa"])) + } +} + +// MARK: Nested unparsed optional decodable type + +fileprivate struct Barr: ParsableCommand { + var baz: Baz? = Baz(name: "Some Name", age: 105) +} + +fileprivate struct Bar: ParsableCommand { + @Flag var bar: Bool = false + var baz: Baz? + var bazz: Bazz? + mutating func validate() throws { + if bar { + baz = Baz(name: "Some", age: 100) + bazz = Bazz(name: "Other", age: 101) + } + } +} + +fileprivate struct Baz: Decodable { + var name: String? + var age: Int! +} + +fileprivate struct Bazz: Decodable { + var name: String? + var age: Int +} + +extension UnparsedValuesEndToEndTests { + func testUnparsedNestedOptionalValue() { + AssertParse(Barr.self, []) { barr in + XCTAssertNotNil(barr.baz) + XCTAssertEqual(barr.baz?.age, 105) + XCTAssertEqual(barr.baz?.name, "Some Name") + } + + AssertParse(Bar.self, []) { bar in + XCTAssertFalse(bar.bar) + XCTAssertNil(bar.baz) + XCTAssertNil(bar.baz?.age) + XCTAssertNil(bar.baz?.name) + XCTAssertNil(bar.bazz) + XCTAssertNil(bar.bazz?.age) + XCTAssertNil(bar.bazz?.name) + } + + AssertParse(Bar.self, ["--bar"]) { bar in + XCTAssertTrue(bar.bar) + XCTAssertNotNil(bar.baz) + XCTAssertEqual(bar.baz?.name, "Some") + XCTAssertEqual(bar.baz?.age, 100) + XCTAssertNotNil(bar.bazz) + XCTAssertEqual(bar.bazz?.name, "Other") + XCTAssertEqual(bar.bazz?.age, 101) + } + } + + func testUnparsedNestedOptionalValue_Fails() { + XCTAssertThrowsError(try Bar.parse(["--baz", "xyz"])) + XCTAssertThrowsError(try Bar.parse(["--bazz", "xyz"])) + XCTAssertThrowsError(try Bar.parse(["--name", "None"])) + XCTAssertThrowsError(try Bar.parse(["--age", "123"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--name", "None"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--age", "123"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--baz"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "--name", "None"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz", "--name"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz", "--name", "None"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "--age", "None"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz", "--age"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--baz", "xyz", "--age", "None"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "--name", "None"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz", "--name"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz", "--name", "None"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "--age", "None"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz", "--age"])) + XCTAssertThrowsError(try Bar.parse(["--bar", "--bazz", "xyz", "--age", "None"])) + } +} diff --git a/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift new file mode 100644 index 0000000..79e2cda --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift @@ -0,0 +1,173 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +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 { + static var usageString: String = """ + Usage: foo [--count ] [ ...] [--version] [--throw] + See 'foo --help' for more information. + """ + + static var helpString: String = """ + USAGE: foo [--count ] [ ...] [--version] [--throw] + + ARGUMENTS: + + + OPTIONS: + --count + --version + --throw + -h, --help Show help information. + """ + + @Option() + var count: Int? + + @Argument() + var names: [String] = [] + + @Flag + var version: Bool = false + + @Flag(name: [.customLong("throw")]) + var throwCustomError: Bool = false + + @Flag(help: .hidden) + var showUsageOnly: Bool = false + + @Flag(help: .hidden) + var failValidationSilently: Bool = false + + @Flag(help: .hidden) + var failSilently: Bool = false + + 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 + } + + if showUsageOnly { + throw ValidationError("") + } + + if failValidationSilently { + throw ExitCode.validationFailure + } + + if failSilently { + throw ExitCode.failure + } + } +} + +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. + + \(Foo.helpString) + + """) + + 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). + \(Foo.usageString) + """) + } + + func testCustomErrorValidation() { + // verify that error description is printed if avaiable via LocalizedError + AssertErrorMessage(Foo.self, ["--throw", "Joe"], UserValidationError.userValidationError.errorDescription!) + } + + func testEmptyErrorValidation() { + AssertErrorMessage(Foo.self, ["--show-usage-only", "Joe"], "") + AssertFullErrorMessage(Foo.self, ["--show-usage-only", "Joe"], Foo.usageString) + AssertFullErrorMessage(Foo.self, ["--fail-validation-silently", "Joe"], "") + AssertFullErrorMessage(Foo.self, ["--fail-silently", "Joe"], "") + } +} + +fileprivate struct FooCommand: ParsableCommand { + @Flag(help: .hidden) + var foo = false + @Flag(help: .hidden) + var bar = false + + mutating func validate() throws { + if foo { + // --foo implies --bar + bar = true + } + } + + func run() throws { + XCTAssertEqual(foo, bar) + } +} + +extension ValidationEndToEndTests { + func testMutationsPreserved() throws { + var foo = try FooCommand.parseAsRoot(["--foo"]) + try foo.run() + } +} diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift new file mode 100644 index 0000000..22c55f3 --- /dev/null +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -0,0 +1,568 @@ +//===----------------------------------------------------------*- 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 ArgumentParserTestHelpers + +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: + --version Show the version. + -h, --help Show help information. + + SUBCOMMANDS: + add (default) Print the sum of the values. + multiply Print the product of the values. + stats Calculate descriptive statistics. + + See 'math help ' for detailed help. + """ + + 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. + --version Show the version. + -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) + --version Show the version. + -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 [] [] [ ...] [--file ] [--directory ] [--shell ] [--custom ] + + ARGUMENTS: + + + A group of floating-point values to operate on. + + OPTIONS: + --file + --directory + --shell + --custom + --version Show the version. + -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_CustomValidation() throws { + AssertExecuteCommand( + command: "math stats average --kind mode", + expected: """ + Error: Please provide at least one value to calculate the mode. + Usage: math stats average [--kind ] [ ...] + See 'math stats average --help' for more information. + """, + exitCode: .validationFailure) + } + + func testMath_Versions() throws { + AssertExecuteCommand( + command: "math --version", + expected: "1.0.0") + AssertExecuteCommand( + command: "math stats --version", + expected: "1.0.0") + AssertExecuteCommand( + command: "math stats average --version", + expected: "1.5.0-alpha") + } + + func testMath_ExitCodes() throws { + AssertExecuteCommand( + command: "math stats quantiles --test-success-exit-code", + expected: "", + exitCode: .success) + AssertExecuteCommand( + command: "math stats quantiles --test-failure-exit-code", + expected: "", + exitCode: .failure) + AssertExecuteCommand( + command: "math stats quantiles --test-validation-exit-code", + expected: "", + exitCode: .validationFailure) + AssertExecuteCommand( + command: "math stats quantiles --test-custom-exit-code 42", + expected: "", + exitCode: ExitCode(42)) + } + + func testMath_Fail() throws { + AssertExecuteCommand( + command: "math --foo", + expected: """ + Error: Unknown option '--foo' + Usage: math add [--hex-output] [ ...] + See 'math add --help' for more information. + """, + exitCode: .validationFailure) + + AssertExecuteCommand( + command: "math ZZZ", + expected: """ + Error: The value 'ZZZ' is invalid for '' + Help: A group of integers to operate on. + Usage: math add [--hex-output] [ ...] + See 'math add --help' for more information. + """, + exitCode: .validationFailure) + } +} + +// MARK: - Completion Script + +extension MathExampleTests { + func testMath_CompletionScript() { + AssertExecuteCommand( + command: "math --generate-completion-script=bash", + expected: bashCompletionScriptText) + AssertExecuteCommand( + command: "math --generate-completion-script bash", + expected: bashCompletionScriptText) + AssertExecuteCommand( + command: "math --generate-completion-script=zsh", + expected: zshCompletionScriptText) + AssertExecuteCommand( + command: "math --generate-completion-script zsh", + expected: zshCompletionScriptText) + AssertExecuteCommand( + command: "math --generate-completion-script=fish", + expected: fishCompletionScriptText) + AssertExecuteCommand( + command: "math --generate-completion-script fish", + expected: fishCompletionScriptText) + } + + func testMath_CustomCompletion() { + AssertExecuteCommand( + command: "math ---completion stats quantiles -- --custom", + expected: """ + hello + helicopter + heliotrope + """) + + AssertExecuteCommand( + command: "math ---completion stats quantiles -- --custom h", + expected: """ + hello + helicopter + heliotrope + """) + + AssertExecuteCommand( + command: "math ---completion stats quantiles -- --custom a", + expected: """ + aardvark + aaaaalbert + """) + } +} + +private let bashCompletionScriptText = """ +#!/bin/bash + +_math() { + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + opts="add multiply stats help -h --help" + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + case ${COMP_WORDS[1]} in + (add) + _math_add 2 + return + ;; + (multiply) + _math_multiply 2 + return + ;; + (stats) + _math_stats 2 + return + ;; + (help) + _math_help 2 + return + ;; + esac + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_math_add() { + opts="--hex-output -x -h --help" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_math_multiply() { + opts="--hex-output -x -h --help" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_math_stats() { + opts="average stdev quantiles -h --help" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + case ${COMP_WORDS[$1]} in + (average) + _math_stats_average $(($1+1)) + return + ;; + (stdev) + _math_stats_stdev $(($1+1)) + return + ;; + (quantiles) + _math_stats_quantiles $(($1+1)) + return + ;; + esac + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_math_stats_average() { + opts="--kind -h --help" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + case $prev in + --kind) + COMPREPLY=( $(compgen -W "mean median mode" -- "$cur") ) + return + ;; + esac + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_math_stats_stdev() { + opts="-h --help" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_math_stats_quantiles() { + opts="--file --directory --shell --custom -h --help" + opts="$opts alphabet alligator branch braggart" + opts="$opts $(math ---completion stats quantiles -- customArg "$COMP_WORDS")" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + case $prev in + --file) + COMPREPLY=( $(compgen -f -- "$cur") ) + return + ;; + --directory) + COMPREPLY=( $(compgen -d -- "$cur") ) + return + ;; + --shell) + COMPREPLY=( $(head -100 /usr/share/dict/words | tail -50) ) + return + ;; + --custom) + COMPREPLY=( $(compgen -W "$(math ---completion stats quantiles -- --custom "$COMP_WORDS")" -- "$cur") ) + return + ;; + esac + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_math_help() { + opts="-h --help" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + +complete -F _math math +""" + +private let zshCompletionScriptText = """ +#compdef math +local context state state_descr line +_math_commandname=$words[1] +typeset -A opt_args + +_math() { + integer ret=1 + local -a args + args+=( + '(-h --help)'{-h,--help}'[Print help information.]' + '(-): :->command' + '(-)*:: :->arg' + ) + _arguments -w -s -S $args[@] && ret=0 + case $state in + (command) + local subcommands + subcommands=( + 'add:Print the sum of the values.' + 'multiply:Print the product of the values.' + 'stats:Calculate descriptive statistics.' + 'help:Show subcommand help information.' + ) + _describe "subcommand" subcommands + ;; + (arg) + case ${words[1]} in + (add) + _math_add + ;; + (multiply) + _math_multiply + ;; + (stats) + _math_stats + ;; + (help) + _math_help + ;; + esac + ;; + esac + + return ret +} + +_math_add() { + integer ret=1 + local -a args + args+=( + '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' + ':values:' + '(-h --help)'{-h,--help}'[Print help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + +_math_multiply() { + integer ret=1 + local -a args + args+=( + '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' + ':values:' + '(-h --help)'{-h,--help}'[Print help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + +_math_stats() { + integer ret=1 + local -a args + args+=( + '(-h --help)'{-h,--help}'[Print help information.]' + '(-): :->command' + '(-)*:: :->arg' + ) + _arguments -w -s -S $args[@] && ret=0 + case $state in + (command) + local subcommands + subcommands=( + 'average:Print the average of the values.' + 'stdev:Print the standard deviation of the values.' + 'quantiles:Print the quantiles of the values (TBD).' + ) + _describe "subcommand" subcommands + ;; + (arg) + case ${words[1]} in + (average) + _math_stats_average + ;; + (stdev) + _math_stats_stdev + ;; + (quantiles) + _math_stats_quantiles + ;; + esac + ;; + esac + + return ret +} + +_math_stats_average() { + integer ret=1 + local -a args + args+=( + '--kind[The kind of average to provide.]:kind:(mean median mode)' + ':values:' + '(-h --help)'{-h,--help}'[Print help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + +_math_stats_stdev() { + integer ret=1 + local -a args + args+=( + ':values:' + '(-h --help)'{-h,--help}'[Print help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + +_math_stats_quantiles() { + integer ret=1 + local -a args + args+=( + ':one-of-four:(alphabet alligator branch braggart)' + ':custom-arg:{_custom_completion $_math_commandname ---completion stats quantiles -- customArg $words}' + ':values:' + '--file:file:_files -g '"'"'*.txt *.md'"'"'' + '--directory:directory:_files -/' + '--shell:shell:{local -a list; list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"}); _describe '''' list}' + '--custom:custom:{_custom_completion $_math_commandname ---completion stats quantiles -- --custom $words}' + '(-h --help)'{-h,--help}'[Print help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + +_math_help() { + integer ret=1 + local -a args + args+=( + ':subcommands:' + '(-h --help)'{-h,--help}'[Print help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + + +_custom_completion() { + local completions=("${(@f)$($*)}") + _describe '' completions +} + +_math +""" + +private let fishCompletionScriptText = """ +function __fish_math_using_command + set cmd (commandline -opc) + if [ (count $cmd) -eq (count $argv) ] + for i in (seq (count $argv)) + if [ $cmd[$i] != $argv[$i] ] + return 1 + end + end + return 0 + end + return 1 +end +complete -c math -n '__fish_math_using_command math' -f -a 'add' -d 'Print the sum of the values.' +complete -c math -n '__fish_math_using_command math' -f -a 'multiply' -d 'Print the product of the values.' +complete -c math -n '__fish_math_using_command math' -f -a 'stats' -d 'Calculate descriptive statistics.' +complete -c math -n '__fish_math_using_command math' -f -a 'help' -d 'Show subcommand help information.' +complete -c math -n '__fish_math_using_command math add' -f -l hex-output -s x -d 'Use hexadecimal notation for the result.' +complete -c math -n '__fish_math_using_command math multiply' -f -l hex-output -s x -d 'Use hexadecimal notation for the result.' +complete -c math -n '__fish_math_using_command math stats' -f -a 'average' -d 'Print the average of the values.' +complete -c math -n '__fish_math_using_command math stats' -f -a 'stdev' -d 'Print the standard deviation of the values.' +complete -c math -n '__fish_math_using_command math stats' -f -a 'quantiles' -d 'Print the quantiles of the values (TBD).' +complete -c math -n '__fish_math_using_command math stats' -f -a 'help' -d 'Show subcommand help information.' +complete -c math -n '__fish_math_using_command math stats average' -f -r -l kind -d 'The kind of average to provide.' +complete -c math -n '__fish_math_using_command math stats average --kind' -f -k -a 'mean median mode' +complete -c math -n '__fish_math_using_command math stats quantiles' -f -r -l file +complete -c math -n '__fish_math_using_command math stats quantiles --file' -f -a '(for i in *.{txt,md}; echo $i;end)' +complete -c math -n '__fish_math_using_command math stats quantiles' -f -r -l directory +complete -c math -n '__fish_math_using_command math stats quantiles --directory' -f -a '(__fish_complete_directories)' +complete -c math -n '__fish_math_using_command math stats quantiles' -f -r -l shell +complete -c math -n '__fish_math_using_command math stats quantiles --shell' -f -a '(head -100 /usr/share/dict/words | tail -50)' +complete -c math -n '__fish_math_using_command math stats quantiles' -f -r -l custom +complete -c math -n '__fish_math_using_command math stats quantiles --custom' -f -a '(command math ---completion stats quantiles -- --custom (commandline -opc)[1..-1])' +""" diff --git a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift new file mode 100644 index 0000000..cab97c2 --- /dev/null +++ b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------*- 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 ArgumentParserTestHelpers + +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] + + 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. + """, + exitCode: .validationFailure) + + AssertExecuteCommand( + command: "repeat hello --count", + expected: """ + Error: Missing value for '--count ' + Usage: repeat [--count ] [--include-counter] + See 'repeat --help' for more information. + """, + exitCode: .validationFailure) + + AssertExecuteCommand( + command: "repeat hello --count ZZZ", + expected: """ + Error: The value 'ZZZ' is invalid for '--count ' + Help: --count The number of times to repeat 'phrase'. + Usage: repeat [--count ] [--include-counter] + See 'repeat --help' for more information. + """, + exitCode: .validationFailure) + + AssertExecuteCommand( + command: "repeat --version hello", + expected: """ + Error: Unknown option '--version' + Usage: repeat [--count ] [--include-counter] + See 'repeat --help' for more information. + """, + exitCode: .validationFailure) + } +} diff --git a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift new file mode 100644 index 0000000..d1c63f5 --- /dev/null +++ b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------*- 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 ArgumentParserTestHelpers + +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] + See 'roll --help' for more information. + """, + exitCode: .validationFailure) + + AssertExecuteCommand( + command: "roll --times ZZZ", + expected: """ + Error: The value 'ZZZ' is invalid for '--times ' + Help: --times Rolls the dice times. + Usage: roll [--times ] [--sides ] [--seed ] [--verbose] + See 'roll --help' for more information. + """, + exitCode: .validationFailure) + } +} diff --git a/Tests/ArgumentParserPackageManagerTests/CMakeLists.txt b/Tests/ArgumentParserPackageManagerTests/CMakeLists.txt new file mode 100644 index 0000000..bc72c94 --- /dev/null +++ b/Tests/ArgumentParserPackageManagerTests/CMakeLists.txt @@ -0,0 +1,8 @@ +add_library(PackageManagerTests + HelpTests.swift + PackageManager/Clean.swift + PackageManager/Config.swift + PackageManager/Describe.swift + PackageManager/GenerateXcodeProject.swift + PackageManager/Options.swift + Tests.swift) diff --git a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift new file mode 100644 index 0000000..ff47ea3 --- /dev/null +++ b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift @@ -0,0 +1,279 @@ +//===----------------------------------------------------------*- 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 ArgumentParserTestHelpers + +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 + + See 'package help ' for detailed help. + """.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 + + See 'package help ' for detailed help. + """.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 + + See 'package help config ' for detailed help. + """.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 [] --package-url + + 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 = false + @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.self].getHelpNames() + XCTAssertEqual(names, [.short("?"), .long("show-help")]) + + AssertFullErrorMessage(CustomHelp.self, ["--error"], """ + Error: Unknown option '--error' + Usage: custom-help + See 'custom-help --show-help' for more information. + """) + } +} + +struct NoHelp: ParsableCommand { + static let configuration = CommandConfiguration( + helpNames: [] + ) + + @Option(help: "How many florps?") var count: Int +} + +extension HelpTests { + func testNoHelpNames() { + let names = [NoHelp.self].getHelpNames() + XCTAssertEqual(names, []) + + AssertFullErrorMessage(NoHelp.self, ["--error"], """ + Error: Missing expected argument '--count ' + Usage: no-help --count + """) + + XCTAssertEqual( + NoHelp.message(for: CleanExit.helpRequest()).trimmingLines(), + """ + USAGE: no-help --count + + OPTIONS: + --count How many florps? + + """) + } +} + +struct SubCommandCustomHelp: ParsableCommand { + static var configuration = CommandConfiguration ( + helpNames: [.customShort("p"), .customLong("parent-help")] + ) + + struct InheritHelp: ParsableCommand { + + } + + struct ModifiedHelp: ParsableCommand { + static var configuration = CommandConfiguration ( + helpNames: [.customShort("s"), .customLong("subcommand-help")] + ) + + struct InheritImmediateParentdHelp: ParsableCommand { + + } + } +} + +extension HelpTests { + func testSubCommandInheritHelpNames() { + let names = [SubCommandCustomHelp.self, SubCommandCustomHelp.InheritHelp.self].getHelpNames() + XCTAssertEqual(names, [.short("p"), .long("parent-help")]) + } + + func testSubCommandCustomHelpNames() { + let names = [SubCommandCustomHelp.self, SubCommandCustomHelp.ModifiedHelp.self].getHelpNames() + XCTAssertEqual(names, [.short("s"), .long("subcommand-help")]) + } + + func testInheritImmediateParentHelpNames() { + let names = [ + SubCommandCustomHelp.self, + SubCommandCustomHelp.ModifiedHelp.self, + SubCommandCustomHelp.ModifiedHelp.InheritImmediateParentdHelp.self + ].getHelpNames() + XCTAssertEqual(names, [.short("s"), .long("subcommand-help")]) + } +} diff --git a/Tests/ArgumentParserPackageManagerTests/PackageManager/Clean.swift b/Tests/ArgumentParserPackageManagerTests/PackageManager/Clean.swift new file mode 100644 index 0000000..f787b5f --- /dev/null +++ b/Tests/ArgumentParserPackageManagerTests/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/ArgumentParserPackageManagerTests/PackageManager/Config.swift b/Tests/ArgumentParserPackageManagerTests/PackageManager/Config.swift new file mode 100644 index 0000000..8d19a6d --- /dev/null +++ b/Tests/ArgumentParserPackageManagerTests/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/ArgumentParserPackageManagerTests/PackageManager/Describe.swift b/Tests/ArgumentParserPackageManagerTests/PackageManager/Describe.swift new file mode 100644 index 0000000..844ee5e --- /dev/null +++ b/Tests/ArgumentParserPackageManagerTests/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/ArgumentParserPackageManagerTests/PackageManager/GenerateXcodeProject.swift b/Tests/ArgumentParserPackageManagerTests/PackageManager/GenerateXcodeProject.swift new file mode 100644 index 0000000..d4a0fed --- /dev/null +++ b/Tests/ArgumentParserPackageManagerTests/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 = false + + @Flag(help: "Use the legacy scheme generator") + var legacySchemeGenerator: Bool = false + + @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 = false + + @Flag(help: "Watch for changes to the Package manifest to regenerate the Xcode project") + var watch: Bool = false + + @Option(help: "Path to xcconfig file") + var xcconfigOverrides: String? + + mutating func run() { + print("Generating Xcode Project.......") + } + } +} diff --git a/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift b/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift new file mode 100644 index 0000000..ccdbc5a --- /dev/null +++ b/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------*- 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(help: "Specify build/cache directory") + var buildPath: String = "./.build" + + enum Configuration: String, ExpressibleByArgument, Decodable { + case debug + case release + } + + @Option(name: .shortAndLong, + help: "Build with configuration") + var configuration: Configuration = .debug + + @Flag(inversion: .prefixedEnableDisable, + help: "Use automatic resolution if Package.resolved file is out-of-date") + var automaticResolution: Bool = true + + @Flag(inversion: .prefixedEnableDisable, + help: "Use indexing-while-building feature") + var indexStore: Bool = true + + @Flag(inversion: .prefixedEnableDisable, + help: "Cache Package.swift manifests") + var packageManifestCaching: Bool = true + + @Flag(inversion: .prefixedEnableDisable) + var prefetching: Bool = true + + @Flag(inversion: .prefixedEnableDisable, + help: "Use sandbox when executing subprocesses") + var sandbox: Bool = true + + @Flag(inversion: .prefixedEnableDisable, + help: "[Experimental] Enable the new Pubgrub dependency resolver") + var pubgrubResolver: Bool = false + @Flag(inversion: .prefixedNo, + help: "Link Swift stdlib statically") + var staticSwiftStdlib: Bool = false + @Option(help: "Change working directory before any other operation") + var packagePath: String = "." + + @Flag(help: "Turn on runtime checks for erroneous behavior") + var sanitize: Bool = false + + @Flag(help: "Skip updating dependencies from their remote during a resolution") + var skipUpdate: Bool = false + + @Flag(name: .shortAndLong, + help: "Increase verbosity of informational output") + var verbose: Bool = false + + @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/ArgumentParserPackageManagerTests/Tests.swift b/Tests/ArgumentParserPackageManagerTests/Tests.swift new file mode 100644 index 0000000..54db792 --- /dev/null +++ b/Tests/ArgumentParserPackageManagerTests/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 ArgumentParserTestHelpers + +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/ArgumentParserUnitTests/CMakeLists.txt b/Tests/ArgumentParserUnitTests/CMakeLists.txt new file mode 100644 index 0000000..2599ff8 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/CMakeLists.txt @@ -0,0 +1,13 @@ +add_library(UnitTests + ParsableArgumentsValidationTests.swift + ErrorMessageTests.swift + HelpGenerationTests.swift + NameSpecificationTests.swift + SplitArgumentTests.swift + StringSnakeCaseTests.swift + StringWrappingTests.swift + TreeTests.swift + UsageGenerationTests.swift) +target_link_libraries(UnitTests PRIVATE + ArgumentParser + ArgumentParserTestHelpers) diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift new file mode 100644 index 0000000..9e53639 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift @@ -0,0 +1,266 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +@testable import ArgumentParser + +final class CompletionScriptTests: XCTestCase { +} + +extension CompletionScriptTests { + struct Path: ExpressibleByArgument { + var path: String + + init?(argument: String) { + self.path = argument + } + + static var defaultCompletionKind: CompletionKind { + .file() + } + } + + enum Kind: String, ExpressibleByArgument, CaseIterable { + case one, two, three = "custom-three" + } + + struct Base: ParsableCommand { + @Option(help: "The user's name.") var name: String + @Option() var kind: Kind + @Option(completion: .list(["1", "2", "3"])) var otherKind: Kind + + @Option() var path1: Path + @Option() var path2: Path? + @Option(completion: .list(["a", "b", "c"])) var path3: Path + + @Flag(help: .hidden) var verbose = false + } + + func testBase_Zsh() throws { + let script1 = try CompletionsGenerator(command: Base.self, shell: .zsh) + .generateCompletionScript() + XCTAssertEqual(zshBaseCompletions, script1) + + let script2 = try CompletionsGenerator(command: Base.self, shellName: "zsh") + .generateCompletionScript() + XCTAssertEqual(zshBaseCompletions, script2) + + let script3 = Base.completionScript(for: .zsh) + XCTAssertEqual(zshBaseCompletions, script3) + } + + func testBase_Bash() throws { + let script1 = try CompletionsGenerator(command: Base.self, shell: .bash) + .generateCompletionScript() + + XCTAssertEqual(bashBaseCompletions, script1) + + let script2 = try CompletionsGenerator(command: Base.self, shellName: "bash") + .generateCompletionScript() + XCTAssertEqual(bashBaseCompletions, script2) + + let script3 = Base.completionScript(for: .bash) + XCTAssertEqual(bashBaseCompletions, script3) + } + + func testBase_Fish() throws { + let script1 = try CompletionsGenerator(command: Base.self, shell: .fish) + .generateCompletionScript() + XCTAssertEqual(fishBaseCompletions, script1) + + let script2 = try CompletionsGenerator(command: Base.self, shellName: "fish") + .generateCompletionScript() + XCTAssertEqual(fishBaseCompletions, script2) + + let script3 = Base.completionScript(for: .fish) + XCTAssertEqual(fishBaseCompletions, script3) + } +} + +extension CompletionScriptTests { + struct Custom: ParsableCommand { + @Option(name: .shortAndLong, completion: .custom { _ in ["a", "b", "c"] }) + var one: String + + @Argument(completion: .custom { _ in ["d", "e", "f"] }) + var two: String + + @Option(name: .customShort("z"), completion: .custom { _ in ["x", "y", "z"] }) + var three: String + } + + func verifyCustomOutput( + _ arg: String, + expectedOutput: String, + file: StaticString = #file, line: UInt = #line + ) throws { + do { + _ = try Custom.parse(["---completion", "--", arg]) + XCTFail("Didn't error as expected", file: (file), line: line) + } catch let error as CommandError { + guard case .completionScriptCustomResponse(let output) = error.parserError else { + throw error + } + XCTAssertEqual(expectedOutput, output, file: (file), line: line) + } + } + + func testCustomCompletions() throws { + try verifyCustomOutput("-o", expectedOutput: "a\nb\nc") + try verifyCustomOutput("--one", expectedOutput: "a\nb\nc") + try verifyCustomOutput("two", expectedOutput: "d\ne\nf") + try verifyCustomOutput("-z", expectedOutput: "x\ny\nz") + + XCTAssertThrowsError(try verifyCustomOutput("--bad", expectedOutput: "")) + } +} + +extension CompletionScriptTests { + struct EscapedCommand: ParsableCommand { + @Option(help: #"Escaped chars: '[]\."#) + var one: String + } + + func testEscaped_Zsh() throws { + XCTAssertEqual(zshEscapedCompletion, EscapedCommand.completionScript(for: .zsh)) + } +} + +private let zshBaseCompletions = """ +#compdef base +local context state state_descr line +_base_commandname=$words[1] +typeset -A opt_args + +_base() { + integer ret=1 + local -a args + args+=( + '--name[The user'"'"'s name.]:name:' + '--kind:kind:(one two custom-three)' + '--other-kind:other-kind:(1 2 3)' + '--path1:path1:_files' + '--path2:path2:_files' + '--path3:path3:(a b c)' + '(-h --help)'{-h,--help}'[Print help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + + +_custom_completion() { + local completions=("${(@f)$($*)}") + _describe '' completions +} + +_base +""" + +private let bashBaseCompletions = """ +#!/bin/bash + +_base() { + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + opts="--name --kind --other-kind --path1 --path2 --path3 -h --help" + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + case $prev in + --name) + + return + ;; + --kind) + COMPREPLY=( $(compgen -W "one two custom-three" -- "$cur") ) + return + ;; + --other-kind) + COMPREPLY=( $(compgen -W "1 2 3" -- "$cur") ) + return + ;; + --path1) + COMPREPLY=( $(compgen -f -- "$cur") ) + return + ;; + --path2) + COMPREPLY=( $(compgen -f -- "$cur") ) + return + ;; + --path3) + COMPREPLY=( $(compgen -W "a b c" -- "$cur") ) + return + ;; + esac + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + +complete -F _base base +""" + +private let zshEscapedCompletion = """ +#compdef escaped-command +local context state state_descr line +_escaped_command_commandname=$words[1] +typeset -A opt_args + +_escaped-command() { + integer ret=1 + local -a args + args+=( + '--one[Escaped chars: '"'"'\\[\\]\\\\.]:one:' + '(-h --help)'{-h,--help}'[Print help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + + +_custom_completion() { + local completions=("${(@f)$($*)}") + _describe '' completions +} + +_escaped-command +""" + +private let fishBaseCompletions = """ +function __fish_base_using_command + set cmd (commandline -opc) + if [ (count $cmd) -eq (count $argv) ] + for i in (seq (count $argv)) + if [ $cmd[$i] != $argv[$i] ] + return 1 + end + end + return 0 + end + return 1 +end +complete -c base -n '__fish_base_using_command base' -f -r -l name -d 'The user\\'s name.' +complete -c base -n '__fish_base_using_command base' -f -r -l kind +complete -c base -n '__fish_base_using_command base --kind' -f -k -a 'one two custom-three' +complete -c base -n '__fish_base_using_command base' -f -r -l other-kind +complete -c base -n '__fish_base_using_command base --other-kind' -f -k -a '1 2 3' +complete -c base -n '__fish_base_using_command base' -f -r -l path1 +complete -c base -n '__fish_base_using_command base --path1' -f -a '(for i in *.{}; echo $i;end)' +complete -c base -n '__fish_base_using_command base' -f -r -l path2 +complete -c base -n '__fish_base_using_command base --path2' -f -a '(for i in *.{}; echo $i;end)' +complete -c base -n '__fish_base_using_command base' -f -r -l path3 +complete -c base -n '__fish_base_using_command base --path3' -f -k -a 'a b c' +""" diff --git a/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift b/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift new file mode 100644 index 0000000..20eb687 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift @@ -0,0 +1,192 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +@testable import ArgumentParser + +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"], "Unknown option '--verbose'") + } + + func testUnknownOption_2() { + AssertErrorMessage(Bar.self, ["--name", "a", "--format", "b", "-q"], "Unknown option '-q'") + } + + func testUnknownOption_3() { + AssertErrorMessage(Bar.self, ["--name", "a", "--format", "b", "-bar"], "Unknown option '-bar'") + } + + func testUnknownOption_4() { + AssertErrorMessage(Bar.self, ["--name", "a", "-foz", "b"], "Unknown option '-o'") + } + + 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 = false +} + +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 '") + } +} + +fileprivate struct Qwz: ParsableArguments { + @Option() var name: String? + @Option(name: [.customLong("title", withSingleDash: true)]) var title: String? +} + +extension ErrorMessageTests { + func testMispelledArgument_1() { + AssertErrorMessage(Qwz.self, ["--nme"], "Unknown option '--nme'. Did you mean '--name'?") + AssertErrorMessage(Qwz.self, ["-name"], "Unknown option '-name'. Did you mean '--name'?") + } + + func testMispelledArgument_2() { + AssertErrorMessage(Qwz.self, ["-ttle"], "Unknown option '-ttle'. Did you mean '-title'?") + AssertErrorMessage(Qwz.self, ["--title"], "Unknown option '--title'. Did you mean '-title'?") + } + + func testMispelledArgument_3() { + AssertErrorMessage(Qwz.self, ["--not-similar"], "Unknown option '--not-similar'") + } + + func testMispelledArgument_4() { + AssertErrorMessage(Qwz.self, ["-x"], "Unknown option '-x'") + } +} + +private struct Options: ParsableArguments { + enum OutputBehaviour: String, EnumerableFlag { + case stats, count, list + + static func name(for value: OutputBehaviour) -> NameSpecification { + .shortAndLong + } + } + + @Flag(help: "Program output") + var behaviour: OutputBehaviour = .list + + @Flag(inversion: .prefixedNo, exclusivity: .exclusive) var bool: Bool +} + +private struct OptOptions: ParsableArguments { + enum OutputBehaviour: String, EnumerableFlag { + case stats, count, list + + static func name(for value: OutputBehaviour) -> NameSpecification { + .short + } + } + + @Flag(help: "Program output") + var behaviour: OutputBehaviour? +} + +extension ErrorMessageTests { + func testDuplicateFlags() { + AssertErrorMessage(Options.self, ["--list", "--bool", "-s"], "Value to be set with flag \'-s\' had already been set with flag \'--list\'") + AssertErrorMessage(Options.self, ["-cbl"], "Value to be set with flag \'l\' in \'-cbl\' had already been set with flag \'c\' in \'-cbl\'") + AssertErrorMessage(Options.self, ["-bc", "--stats", "-l"], "Value to be set with flag \'--stats\' had already been set with flag \'c\' in \'-bc\'") + + AssertErrorMessage(Options.self, ["--no-bool", "--bool"], "Value to be set with flag \'--bool\' had already been set with flag \'--no-bool\'") + + AssertErrorMessage(OptOptions.self, ["-cbl"], "Value to be set with flag \'l\' in \'-cbl\' had already been set with flag \'c\' in \'-cbl\'") + } +} + +// MARK: - + +fileprivate struct Repeat: ParsableArguments { + @Option() var count: Int? + @Argument() var phrase: String +} + +extension ErrorMessageTests { + func testBadOptionBeforeArgument() { + AssertErrorMessage( + Repeat.self, + ["--cont", "5", "Hello"], + "Unknown option '--cont'. Did you mean '--count'?") + } +} diff --git a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift new file mode 100644 index 0000000..1895aba --- /dev/null +++ b/Tests/ArgumentParserUnitTests/ExitCodeTests.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 +// +//===----------------------------------------------------------------------===// + +import XCTest +@testable import ArgumentParser + +final class ExitCodeTests: XCTestCase { +} + +// MARK: - + +extension ExitCodeTests { + struct A: ParsableArguments {} + struct E: Error {} + struct C: ParsableCommand { + static var configuration = CommandConfiguration(version: "v1") + } + + func testExitCodes() { + XCTAssertEqual(ExitCode.failure, A.exitCode(for: E())) + XCTAssertEqual(ExitCode.validationFailure, A.exitCode(for: ValidationError(""))) + + do { + _ = try A.parse(["-h"]) + XCTFail("Didn't throw help request error.") + } catch { + XCTAssertEqual(ExitCode.success, A.exitCode(for: error)) + } + + do { + _ = try A.parse(["--version"]) + XCTFail("Didn't throw unrecognized --version error.") + } catch { + XCTAssertEqual(ExitCode.validationFailure, A.exitCode(for: error)) + } + + do { + _ = try C.parse(["--version"]) + XCTFail("Didn't throw version request error.") + } catch { + XCTAssertEqual(ExitCode.success, C.exitCode(for: error)) + } + } + + func testExitCode_Success() { + XCTAssertFalse(A.exitCode(for: E()).isSuccess) + XCTAssertFalse(A.exitCode(for: ValidationError("")).isSuccess) + + do { + _ = try A.parse(["-h"]) + XCTFail("Didn't throw help request error.") + } catch { + XCTAssertTrue(A.exitCode(for: error).isSuccess) + } + + do { + _ = try A.parse(["--version"]) + XCTFail("Didn't throw unrecognized --version error.") + } catch { + XCTAssertFalse(A.exitCode(for: error).isSuccess) + } + + do { + _ = try C.parse(["--version"]) + XCTFail("Didn't throw version request error.") + } catch { + XCTAssertTrue(C.exitCode(for: error).isSuccess) + } + } +} + +// MARK: - NSError tests + +extension ExitCodeTests { + func testNSErrorIsHandled() { + struct NSErrorCommand: ParsableCommand { + static let fileNotFoundNSError = NSError(domain: "", code: 1, userInfo: [NSLocalizedDescriptionKey: "The file “foo/bar” couldn’t be opened because there is no such file"]) + } + XCTAssertEqual(NSErrorCommand.exitCode(for: NSErrorCommand.fileNotFoundNSError), ExitCode(rawValue: 1)) + XCTAssertEqual(NSErrorCommand.message(for: NSErrorCommand.fileNotFoundNSError), "The file “foo/bar” couldn’t be opened because there is no such file") + } +} diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift new file mode 100644 index 0000000..a833ca2 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -0,0 +1,508 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +@testable import ArgumentParser + +final class HelpGenerationTests: XCTestCase { +} + +extension URL: ExpressibleByArgument { + public init?(argument: String) { + guard let url = URL(string: argument) else { + return nil + } + self = url + } + + public var defaultValueDescription: String { + self.absoluteString == FileManager.default.currentDirectoryPath + ? "current directory" + : String(describing: self) + } +} + +// 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 = false + @Flag(inversion: .prefixedNo, help: .hidden) var hiddenInvertedFlag: Bool = true + } + + 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. + + """) + } + + struct Issue27: ParsableArguments { + @Option + var two: String = "42" + @Option(help: "The third option") + var three: String + @Option(help: "A fourth option") + var four: String? + @Option(help: "A fifth option") + var five: String = "" + } + + func testHelpWithDefaultValueButNoDiscussion() { + AssertHelp(for: Issue27.self, equals: """ + USAGE: issue27 [--two <two>] --three <three> [--four <four>] [--five <five>] + + OPTIONS: + --two <two> (default: 42) + --three <three> The third option + --four <four> A fourth option + --five <five> A fifth option + -h, --help Show help information. + + """) + } + + enum OptionFlags: String, EnumerableFlag { case optional, required } + enum Degree { + case bachelor, master, doctor + static func degreeTransform(_ string: String) throws -> Degree { + switch string { + case "bachelor": + return .bachelor + case "master": + return .master + case "doctor": + return .doctor + default: + throw ValidationError("Not a valid string for 'Degree'") + } + } + } + + struct D: ParsableCommand { + @Argument(help: "Your occupation.") + var occupation: String = "--" + + @Option(help: "Your name.") + var name: String = "John" + + @Option(help: "Your age.") + var age: Int = 20 + + @Option(help: "Whether logging is enabled.") + var logging: Bool = false + + @Option(parsing: .upToNextOption, help: ArgumentHelp("Your lucky numbers.", valueName: "numbers")) + var lucky: [Int] = [7, 14] + + @Flag(help: "Vegan diet.") + var nda: OptionFlags = .optional + + @Option(help: "Your degree.", transform: Degree.degreeTransform) + var degree: Degree = .bachelor + + @Option(help: "Directory.") + var directory: URL = URL(string: FileManager.default.currentDirectoryPath)! + } + + func testHelpWithDefaultValues() { + AssertHelp(for: D.self, equals: """ + USAGE: d [<occupation>] [--name <name>] [--age <age>] [--logging <logging>] [--lucky <numbers> ...] [--optional] [--required] [--degree <degree>] [--directory <directory>] + + ARGUMENTS: + <occupation> Your occupation. (default: --) + + OPTIONS: + --name <name> Your name. (default: John) + --age <age> Your age. (default: 20) + --logging <logging> Whether logging is enabled. (default: false) + --lucky <numbers> Your lucky numbers. (default: 7, 14) + --optional/--required Vegan diet. (default: optional) + --degree <degree> Your degree. (default: bachelor) + --directory <directory> Directory. (default: current directory) + -h, --help Show help information. + + """) + } + + struct E: ParsableCommand { + enum OutputBehaviour: String, EnumerableFlag { + case stats, count, list + + static func name(for value: OutputBehaviour) -> NameSpecification { + .shortAndLong + } + } + + @Flag(help: "Change the program output") + var behaviour: OutputBehaviour + } + + struct F: ParsableCommand { + enum OutputBehaviour: String, EnumerableFlag { + case stats, count, list + + static func name(for value: OutputBehaviour) -> NameSpecification { + .short + } + } + + @Flag(help: "Change the program output") + var behaviour: OutputBehaviour = .list + } + + struct G: ParsableCommand { + @Flag(inversion: .prefixedNo, help: "Whether to flag") + var flag: Bool = false + } + + func testHelpWithMutuallyExclusiveFlags() { + AssertHelp(for: E.self, equals: """ + USAGE: e --stats --count --list + + OPTIONS: + -s, --stats/-c, --count/-l, --list + Change the program output + -h, --help Show help information. + + """) + + AssertHelp(for: F.self, equals: """ + USAGE: f [-s] [-c] [-l] + + OPTIONS: + -s/-c/-l Change the program output (default: list) + -h, --help Show help information. + + """) + + AssertHelp(for: G.self, equals: """ + USAGE: g [--flag] [--no-flag] + + OPTIONS: + --flag/--no-flag Whether to flag (default: false) + -h, --help Show help information. + + """) + } + + struct H: ParsableCommand { + struct CommandWithVeryLongName: ParsableCommand {} + struct ShortCommand: ParsableCommand { + static var configuration: CommandConfiguration = CommandConfiguration(abstract: "Test short command name.") + } + struct AnotherCommandWithVeryLongName: ParsableCommand { + static var configuration: CommandConfiguration = CommandConfiguration(abstract: "Test long command name.") + } + struct AnotherCommand: ParsableCommand { + @Option() + var someOptionWithVeryLongName: String? + + @Option() + var option: String? + + @Argument(help: "This is an argument with a long name.") + var argumentWithVeryLongNameAndHelp: String = "" + + @Argument + var argumentWithVeryLongName: String = "" + + @Argument + var argument: String = "" + } + static var configuration = CommandConfiguration(subcommands: [CommandWithVeryLongName.self,ShortCommand.self,AnotherCommandWithVeryLongName.self,AnotherCommand.self]) + } + + func testHelpWithSubcommands() { + AssertHelp(for: H.self, equals: """ + USAGE: h <subcommand> + + OPTIONS: + -h, --help Show help information. + + SUBCOMMANDS: + command-with-very-long-name + short-command Test short command name. + another-command-with-very-long-name + Test long command name. + another-command + + See 'h help <subcommand>' for detailed help. + """) + + AssertHelp(for: H.AnotherCommand.self, root: H.self, equals: """ + USAGE: h another-command [--some-option-with-very-long-name <some-option-with-very-long-name>] [--option <option>] [<argument-with-very-long-name-and-help>] [<argument-with-very-long-name>] [<argument>] + + ARGUMENTS: + <argument-with-very-long-name-and-help> + This is an argument with a long name. + <argument-with-very-long-name> + <argument> + + OPTIONS: + --some-option-with-very-long-name <some-option-with-very-long-name> + --option <option> + -h, --help Show help information. + + """) + } + + struct I: ParsableCommand { + static var configuration = CommandConfiguration(version: "1.0.0") + } + + func testHelpWithVersion() { + AssertHelp(for: I.self, equals: """ + USAGE: i + + OPTIONS: + --version Show the version. + -h, --help Show help information. + + """) + + } + + struct J: ParsableCommand { + static var configuration = CommandConfiguration(discussion: "test") + } + + func testOverviewButNoAbstractSpacing() { + let renderedHelp = HelpGenerator(J.self).rendered() + AssertEqualStringsIgnoringTrailingWhitespace(renderedHelp, """ + OVERVIEW: + test + + USAGE: j + + OPTIONS: + -h, --help Show help information. + + """) + } + + struct K: ParsableCommand { + @Argument(help: "A list of paths.") + var paths: [String] = [] + + func validate() throws { + if paths.isEmpty { + throw ValidationError("At least one path must be specified.") + } + } + } + + func testHelpWithNoValueForArray() { + AssertHelp(for: K.self, equals: """ + USAGE: k [<paths> ...] + + ARGUMENTS: + <paths> A list of paths. + + OPTIONS: + -h, --help Show help information. + + """) + } + + struct L: ParsableArguments { + @Option( + name: [.short, .customLong("remote"), .customLong("remote"), .short, .customLong("when"), .long, .customLong("other", withSingleDash: true), .customLong("there"), .customShort("x"), .customShort("y")], + help: "Help Message") + var time: String? + } + + func testHelpWithMultipleCustomNames() { + AssertHelp(for: L.self, equals: """ + USAGE: l [--remote <remote>] + + OPTIONS: + -t, -x, -y, --remote, --when, --time, -other, --there <remote> + Help Message + -h, --help Show help information. + + """) + } + + struct M: ParsableCommand { + } + struct N: ParsableCommand { + static var configuration = CommandConfiguration(subcommands: [M.self], defaultSubcommand: M.self) + } + + func testHelpWithDefaultCommand() { + AssertHelp(for: N.self, equals: """ + USAGE: n <subcommand> + + OPTIONS: + -h, --help Show help information. + + SUBCOMMANDS: + m (default) + + See 'n help <subcommand>' for detailed help. + """) + } + + enum O: String, ExpressibleByArgument { + case small + case medium + case large + + init?(argument: String) { + guard let result = Self(rawValue: argument) else { + return nil + } + self = result + } + } + struct P: ParsableArguments { + @Option(name: [.short], help: "Help Message") + var o: [O] = [.small, .medium] + + @Argument(help: "Help Message") + var remainder: [O] = [.large] + } + + func testHelpWithDefaultValueForArray() { + AssertHelp(for: P.self, equals: """ + USAGE: p [-o <o> ...] [<remainder> ...] + + ARGUMENTS: + <remainder> Help Message (default: large) + + OPTIONS: + -o <o> Help Message (default: small, medium) + -h, --help Show help information. + + """) + } + + struct Foo: ParsableCommand { + public static var configuration = CommandConfiguration( + commandName: "foo", + abstract: "Perform some foo", + subcommands: [ + Bar.self + ], + helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) + + @Option(help: "Name for foo") + var fooName: String? + + public init() {} + } + + struct Bar: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "bar", + _superCommandName: "foo", + abstract: "Perform bar operations", + helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) + + @Option(help: "Bar Strength") + var barStrength: String? + + public init() {} + } + + func testHelpExcludingSuperCommand() throws { + AssertHelp(for: Bar.self, root: Foo.self, equals: """ + OVERVIEW: Perform bar operations + + USAGE: foo bar [--bar-strength <bar-strength>] + + OPTIONS: + --bar-strength <bar-strength> + Bar Strength + -help, -h, --help Show help information. + + """) + } + + struct optionsToHide: ParsableArguments { + @Flag(help: "Verbose") + var verbose: Bool = false + + @Option(help: "Custom Name") + var customName: String? + } + + struct HideDriver: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "driver", abstract: "Demo hiding option groups") + + @OptionGroup(_hiddenFromHelp: true) + var hideMe: optionsToHide + + @Option(help: "Time to wait before timeout (in seconds)") + var timeout: Int? + } + + func testHidingOptionGroup() throws { + AssertHelp(for: HideDriver.self, equals: """ + OVERVIEW: Demo hiding option groups + + USAGE: driver [--verbose] [--custom-name <custom-name>] [--timeout <timeout>] + + OPTIONS: + --timeout <timeout> Time to wait before timeout (in seconds) + -h, --help Show help information. + + """ + ) + } +} diff --git a/Tests/ArgumentParserUnitTests/InputOriginTests.swift b/Tests/ArgumentParserUnitTests/InputOriginTests.swift new file mode 100644 index 0000000..a176792 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/InputOriginTests.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 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 InputOriginTests: XCTestCase {} + +extension InputOriginTests { + func testIsDefaultValue() { + func Assert(elements: [InputOrigin.Element], expectedIsDefaultValue: Bool) { + let inputOrigin = InputOrigin(elements: elements) + if expectedIsDefaultValue { + XCTAssertTrue(inputOrigin.isDefaultValue) + } else { + XCTAssertFalse(inputOrigin.isDefaultValue) + } + } + + Assert(elements: [], expectedIsDefaultValue: false) + Assert(elements: [.defaultValue], expectedIsDefaultValue: true) + Assert(elements: [.argumentIndex(SplitArguments.Index(inputIndex: 1))], expectedIsDefaultValue: false) + Assert(elements: [.defaultValue, .argumentIndex(SplitArguments.Index(inputIndex: 1))], expectedIsDefaultValue: false) + } +} diff --git a/Tests/ArgumentParserUnitTests/MirrorTests.swift b/Tests/ArgumentParserUnitTests/MirrorTests.swift new file mode 100644 index 0000000..cf2f2f7 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/MirrorTests.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 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 MirrorTests: XCTestCase {} + +extension MirrorTests { + private struct Foo { + let foo: String? + let bar: String + let baz: String! + } + func testRealValue() { + func checkChildValue(_ child: Mirror.Child, expectedString: String?) { + if let expectedString = expectedString { + guard let stringValue = child.value as? String else { + XCTFail("child.value is not a String type") + return + } + XCTAssertEqual(stringValue, expectedString) + } else { + XCTAssertNil(nilOrValue(child.value)) + // This is why we use `unwrapedOptionalValue` for optionality checks + // Even though the `value` is `nil` this returns `false` + XCTAssertFalse(child.value as Any? == nil) + } + } + func performTest(foo: String?, baz: String!) { + let fooChild = Foo(foo: foo, bar: "foobar", baz: baz) + Mirror(reflecting: fooChild).children.forEach { child in + switch child.label { + case "foo": + checkChildValue(child, expectedString: foo) + case "bar": + checkChildValue(child, expectedString: "foobar") + case "baz": + checkChildValue(child, expectedString: baz) + default: + XCTFail("Unexpected child") + } + } + } + + performTest(foo: "foo", baz: "baz") + performTest(foo: "foo", baz: nil) + performTest(foo: nil, baz: "baz") + performTest(foo: nil, baz: nil) + } +} diff --git a/Tests/ArgumentParserUnitTests/NameSpecificationTests.swift b/Tests/ArgumentParserUnitTests/NameSpecificationTests.swift new file mode 100644 index 0000000..956a4a8 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/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/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift new file mode 100644 index 0000000..b7a3243 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift @@ -0,0 +1,464 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +@testable import ArgumentParser + +final class ParsableArgumentsValidationTests: XCTestCase { + private struct A: ParsableCommand { + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? + + @Argument(help: "The phrase to repeat.") + var phrase: String + + enum CodingKeys: String, CodingKey { + case count + case phrase + } + + mutating func run() throws {} + } + + private struct B: ParsableCommand { + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? + + @Argument(help: "The phrase to repeat.") + var phrase: String + + mutating func run() throws {} + } + + private struct C: ParsableCommand { + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? + + @Argument(help: "The phrase to repeat.") + var phrase: String + + enum CodingKeys: String, CodingKey { + case phrase + } + + mutating func run() throws {} + } + + private struct D: ParsableArguments { + @Argument(help: "The phrase to repeat.") + var phrase: String + + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? + + enum CodingKeys: String, CodingKey { + case count + } + } + + private struct E: ParsableArguments { + @Argument(help: "The phrase to repeat.") + var phrase: String + + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? + + @Flag(help: "Include a counter with each repetition.") + var includeCounter: Bool = false + + enum CodingKeys: String, CodingKey { + case count + } + } + + func testCodingKeyValidation() throws { + XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(A.self)) + XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(B.self)) + + if let error = ParsableArgumentsCodingKeyValidator.validate(C.self) + as? ParsableArgumentsCodingKeyValidator.Error + { + XCTAssert(error.missingCodingKeys == ["count"]) + } else { + XCTFail() + } + + if let error = ParsableArgumentsCodingKeyValidator.validate(D.self) + as? ParsableArgumentsCodingKeyValidator.Error + { + XCTAssert(error.missingCodingKeys == ["phrase"]) + } else { + XCTFail() + } + + if let error = ParsableArgumentsCodingKeyValidator.validate(E.self) + as? ParsableArgumentsCodingKeyValidator.Error + { + XCTAssert(error.missingCodingKeys == ["phrase", "includeCounter"]) + } else { + XCTFail() + } + } + + private struct F: ParsableArguments { + @Argument() + var phrase: String + + @Argument() + var items: [Int] = [] + } + + private struct G: ParsableArguments { + @Argument() + var items: [Int] = [] + + @Argument() + var phrase: String + } + + private struct H: ParsableArguments { + @Argument() + var items: [Int] = [] + + @Option() + var option: Bool + } + + private struct I: ParsableArguments { + @Argument() + var name: String + + @OptionGroup() + var options: F + } + + private struct J: ParsableArguments { + struct Options: ParsableArguments { + @Argument() + var numberOfItems: [Int] = [] + } + + @OptionGroup() + var options: Options + + @Argument() + var phrase: String + } + + private struct K: ParsableArguments { + struct Options: ParsableArguments { + @Argument() + var items: [Int] = [] + } + + @Argument() + var phrase: String + + @OptionGroup() + var options: Options + } + + // Compilation test to verify that property wrappers can be written without () + private struct L: ParsableArguments { + struct Options: ParsableArguments { + @Argument var items: [Int] = [] + } + + @Argument var foo: String + @Option var bar: String + @OptionGroup var options: Options + @Flag var flag = false + } + + func testPositionalArgumentsValidation() throws { + XCTAssertNil(PositionalArgumentsValidator.validate(A.self)) + XCTAssertNil(PositionalArgumentsValidator.validate(F.self)) + XCTAssertNil(PositionalArgumentsValidator.validate(H.self)) + XCTAssertNil(PositionalArgumentsValidator.validate(I.self)) + XCTAssertNil(PositionalArgumentsValidator.validate(K.self)) + + if let error = PositionalArgumentsValidator.validate(G.self) as? PositionalArgumentsValidator.Error { + XCTAssert(error.positionalArgumentFollowingRepeated == "phrase") + XCTAssert(error.repeatedPositionalArgument == "items") + } else { + XCTFail() + } + + if let error = PositionalArgumentsValidator.validate(J.self) as? PositionalArgumentsValidator.Error { + XCTAssert(error.positionalArgumentFollowingRepeated == "phrase") + XCTAssert(error.repeatedPositionalArgument == "numberOfItems") + } else { + XCTFail() + } + } + + // MARK: ParsableArgumentsUniqueNamesValidator tests + fileprivate let unexpectedErrorMessage = "Expected error of type `ParsableArgumentsUniqueNamesValidator.Error`, but got something else." + + // MARK: Names are unique + fileprivate struct DifferentNames: ParsableArguments { + @Option() + var foo: String + + @Option() + var bar: String + } + + func testUniqueNamesValidation_NoViolation() throws { + XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DifferentNames.self)) + } + + // MARK: One name is duplicated + fileprivate struct TwoOfTheSameName: ParsableCommand { + @Option() + var foo: String + + @Option(name: .customLong("foo")) + var notActuallyFoo: String + } + + func testUniqueNamesValidation_TwoOfSameName() throws { + if let error = ParsableArgumentsUniqueNamesValidator.validate(TwoOfTheSameName.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"--foo\".") + } else { + XCTFail(unexpectedErrorMessage) + } + } + + // MARK: Multiple names are duplicated + fileprivate struct MultipleUniquenessViolations: ParsableArguments { + @Option() + var foo: String + + @Option(name: .customLong("foo")) + var notActuallyFoo: String + + @Option() + var bar: String + + @Flag(name: .customLong("bar")) + var notBar: Bool = false + + @Option(name: [.long, .customLong("help", withSingleDash: true)]) + var help: String + } + + func testUniqueNamesValidation_TwoDuplications() throws { + if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleUniquenessViolations.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssert( + /// The `Mirror` reflects the properties `foo` and `bar` in a random order each time it's built. + error.description == """ + Multiple (2) `Option` or `Flag` arguments are named \"--bar\". + Multiple (2) `Option` or `Flag` arguments are named \"--foo\". + """ + || error.description == """ + Multiple (2) `Option` or `Flag` arguments are named \"--foo\". + Multiple (2) `Option` or `Flag` arguments are named \"--bar\". + """ + ) + } else { + XCTFail(unexpectedErrorMessage) + } + } + + // MARK: Argument has multiple names and one is duplicated + fileprivate struct MultipleNamesPerArgument: ParsableCommand { + @Flag(name: [.customShort("v"), .customLong("very-chatty")]) + var verbose: Bool = false + + enum Versimilitude: String, ExpressibleByArgument { + case yes + case some + case none + } + + @Option(name: .customShort("v")) + var versimilitude: Versimilitude + } + + func testUniqueNamesValidation_ArgumentHasMultipleNames() throws { + if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleNamesPerArgument.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"-v\".") + } else { + XCTFail(unexpectedErrorMessage) + } + } + + // MARK: One name duplicated several times + fileprivate struct FourDuplicateNames: ParsableArguments { + @Option() + var foo: String + + @Option(name: .customLong("foo")) + var notActuallyFoo: String + + @Flag(name: .customLong("foo")) + var stillNotFoo: Bool = false + + enum Numbers: Int, ExpressibleByArgument { + case one = 1 + case two + case three + } + + @Option(name: .customLong("foo")) + var alsoNotFoo: Numbers + } + + func testUniqueNamesValidation_MoreThanTwoDuplications() throws { + if let error = ParsableArgumentsUniqueNamesValidator.validate(FourDuplicateNames.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssertEqual(error.description, "Multiple (4) `Option` or `Flag` arguments are named \"--foo\".") + } else { + XCTFail(unexpectedErrorMessage) + } + } + + // MARK: EnumerableFlag has first letter duplication + + fileprivate struct DuplicatedFirstLettersShortNames: ParsableCommand { + enum ExampleEnum: String, EnumerableFlag { + case first + case second + case other + case forth + case fith + + static func name(for value: ExampleEnum) -> NameSpecification { + .short + } + } + + @Flag + var enumFlag: ExampleEnum = .first + } + + fileprivate struct DuplicatedFirstLettersLongNames: ParsableCommand { + enum ExampleEnum: String, EnumerableFlag { + case first + case second + case other + case forth + case fith + } + + @Flag + var enumFlag2: ExampleEnum = .first + } + + func testUniqueNamesValidation_DuplicatedFlagFirstLetters_ShortNames() throws { + if let error = ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersShortNames.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssertEqual(error.description, "Multiple (3) `Option` or `Flag` arguments are named \"-f\".") + } else { + XCTFail(unexpectedErrorMessage) + } + } + + func testUniqueNamesValidation_DuplicatedFlagFirstLetters_LongNames() throws { + XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersLongNames.self)) + } + + fileprivate struct HasOneNonsenseFlag: ParsableCommand { + enum ExampleEnum: String, EnumerableFlag { + case first + case second + case other + case forth + case fith + } + + @Flag + var enumFlag: ExampleEnum = .first + + @Flag + var fine: Bool = false + + @Flag(inversion: .prefixedNo) + var alsoFine: Bool = false + + @Flag(inversion: .prefixedNo) + var stillFine: Bool = true + + @Flag(inversion: .prefixedNo) + var yetStillFine: Bool + + @Flag + var nonsense: Bool = true + } + + func testNonsenseFlagsValidation_OneFlag() throws { + if let error = NonsenseFlagsValidator.validate(HasOneNonsenseFlag.self) + as? NonsenseFlagsValidator.Error + { + XCTAssertEqual( + error.description, + """ + One or more Boolean flags is declared with an initial value of `true`. + This results in the flag always being `true`, no matter whether the user + specifies the flag or not. To resolve this error, change the default to + `false`, provide a value for the `inversion:` parameter, or remove the + `@Flag` property wrapper altogether. + + Affected flag(s): + --nonsense + """) + } else { + XCTFail(unexpectedErrorMessage) + } + } + + fileprivate struct MultipleNonsenseFlags: ParsableCommand { + @Flag + var stuff = true + + @Flag + var nonsense = true + + @Flag + var okay = false + + @Flag + var moreNonsense = true + } + + func testNonsenseFlagsValidation_MultipleFlags() throws { + if let error = NonsenseFlagsValidator.validate(MultipleNonsenseFlags.self) + as? NonsenseFlagsValidator.Error + { + XCTAssertEqual( + error.description, + """ + One or more Boolean flags is declared with an initial value of `true`. + This results in the flag always being `true`, no matter whether the user + specifies the flag or not. To resolve this error, change the default to + `false`, provide a value for the `inversion:` parameter, or remove the + `@Flag` property wrapper altogether. + + Affected flag(s): + --stuff + --nonsense + --more-nonsense + """) + } else { + XCTFail(unexpectedErrorMessage) + } + } +} diff --git a/Tests/ArgumentParserUnitTests/SequenceExtensionTests.swift b/Tests/ArgumentParserUnitTests/SequenceExtensionTests.swift new file mode 100644 index 0000000..5c409be --- /dev/null +++ b/Tests/ArgumentParserUnitTests/SequenceExtensionTests.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------*- 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 SequenceExtensionTests: XCTestCase {} + +extension SequenceExtensionTests { + func testUniquing() { + XCTAssertEqual([], (0..<0).uniquing()) + XCTAssertEqual([0, 1, 2, 3, 4], (0..<5).uniquing()) + XCTAssertEqual([0, 1, 2, 3, 4], [0, 1, 2, 3, 4, 0, 1, 2, 3, 4].uniquing()) + XCTAssertEqual([0, 1, 2, 3, 4], [0, 1, 2, 3, 4, 4, 3, 2, 1, 0].uniquing()) + } + + func testUniquingAdjacentElements() { + XCTAssertEqual([], (0..<0).uniquingAdjacentElements()) + XCTAssertEqual([0, 1, 2, 3, 4], (0..<5).uniquingAdjacentElements()) + XCTAssertEqual( + [0, 1, 2, 3, 4], + [0, 0, 1, 1, 1, 1, 2, 3, 3, 3, 4, 4].uniquingAdjacentElements()) + XCTAssertEqual( + [0, 1, 2, 3, 4, 3, 2, 1, 0], + [0, 1, 2, 3, 4, 4, 3, 2, 1, 0].uniquingAdjacentElements()) + } +} diff --git a/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift b/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift new file mode 100644 index 0000000..5b1c1c2 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/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 ArgumentParserTestHelpers + +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.endIndex 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].index + 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.Value, file: StaticString = #file, line: UInt = #line) { + guard index < sut.elements.endIndex 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].value, 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: 2, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sutB, at: 2, .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: 2, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sutB, at: 2, .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.value, .option(.name(.long("foo")))) + + let b = try XCTUnwrap(sut.popNext()) + XCTAssertEqual(b.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(b.1.value, .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.value, .option(.name(.long("foo")))) + + let b = try XCTUnwrap(sut.peekNext()) + XCTAssertEqual(b.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) + XCTAssertEqual(b.1.value, .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/ArgumentParserUnitTests/StringEditDistanceTests.swift b/Tests/ArgumentParserUnitTests/StringEditDistanceTests.swift new file mode 100644 index 0000000..5967c21 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/StringEditDistanceTests.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------*- 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 StringEditDistanceTests: XCTestCase {} + +extension StringEditDistanceTests { + func testStringEditDistance() { + XCTAssertEqual("".editDistance(to: ""), 0) + XCTAssertEqual("".editDistance(to: "foo"), 3) + XCTAssertEqual("foo".editDistance(to: ""), 3) + XCTAssertEqual("foo".editDistance(to: "bar"), 3) + XCTAssertEqual("bar".editDistance(to: "foo"), 3) + XCTAssertEqual("bar".editDistance(to: "baz"), 1) + XCTAssertEqual("baz".editDistance(to: "bar"), 1) + } +} diff --git a/Tests/ArgumentParserUnitTests/StringSnakeCaseTests.swift b/Tests/ArgumentParserUnitTests/StringSnakeCaseTests.swift new file mode 100644 index 0000000..d0c6fd6 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/StringSnakeCaseTests.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------*- 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 StringSnakeCaseTests: XCTestCase {} + +extension StringSnakeCaseTests { + func testStringSnakeCase() { + let toSnakeCaseTests = [ + ("simpleOneTwo", "simple_one_two"), + ("myURL", "my_url"), + ("singleCharacterAtEndX", "single_character_at_end_x"), + ("thisIsAnXMLProperty", "this_is_an_xml_property"), + ("single", "single"), // no underscore + ("", ""), // don't die on empty string + ("a", "a"), // single character + ("aA", "a_a"), // two characters + ("version4Thing", "version4_thing"), // numerics + ("partCAPS", "part_caps"), // only insert underscore before first all caps + ("partCAPSLowerAGAIN", "part_caps_lower_again"), // switch back and forth caps. + ("manyWordsInThisThing", "many_words_in_this_thing"), // simple lowercase + underscore + more + ("asdfĆqer", "asdf_ćqer"), + ("already_snake_case", "already_snake_case"), + ("dataPoint22", "data_point22"), + ("dataPoint22Word", "data_point22_word"), + ("_oneTwoThree", "_one_two_three"), + ("oneTwoThree_", "one_two_three_"), + ("__oneTwoThree", "__one_two_three"), + ("oneTwoThree__", "one_two_three__"), + ("_oneTwoThree_", "_one_two_three_"), + ("__oneTwoThree", "__one_two_three"), + ("__oneTwoThree__", "__one_two_three__"), + ("_test", "_test"), + ("_test_", "_test_"), + ("__test", "__test"), + ("test__", "test__"), + ("m͉̟̹y̦̳G͍͚͎̳r̤͉̤͕ͅea̲͕t͇̥̼͖U͇̝̠R͙̻̥͓̣L̥̖͎͓̪̫ͅR̩͖̩eq͈͓u̞e̱s̙t̤̺ͅ", "m͉̟̹y̦̳_g͍͚͎̳r̤͉̤͕ͅea̲͕t͇̥̼͖_u͇̝̠r͙̻̥͓̣l̥̖͎͓̪̫ͅ_r̩͖̩eq͈͓u̞e̱s̙t̤̺ͅ"), // because Itai wanted to test this + ("🐧🐟", "🐧🐟"), // fishy emoji example? + ("URLSession", "url_session"), + ("RADAR", "radar"), + ("Sample", "sample"), + ("_Sample", "_sample"), + ("_IAmAnAPPDeveloper", "_i_am_an_app_developer") + ] + for test in toSnakeCaseTests { + XCTAssertEqual(test.0.convertedToSnakeCase(), test.1) + } + } + + func testStringSnakeCaseWithSeparator() { + let toSnakeCaseTests = [ + ("simpleOneTwo", "simple-one-two"), + ("myURL", "my-url"), + ("singleCharacterAtEndX", "single-character-at-end-x"), + ("thisIsAnXMLProperty", "this-is-an-xml-property"), + ("single", "single"), // no underscore + ("", ""), // don't die on empty string + ("a", "a"), // single character + ("aA", "a-a"), // two characters + ("version4Thing", "version4-thing"), // numerics + ("partCAPS", "part-caps"), // only insert underscore before first all caps + ("partCAPSLowerAGAIN", "part-caps-lower-again"), // switch back and forth caps. + ("manyWordsInThisThing", "many-words-in-this-thing"), // simple lowercase + underscore + more + ("asdfĆqer", "asdf-ćqer"), + ("already_snake_case", "already_snake_case"), + ("dataPoint22", "data-point22"), + ("dataPoint22Word", "data-point22-word"), + ("_oneTwoThree", "_one-two-three"), + ("oneTwoThree_", "one-two-three_"), + ("__oneTwoThree", "__one-two-three"), + ("oneTwoThree__", "one-two-three__"), + ("_oneTwoThree_", "_one-two-three_"), + ("__oneTwoThree", "__one-two-three"), + ("__oneTwoThree__", "__one-two-three__"), + ("_test", "_test"), + ("_test_", "_test_"), + ("__test", "__test"), + ("test__", "test__"), + ("m͉̟̹y̦̳G͍͚͎̳r̤͉̤͕ͅea̲͕t͇̥̼͖U͇̝̠R͙̻̥͓̣L̥̖͎͓̪̫ͅR̩͖̩eq͈͓u̞e̱s̙t̤̺ͅ", "m͉̟̹y̦̳-g͍͚͎̳r̤͉̤͕ͅea̲͕t͇̥̼͖-u͇̝̠r͙̻̥͓̣l̥̖͎͓̪̫ͅ-r̩͖̩eq͈͓u̞e̱s̙t̤̺ͅ"), // because Itai wanted to test this + ("🐧🐟", "🐧🐟"), // fishy emoji example? + ("URLSession", "url-session"), + ("RADAR", "radar"), + ("Sample", "sample"), + ("_Sample", "_-sample"), + ("_IAmAnAPPDeveloper", "_-i-am-an-app-developer") + ] + for test in toSnakeCaseTests { + XCTAssertEqual(test.0.convertedToSnakeCase(separator: "-"), test.1) + } + } +} diff --git a/Tests/ArgumentParserUnitTests/StringWrappingTests.swift b/Tests/ArgumentParserUnitTests/StringWrappingTests.swift new file mode 100644 index 0000000..2829160 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/StringWrappingTests.swift @@ -0,0 +1,167 @@ +//===----------------------------------------------------------*- 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. +""" + +let jsonSample = """ +{ + "level1": { + "level2": { + "level3": true + } + } +} +""" + +// 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. + """) + + } + + func testJSON() { + XCTAssertEqual(jsonSample.wrapped(to: 80), """ + { + "level1": { + "level2": { + "level3": true + } + } + } + """) + } + + func testJSONWithIndent() { + XCTAssertEqual(jsonSample.wrapped(to: 80, wrappingIndent: 10), """ + { + "level1": { + "level2": { + "level3": true + } + } + } + """) + } +} diff --git a/Tests/ArgumentParserUnitTests/TreeTests.swift b/Tests/ArgumentParserUnitTests/TreeTests.swift new file mode 100644 index 0000000..5196e79 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/TreeTests.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------*- 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) + } +} + +extension TreeTests { + struct A: ParsableCommand { + static let configuration = CommandConfiguration(subcommands: [A.self]) + } + struct Root: ParsableCommand { + static let configuration = CommandConfiguration(subcommands: [Sub.self]) + } + struct Sub: ParsableCommand { + static let configuration = CommandConfiguration(subcommands: [Sub.self]) + } + + func testInitializationWithRecursiveSubcommand() { + XCTAssertThrowsError(try Tree(root: A.asCommand)) + XCTAssertThrowsError(try Tree(root: Root.asCommand)) + } +} diff --git a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift new file mode 100644 index 0000000..bf50b4b --- /dev/null +++ b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift @@ -0,0 +1,200 @@ +//===----------------------------------------------------------*- 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 = false + @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 + var name: String = "no-name" + + @Option + var count: Int = 0 + + @Argument + var arg: String = "no-arg" + } + + func testSynopsisWithDefaults() { + let help = UsageGenerator(toolName: "bar", parsable: E()) + XCTAssertEqual(help.synopsis, "bar [--name <name>] [--count <count>] [<arg>]") + } + + 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") + } + + struct I: ParsableArguments { + enum Color { + case red, blue + static func transform(_ string: String) throws -> Color { + switch string { + case "red": + return .red + case "blue": + return .blue + default: + throw ValidationError("Not a valid string for 'Color'") + } + } + } + + @Option(transform: Color.transform) + var color: Color = .red + } + + func testSynopsisWithDefaultValueAndTransform() { + let help = UsageGenerator(toolName: "bar", parsable: I()) + XCTAssertEqual(help.synopsis, "bar [--color <color>]") + } + + struct J: ParsableArguments { + struct Foo {} + @Option(transform: { _ in Foo() }) var req: Foo + @Option(transform: { _ in Foo() }) var opt: Foo? + } + + func testSynopsisWithTransform() { + let help = UsageGenerator(toolName: "bar", parsable: J()) + XCTAssertEqual(help.synopsis, "bar --req <req> [--opt <opt>]") + } + + struct K: ParsableArguments { + @Option( + name: [.short, .customLong("remote"), .customLong("when"), .customLong("there")], + help: "Help Message") + var time: String? + } + + func testSynopsisWithMultipleCustomNames() { + let help = UsageGenerator(toolName: "bar", parsable: K()) + XCTAssertEqual(help.synopsis, "bar [--remote <remote>]") + } + + struct L: ParsableArguments { + @Option( + name: [.short, .short, .customLong("remote", withSingleDash: true), .short, .customLong("remote", withSingleDash: true)], + help: "Help Message") + var time: String? + } + + func testSynopsisWithSingleDashLongNameFirst() { + let help = UsageGenerator(toolName: "bar", parsable: L()) + XCTAssertEqual(help.synopsis, "bar [-remote <remote>]") + } + + struct M: ParsableArguments { + @Flag var a: Bool = false + @Flag var b: Bool = false + @Flag var c: Bool = false + @Flag var d: Bool = false + @Flag var e: Bool = false + @Flag var f: Bool = false + @Flag var g: Bool = false + @Flag var h: Bool = false + @Flag var i: Bool = false + @Flag var j: Bool = false + @Flag var k: Bool = false + @Flag var l: Bool = false + @Option var option: Bool + @Argument var input: String + @Argument var output: String? + } + + func testSynopsisWithTooManyOptions() { + let help = UsageGenerator(toolName: "foo", parsable: M()) + XCTAssertEqual(help.synopsis, + "foo [<options>] --option <option> <input> [<output>]") + } +} diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 0000000..20530e9 --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(ArgumentParserEndToEndTests) +add_subdirectory(ArgumentParserUnitTests) diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..917e331 --- /dev/null +++ b/Tests/LinuxMain.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 +// +//===----------------------------------------------------------------------===// + +#error(""" + + ----------------------------------------------------- + + Please test with `swift test --enable-test-discovery` + + ----------------------------------------------------- + """) diff --git a/Tools/changelog-authors/main.swift b/Tools/changelog-authors/main.swift new file mode 100644 index 0000000..3c5231b --- /dev/null +++ b/Tools/changelog-authors/main.swift @@ -0,0 +1,143 @@ +//===----------------------------------------------------------*- 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 +import Foundation + +// MARK: GitHub API response modeling + +struct Comparison: Codable { + var commits: [Commit] +} + +struct Commit: Codable { + var sha: String + var author: Author +} + +struct Author: Codable { + var login: String + var htmlURL: String + + enum CodingKeys: String, CodingKey { + case login + case htmlURL = "html_url" + } + + var commitURL: String { + "https://github.com/apple/swift-argument-parser/commits?author=\(login)" + } + + var inlineLink: String { + "[\(login)]" + } + + var linkReference: String { + "[\(login)]: \(commitURL)" + } +} + +// MARK: Helpers + +extension Sequence { + func uniqued<T: Hashable>(by transform: (Element) throws -> T) rethrows -> [Element] { + var seen: Set<T> = [] + var result: [Element] = [] + + for element in self { + if try seen.insert(transform(element)).inserted { + result.append(element) + } + } + return result + } +} + +// MARK: Command + +struct ChangelogAuthors: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + abstract: "A helper tool for generating author info for the changelog.", + discussion: """ + Call this tool with a starting and ending tag to list authors of + commits between those two releases. Provide only a single tag to + list authors from that release up to the current top-of-tree. + """) + } + + @Argument(help: "The starting point for the comparison.") + var startingTag: String + + @Argument(help: "The ending point for the comparison.") + var endingTag: String? + + func validate() throws { + func checkTag(_ tag: String) -> Bool { + tag.allSatisfy { + $0.isLetter || $0.isNumber || $0 == "." + } + } + + guard checkTag(startingTag) else { + throw ValidationError("Invalid starting tag: \(startingTag)") + } + + if let endingTag = endingTag { + guard checkTag(endingTag) else { + throw ValidationError("Invalid ending tag: \(endingTag)") + } + } + } + + func links(for authors: [Author]) -> String { + if authors.count <= 2 { + return authors.map({ $0.inlineLink }).joined(separator: " and ") + } else { + let result = authors.dropLast() + .map({ $0.inlineLink }) + .joined(separator: ", ") + return "\(result), and \(authors.last!.inlineLink)" + } + } + + func references(for authors: [Author]) -> String { + authors + .map({ $0.linkReference }) + .joined(separator: "\n") + } + + func comparisonURL() throws -> URL { + guard let url = URL( + string: "https://api.github.com/repos/apple/swift-argument-parser/compare/\(startingTag)...\(endingTag ?? "HEAD")") + else { + print("Couldn't create url string") + throw ExitCode.failure + } + + return url + } + + mutating func run() throws { + let data = try Data(contentsOf: try comparisonURL()) + let comparison = try JSONDecoder().decode(Comparison.self, from: data) + let authors = comparison.commits.map({ $0.author }) + .uniqued(by: { $0.login }) + .sorted(by: { $0.login.lowercased() < $1.login.lowercased() }) + + print(links(for: authors)) + print("---") + print(references(for: authors)) + } +} + +ChangelogAuthors.main() + diff --git a/cmake/modules/ArgumentParserConfig.cmake.in b/cmake/modules/ArgumentParserConfig.cmake.in new file mode 100644 index 0000000..652723f --- /dev/null +++ b/cmake/modules/ArgumentParserConfig.cmake.in @@ -0,0 +1,3 @@ +if(NOT TARGET ArgumentParser) + include(@ArgumentParser_EXPORTS_FILE@) +endif() \ No newline at end of file diff --git a/cmake/modules/CMakeLists.txt b/cmake/modules/CMakeLists.txt new file mode 100644 index 0000000..d628b08 --- /dev/null +++ b/cmake/modules/CMakeLists.txt @@ -0,0 +1,8 @@ +set(ArgumentParser_EXPORTS_FILE ${CMAKE_CURRENT_BINARY_DIR}/ArgumentParserExports.cmake) + + configure_file(ArgumentParserConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/ArgumentParserConfig.cmake) + + get_property(ArgumentParser_EXPORTS GLOBAL PROPERTY ArgumentParser_EXPORTS) + export(TARGETS ${ArgumentParser_EXPORTS} + FILE ${ArgumentParser_EXPORTS_FILE}) \ No newline at end of file diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake new file mode 100644 index 0000000..2b2ac76 --- /dev/null +++ b/cmake/modules/SwiftSupport.cmake @@ -0,0 +1,99 @@ +# Returns the architecture name in a variable +# +# Usage: +# get_swift_host_arch(result_var_name) +# +# Sets ${result_var_name} with the converted architecture name derived from +# CMAKE_SYSTEM_PROCESSOR. +function(get_swift_host_arch result_var_name) + if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64") + set("${result_var_name}" "x86_64" PARENT_SCOPE) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64|arm64") + if(NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET OR + "${CMAKE_OSX_DEPLOYMENT_TARGET}" STREQUAL "") + set("${result_var_name}" "aarch64" PARENT_SCOPE) + else() + set("${result_var_name}" "arm64" PARENT_SCOPE) + endif() + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64") + set("${result_var_name}" "powerpc64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64le") + set("${result_var_name}" "powerpc64le" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "s390x") + set("${result_var_name}" "s390x" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv6l") + set("${result_var_name}" "armv6" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7-a") + set("${result_var_name}" "armv7" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7l") + set("${result_var_name}" "armv7" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "amd64") + set("${result_var_name}" "amd64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64") + set("${result_var_name}" "x86_64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "IA64") + set("${result_var_name}" "itanium" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86") + set("${result_var_name}" "i686" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "i686") + set("${result_var_name}" "i686" PARENT_SCOPE) + else() + message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}") + endif() +endfunction() + +# Returns the os name in a variable +# +# Usage: +# get_swift_host_os(result_var_name) +# +# +# Sets ${result_var_name} with the converted OS name derived from +# CMAKE_SYSTEM_NAME. +function(get_swift_host_os result_var_name) + if(CMAKE_SYSTEM_NAME STREQUAL Darwin) + set(${result_var_name} macosx PARENT_SCOPE) + else() + string(TOLOWER ${CMAKE_SYSTEM_NAME} cmake_system_name_lc) + set(${result_var_name} ${cmake_system_name_lc} PARENT_SCOPE) + endif() +endfunction() + +function(_install_target module) + get_swift_host_os(swift_os) + get_target_property(type ${module} TYPE) + + if(type STREQUAL STATIC_LIBRARY) + set(swift swift_static) + else() + set(swift swift) + endif() + + install(TARGETS ${module} + ARCHIVE DESTINATION lib/${swift}/${swift_os} + LIBRARY DESTINATION lib/${swift}/${swift_os} + RUNTIME DESTINATION bin) + if(type STREQUAL EXECUTABLE) + return() + endif() + + get_swift_host_arch(swift_arch) + get_target_property(module_name ${module} Swift_MODULE_NAME) + if(NOT module_name) + set(module_name ${module}) + endif() + + if(CMAKE_SYSTEM_NAME STREQUAL Darwin) + install(FILES $<TARGET_PROPERTY:${module},Swift_MODULE_DIRECTORY>/${module_name}.swiftdoc + DESTINATION lib/${swift}/${swift_os}/${module_name}.swiftmodule + RENAME ${swift_arch}.swiftdoc) + install(FILES $<TARGET_PROPERTY:${module},Swift_MODULE_DIRECTORY>/${module_name}.swiftmodule + DESTINATION lib/${swift}/${swift_os}/${module_name}.swiftmodule + RENAME ${swift_arch}.swiftmodule) + else() + install(FILES + $<TARGET_PROPERTY:${module},Swift_MODULE_DIRECTORY>/${module_name}.swiftdoc + $<TARGET_PROPERTY:${module},Swift_MODULE_DIRECTORY>/${module_name}.swiftmodule + DESTINATION lib/${swift}/${swift_os}/${swift_arch}) + endif() +endfunction() diff --git a/swift-argument-parser b/swift-argument-parser new file mode 100644 index 0000000..e69de29 diff --git a/swift-argument-parser.podspec b/swift-argument-parser.podspec new file mode 100644 index 0000000..4eb8bd4 --- /dev/null +++ b/swift-argument-parser.podspec @@ -0,0 +1,16 @@ +Pod::Spec.new do |s| + s.name = 'swift-argument-parser' + s.version = '0.4.4' + s.summary = 'Type-safe and easy way for parsing command line arguments in your macOS command line tools.' + s.homepage = 'https://github.com/ayush-yadav001/swift-argument-parser' + s.license = { :type => 'Apache License v2.0 with Runtime Library Exception', :file => 'LICENSE' } + s.author = { 'Apple Inc. and the Swift project authors' => '' } + s.source = { :git => 'https://github.com/ayush-yadav001/swift-argument-parser.git', :tag => s.version.to_s } + + s.cocoapods_version = '>= 1.4' + s.swift_version = '5.1' + s.ios.deployment_target = '10.0' + s.source_files = 'Sources/ArgumentParser/**/*' + s.frameworks = 'Foundation' + +end \ No newline at end of file