diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fac09ad1e..2cc2fdc67 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,5 +17,5 @@ Replace this paragraph with a description of your changes and rationale. Provide ### Checklist - [ ] I've added at least one test that validates that my change is working, if appropriate - [ ] I've followed the code style of the rest of the project -- [ ] I've read the [Contribution Guidelines](CONTRIBUTING.md) +- [ ] I've read the [Contribution Guidelines](https://github.com/apple/swift-argument-parser/blob/main/CONTRIBUTING.md) - [ ] I've updated the documentation if necessary diff --git a/.github/PULL_REQUEST_TEMPLATE/NEW.md b/.github/PULL_REQUEST_TEMPLATE/NEW.md index dfbd6323c..4f853a515 100644 --- a/.github/PULL_REQUEST_TEMPLATE/NEW.md +++ b/.github/PULL_REQUEST_TEMPLATE/NEW.md @@ -32,5 +32,5 @@ What is the impact of this change on existing users? Does it deprecate or remove ### Checklist - [ ] I've added at least one test that validates that my change is working, if appropriate - [ ] I've followed the code style of the rest of the project -- [ ] I've read the [Contribution Guidelines](CONTRIBUTING.md) +- [ ] I've read the [Contribution Guidelines](https://github.com/apple/swift-argument-parser/blob/main/CONTRIBUTING.md) - [ ] I've updated the documentation if necessary diff --git a/.gitignore b/.gitignore index 05078dae0..4b8005bc6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /Packages /*.xcodeproj .swiftpm +.vscode .*.sw? diff --git a/.xcodesamplecode.plist b/.xcodesamplecode.plist deleted file mode 100644 index 8d1819693..000000000 --- a/.xcodesamplecode.plist +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 23488ce13..d09263b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,302 @@ Add new items at the end of the relevant section under **Unreleased**. --> -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.3.0")` as the requirement. - ## [Unreleased] *No changes yet.* --- +## [1.1.2] - 2022-04-11 + +### Changes + +- CMake builds now always statically links `ArgumentParserToolInfo`. + ([#424]) + +### Fixes + +- When a user provides an array-based option's key (e.g. `--key`) + without any values, the error message now correctly describes the + problem. ([#435]) + +The 1.1.2 release includes contributions from [compnerd] and [KeithBird]. +Thank you! + +## [1.1.1] - 2022-03-16 + +### Fixes + +- Moves the platform requirement from the package level down to the new + types and protocols with `async` members. This was a source-breaking + change in 1.1.0. ([#427]) +- Fixed issues in the CMake build configuration. + +## [1.1.0] - 2022-03-14 + +### Additions + +- A command's `run()` method now supports `async`/`await` when the command + conforms to `AsyncParsableCommand`. ([#404]) +- New API for distinguishing between public, hidden, and private arguments + and option groups, and a new extended help screen accessible via + `--help-hidden`. ([#366], [#390], and [#405 through #413][1.1.0]) +- You can now override the autogenerated usage string when configuring a + command. ([#400]) + +### Changes + +- `ArgumentParser` now requires Swift 5.5. + +### Fixes + +- The auto-generated usage string now correctly hides all optional parameters + when over the length limit. ([#416]) +- One `@Option` initializer now has its parameters in the correct order; the + incorrect initializer is deprecated. ([#391]) +- Help flags are now correctly captured in `.unconditionalRemaining` argument + arrays. +- Documentation fixes and improvements. + +The 1.1.0 release includes contributions from [keith], [MartinP7r], [McNight], +[natecook1000], [rauhul], and [zkiraly]. Thank you! + +--- + +## [1.0.3] - 2022-01-31 + +### Changes + +- When a user provides an incorrect value for an option, an + `ArgumentParser`-based program now includes the valid values when possible. + + ``` + $ example --format png + Error: The value 'png' is invalid for '--format '. + Please provide one of 'text', 'json' or 'csv'. + ``` + +### Fixes + +- Resolves an issue with `zsh` custom completions for command names that include + a dash. +- Improves the generated completions scripts for `fish`. +- Resolves issues that prevented building `ArgumentParser` for WebAssembly using + SwiftWasm toolchains. +- Improved window size handling on Windows. +- Fixed a crash when using `--experimental-dump-help` with commands that provide + non-parsed values. +- Fixes an issue where subcommands that declare array arguments with the + `.unconditionalRemaining` parsing strategy unexpectedly miss arguments, + extending the change in [#333] to subcommands. ([#397]) +- Corrects the order of an `@Option` initializer's parameters, deprecating the + old version. ([#391]) +- Expanded and corrected documentation. + +The 1.0.3 release includes contributions from [atierian], [CraigSiemens], +[dduan], [floam], [KS1019], [McNight], [mdznr], [natecook1000], [rauhul], and +[yonihemi]. Thank you! + + +## [1.0.2] - 2021-11-09 + +### Fixes + +- Addresses an issue when building tests under Mac Catalyst. + +The 1.0.2 release includes a contribution from [jakepetroules]. Thank you! + +## [1.0.1] - 2021-09-14 + +### Fixes + +- Addresses an issue when compiling under Mac Catalyst. + +The 1.0.1 release includes a contribution from [imxieyi]. Thank you! + +## [1.0.0] - 2021-09-10 + +The 1.0 release marks an important milestone — +`ArgumentParser` is now source stable! + +### Changes + +- `ArgumentParser` now provides a DocC documentation catalog, so you + can view rendered articles and symbol documentation directly within + Xcode. + +### Fixes + +- Parsing works as expected for options with single-dash names that + are declared using the `.upToNextOption` parsing strategy. + +--- + +## [0.5.0] - 2021-09-02 + +### Additions + +- When a user doesn't provide a required argument, the error message now + includes that argument's help text. ([#324]) +- Command-line tools built with `ArgumentParser` now include an experimental + flag to dump command/argument/help information as JSON: + `--experimental-dump-help`. ([#310]) + +### Changes + +- All public enumerations are now structs with static properties, to make + compatibility with future additions simpler. + +### Fixes + +- Array properties defined as `@Option` with the `.upToNextOption` parsing + strategy now include all provided values. ([#304]) In the example below, all + four values are now included in the resulting array, where only the last two + were included in previous releases: + + ```swift + struct Example: ParsableCommand { + @Option(parsing: .upToNextOption) + var option: [String] + } + ``` + ``` + $ example --option one two --option three four + ``` + +- When a command defines an array property as an `@Argument` with the + `.unconditionalRemaining` parsing strategy, option and flag parsing now stops + at the first positional argument or unrecognized flag. ([#333]) +- Completion scripts correctly use customized help flags. ([#308]) +- Fixes errors with bash custom completion arguments and the executable path. + ([#320], [#323]) +- Fixes the behavior when a user specifies both the `help` subcommand and a help + flag. ([#309]) +- A variety of internal improvements. ([#315], [#316], [#321], [#341]) + +The 0.5.0 release includes contributions from [atierian], [compnerd], +[dirtyhabits97], [Frizlab], [KS1019], [natecook1000], and [rauhul]. Thank you! + +--- + +## [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 @@ -31,8 +315,6 @@ package updates, you can specify your package dependency using - Parsing performance improvements. ---- - ## [0.3.0] - 2020-08-15 ### Additions @@ -59,6 +341,8 @@ package updates, you can specify your package dependency using The 0.3.0 release includes contributions from [dduan], [MPLew-is], [natecook1000], and [thomasvl]. Thank you! +--- + ## [0.2.2] - 2020-08-05 ### Fixes @@ -140,6 +424,8 @@ 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 @@ -197,6 +483,8 @@ Thank you! 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 @@ -346,7 +634,21 @@ This changelog's format is based on [Keep a Changelog](https://keepachangelog.co -[Unreleased]: https://github.com/apple/swift-argument-parser/compare/0.3.1...HEAD +[Unreleased]: https://github.com/apple/swift-argument-parser/compare/1.1.2...HEAD +[1.1.2]: https://github.com/apple/swift-argument-parser/compare/1.1.1...1.1.2 +[1.1.1]: https://github.com/apple/swift-argument-parser/compare/1.1.0...1.1.1 +[1.1.0]: https://github.com/apple/swift-argument-parser/compare/1.0.3...1.1.0 +[1.0.3]: https://github.com/apple/swift-argument-parser/compare/1.0.2...1.0.3 +[1.0.2]: https://github.com/apple/swift-argument-parser/compare/1.0.1...1.0.2 +[1.0.1]: https://github.com/apple/swift-argument-parser/compare/1.0.0...1.0.1 +[1.0.0]: https://github.com/apple/swift-argument-parser/compare/0.5.0...1.0.0 +[0.5.0]: https://github.com/apple/swift-argument-parser/compare/0.4.4...0.5.0 +[0.4.4]: https://github.com/apple/swift-argument-parser/compare/0.4.3...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 @@ -363,44 +665,103 @@ This changelog's format is based on [Keep a Changelog](https://keepachangelog.co [#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 +[#299]: https://github.com/apple/swift-argument-parser/pull/299 +[#304]: https://github.com/apple/swift-argument-parser/pull/304 +[#308]: https://github.com/apple/swift-argument-parser/pull/308 +[#309]: https://github.com/apple/swift-argument-parser/pull/309 +[#310]: https://github.com/apple/swift-argument-parser/pull/310 +[#315]: https://github.com/apple/swift-argument-parser/pull/315 +[#316]: https://github.com/apple/swift-argument-parser/pull/316 +[#320]: https://github.com/apple/swift-argument-parser/pull/320 +[#321]: https://github.com/apple/swift-argument-parser/pull/321 +[#323]: https://github.com/apple/swift-argument-parser/pull/323 +[#324]: https://github.com/apple/swift-argument-parser/pull/324 +[#333]: https://github.com/apple/swift-argument-parser/pull/333 +[#341]: https://github.com/apple/swift-argument-parser/pull/341 +[#366]: https://github.com/apple/swift-argument-parser/pull/366 +[#390]: https://github.com/apple/swift-argument-parser/pull/390 +[#391]: https://github.com/apple/swift-argument-parser/pull/391 +[#397]: https://github.com/apple/swift-argument-parser/pull/397 +[#400]: https://github.com/apple/swift-argument-parser/pull/400 +[#404]: https://github.com/apple/swift-argument-parser/pull/404 +[#416]: https://github.com/apple/swift-argument-parser/pull/416 +[#424]: https://github.com/apple/swift-argument-parser/pull/424 +[#427]: https://github.com/apple/swift-argument-parser/pull/427 +[#435]: https://github.com/apple/swift-argument-parser/pull/435 +[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 +[atierian]: https://github.com/apple/swift-argument-parser/commits?author=atierian [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 +[CraigSiemens]: https://github.com/apple/swift-argument-parser/commits?author=CraigSiemens +[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 +[dirtyhabits97]: https://github.com/apple/swift-argument-parser/commits?author=dirtyhabits97 +[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 +[floam]: https://github.com/apple/swift-argument-parser/commits?author=floam +[Frizlab]: https://github.com/apple/swift-argument-parser/commits?author=Frizlab [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 +[imxieyi]: https://github.com/apple/swift-argument-parser/commits?author=imxieyi [IngmarStein]: https://github.com/apple/swift-argument-parser/commits?author=IngmarStein [interstateone]: https://github.com/apple/swift-argument-parser/commits?author=interstateone +[jakepetroules]: https://github.com/apple/swift-argument-parser/commits?author=jakepetroules [john-mueller]: https://github.com/apple/swift-argument-parser/commits?author=john-mueller [jonathanpenn]: https://github.com/apple/swift-argument-parser/commits?author=jonathanpenn +[keith]: https://github.com/apple/swift-argument-parser/commits?author=keith +[KeithBird]: https://github.com/apple/swift-argument-parser/commits?author=KeithBird [kennyyork]: https://github.com/apple/swift-argument-parser/commits?author=kennyyork [klaaspieter]: https://github.com/apple/swift-argument-parser/commits?author=klaaspieter +[KS1019]: https://github.com/apple/swift-argument-parser/commits?author=KS1019 +[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 +[MartinP7r]: https://github.com/apple/swift-argument-parser/commits?author=MartinP7r +[MaxDesiatov]: https://github.com/apple/swift-argument-parser/commits?author=MaxDesiatov +[McNight]: https://github.com/apple/swift-argument-parser/commits?author=McNight +[mdznr]: https://github.com/apple/swift-argument-parser/commits?author=mdznr +[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 +[rauhul]: https://github.com/apple/swift-argument-parser/commits?author=rauhul [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 +[yonihemi]: https://github.com/apple/swift-argument-parser/commits?author=yonihemi [YuAo]: https://github.com/apple/swift-argument-parser/commits?author=YuAo +[zkiraly]: https://github.com/apple/swift-argument-parser/commits?author=zkiraly [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/Documentation/04 Customizing Help.md b/Documentation/04 Customizing Help.md deleted file mode 100644 index a3e11c58a..000000000 --- a/Documentation/04 Customizing Help.md +++ /dev/null @@ -1,181 +0,0 @@ -# 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. -``` - -## 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/07 Completion Scripts.md b/Documentation/07 Completion Scripts.md deleted file mode 100644 index 4cf0a411f..000000000 --- a/Documentation/07 Completion Scripts.md +++ /dev/null @@ -1,118 +0,0 @@ -# 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 index 06c366e6b..77821be47 100644 --- a/Examples/CMakeLists.txt +++ b/Examples/CMakeLists.txt @@ -1,11 +1,15 @@ add_executable(math - math/main.swift) + math/Math.swift) +target_compile_options(math PRIVATE + -parse-as-library) target_link_libraries(math PRIVATE ArgumentParser $<$:m>) add_executable(repeat - repeat/main.swift) + repeat/Repeat.swift) +target_compile_options(repeat PRIVATE + -parse-as-library) target_link_libraries(repeat PRIVATE ArgumentParser) diff --git a/Examples/count-lines/CountLines.swift b/Examples/count-lines/CountLines.swift new file mode 100644 index 000000000..9bda53576 --- /dev/null +++ b/Examples/count-lines/CountLines.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation + +@main +@available(macOS 10.15, *) +struct CountLines: AsyncParsableCommand { + @Argument( + help: "A file to count lines in. If omitted, counts the lines of stdin.", + completion: .file(), transform: URL.init(fileURLWithPath:)) + var inputFile: URL? + + @Option(help: "Only count lines with this prefix.") + var prefix: String? + + @Flag(help: "Include extra information in the output.") + var verbose = false +} + +@available(macOS 10.15, *) +extension CountLines { + var fileHandle: FileHandle { + get throws { + guard let inputFile = inputFile else { + return .standardInput + } + return try FileHandle(forReadingFrom: inputFile) + } + } + + func printCount(_ count: Int) { + guard verbose else { + print(count) + return + } + + if let filename = inputFile?.lastPathComponent { + print("Lines in '\(filename)'", terminator: "") + } else { + print("Lines from stdin", terminator: "") + } + + if let prefix = prefix { + print(", prefixed by '\(prefix)'", terminator: "") + } + + print(": \(count)") + } + + mutating func run() async throws { + guard #available(macOS 12, *) else { + print("'count-lines' isn't supported on this platform.") + return + } + + let countAllLines = prefix == nil + let lineCount = try await fileHandle.bytes.lines.reduce(0) { count, line in + if countAllLines || line.starts(with: prefix!) { + return count + 1 + } else { + return count + } + } + + printCount(lineCount) + } +} diff --git a/Examples/math/main.swift b/Examples/math/Math.swift similarity index 92% rename from Examples/math/main.swift rename to Examples/math/Math.swift index 06110e1db..7684d8f2b 100644 --- a/Examples/math/main.swift +++ b/Examples/math/Math.swift @@ -11,6 +11,7 @@ import ArgumentParser +@main struct Math: ParsableCommand { // Customize your command's help and subcommands by implementing the // `configuration` property. @@ -175,7 +176,7 @@ extension Math.Statistics { let squaredErrors = values .map { $0 - mean } .map { $0 * $0 } - let variance = squaredErrors.reduce(0, +) + let variance = squaredErrors.reduce(0, +) / Double(values.count) let result = variance.squareRoot() print(result) } @@ -186,10 +187,10 @@ extension Math.Statistics { static var configuration = CommandConfiguration( abstract: "Print the quantiles of the values (TBD).") - @Argument(help: .hidden, completion: .list(["alphabet", "alligator", "branch", "braggart"])) + @Argument(completion: .list(["alphabet", "alligator", "branch", "braggart"])) var oneOfFour: String? - @Argument(help: .hidden, completion: .custom { _ in ["alabaster", "breakfast", "crunch", "crash"] }) + @Argument(completion: .custom { _ in ["alabaster", "breakfast", "crunch", "crash"] }) var customArg: String? @Argument(help: "A group of floating-point values to operate on.") @@ -206,17 +207,15 @@ extension Math.Statistics { var testCustomExitCode: Int32? // These args are for testing custom completion scripts: - @Option(help: .hidden, completion: .file(extensions: ["txt", "md"])) + @Option(completion: .file(extensions: ["txt", "md"])) var file: String? - @Option(help: .hidden, completion: .directory) + @Option(completion: .directory) var directory: String? - @Option( - help: .hidden, - completion: .shellCommand("head -100 /usr/share/dict/words | tail -50")) + @Option(completion: .shellCommand("head -100 /usr/share/dict/words | tail -50")) var shell: String? - @Option(help: .hidden, completion: .custom(customCompletion)) + @Option(completion: .custom(customCompletion)) var custom: String? func validate() throws { @@ -244,5 +243,3 @@ func customCompletion(_ s: [String]) -> [String] { ? ["aardvark", "aaaaalbert"] : ["hello", "helicopter", "heliotrope"] } - -Math.main() diff --git a/Examples/repeat/main.swift b/Examples/repeat/Repeat.swift similarity index 94% rename from Examples/repeat/main.swift rename to Examples/repeat/Repeat.swift index bc5e8b1cc..bb3768f88 100644 --- a/Examples/repeat/main.swift +++ b/Examples/repeat/Repeat.swift @@ -11,6 +11,7 @@ import ArgumentParser +@main struct Repeat: ParsableCommand { @Option(help: "The number of times to repeat 'phrase'.") var count: Int? @@ -22,7 +23,7 @@ struct Repeat: ParsableCommand { var phrase: String mutating func run() throws { - let repeatCount = count ?? .max + let repeatCount = count ?? 2 for i in 1...repeatCount { if includeCounter { @@ -33,5 +34,3 @@ struct Repeat: ParsableCommand { } } } - -Repeat.main() diff --git a/Package.swift b/Package.swift index a8caf6ccb..ec8ac2311 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.5 //===----------------------------------------------------------*- swift -*-===// // // This source file is part of the Swift Argument Parser open source project @@ -21,47 +21,66 @@ var package = Package( ], dependencies: [], targets: [ + // Core Library .target( name: "ArgumentParser", - dependencies: []), + dependencies: ["ArgumentParserToolInfo"], + exclude: ["CMakeLists.txt"]), .target( name: "ArgumentParserTestHelpers", - dependencies: ["ArgumentParser"]), - + dependencies: ["ArgumentParser", "ArgumentParserToolInfo"], + exclude: ["CMakeLists.txt"]), .target( + name: "ArgumentParserToolInfo", + dependencies: [], + exclude: ["CMakeLists.txt"]), + + // Examples + .executableTarget( name: "roll", dependencies: ["ArgumentParser"], path: "Examples/roll"), - .target( + .executableTarget( name: "math", dependencies: ["ArgumentParser"], path: "Examples/math"), - .target( + .executableTarget( name: "repeat", dependencies: ["ArgumentParser"], path: "Examples/repeat"), - .target( - name: "changelog-authors", - dependencies: ["ArgumentParser"], - path: "Tools/changelog-authors"), - + // Tests .testTarget( name: "ArgumentParserEndToEndTests", - dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]), + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), .testTarget( name: "ArgumentParserUnitTests", - dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]), + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), + .testTarget( + name: "ArgumentParserPackageManagerTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), .testTarget( name: "ArgumentParserExampleTests", - dependencies: ["ArgumentParserTestHelpers"]), + dependencies: ["ArgumentParserTestHelpers"], + resources: [.copy("CountLinesTest.txt")]), ] ) -#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"])) +#if swift(>=5.6) && os(macOS) +package.targets.append(contentsOf: [ + // Examples + .executableTarget( + name: "count-lines", + dependencies: ["ArgumentParser"], + path: "Examples/count-lines"), + + // Tools + .executableTarget( + name: "changelog-authors", + dependencies: ["ArgumentParser"], + path: "Tools/changelog-authors"), + ]) #endif diff --git a/Package@swift-5.6.swift b/Package@swift-5.6.swift new file mode 100644 index 000000000..72c83e425 --- /dev/null +++ b/Package@swift-5.6.swift @@ -0,0 +1,104 @@ +// swift-tools-version:5.6 +//===----------------------------------------------------------*- 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: [ + // Core Library + .target( + name: "ArgumentParser", + dependencies: ["ArgumentParserToolInfo"], + exclude: ["CMakeLists.txt"]), + .target( + name: "ArgumentParserTestHelpers", + dependencies: ["ArgumentParser", "ArgumentParserToolInfo"], + exclude: ["CMakeLists.txt"]), + .target( + name: "ArgumentParserToolInfo", + dependencies: [ ], + exclude: ["CMakeLists.txt"]), + + // Plugins + .plugin( + name: "GenerateManualPlugin", + capability: .command( + intent: .custom( + verb: "experimental-generate-manual", + description: "Generate a manual entry for a specified target.")), + dependencies: ["generate-manual"]), + + // Examples + .executableTarget( + name: "roll", + dependencies: ["ArgumentParser"], + path: "Examples/roll"), + .executableTarget( + name: "math", + dependencies: ["ArgumentParser"], + path: "Examples/math"), + .executableTarget( + name: "repeat", + dependencies: ["ArgumentParser"], + path: "Examples/repeat"), + + // Tools + .executableTarget( + name: "generate-manual", + dependencies: ["ArgumentParser", "ArgumentParserToolInfo"], + path: "Tools/generate-manual"), + + // Tests + .testTarget( + name: "ArgumentParserEndToEndTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), + .testTarget( + name: "ArgumentParserExampleTests", + dependencies: ["ArgumentParserTestHelpers"], + resources: [.copy("CountLinesTest.txt")]), + .testTarget( + name: "ArgumentParserGenerateManualTests", + dependencies: ["ArgumentParserTestHelpers"]), + .testTarget( + name: "ArgumentParserPackageManagerTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), + .testTarget( + name: "ArgumentParserUnitTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), + ] +) + +#if os(macOS) +package.targets.append(contentsOf: [ + // Examples + .executableTarget( + name: "count-lines", + dependencies: ["ArgumentParser"], + path: "Examples/count-lines"), + + // Tools + .executableTarget( + name: "changelog-authors", + dependencies: ["ArgumentParser"], + path: "Tools/changelog-authors"), + ]) +#endif diff --git a/Plugins/GenerateManualPlugin/GenerateManualPlugin.swift b/Plugins/GenerateManualPlugin/GenerateManualPlugin.swift new file mode 100644 index 000000000..636d756eb --- /dev/null +++ b/Plugins/GenerateManualPlugin/GenerateManualPlugin.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------*- 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 PackagePlugin +import Foundation + +@main +struct GenerateManualPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) async throws { + // Locate generation tool. + let generationToolFile = try context.tool(named: "generate-manual").path + + // Create an extractor to extract plugin-only arguments from the `arguments` + // array. + var extractor = ArgumentExtractor(arguments) + + // Run generation tool once if help is requested. + if extractor.helpRequest() { + try generationToolFile.exec(arguments: ["--help"]) + print(""" + ADDITIONAL OPTIONS: + --configuration + Tool build configuration used to generate the + manual. (default: release) + + NOTE: The "GenerateManual" plugin handles passing the "" and + "--output-directory " arguments. Manually supplying + these arguments will result in a runtime failure. + """) + return + } + + // Extract configuration argument before making it to the + // "generate-manual" tool. + let configuration = try extractor.configuration() + + // Build all products first. + print("Building package in \(configuration) mode...") + let buildResult = try packageManager.build( + .all(includingTests: false), + parameters: .init(configuration: configuration)) + + guard buildResult.succeeded else { + throw GenerateManualPluginError.buildFailed(buildResult.logText) + } + print("Built package in \(configuration) mode") + + // Run generate-manual on all executable artifacts. + for builtArtifact in buildResult.builtArtifacts { + // Skip non-executable targets + guard builtArtifact.kind == .executable else { continue } + + // Skip executables without a matching product. + guard let product = builtArtifact.matchingProduct(context: context) + else { continue } + + // Skip products without a dependency on ArgumentParser. + guard product.hasDependency(named: "ArgumentParser") else { continue } + + // Get the artifacts name. + let executableName = builtArtifact.path.lastComponent + print("Generating manual for \(executableName)...") + + // Create output directory. + let outputDirectory = context + .pluginWorkDirectory + .appending(executableName) + try outputDirectory.createOutputDirectory() + + // Create generation tool arguments. + var generationToolArguments = [ + builtArtifact.path.string, + "--output-directory", + outputDirectory.string + ] + generationToolArguments.append( + contentsOf: extractor.unextractedOptionsOrFlags) + + // Spawn generation tool. + try generationToolFile.exec(arguments: generationToolArguments) + print("Generated manual in '\(outputDirectory)'") + } + } +} diff --git a/Plugins/GenerateManualPlugin/GenerateManualPluginError.swift b/Plugins/GenerateManualPlugin/GenerateManualPluginError.swift new file mode 100644 index 000000000..24b79210f --- /dev/null +++ b/Plugins/GenerateManualPlugin/GenerateManualPluginError.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------*- 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 Foundation +import PackagePlugin + +enum GenerateManualPluginError: Error { + case unknownBuildConfiguration(String) + case buildFailed(String) + case createOutputDirectoryFailed(Error) + case subprocessFailedNonZeroExit(Path, Int32) + case subprocessFailedError(Path, Error) +} + +extension GenerateManualPluginError: CustomStringConvertible { + var description: String { + switch self { + case .unknownBuildConfiguration(let configuration): + return "Build failed: Unknown build configuration '\(configuration)'." + case .buildFailed(let logText): + return "Build failed: \(logText)." + case .createOutputDirectoryFailed(let error): + return """ + Failed to create output directory: '\(error.localizedDescription)' + """ + case .subprocessFailedNonZeroExit(let tool, let exitCode): + return """ + '\(tool.lastComponent)' invocation failed with a nonzero exit code: \ + '\(exitCode)'. + """ + case .subprocessFailedError(let tool, let error): + return """ + '\(tool.lastComponent)' invocation failed: \ + '\(error.localizedDescription)' + """ + } + } +} + +extension GenerateManualPluginError: LocalizedError { + var localizedDescription: String { self.description } +} diff --git a/Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift b/Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift new file mode 100644 index 000000000..1aed3a81e --- /dev/null +++ b/Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------*- 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 Foundation +import PackagePlugin + +extension ArgumentExtractor { + mutating func helpRequest() -> Bool { + self.extractFlag(named: "help") > 0 + } + + mutating func configuration() throws -> PackageManager.BuildConfiguration { + switch self.extractOption(named: "configuration").first { + case .some(let configurationString): + switch configurationString { + case "debug": + return .debug + case "release": + return .release + default: + throw GenerateManualPluginError + .unknownBuildConfiguration(configurationString) + } + case .none: + return .release + } + } +} + +extension Path { + func createOutputDirectory() throws { + do { + try FileManager.default.createDirectory( + atPath: self.string, + withIntermediateDirectories: true) + } catch { + throw GenerateManualPluginError.createOutputDirectoryFailed(error) + } + } + + func exec(arguments: [String]) throws { + do { + let process = Process() + process.executableURL = URL(fileURLWithPath: self.string) + process.arguments = arguments + try process.run() + process.waitUntilExit() + guard + process.terminationReason == .exit, + process.terminationStatus == 0 + else { + throw GenerateManualPluginError.subprocessFailedNonZeroExit( + self, process.terminationStatus) + } + } catch { + throw GenerateManualPluginError.subprocessFailedError(self, error) + } + } +} + +extension PackageManager.BuildResult.BuiltArtifact { + func matchingProduct(context: PluginContext) -> Product? { + context + .package + .products + .first { $0.name == self.path.lastComponent } + } +} + +extension Product { + func hasDependency(named name: String) -> Bool { + recursiveTargetDependencies + .contains { $0.name == name } + } + + var recursiveTargetDependencies: [Target] { + var dependencies = [Target.ID: Target]() + for target in self.targets { + for dependency in target.recursiveTargetDependencies { + dependencies[dependency.id] = dependency + } + } + return Array(dependencies.values) + } +} diff --git a/README.md b/README.md index adcf7d408..8a215408d 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ 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. +and then declare conformance to `ParsableCommand` and add the `@main` attribute. +Finally, implement your command's logic in the `run()` method. ```swift import ArgumentParser +@main struct Repeat: ParsableCommand { @Flag(help: "Include a counter with each repetition.") var includeCounter = false @@ -22,7 +23,7 @@ struct Repeat: ParsableCommand { var phrase: String mutating func run() throws { - let repeatCount = count ?? .max + let repeatCount = count ?? 2 for i in 1...repeatCount { if includeCounter { @@ -33,11 +34,8 @@ struct Repeat: ParsableCommand { } } } - -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. @@ -53,6 +51,7 @@ hello hello $ repeat --count 3 Error: Missing expected argument 'phrase'. +Help: The phrase to repeat. Usage: repeat [--count ] [--include-counter] See 'repeat --help' for more information. $ repeat --help @@ -67,50 +66,65 @@ OPTIONS: -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). +## Documentation + +For guides, articles, and API documentation see the +[library's documentation on the Web][docs] or in Xcode. + +- [ArgumentParser documentation][docs] +- [Getting Started with ArgumentParser](https://apple.github.io/swift-argument-parser/documentation/argumentparser/gettingstarted) +- [`ParsableCommand` documentation](https://apple.github.io/swift-argument-parser/documentation/argumentparser/parsablecommand) + +[docs]: https://apple.github.io/swift-argument-parser/documentation/argumentparser/ -## Examples +#### Examples This repository includes a few examples of using the library: -- [`repeat`](Examples/repeat/main.swift) is the example shown above. +- [`repeat`](Examples/repeat/Repeat.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. +- [`math`](Examples/math/Math.swift) is an annotated example of using nested commands and subcommands. +- [`count-lines`](Examples/count-lines/CountLines.swift) uses `async`/`await` code in its implementation. 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. +- [`swift-format`](https://github.com/apple/swift-format/) uses some advanced features, like custom option values and hidden flags. +- [`swift-package-manager`](https://github.com/apple/swift-package-manager/) includes a deep command hierarchy and extensive use of option groups. -## Adding `ArgumentParser` as a Dependency +## Project Status -To use the `ArgumentParser` library in a SwiftPM project, -add the following line to the dependencies in your `Package.swift` file: +The Swift Argument Parser package is source stable; +version numbers follow semantic versioning. +Source breaking changes to public API can only land in a new major version. -```swift -.package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.0"), -``` +The public API of version 1.0 of the `swift-argument-parser` package +consists of non-underscored declarations that are marked public in the `ArgumentParser` module. +Interfaces that aren't part of the public API may continue to change in any release, +including the exact wording and formatting of the autogenerated help and error messages, +as well as the package’s examples, tests, utilities, and documentation. -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: +Future minor versions of the package may introduce changes to these rules as needed. -```swift -.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.0")), -``` +We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. +Accordingly, from time to time, +we expect that new versions of this package will require clients to upgrade to a more recent Swift toolchain release. +Requiring a new Swift release will only require a minor version bump. -Finally, include `"ArgumentParser"` as a dependency for your executable target: +## Adding `ArgumentParser` as a Dependency + +To use the `ArgumentParser` library in a SwiftPM project, +add it to the dependencies for your package and your command-line executable target: ```swift let package = Package( // name, platforms, products, etc. dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.0"), // other dependencies + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), ], targets: [ - .target(name: "", dependencies: [ + .executableTarget(name: "", dependencies: [ + // other dependencies .product(name: "ArgumentParser", package: "swift-argument-parser"), ]), // other targets diff --git a/Sources/ArgumentParser/CMakeLists.txt b/Sources/ArgumentParser/CMakeLists.txt index bace0ad5e..312ca71d0 100644 --- a/Sources/ArgumentParser/CMakeLists.txt +++ b/Sources/ArgumentParser/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(ArgumentParser "Parsable Properties/Argument.swift" "Parsable Properties/ArgumentHelp.swift" + "Parsable Properties/ArgumentVisibility.swift" "Parsable Properties/CompletionKind.swift" "Parsable Properties/Errors.swift" "Parsable Properties/Flag.swift" @@ -13,6 +14,7 @@ add_library(ArgumentParser "Parsable Properties/Option.swift" "Parsable Properties/OptionGroup.swift" + "Parsable Types/AsyncParsableCommand.swift" "Parsable Types/CommandConfiguration.swift" "Parsable Types/EnumerableFlag.swift" "Parsable Types/ExpressibleByArgument.swift" @@ -31,11 +33,13 @@ add_library(ArgumentParser Parsing/ParserError.swift Parsing/SplitArguments.swift + Usage/DumpHelpGenerator.swift Usage/HelpCommand.swift Usage/HelpGenerator.swift Usage/MessageInfo.swift Usage/UsageGenerator.swift + Utilities/CollectionExtensions.swift Utilities/SequenceExtensions.swift Utilities/StringExtensions.swift Utilities/Tree.swift) @@ -45,7 +49,8 @@ set_target_properties(ArgumentParser PROPERTIES target_compile_options(ArgumentParser PRIVATE $<$:-enable-testing>) target_link_libraries(ArgumentParser PRIVATE - $<$>:Foundation>) + $<$>:Foundation> + ArgumentParserToolInfo) _install_target(ArgumentParser) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 3ec9e54ef..e444f81dd 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -35,6 +35,7 @@ struct BashCompletionsGenerator { // Include 'help' in the list of subcommands for the root command. var subcommands = type.configuration.subcommands + .filter { $0.configuration.shouldDisplay } if !subcommands.isEmpty && isRootCommand { subcommands.append(HelpCommand.self) } @@ -44,8 +45,6 @@ struct BashCompletionsGenerator { // 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. @@ -124,7 +123,8 @@ struct BashCompletionsGenerator { /// Returns the option and flag names that can be top-level completions. fileprivate static func generateArgumentWords(_ commands: [ParsableCommand.Type]) -> [String] { - ArgumentSet(commands.last!) + commands + .argumentsForHelp(visibility: .default) .flatMap { $0.bashCompletionWords() } } @@ -132,7 +132,7 @@ struct BashCompletionsGenerator { /// /// These consist of completions that are defined as `.list` or `.custom`. fileprivate static func generateArgumentCompletions(_ commands: [ParsableCommand.Type]) -> [String] { - ArgumentSet(commands.last!) + ArgumentSet(commands.last!, visibility: .default) .compactMap { arg -> String? in guard arg.isPositional else { return nil } @@ -145,14 +145,13 @@ struct BashCompletionsGenerator { 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 + let argumentName = arg.names.preferredName?.synopsisString ?? arg.help.keys.first?.rawValue ?? "---" return """ - $(\(commandName) ---completion \(subcommandNames) -- \(argumentName) "$COMP_WORDS") + $("${COMP_WORDS[0]}" ---completion \(subcommandNames) -- \(argumentName) "${COMP_WORDS[@]}") """ } } @@ -160,7 +159,7 @@ struct BashCompletionsGenerator { /// Returns the case-matching statements for supplying completions after an option or flag. fileprivate static func generateOptionHandlers(_ commands: [ParsableCommand.Type]) -> String { - ArgumentSet(commands.last!) + ArgumentSet(commands.last!, visibility: .default) .compactMap { arg -> String? in let words = arg.bashCompletionWords() if words.isEmpty { return nil } @@ -182,7 +181,9 @@ struct BashCompletionsGenerator { extension ArgumentDefinition { /// Returns the different completion names for this argument. fileprivate func bashCompletionWords() -> [String] { - names.map { $0.synopsisString } + return help.visibility.base == .default + ? names.map { $0.synopsisString } + : [] } /// Returns the bash completions that can follow this argument's `--name`. @@ -207,8 +208,7 @@ extension ArgumentDefinition { 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") )"# + return #"COMPREPLY=( $(compgen -W "$("${COMP_WORDS[0]}" \#(customCompletionCall(commands)) "${COMP_WORDS[@]}")" -- "$cur") )"# } } } diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 4c45f9f0e..11f3a0b7a 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -15,6 +15,8 @@ import Glibc import Darwin #elseif canImport(CRT) import CRT +#elseif canImport(WASILibc) +import WASILibc #endif /// A shell for which the parser can generate a completion script. @@ -102,7 +104,7 @@ extension ArgumentDefinition { /// this argument. func customCompletionCall(_ commands: [ParsableCommand.Type]) -> String { let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ") - let argumentName = preferredNameForSynopsis?.synopsisString + let argumentName = names.preferredName?.synopsisString ?? self.help.keys.first?.rawValue ?? "---" return "---completion \(subcommandNames) -- \(argumentName)" } diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index c95455ad7..cfbe9a9fe 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -2,8 +2,8 @@ struct FishCompletionsGenerator { static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { let programName = type._commandName let helper = """ - function __fish_\(programName)_using_command - set cmd (commandline -opc) + function _swift_\(programName)_using_command + set -l cmd (commandline -opc) if [ (count $cmd) -eq (count $argv) ] for i in (seq (count $argv)) if [ $cmd[$i] != $argv[$i] ] @@ -14,7 +14,7 @@ struct FishCompletionsGenerator { end return 1 end - + """ let completions = generateCompletions(commandChain: [programName], [type]) @@ -30,6 +30,7 @@ struct FishCompletionsGenerator { let isRootCommand = commands.count == 1 let programName = commandChain[0] var subcommands = type.configuration.subcommands + .filter { $0.configuration.shouldDisplay } if !subcommands.isEmpty { if isRootCommand { @@ -37,11 +38,11 @@ struct FishCompletionsGenerator { } } - let prefix = "complete -c \(programName) -n '__fish_\(programName)_using_command" + let prefix = "complete -c \(programName) -n '_swift_\(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) + /// this suggestion to be considered. This could be a combination of (nested) /// subcommands and flags. /// - suggestion: text for the actual suggestion /// - Returns: A completion expression @@ -55,7 +56,8 @@ struct FishCompletionsGenerator { return complete(ancestors: commandChain, suggestion: suggestion) } - let argumentCompletions = ArgumentSet(type) + let argumentCompletions = commands + .argumentsForHelp(visibility: .default) .flatMap { $0.argumentSegments(commandChain) } .map { complete(ancestors: $0, suggestion: $1) } @@ -78,7 +80,7 @@ extension Name { switch self { case .long(let longName): return "-l \(longName)" - case .short(let shortName): + case .short(let shortName, _): return "-s \(shortName)" case .longWithSingleDash(let dashedName): return "-o \(dashedName)" @@ -89,7 +91,7 @@ extension Name { switch self { case .long(let longName): return "--\(longName)" - case .short(let shortName): + case .short(let shortName, _): return "-\(shortName)" case .longWithSingleDash(let dashedName): return "-\(dashedName)" @@ -99,11 +101,13 @@ extension Name { extension ArgumentDefinition { fileprivate func argumentSegments(_ commandChain: [String]) -> [([String], String)] { + guard help.visibility.base == .default else { return [] } + var results = [([String], String)]() var formattedFlags = [String]() var flags = [String]() switch self.kind { - case .positional: + case .positional, .default: break case .named(let names): flags = names.map { $0.asFishSuggestion } @@ -111,8 +115,8 @@ extension ArgumentDefinition { 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())'" + if !help.abstract.isEmpty { + suggestion += " -d '\(help.abstract.fishEscape())'" } results.append((commandChain, suggestion)) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 82a5c8123..dea09615e 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -17,7 +17,7 @@ struct ZshCompletionsGenerator { return """ #compdef \(type._commandName) local context state state_descr line - _\(type._commandName)_commandname=$words[1] + _\(type._commandName.zshEscapingCommandName())_commandname=$words[1] typeset -A opt_args \(generateCompletionFunction([type])) @@ -35,10 +35,9 @@ struct ZshCompletionsGenerator { let functionName = commands.completionFunctionName() let isRootCommand = commands.count == 1 - var args = generateCompletionArguments(commands) - args.append("'(-h --help)'{-h,--help}'[Print help information.]'") - + var args = generateCompletionArguments(commands) var subcommands = type.configuration.subcommands + .filter { $0.configuration.shouldDisplay } var subcommandHandler = "" if !subcommands.isEmpty { args.append("'(-): :->command'") @@ -105,7 +104,8 @@ struct ZshCompletionsGenerator { } static func generateCompletionArguments(_ commands: [ParsableCommand.Type]) -> [String] { - ArgumentSet(commands.last!) + commands + .argumentsForHelp(visibility: .default) .compactMap { $0.zshCompletionString(commands) } } } @@ -122,18 +122,21 @@ extension String { 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())]" + guard !help.abstract.isEmpty else { return "" } + return "[\(help.abstract.zshEscaped())]" } func zshCompletionString(_ commands: [ParsableCommand.Type]) -> String? { + guard help.visibility.base == .default else { return nil } + var inputs: String switch update { case .unary: @@ -185,7 +188,7 @@ extension ArgumentDefinition { case .custom: // Generate a call back into the command to retrieve a completions list - let commandName = commands.first!._commandName + let commandName = commands.first!._commandName.zshEscapingCommandName() return "{_custom_completion $_\(commandName)_commandname \(customCompletionCall(commands)) $words}" } } diff --git a/Sources/ArgumentParser/Documentation.docc/ArgumentParser.md b/Sources/ArgumentParser/Documentation.docc/ArgumentParser.md new file mode 100644 index 000000000..54db29a96 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/ArgumentParser.md @@ -0,0 +1,92 @@ +# ``ArgumentParser`` + +Straightforward, type-safe argument parsing for Swift. + +## Overview + +By using `ArgumentParser`, you can create a command-line interface tool +by declaring simple Swift types. +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 its `run()` method. + +```swift +import ArgumentParser + +@main +struct Repeat: ParsableCommand { + @Argument(help: "The phrase to repeat.") + var phrase: String + + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? + + mutating func run() throws { + let repeatCount = count ?? 2 + for _ in 0.. +- ``ParsableCommand`` +- ``AsyncParsableCommand`` + +### Arguments, Options, and Flags + +- +- ``Argument`` +- ``Option`` +- ``Flag`` +- ``OptionGroup`` +- ``ParsableArguments`` + +### Property Customization + +- +- ``ArgumentHelp`` +- ``ArgumentVisibility`` +- ``NameSpecification`` + +### Custom Types + +- ``ExpressibleByArgument`` +- ``EnumerableFlag`` + +### Validation and Errors + +- +- ``ValidationError`` +- ``CleanExit`` +- ``ExitCode`` + +### Shell Completion Scripts + +- +- +- ``CompletionKind`` + +### Advanced Topics + +- +- diff --git a/Documentation/03 Commands and Subcommands.md b/Sources/ArgumentParser/Documentation.docc/Articles/CommandsAndSubcommands.md similarity index 84% rename from Documentation/03 Commands and Subcommands.md rename to Sources/ArgumentParser/Documentation.docc/Articles/CommandsAndSubcommands.md index 1825af795..7beb63e99 100644 --- a/Documentation/03 Commands and Subcommands.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/CommandsAndSubcommands.md @@ -1,5 +1,9 @@ # Defining Commands and Subcommands +Break complex command-line tools into a tree of subcommands. + +## Overview + 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. @@ -31,7 +35,7 @@ SUBCOMMANDS: 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. +Start by defining the root `Math` command. You can provide a static ``ParsableCommand/configuration-35km1`` property for a command that specifies its subcommands and a default subcommand, if any. ```swift struct Math: ParsableCommand { @@ -49,7 +53,7 @@ struct Math: ParsableCommand { 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. +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. @@ -63,7 +67,7 @@ struct Options: ParsableArguments { } ``` -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. +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 (see ``OptionGroup``). `@OptionGroup` doesn't define any new arguments for a command; instead, it splats in the arguments defined by another `ParsableArguments` type. ```swift extension Math { @@ -160,7 +164,7 @@ extension Math.Statistics { let squaredErrors = values .map { $0 - mean } .map { $0 * $0 } - let variance = squaredErrors.reduce(0, +) + let variance = squaredErrors.reduce(0, +) / Double(values.count) let result = variance.squareRoot() print(result) } @@ -169,10 +173,13 @@ extension Math.Statistics { } ``` -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. +Last but not least, we add the `@main` attribute to the root of our command tree, to tell the compiler to use that as the program's entry point. Upon execution, this 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() +@main +struct Math: ParsableCommand { + // ... +} ``` -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). +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/Math.swift). diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md new file mode 100644 index 000000000..c1549d2cd --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md @@ -0,0 +1,142 @@ +# Customizing Help for Commands + +Define your command's abstract, extended discussion, or usage string, and set the flags used to invoke the help display. + +## Overview + +In addition to configuring the command name and subcommands, as described in , you can also configure a command's help text by providing an abstract, discussion, or custom usage string. + +```swift +struct Repeat: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Repeats your input phrase.", + usage: """ + repeat + repeat --count + """, + discussion: """ + Prints to stdout forever, or until you halt the program. + """) + + @Argument(help: "The phrase to repeat.") + var phrase: String + + @Option(help: "How many times to repeat.") + var count: Int? + + mutating func run() throws { + for _ in 0..<(count ?? 2) { + print(phrase) + } + } +} +``` + +The customized components now 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 + repeat --count + +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 +nmap -v -sS -O 10.2.2.2 +sshnuke 10.2.2.2 -rootpw="Z1ON0101" +ssh 10.2.2.2 -l root +% 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 Commands + +You may not want to show every one of your command as part of your command-line interface. To render a command invisible (but still usable), pass `shouldDisplay: false` to the ``CommandConfiguration`` initializer. + +## 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/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCompletions.md b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCompletions.md new file mode 100644 index 000000000..0876e0141 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCompletions.md @@ -0,0 +1,62 @@ +# Customizing Completions + +Provide custom shell completions for your command-line tool's arguments and options. + +## Overview + +`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 ``ExpressibleByArgument/defaultCompletionKind-866se``. 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/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingHelp.md b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingHelp.md new file mode 100644 index 000000000..02f584939 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingHelp.md @@ -0,0 +1,143 @@ +# Customizing Help + +Support your users (and yourself) by providing rich help for arguments, options, and flags. + +## Overview + +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 + +For more control over the help text, pass an ``ArgumentHelp`` instance instead of a string literal. 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 visibility level for that argument. + +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. +``` + +### Controlling Argument Visibility + +You can specify the visibility of any argument, option, or flag. + +```swift +struct Example: ParsableCommand { + @Flag(help: ArgumentHelp("Show extra info.", visibility: .hidden)) + var verbose: Bool = false + + @Flag(help: ArgumentHelp("Use the legacy format.", visibility: .private)) + var useLegacyFormat: Bool = false +} +``` + +The `--verbose` flag is only visible in the extended help screen. The `--use-legacy-format` stays hidden even in the extended help screen, due to its `.private` visibility. + +``` +% example --help +USAGE: example + +OPTIONS: + -h, --help Show help information. + +% example --help-hidden +USAGE: example [--verbose] + +OPTIONS: + --verbose Show extra info. + -h, --help Show help information. +``` + +Alternatively, you can group multiple arguments, options, and flags together as part of a ``ParsableArguments`` type, and set the visibility when including them as an `@OptionGroup` property. + +```swift +struct ExperimentalFlags: ParsableArguments { + @Flag(help: "Use the remote access token. (experimental)") + var experimentalUseRemoteAccessToken: Bool = false + + @Flag(help: "Use advanced security. (experimental)") + var experimentalAdvancedSecurity: Bool = false +} + +struct Example: ParsableCommand { + @OptionGroup(visibility: .hidden) + var flags: ExperimentalFlags +} +``` + +The members of `ExperimentalFlags` are only shown in the extended help screen: + +``` +% example --help +USAGE: example + +OPTIONS: + -h, --help Show help information. + +% example --help-hidden +USAGE: example [--experimental-use-remote-access-token] [--experimental-advanced-security] + +OPTIONS: + --experimental-use-remote-access-token + Use the remote access token. (experimental) + --experimental-advanced-security + Use advanced security. (experimental) + -h, --help Show help information. +``` diff --git a/Documentation/02 Arguments, Options, and Flags.md b/Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md similarity index 94% rename from Documentation/02 Arguments, Options, and Flags.md rename to Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md index 886bc7fa4..13bd342ae 100644 --- a/Documentation/02 Arguments, Options, and Flags.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md @@ -2,21 +2,23 @@ Use the `@Argument`, `@Option` and `@Flag` property wrappers to declare the command-line interface for your command. +## Overview + 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: +- *Arguments* are values given by a user and are read in order from first to last (see ``Argument``). 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: +- *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 (see ``Option``). 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: +- *Flags* are like options, but without a paired value. Instead, their presence indicates a particular value (see ``Flag``). This command is called with two flags: ``` % example --verbose --strip-whitespace @@ -67,14 +69,14 @@ When providing a default value for an array property, any user-supplied values r ```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: " ")) - """) - } + @Argument var numbers = [7, 14, 21] + + mutating func run() throws { + print(""" + Your lucky numbers are: + \(numbers.map(String.init).joined(separator: " ")) + """) + } } ``` @@ -132,9 +134,9 @@ struct Example: ParsableCommand { ## 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`. +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:)`: +You can make your own custom types conform to `ExpressibleByArgument` by implementing ``ExpressibleByArgument/init(argument:)``: ```swift struct Path: ExpressibleByArgument { @@ -197,7 +199,7 @@ struct Example: ParsableCommand { } ``` -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. +Throw an error from the `transform` function to indicate that the user provided an invalid value for that type. See for more about customizing `transform` function errors. ## Using flag inversions, enumerations, and counts @@ -253,7 +255,7 @@ struct Example: ParsableCommand { } ``` -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). +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. ``` % example --in-memory-cache --pink --silver diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/ExperimentalFeatures.md b/Sources/ArgumentParser/Documentation.docc/Articles/ExperimentalFeatures.md new file mode 100644 index 000000000..c148244e2 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Articles/ExperimentalFeatures.md @@ -0,0 +1,19 @@ +# Experimental Features + +Learn about ArgumentParser's experimental features. + +## Overview + +Command-line programs built using `ArgumentParser` may include some built-in experimental features, available with the prefix `--experimental`. These features should not be considered stable while still prefixed, as future releases may change their behavior or remove them. + +If you have any feedback on experimental features, please [open a GitHub issue][issue]. + +## List of Experimental Features + +| Name | Description | related PRs | Version | +| ------------- | ------------- | ------------- | ------------- | +| `--experimental-dump-help` | Dumps command/argument/help information as JSON | [#310][] [#335][] | 0.5.0 or newer | + +[#310]: https://github.com/apple/swift-argument-parser/pull/310 +[#335]: https://github.com/apple/swift-argument-parser/pull/335 +[issue]: https://github.com/apple/swift-argument-parser/issues/new/choose diff --git a/Documentation/01 Getting Started.md b/Sources/ArgumentParser/Documentation.docc/Articles/GettingStarted.md similarity index 93% rename from Documentation/01 Getting Started.md rename to Sources/ArgumentParser/Documentation.docc/Articles/GettingStarted.md index f38adf8bf..070b8ca1a 100644 --- a/Documentation/01 Getting Started.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/GettingStarted.md @@ -1,10 +1,12 @@ -# Getting Started with `ArgumentParser` +# Getting Started with ArgumentParser Learn to set up and customize a simple command-line tool. +## Overview + 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 +## 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. @@ -13,13 +15,13 @@ 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 +// swift-tools-version:5.5 import PackageDescription let package = Package( - name: "random", + name: "Count", dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.3.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), ], targets: [ .target( @@ -29,8 +31,6 @@ let package = Package( ) ``` -> **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: @@ -45,6 +45,7 @@ We'll define the initial version of the command as a type that conforms to the ` ```swift import ArgumentParser +@main struct Count: ParsableCommand { @Argument var inputFile: String @Argument var outputFile: String @@ -58,15 +59,13 @@ struct Count: ParsableCommand { // 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. +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 the 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. +Finally, you designate the `Count` command as the program's entry point by applying the `@main` attribute. When running your command, the `ArgumentParser` library 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 @@ -226,6 +225,7 @@ As promised, here's the complete `count` command, for your experimentation: import ArgumentParser import Foundation +@main struct Count: ParsableCommand { static let configuration = CommandConfiguration(abstract: "Word counter.") @@ -281,6 +281,4 @@ struct RuntimeError: Error, CustomStringConvertible { self.description = description } } - -Count.main() ``` diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md b/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md new file mode 100644 index 000000000..24265bcaf --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md @@ -0,0 +1,59 @@ +# Generating and Installing Completion Scripts + +Install shell completion scripts generated by your command-line tool. + +## Overview + +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` option to generate completions for your 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 can depend on both 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`. diff --git a/Documentation/06 Manual Parsing and Testing.md b/Sources/ArgumentParser/Documentation.docc/Articles/ManualParsing.md similarity index 62% rename from Documentation/06 Manual Parsing and Testing.md rename to Sources/ArgumentParser/Documentation.docc/Articles/ManualParsing.md index da70b8895..d43d1df4b 100644 --- a/Documentation/06 Manual Parsing and Testing.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/ManualParsing.md @@ -1,14 +1,16 @@ # Manual Parsing and Testing -Provide your own array of command-line inputs and work with parsed results by calling alternatives to `main()`. +Provide your own array of command-line inputs or work directly with parsed command-line arguments. -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. +## Overview + +For most programs, denoting the root command type as `@main` is all that's necessary. As the program's entry point, that type 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. +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: +Let's implement the `Select` command discussed in , but using a scripty style instead of the typical command. First, we define the options as a `ParsableArguments` type: ```swift struct SelectOptions: ParsableArguments { @@ -23,18 +25,18 @@ The next step is to parse our options from the command-line input: 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. +The static ``ParsableArguments/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 ``ParsableArguments/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 { +guard 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`. +As you would expect, the ``ParsableArguments/exit(withError:)`` method includes usage information when you pass it a ``ValidationError``. Finally, we print out the requested number of elements: @@ -47,9 +49,9 @@ 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`. +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 ``ParsableCommand/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: +Let's see how this works by using the `Math` command and subcommands defined in [Commands and Subcommands](./CommandsAndSubcommands.md). This time, instead of calling `Math.main()`, we'll call `Math.parseAsRoot()`, and switch over the result: ```swift do { @@ -101,12 +103,3 @@ howdy howdy hi ``` - - - - - - - - - diff --git a/Documentation/05 Validation and Errors.md b/Sources/ArgumentParser/Documentation.docc/Articles/Validation.md similarity index 88% rename from Documentation/05 Validation and Errors.md rename to Sources/ArgumentParser/Documentation.docc/Articles/Validation.md index 941df831b..75f1b1ada 100644 --- a/Documentation/05 Validation and Errors.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/Validation.md @@ -1,12 +1,14 @@ -# Validation and Errors +# Providing Custom Validation Provide helpful feedback to users when things go wrong. -## Validating Command-Line Input +## Overview 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. +### Validating Command-Line Input + +To validate your commands properties after parsing, implement the ``ParsableArguments/validate()-5r0ge`` 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. @@ -57,7 +59,7 @@ 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. +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 { diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/Argument.md b/Sources/ArgumentParser/Documentation.docc/Extensions/Argument.md new file mode 100644 index 000000000..63162d3f7 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/Argument.md @@ -0,0 +1,25 @@ +# ``ArgumentParser/Argument`` + +## Topics + +### Single Arguments + +- ``init(help:completion:)-6pqzn`` +- ``init(help:completion:transform:)`` +- ``init(help:completion:)-4p94d`` +- ``init(wrappedValue:help:completion:)`` +- ``init(wrappedValue:help:completion:transform:)`` + +### Array Arguments + +- ``init(parsing:help:completion:)`` +- ``init(parsing:help:completion:transform:)`` +- ``init(wrappedValue:parsing:help:completion:)`` +- ``init(wrappedValue:parsing:help:completion:transform:)`` +- ``ArgumentArrayParsingStrategy`` + +### Infrequently Used APIs + +- ``init()`` +- ``init(from:)`` +- ``wrappedValue`` diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/AsyncParsableCommand.md b/Sources/ArgumentParser/Documentation.docc/Extensions/AsyncParsableCommand.md new file mode 100644 index 000000000..33b795dea --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/AsyncParsableCommand.md @@ -0,0 +1,30 @@ +# ``ArgumentParser/AsyncParsableCommand`` + +To use `async`/`await` code in your commands' `run()` method implementations, follow these steps: + +1. For the root command in your command-line tool, declare conformance to `AsyncParsableCommand`, even if that command doesn't use asynchronous code. +2. Apply the `@main` attribute to the root command. (Note: If your root command is in a `main.swift` file, rename the file to the name of the command.) +3. For any command that needs to use asynchronous code, declare conformance to `AsyncParsableCommand` and mark the `run()` method as `async`. No changes are needed for commands that don't use asynchronous code. + + +### Usage in Swift 5.5 + +In Swift 5.5, you need to declare a separate, standalone type as your asynchronous `@main` entry point. Instead of designating your root command as `@main`, as described above, use the code snippet below, replacing the placeholder with the name of your own root command. Otherwise, follow the steps above to use `async`/`await` code within your commands' `run()` methods. + +```swift +@main struct AsyncMain: AsyncMainProtocol { + typealias Command = <#RootCommand#> +} +``` + +## Topics + +### Implementing a Command's Behavior + +- ``run()`` + +### Starting the Program + +- ``main()`` +- ``AsyncMainProtocol`` + diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/CommandConfiguration.md b/Sources/ArgumentParser/Documentation.docc/Extensions/CommandConfiguration.md new file mode 100644 index 000000000..554485286 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/CommandConfiguration.md @@ -0,0 +1,30 @@ +# ``ArgumentParser/CommandConfiguration`` + +## Topics + +### Creating a Configuration + +- ``init(commandName:abstract:usage:discussion:version:shouldDisplay:subcommands:defaultSubcommand:helpNames:)`` + +### Customizing the Help Screen + +- ``abstract`` +- ``discussion`` +- ``usage`` +- ``helpNames`` + +### Declaring Subcommands + +- ``subcommands`` +- ``defaultSubcommand`` + +### Defining Command Properties + +- ``commandName`` +- ``version`` +- ``shouldDisplay`` + +### Deprecated APIs + +- ``init(commandName:abstract:discussion:version:shouldDisplay:subcommands:defaultSubcommand:helpNames:)`` + diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/Flag.md b/Sources/ArgumentParser/Documentation.docc/Extensions/Flag.md new file mode 100644 index 000000000..f72c7a3b5 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/Flag.md @@ -0,0 +1,39 @@ +# ``ArgumentParser/Flag`` + +## Topics + +### Boolean Flags + +- ``init(wrappedValue:name:help:)`` + +### Boolean Flags with Inversions + +- ``init(wrappedValue:name:inversion:exclusivity:help:)`` +- ``init(name:inversion:exclusivity:help:)-12okg`` +- ``init(name:inversion:exclusivity:help:)-1h8f7`` +- ``FlagInversion`` + +### Counted Flags + +- ``init(name:help:)`` + +### Custom Enumerable Flags + +- ``init(help:)`` +- ``init(exclusivity:help:)-38n7u`` +- ``init(exclusivity:help:)-5fggj`` +- ``init(wrappedValue:exclusivity:help:)`` +- ``init(wrappedValue:help:)`` + +### Infrequently Used APIs + +- ``init(from:)`` +- ``wrappedValue`` + +### Supporting Types + +- ``FlagExclusivity`` + +### Deprecated APIs + +- ``init()`` diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/Option.md b/Sources/ArgumentParser/Documentation.docc/Extensions/Option.md new file mode 100644 index 000000000..c54466710 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/Option.md @@ -0,0 +1,30 @@ +# ``ArgumentParser/Option`` + +## Topics + +### Single Options + +- ``init(name:parsing:help:completion:)-4yske`` +- ``init(name:parsing:help:completion:)-7slrf`` +- ``init(name:parsing:help:completion:transform:)-2wf44`` +- ``init(wrappedValue:name:parsing:help:completion:)-7ilku`` +- ``init(wrappedValue:name:parsing:help:completion:transform:)-2llve`` +- ``SingleValueParsingStrategy`` + +### Array Options + +- ``init(name:parsing:help:completion:)-238hg`` +- ``init(name:parsing:help:completion:transform:)-74hnp`` +- ``init(wrappedValue:name:parsing:help:completion:)-1dtbf`` +- ``init(wrappedValue:name:parsing:help:completion:transform:)-1kpto`` +- ``ArrayParsingStrategy`` + +### Infrequently Used APIs + +- ``init()`` +- ``init(from:)`` +- ``wrappedValue`` + +### Deprecated APIs + +- ``init(wrappedValue:name:parsing:completion:help:)`` diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/OptionGroup.md b/Sources/ArgumentParser/Documentation.docc/Extensions/OptionGroup.md new file mode 100644 index 000000000..3199b02cf --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/OptionGroup.md @@ -0,0 +1,16 @@ +# ``ArgumentParser/OptionGroup`` + +## Topics + +### Creating an Option Group + +- ``init(visibility:)`` + +### Infrequently Used APIs + +- ``init()`` +- ``init(from:)`` +- ``wrappedValue`` +- ``description`` + + diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableArguments.md b/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableArguments.md new file mode 100644 index 000000000..bd631fa72 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableArguments.md @@ -0,0 +1,35 @@ +# ``ArgumentParser/ParsableArguments`` + +## Topics + +### Handling Validation + +- ``validate()-5r0ge`` + +### Parsing a Type + +- ``parse(_:)`` +- ``parseOrExit(_:)`` + +### Exiting a Program + +- ``exit(withError:)`` + +### Generating Help Text + +- ``helpMessage(includeHidden:columns:)`` + +### Handling Errors + +- ``message(for:)`` +- ``fullMessage(for:)`` +- ``exitCode(for:)`` + +### Generating Completion Scripts + +- ``completionScript(for:)`` +- ``CompletionShell`` + +### Infrequently Used APIs + +- ``init()`` diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableCommand.md b/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableCommand.md new file mode 100644 index 000000000..d720bc8b3 --- /dev/null +++ b/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableCommand.md @@ -0,0 +1,52 @@ +# ``ArgumentParser/ParsableCommand`` + +`ParsableCommand` types are the basic building blocks for command-line tools built using `ArgumentParser`. To create a command, declare properties using the `@Argument`, `@Option`, and `@Flag` property wrappers, or include groups of options with `@OptionGroup`. Finally, implement your command's functionality in the ``run()-7p2fr`` method. + +```swift +@main +struct Repeat: ParsableCommand { + @Argument(help: "The phrase to repeat.") + var phrase: String + + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? + + mutating func run() throws { + let repeatCount = count ?? 2 + for _ in 0.. +- + +### Implementing a Command's Behavior + +- ``run()-7p2fr`` +- ``ParsableArguments/validate()-5r0ge`` + +### Customizing a Command + +- ``configuration-35km1`` +- ``CommandConfiguration`` + +### Generating Help Text + +- ``helpMessage(for:includeHidden:columns:)`` + +### Starting the Program + +- ``main()`` +- ``main(_:)`` + +### Manually Parsing Input + +- ``parseAsRoot(_:)`` + diff --git a/Sources/ArgumentParser/Documentation.docc/Images/repeat.png b/Sources/ArgumentParser/Documentation.docc/Images/repeat.png new file mode 100644 index 000000000..939558d97 Binary files /dev/null and b/Sources/ArgumentParser/Documentation.docc/Images/repeat.png differ diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index 69a55ab41..54e0e0998 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -9,25 +9,42 @@ // //===----------------------------------------------------------------------===// -/// A wrapper that represents a positional command-line argument. +/// A property 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. +/// Use the `@Argument` wrapper to define a property of your custom command as +/// a positional argument. A *positional argument* for a command-line tool is +/// specified without a label and must appear in declaration order. `@Argument` +/// properties with `Optional` type or a default value are optional for the user +/// of your command-line tool. /// -/// struct Options: ParsableArguments { +/// For example, the following program has two positional arguments. The `name` +/// argument is required, while `greeting` is optional because it has a default +/// value. +/// +/// @main +/// struct Greet: ParsableCommand { /// @Argument var name: String -/// @Argument var greeting: String? +/// @Argument var greeting: String = "Hello" +/// +/// mutating func run() { +/// print("\(greeting) \(name)!") +/// } /// } /// -/// 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`. +/// You can call this program with just a name or with a name and a +/// greeting. When you supply both arguments, the first argument is always +/// treated as the name, due to the order of the property declarations. +/// +/// $ greet Nadia +/// Hello Nadia! +/// $ greet Tamara Hi +/// Hi Tamara! @propertyWrapper public struct Argument: Decodable, ParsedWrapper { internal var _parsedValue: Parsed - + internal init(_parsedValue: Parsed) { self._parsedValue = _parsedValue } @@ -95,35 +112,6 @@ extension Argument where Value: ExpressibleByArgument { }) } - /// Creates a property that reads its value from an argument. - /// - /// This method is deprecated, with usage split into two other methods below: - /// - `init(wrappedValue:help:)` for properties with a default value - /// - `init(help:)` for properties with no default value - /// - /// Existing usage of the `default` parameter should be replaced such as follows: - /// ```diff - /// -@Argument(default: "bar") - /// -var foo: String - /// +@Argument var foo: String = "bar" - /// ``` - /// - /// - Parameters: - /// - initial: A default value to use for this property. If `initial` is - /// `nil`, the user must supply a value for this argument. - /// - help: Information about how to use this argument. - @available(*, deprecated, message: "Use regular property initialization for default values (`var foo: String = \"bar\"`)") - public init( - default initial: Value?, - help: ArgumentHelp? = nil - ) { - self.init( - initial: initial, - help: help, - completion: nil - ) - } - /// 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: @@ -132,7 +120,7 @@ extension Argument where Value: ExpressibleByArgument { /// ``` /// /// - Parameters: - /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property wrapper initialization. /// - help: Information about how to use this argument. public init( wrappedValue: Value, @@ -167,11 +155,13 @@ extension Argument where Value: ExpressibleByArgument { } } -/// The strategy to use when parsing multiple values from `@Option` arguments +/// The strategy to use when parsing multiple values from positional arguments /// into an array. -public enum ArgumentArrayParsingStrategy { +public struct ArgumentArrayParsingStrategy: Hashable { + internal var base: ArgumentDefinition.ParsingStrategy + /// Parse only unprefixed values from the command-line input, ignoring - /// any inputs that have a dash prefix. + /// any inputs that have a dash prefix. This is the default strategy. /// /// For example, for a parsable type defined as following: /// @@ -185,29 +175,39 @@ public enum ArgumentArrayParsingStrategy { /// `one two --other` would result in an unknown option error for `--other`. /// /// This is the default strategy for parsing argument arrays. - case remaining + public static var remaining: ArgumentArrayParsingStrategy { + self.init(base: .default) + } /// 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: + /// When you use the `unconditionalRemaining` parsing strategy, the parser + /// stops parsing flags and options as soon as it encounters a positional + /// argument or an unrecognized flag. For example, for a parsable type + /// defined as following: /// /// struct Options: ParsableArguments { - /// @Flag var verbose: Bool - /// @Argument(parsing: .unconditionalRemaining) var words: [String] + /// @Flag + /// var verbose: Bool = false + /// + /// @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"])`. + /// Parsing the input `--verbose one two --verbose` includes the second + /// `--verbose` flag in `words`, resulting in + /// `Options(verbose: true, words: ["one", "two", "--verbose"])`. /// /// - 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 + /// `--verbose -- one two --verbose` would have the same result as the above + /// example: `Options(verbose: true, words: ["one", "two", "--verbose"])`. + public static var unconditionalRemaining: ArgumentArrayParsingStrategy { + self.init(base: .allRemainingInput) + } } extension Argument { @@ -225,37 +225,15 @@ extension Argument { var arg = ArgumentDefinition( key: key, kind: .positional, - parsingStrategy: .nextAsValue, + parsingStrategy: .default, parser: T.init(argument:), default: nil, completion: completion ?? T.defaultCompletionKind) - arg.help.help = help + arg.help.updateArgumentHelp(help: help) return ArgumentSet(arg.optional) }) } - @available(*, deprecated, message: """ - Default values don't make sense for optional properties. - Remove the 'default' parameter if its value is nil, - or make your property non-optional if it's non-nil. - """) - public init( - default initial: T?, - help: ArgumentHelp? = nil - ) where Value == T? { - self.init(_parsedValue: .init { key in - ArgumentSet( - key: key, - kind: .positional, - parsingStrategy: .nextAsValue, - parseType: T.self, - name: .long, - default: initial, - help: help, - completion: T.defaultCompletionKind) - }) - } - /// 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. @@ -284,40 +262,6 @@ extension Argument { }) } - /// Creates a property that reads its value from an argument, parsing with - /// the given closure. - /// - /// This method is deprecated, with usage split into two other methods below: - /// - `init(wrappedValue:help:transform:)` for properties with a default value - /// - `init(help:transform:)` for properties with no default value - /// - /// Existing usage of the `default` parameter should be replaced such as follows: - /// ```diff - /// -@Argument(default: "bar", transform: baz) - /// -var foo: String - /// +@Argument(transform: baz) - /// +var foo: String = "bar" - /// ``` - /// - /// - Parameters: - /// - initial: A default value to use for this property. - /// - help: Information about how to use this argument. - /// - transform: A closure that converts a string into this property's - /// type or throws an error. - @available(*, deprecated, message: "Use regular property initialization for default values (`var foo: String = \"bar\"`)") - public init( - default initial: Value?, - help: ArgumentHelp? = nil, - transform: @escaping (String) throws -> Value - ) { - self.init( - initial: initial, - help: help, - completion: nil, - transform: transform - ) - } - /// 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: @@ -394,12 +338,17 @@ extension Argument { helpDefaultValue = nil } - let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + let help = ArgumentDefinition.Help( + allValues: Element.allValueStrings, + options: [.isOptional, .isRepeating], + help: help, + key: key + ) var arg = ArgumentDefinition( kind: .positional, help: help, completion: completion ?? Element.defaultCompletionKind, - parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput, + parsingStrategy: parsingStrategy.base, update: .appendToArray(forType: Element.self, key: key), initial: setInitialValue) arg.help.defaultValue = helpDefaultValue @@ -487,7 +436,7 @@ extension Argument { kind: .positional, help: help, completion: completion ?? .default, - parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput, + parsingStrategy: parsingStrategy.base, update: .unary({ (origin, name, valueString, parsedValues) in do { @@ -537,7 +486,7 @@ extension Argument { /// /// This method is called to initialize an array `Argument` with no default value such as: /// ```swift - /// @Argument(tranform: baz) + /// @Argument(transform: baz) /// var foo: [String] /// ``` /// diff --git a/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift b/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift index 546f5b0c8..b4b5643cb 100644 --- a/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift +++ b/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift @@ -24,26 +24,57 @@ public struct ArgumentHelp { /// flags don't include a value. public var valueName: String? + /// A visibility level indicating whether this argument should be shown in + /// the extended help display. + public var visibility: ArgumentVisibility = .default + /// A Boolean value indicating whether this argument should be shown in /// the extended help display. - public var shouldDisplay: Bool = true + @available(*, deprecated, message: "Use visibility level instead.") + public var shouldDisplay: Bool { + get { + return visibility.base == .default + } + set { + visibility = newValue ? .default : .hidden + } + } /// Creates a new help instance. + @available(*, deprecated, message: "Use init(_:discussion:valueName:visibility:) instead.") public init( _ abstract: String = "", discussion: String = "", valueName: String? = nil, - shouldDisplay: Bool = true) + shouldDisplay: Bool) { self.abstract = abstract self.discussion = discussion self.valueName = valueName self.shouldDisplay = shouldDisplay } - - /// A `Help` instance that hides an argument from the extended help display. + + /// Creates a new help instance. + public init( + _ abstract: String = "", + discussion: String = "", + valueName: String? = nil, + visibility: ArgumentVisibility = .default) + { + self.abstract = abstract + self.discussion = discussion + self.valueName = valueName + self.visibility = visibility + } + + /// A `Help` instance that shows an argument only in the extended help display. public static var hidden: ArgumentHelp { - ArgumentHelp(shouldDisplay: false) + ArgumentHelp(visibility: .hidden) + } + + /// A `Help` instance that hides an argument from the extended help display. + public static var `private`: ArgumentHelp { + ArgumentHelp(visibility: .private) } } diff --git a/Sources/ArgumentParser/Parsable Properties/ArgumentVisibility.swift b/Sources/ArgumentParser/Parsable Properties/ArgumentVisibility.swift new file mode 100644 index 000000000..9570f5624 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Properties/ArgumentVisibility.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 +// +//===----------------------------------------------------------------------===// + +/// Visibility level of an argument's help. +public struct ArgumentVisibility { + /// Internal implementation of `ArgumentVisibility` to allow for easier API + /// evolution. + internal enum Representation { + case `default` + case hidden + case `private` + } + + internal var base: Representation + + /// Show help for this argument whenever appropriate. + public static let `default` = Self(base: .default) + + /// Only show help for this argument in the extended help screen. + public static let hidden = Self(base: .hidden) + + /// Never show help for this argument. + public static let `private` = Self(base: .private) +} + +extension ArgumentVisibility.Representation { + /// A raw Integer value that represents each visibility level. + /// + /// `_comparableLevel` can be used to test if a Visibility case is more or + /// less visible than another, without committing this behavior to API. + /// A lower `_comparableLevel` indicates that the case is less visible (more + /// secret). + internal var _comparableLevel: Int { + switch self { + case .default: + return 2 + case .hidden: + return 1 + case .private: + return 0 + } + } +} + +extension ArgumentVisibility { + /// - Returns: true if `self` is at least as visible as the supplied argument. + internal func isAtLeastAsVisible(as other: Self) -> Bool { + self.base._comparableLevel >= other.base._comparableLevel + } +} diff --git a/Sources/ArgumentParser/Parsable Properties/Errors.swift b/Sources/ArgumentParser/Parsable Properties/Errors.swift index 68a6de2d0..c08b008e7 100644 --- a/Sources/ArgumentParser/Parsable Properties/Errors.swift +++ b/Sources/ArgumentParser/Parsable Properties/Errors.swift @@ -15,6 +15,8 @@ import Glibc import Darwin #elseif canImport(CRT) import CRT +#elseif canImport(WASILibc) +import WASILibc #endif #if os(Windows) @@ -41,7 +43,7 @@ public struct ValidationError: Error, CustomStringConvertible { /// An error type that only includes an exit code. /// -/// If you're printing custom errors messages yourself, you can throw this error +/// If you're printing custom error 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 { @@ -66,6 +68,8 @@ public struct ExitCode: Error, RawRepresentable, Hashable { /// An exit code that indicates that the user provided invalid input. #if os(Windows) public static let validationFailure = ExitCode(ERROR_BAD_ARGUMENTS) +#elseif os(WASI) + public static let validationFailure = ExitCode(EXIT_FAILURE) #else public static let validationFailure = ExitCode(EX_USAGE) #endif @@ -82,7 +86,15 @@ public struct ExitCode: Error, RawRepresentable, Hashable { /// /// 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 { +public struct CleanExit: Error, CustomStringConvertible { + internal enum Representation { + case helpRequest(ParsableCommand.Type? = nil) + case message(String) + case dumpRequest(ParsableCommand.Type? = nil) + } + + internal var base: Representation + /// 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 @@ -90,16 +102,13 @@ public enum CleanExit: Error, CustomStringConvertible { /// /// - Parameter command: The command type to offer help for, if different /// from the root command. - case helpRequest(ParsableCommand.Type? = nil) + public static func helpRequest(_ type: ParsableCommand.Type? = nil) -> CleanExit { + self.init(base: .helpRequest(type)) + } /// 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 - } + public static func message(_ text: String) -> CleanExit { + self.init(base: .message(text)) } /// Treat this error as a help request and display the full help message. @@ -112,4 +121,12 @@ public enum CleanExit: Error, CustomStringConvertible { public static func helpRequest(_ command: ParsableCommand) -> CleanExit { return .helpRequest(type(of: command)) } + + public var description: String { + switch self.base { + case .helpRequest: return "--help" + case .message(let message): return message + case .dumpRequest: return "--experimental-dump-help" + } + } } diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index d7d313fd3..d4d1b58a5 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -9,37 +9,67 @@ // //===----------------------------------------------------------------------===// -/// A wrapper that represents a command-line flag. +/// A property 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: +/// Use the `@Flag` wrapper to define a property of your custom type as a +/// command-line flag. A *flag* is a dash-prefixed label that can be provided on +/// the command line, such as `-d` and `--debug`. /// -/// struct Options: ParsableArguments { -/// @Flag var verbose: Bool +/// For example, the following program declares a flag that lets a user indicate +/// that seconds should be included when printing the time. +/// +/// @main +/// struct Time: ParsableCommand { +/// @Flag var includeSeconds = false +/// +/// mutating func run() { +/// if includeSeconds { +/// print(Date.now.formatted(.dateTime.hour().minute().second())) +/// } else { +/// print(Date.now.formatted(.dateTime.hour().minute())) +/// } +/// } /// } /// -/// `verbose` has a default value of `false`, but becomes `true` if `--verbose` -/// is provided on the command line. +/// `includeSeconds` has a default value of `false`, but becomes `true` if +/// `--include-seconds` is provided on the command line. +/// +/// $ time +/// 11:09 AM +/// $ time --include-seconds +/// 11:09:15 AM /// /// 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 { +/// @main +/// struct Math: ParsableCommand { /// enum Operation: EnumerableFlag { /// case add /// case multiply /// } /// /// @Flag var operation: Operation +/// +/// mutating func run() { +/// print("Time to \(operation)!") +/// } /// } /// -/// // usage: command --add -/// // or: command --multiply +/// Instead of using the name of the `operation` property as the flag in this +/// case, the two cases of the `Operation` enumeration become valid flags. +/// The `operation` property is neither optional nor given a default value, so +/// one of the two flags is required. +/// +/// $ math --add +/// Time to add! +/// $ math +/// Error: Missing one of: '--add', '--multiply' @propertyWrapper public struct Flag: Decodable, ParsedWrapper { internal var _parsedValue: Parsed - + internal init(_parsedValue: Parsed) { self._parsedValue = _parsedValue } @@ -93,7 +123,14 @@ extension Flag: CustomStringConvertible { extension Flag: DecodableParsedWrapper where Value: Decodable {} /// The options for converting a Boolean flag into a `true`/`false` pair. -public enum FlagInversion { +public struct FlagInversion: Hashable { + internal enum Representation { + case prefixedNo + case prefixedEnableDisable + } + + internal var base: Representation + /// Adds a matching flag with a `no-` prefix to represent `false`. /// /// For example, the `shouldRender` property in this declaration is set to @@ -102,7 +139,9 @@ public enum FlagInversion { /// /// @Flag(name: .customLong("render"), inversion: .prefixedNo) /// var shouldRender: Bool - case prefixedNo + public static var prefixedNo: FlagInversion { + self.init(base: .prefixedNo) + } /// Uses matching flags with `enable-` and `disable-` prefixes. /// @@ -112,19 +151,35 @@ public enum FlagInversion { /// /// @Flag(inversion: .prefixedEnableDisable) /// var extraOutput: Bool - case prefixedEnableDisable + public static var prefixedEnableDisable: FlagInversion { + self.init(base: .prefixedEnableDisable) + } } /// The options for treating enumeration-based flags as exclusive. -public enum FlagExclusivity { +public struct FlagExclusivity: Hashable { + internal enum Representation { + case exclusive + case chooseFirst + case chooseLast + } + + internal var base: Representation + /// Only one of the enumeration cases may be provided. - case exclusive + public static var exclusive: FlagExclusivity { + self.init(base: .exclusive) + } /// The first enumeration case that is provided is used. - case chooseFirst + public static var chooseFirst: FlagExclusivity { + self.init(base: .chooseFirst) + } /// The last enumeration case that is provided is used. - case chooseLast + public static var chooseLast: FlagExclusivity { + self.init(base: .chooseLast) + } } extension Flag where Value == Optional { @@ -154,7 +209,14 @@ extension Flag where Value == Optional { help: ArgumentHelp? = nil ) { self.init(_parsedValue: .init { key in - .flag(key: key, name: name, default: nil, inversion: inversion, exclusivity: exclusivity, help: help) + .flag( + key: key, + name: name, + default: nil, + required: false, + inversion: inversion, + exclusivity: exclusivity, + help: help) }) } } @@ -173,30 +235,10 @@ extension Flag where Value == Bool { }) } - /// Creates a Boolean property that reads its value from the presence of a - /// flag. - /// - /// This property defaults to a value of `false`. - /// - /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - help: Information about how to use this flag. - @available(*, deprecated, message: "Provide an explicit default value of `false` for this flag (`@Flag var foo: Bool = false`)") - public init( - name: NameSpecification = .long, - help: ArgumentHelp? = nil - ) { - self.init( - name: name, - initial: false, - 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. + /// - 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. /// - help: Information about how to use this flag. public init( @@ -222,72 +264,17 @@ extension Flag where Value == Bool { help: ArgumentHelp? ) { self.init(_parsedValue: .init { key in - .flag(key: key, name: name, default: initial, inversion: inversion, exclusivity: exclusivity, help: help) + .flag( + key: key, + name: name, + default: initial, + required: initial == nil, + inversion: inversion, + exclusivity: exclusivity, + help: help) }) } - /// Creates a Boolean property that reads its value from the presence of - /// one or more inverted flags. - /// - /// /// This method is deprecated, with usage split into two other methods below: - /// - `init(wrappedValue:name:inversion:exclusivity:help:)` for properties with a default value - /// - `init(name:inversion:exclusivity:help:)` for properties with no default value - /// - /// Existing usage of the `default` parameter should be replaced such as follows: - /// ```diff - /// -@Flag(default: true) - /// -var foo: Bool - /// +@Flag var foo: Bool = true - /// ``` - /// - /// Use this initializer to create a Boolean flag with an on/off pair. With - /// the following declaration, for example, the user can specify either - /// `--use-https` or `--no-use-https` to set the `useHTTPS` flag to `true` - /// or `false`, respectively. - /// - /// @Flag(inversion: .prefixedNo) - /// var useHTTPS: Bool - /// - /// To customize the names of the two states further, define a - /// `CaseIterable` enumeration with a case for each state, and use that - /// as the type for your flag. In this case, the user can specify either - /// `--use-production-server` or `--use-development-server` to set the - /// flag's value. - /// - /// enum ServerChoice { - /// case useProductionServer - /// case useDevelopmentServer - /// } - /// - /// @Flag var serverChoice: ServerChoice - /// - /// - Parameters: - /// - name: A specification for what names are allowed for this flag. - /// - initial: A default value to use for this property. If `initial` is - /// `nil`, one of the flags declared by this `@Flag` attribute is required - /// from the user. - /// - 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. - @available(*, deprecated, message: "Use regular property initialization for default values (`var foo: Bool = false`)") - public init( - name: NameSpecification = .long, - default initial: Bool?, - inversion: FlagInversion, - exclusivity: FlagExclusivity = .chooseLast, - help: ArgumentHelp? = nil - ) { - self.init( - name: name, - initial: 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. @@ -300,7 +287,7 @@ extension Flag where Value == 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. + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property 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. @@ -332,7 +319,7 @@ extension Flag where Value == 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. + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property 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. @@ -396,7 +383,7 @@ extension Flag where Value: EnumerableFlag { 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 + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: initial, update: .nullary({ (origin, name, values) in hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) })) } @@ -404,39 +391,6 @@ extension Flag where Value: EnumerableFlag { }) } - /// Creates a property that gets its value from the presence of a flag, - /// where the allowed flags are defined by an `EnumerableFlag` type. - /// - /// This method is deprecated, with usage split into two other methods below: - /// - `init(wrappedValue:exclusivity:help:)` for properties with a default value - /// - `init(exclusivity:help:)` for properties with no default value - /// - /// Existing usage of the `default` parameter should be replaced such as follows: - /// ```diff - /// -@Flag(default: .baz) - /// -var foo: Bar - /// +@Flag var foo: Bar = baz - /// ``` - /// - /// - Parameters: - /// - initial: A default value to use for this property. If `initial` is - /// `nil`, one of the flags declared by this `@Flag` attribute is required - /// from the user. - /// - exclusivity: The behavior to use when multiple flags are specified. - /// - help: Information about how to use this flag. - @available(*, deprecated, message: "Use regular property initialization for default values (`var foo: Bar = .baz`)") - public init( - default initial: Value?, - exclusivity: FlagExclusivity = .exclusive, - help: ArgumentHelp? = nil - ) { - self.init( - initial: initial, - exclusivity: exclusivity, - help: help - ) - } - /// 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`. @@ -453,7 +407,7 @@ extension Flag where Value: EnumerableFlag { /// ``` /// /// - Parameters: - /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property wrapper initialization. /// - exclusivity: The behavior to use when multiple flags are specified. /// - help: Information about how to use this flag. public init( @@ -518,7 +472,7 @@ extension Flag { 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 + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity) })) @@ -543,7 +497,7 @@ extension Flag { 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 + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .default, initialValue: initial, update: .nullary({ (origin, name, values) in values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { $0.append(value) }) @@ -592,97 +546,6 @@ extension Flag { } } -// - 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 diff --git a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift index 77fa74ed3..7a71c24a7 100644 --- a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift +++ b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift @@ -12,31 +12,62 @@ /// A specification for how to represent a property as a command-line argument /// label. public struct NameSpecification: ExpressibleByArrayLiteral { - public enum Element: Hashable { + /// An individual property name translation. + public struct Element: Hashable { + internal enum Representation: Hashable { + case long + case customLong(_ name: String, withSingleDash: Bool) + case short + case customShort(_ char: Character, allowingJoined: Bool) + } + + internal var base: Representation + /// 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 + public static var long: Element { + self.init(base: .long) + } /// Use the given string instead of the property's name. /// /// To create a single-dash argument, pass `true` as `withSingleDash`. Note /// that combining single-dash options and options with short, /// single-character names can lead to ambiguities for the user. - case customLong(_ name: String, withSingleDash: Bool = false) + /// + /// - 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) -> Element { + self.init(base: .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. - case short + public static var short: Element { + self.init(base: .short) + } /// Use the given character as a short option label. /// - /// Short labels can be combined into groups. - case customShort(Character) + /// 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) -> Element { + self.init(base: .customShort(char, allowingJoined: allowingJoined)) + } } var elements: [Element] @@ -62,6 +93,11 @@ extension NameSpecification { /// 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)] } @@ -74,9 +110,17 @@ extension NameSpecification { /// Use the given character as a short option label. /// - /// Short labels can be combined into groups. - public static func customShort(_ char: Character) -> NameSpecification { - [.customShort(char)] + /// 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 @@ -90,7 +134,7 @@ extension NameSpecification { extension NameSpecification.Element { /// Creates the argument name for this specification element. internal func name(for key: InputKey) -> Name? { - switch self { + switch self.base { case .long: return .long(key.rawValue.convertedToSnakeCase(separator: "-")) case .short: @@ -100,8 +144,8 @@ extension NameSpecification.Element { return withSingleDash ? .longWithSingleDash(name) : .long(name) - case .customShort(let name): - return .short(name) + case .customShort(let name, let allowingJoined): + return .short(name, allowingJoined: allowingJoined) } } } @@ -119,7 +163,7 @@ extension FlagInversion { func makeNames(withPrefix prefix: String, includingShort: Bool) -> [Name] { return name.elements.compactMap { element -> Name? in - switch element { + switch element.base { case .short, .customShort: return includingShort ? element.name(for: key) : nil case .long: @@ -133,7 +177,7 @@ extension FlagInversion { } } - switch (self) { + switch self.base { case .prefixedNo: return ( name.makeNames(key), diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index c4851cffa..7e23e4baf 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -9,23 +9,43 @@ // //===----------------------------------------------------------------------===// -/// A wrapper that represents a command-line option. +/// A property 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 +/// Use the `@Option` wrapper to define a property of your custom command as a +/// command-line option. An *option* is a named value passed to a command-line +/// tool, like `--configuration debug`. Options can be specified in any order. +/// +/// An option can have a default value specified as part of its /// declaration; options with optional `Value` types implicitly have `nil` as -/// their default value. +/// their default value. Options that are neither declared as `Optional` nor +/// given a default value are required for users of your command-line tool. /// -/// struct Options: ParsableArguments { -/// @Option(default: "Hello") var greeting: String -/// @Option var name: String +/// For example, the following program defines three options: +/// +/// @main +/// struct Greet: ParsableCommand { +/// @Option var greeting = "Hello" /// @Option var age: Int? +/// @Option var name: String +/// +/// mutating func run() { +/// print("\(greeting) \(name)!") +/// if let age = age { +/// print("Congrats on making it to the ripe old age of \(age)!") +/// } +/// } /// } /// /// `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 +/// providing a different string as an argument, while `age` defaults to `nil`. +/// `name` is a required option because it is non-`nil` and has no default /// value. +/// +/// $ greet --name Alicia +/// Hello Alicia! +/// $ greet --age 28 --name Seungchin --greeting Hi +/// Hi Seungchin! +/// Congrats on making it to the ripe old age of 28! @propertyWrapper public struct Option: Decodable, ParsedWrapper { internal var _parsedValue: Parsed @@ -98,7 +118,7 @@ extension Option where Value: ExpressibleByArgument { ArgumentSet( key: key, kind: .name(key: key, specification: name), - parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + parsingStrategy: parsingStrategy.base, parseType: Value.self, name: name, default: initial, help: help, completion: completion ?? Value.defaultCompletionKind) @@ -106,39 +126,21 @@ extension Option where Value: ExpressibleByArgument { ) } - /// Creates a property that reads its value from a labeled option. - /// - /// This method is deprecated, with usage split into two other methods below: - /// - `init(wrappedValue:name:parsing:help:)` for properties with a default value - /// - `init(name:parsing:help:)` for properties with no default value - /// - /// Existing usage of the `default` parameter should be replaced such as follows: - /// ```diff - /// -@Option(default: "bar") - /// -var foo: String - /// +@Option var foo: String = "bar" - /// ``` - /// - /// - 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 and value are required from the user. - /// - parsingStrategy: The behavior to use when looking for this option's - /// value. - /// - help: Information about how to use this option. - @available(*, deprecated, message: "Use regular property initialization for default values (`var foo: String = \"bar\"`)") + /// Creates a property with a default value provided by standard Swift default value syntax. + @available(*, deprecated, message: "Swap the order of your 'help' and 'completion' arguments.") public init( + wrappedValue: Value, name: NameSpecification = .long, - default initial: Value?, parsing parsingStrategy: SingleValueParsingStrategy = .next, - help: ArgumentHelp? = nil + completion: CompletionKind?, + help: ArgumentHelp? ) { self.init( name: name, - initial: initial, + initial: wrappedValue, parsingStrategy: parsingStrategy, help: help, - completion: nil) + completion: completion) } /// Creates a property with a default value provided by standard Swift default value syntax. @@ -149,16 +151,17 @@ extension Option where Value: ExpressibleByArgument { /// ``` /// /// - Parameters: - /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during propery wrapper initialization. + /// - wrappedValue: A default value to use for this property, provided implicitly by the compiler during property wrapper initialization. /// - name: A specification for what names are allowed for this flag. /// - parsingStrategy: The behavior to use when looking for this option's value. /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. public init( wrappedValue: Value, name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, - completion: CompletionKind? = nil, - help: ArgumentHelp? = nil + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil ) { self.init( name: name, @@ -179,6 +182,7 @@ extension Option where Value: ExpressibleByArgument { /// - name: A specification for what names are allowed for this flag. /// - parsingStrategy: The behavior to use when looking for this option's value. /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. public init( name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, @@ -196,8 +200,10 @@ extension Option where Value: ExpressibleByArgument { /// The strategy to use when parsing a single value from `@Option` arguments. /// -/// - SeeAlso: `ArrayParsingStrategy`` -public enum SingleValueParsingStrategy { +/// - SeeAlso: ``ArrayParsingStrategy`` +public struct SingleValueParsingStrategy: Hashable { + internal var base: ArgumentDefinition.ParsingStrategy + /// Parse the input after the option. Expect it to be a value. /// /// For inputs such as `--foo foo`, this would parse `foo` as the @@ -209,7 +215,9 @@ public enum SingleValueParsingStrategy { /// Usage: command [--foo ] /// /// This is the **default behavior** for `@Option`-wrapped properties. - case next + public static var next: SingleValueParsingStrategy { + self.init(base: .default) + } /// Parse the next input, even if it could be interpreted as an option or /// flag. @@ -223,7 +231,9 @@ public enum SingleValueParsingStrategy { /// interpreted as the start of another option. /// /// - Note: This is usually *not* what users would expect. Use with caution. - case unconditional + public static var unconditional: SingleValueParsingStrategy { + self.init(base: .unconditional) + } /// Parse the next input, as long as that input can't be interpreted as /// an option or flag. @@ -234,12 +244,16 @@ public enum SingleValueParsingStrategy { /// /// 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 + public static var scanningForValue: SingleValueParsingStrategy { + self.init(base: .scanningForValue) + } } /// The strategy to use when parsing multiple values from `@Option` arguments into an /// array. -public enum ArrayParsingStrategy { +public struct ArrayParsingStrategy: Hashable { + internal var base: ArgumentDefinition.ParsingStrategy + /// Parse one value per option, joining multiple into an array. /// /// For example, for a parsable type with a property defined as @@ -252,7 +266,9 @@ public enum ArrayParsingStrategy { /// 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 + public static var singleValue: ArrayParsingStrategy { + self.init(base: .default) + } /// Parse the value immediately after the option while allowing repeating options, joining multiple into an array. /// @@ -267,7 +283,9 @@ public enum ArrayParsingStrategy { /// - 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 + public static var unconditionalSingleValue: ArrayParsingStrategy { + self.init(base: .unconditional) + } /// Parse all values up to the next option. /// @@ -279,8 +297,10 @@ public enum ArrayParsingStrategy { /// 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 - + public static var upToNextOption: ArrayParsingStrategy { + self.init(base: .upToNextOption) + } + /// Parse all remaining arguments into an array. /// /// `.remaining` can be used for capturing pass-through flags. For example, for @@ -304,7 +324,9 @@ public enum ArrayParsingStrategy { /// ``` /// would parse the input `--name Foo -- Bar --baz` such that the `remainder` /// would hold the value `["Bar", "--baz"]`. - case remaining + public static var remaining: ArrayParsingStrategy { + self.init(base: .allRemainingInput) + } } extension Option { @@ -319,6 +341,7 @@ extension Option { /// - parsingStrategy: The behavior to use when looking for this option's /// value. /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. public init( name: NameSpecification = .long, parsing parsingStrategy: SingleValueParsingStrategy = .next, @@ -329,35 +352,11 @@ extension Option { var arg = ArgumentDefinition( key: key, kind: .name(key: key, specification: name), - parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + parsingStrategy: parsingStrategy.base, parser: T.init(argument:), default: nil, completion: completion ?? T.defaultCompletionKind) - arg.help.help = help - return ArgumentSet(arg.optional) - }) - } - - @available(*, deprecated, message: """ - Default values don't make sense for optional properties. - Remove the 'default' parameter if its value is nil, - or make your property non-optional if it's non-nil. - """) - public init( - name: NameSpecification = .long, - default initial: T?, - parsing parsingStrategy: SingleValueParsingStrategy = .next, - help: ArgumentHelp? = 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: initial, - completion: T.defaultCompletionKind) - arg.help.help = help + arg.help.updateArgumentHelp(help: help) return ArgumentSet(arg.optional) }) } @@ -376,7 +375,7 @@ extension Option { 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({ + var arg = ArgumentDefinition(kind: kind, help: help, completion: completion ?? .default, parsingStrategy: parsingStrategy.base, update: .unary({ (origin, name, valueString, parsedValues) in do { let transformedValue = try transform(valueString) @@ -395,48 +394,6 @@ extension Option { }) } - /// Creates a property that reads its value from a labeled option, parsing - /// with the given closure. - /// - /// This method is deprecated, with usage split into two other methods below: - /// - `init(wrappedValue:name:parsing:help:transform:)` for properties with a default value - /// - `init(name:parsing:help:transform:)` for properties with no default value - /// - /// Existing usage of the `default` parameter should be replaced such as follows: - /// ```diff - /// -@Option(default: "bar", transform: baz) - /// -var foo: String - /// +@Option(transform: baz) - /// +var foo: String = "bar" - /// ``` - /// - /// - 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 and value are required from the user. - /// - 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. - @available(*, deprecated, message: "Use regular property initialization for default values (`var foo: String = \"bar\"`)") - public init( - name: NameSpecification = .long, - default initial: Value?, - parsing parsingStrategy: SingleValueParsingStrategy = .next, - help: ArgumentHelp? = nil, - transform: @escaping (String) throws -> Value - ) { - self.init( - name: name, - initial: initial, - parsingStrategy: parsingStrategy, - help: help, - completion: nil, - transform: transform - ) - } - /// 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: @@ -449,6 +406,7 @@ extension Option { /// - name: A specification for what names are allowed for this flag. /// - parsingStrategy: The behavior to use when looking for this option's value. /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. /// - transform: A closure that converts a string into this property's type or throws an error. public init( wrappedValue: Value, @@ -480,6 +438,7 @@ extension Option { /// - name: A specification for what names are allowed for this flag. /// - parsingStrategy: The behavior to use when looking for this option's value. /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. /// - transform: A closure that converts a string into this property's type or throws an error. public init( name: NameSpecification = .long, @@ -529,7 +488,7 @@ extension Option { kind: kind, help: help, completion: completion ?? Element.defaultCompletionKind, - parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + parsingStrategy: parsingStrategy.base, update: .appendToArray(forType: Element.self, key: key), initial: setInitialValue ) @@ -547,6 +506,7 @@ extension Option { /// - parsingStrategy: The behavior to use when parsing multiple values /// from the command-line arguments. /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. public init( wrappedValue: [Element], name: NameSpecification = .long, @@ -575,6 +535,7 @@ extension Option { /// - 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. + /// - completion: Kind of completion provided to the user for this option. public init( name: NameSpecification = .long, parsing parsingStrategy: ArrayParsingStrategy = .singleValue, @@ -622,7 +583,7 @@ extension Option { kind: kind, help: help, completion: completion ?? .default, - parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), + parsingStrategy: parsingStrategy.base, update: .unary({ (origin, name, valueString, parsedValues) in do { let transformedElement = try transform(valueString) @@ -653,6 +614,7 @@ extension Option { /// - parsingStrategy: The behavior to use when parsing multiple values /// from the command-line arguments. /// - help: Information about how to use this option. + /// - completion: Kind of completion provided to the user for this option. /// - transform: A closure that converts a string into this property's /// element type or throws an error. public init( @@ -685,6 +647,7 @@ extension Option { /// - 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. + /// - completion: Kind of completion provided to the user for this option. /// - transform: A closure that converts a string into this property's element type or throws an error. public init( name: NameSpecification = .long, diff --git a/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift index 65f90d3b6..bb8555614 100644 --- a/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift +++ b/Sources/ArgumentParser/Parsable Properties/OptionGroup.swift @@ -31,9 +31,15 @@ @propertyWrapper public struct OptionGroup: Decodable, ParsedWrapper { internal var _parsedValue: Parsed - + internal var _visibility: ArgumentVisibility + + // 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 + self._visibility = .default } public init(from decoder: Decoder) throws { @@ -55,11 +61,13 @@ public struct OptionGroup: Decodable, ParsedWrapper { } } - /// Creates a property that represents another parsable type. - public init() { + /// Creates a property that represents another parsable type, using the + /// specified visibility. + public init(visibility: ArgumentVisibility = .default) { self.init(_parsedValue: .init { _ in - ArgumentSet(Value.self) + ArgumentSet(Value.self, visibility: .private) }) + self._visibility = visibility } /// The value presented by this property wrapper. @@ -88,3 +96,18 @@ extension OptionGroup: CustomStringConvertible { } } } + +// Experimental use with caution +extension OptionGroup { + @available(*, deprecated, renamed: "init(visibility:)") + public init(_hiddenFromHelp: Bool) { + self.init(visibility: .hidden) + } + + /// Creates a property that represents another parsable type. + @available(*, deprecated, renamed: "init(visibility:)") + @_disfavoredOverload + public init() { + self.init(visibility: .default) + } +} diff --git a/Sources/ArgumentParser/Parsable Types/AsyncParsableCommand.swift b/Sources/ArgumentParser/Parsable Types/AsyncParsableCommand.swift new file mode 100644 index 000000000..3b0349765 --- /dev/null +++ b/Sources/ArgumentParser/Parsable Types/AsyncParsableCommand.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------*- 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 asynchronously, as part of a nested tree of +/// commands. +@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) +public protocol AsyncParsableCommand: ParsableCommand { + /// The behavior or functionality of this command. + /// + /// Implement this method in your `ParsableCommand`-conforming type with the + /// functionality that this command represents. + /// + /// This method has a default implementation that prints the help screen for + /// this command. + mutating func run() async throws +} + +@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncParsableCommand { + /// Executes this command, or one of its subcommands, with the program's + /// command-line arguments. + /// + /// Instead of calling this method directly, you can add `@main` to the root + /// command for your command-line tool. + public static func main() async { + do { + var command = try parseAsRoot() + if var asyncCommand = command as? AsyncParsableCommand { + try await asyncCommand.run() + } else { + try command.run() + } + } catch { + exit(withError: error) + } + } +} + +/// A type that can designate an `AsyncParsableCommand` as the program's +/// entry point. +/// +/// See the ``AsyncParsableCommand`` documentation for usage information. +@available( + swift, deprecated: 5.6, + message: "Use @main directly on your root `AsyncParsableCommand` type.") +public protocol AsyncMainProtocol { + associatedtype Command: ParsableCommand +} + +@available(swift, deprecated: 5.6) +@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncMainProtocol { + /// Executes the designated command type, or one of its subcommands, with + /// the program's command-line arguments. + public static func main() async { + do { + var command = try Command.parseAsRoot() + if var asyncCommand = command as? AsyncParsableCommand { + try await asyncCommand.run() + } else { + try command.run() + } + } catch { + Command.exit(withError: error) + } + } +} diff --git a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift index a06a0d272..5c6614fc3 100644 --- a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift +++ b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift @@ -27,6 +27,14 @@ public struct CommandConfiguration { /// A one-line description of this command. public var abstract: String + /// A customized usage string to be shown in the help display and error + /// messages. + /// + /// If `usage` is `nil`, the help display and errors show the autogenerated + /// usage string. To hide the usage string entirely, set `usage` to the empty + /// string. + public var usage: String? + /// A longer description of this command, to be shown in the extended help /// display. public var discussion: String @@ -45,7 +53,7 @@ public struct CommandConfiguration { public var defaultSubcommand: ParsableCommand.Type? /// Flag names to be used for help. - public var helpNames: NameSpecification + public var helpNames: NameSpecification? /// Creates the configuration for a command. /// @@ -54,6 +62,10 @@ public struct CommandConfiguration { /// `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. + /// - usage: A custom usage description for the command. When you provide + /// a non-`nil` string, the argument parser uses `usage` instead of + /// automatically generating a usage description. Passing an empty string + /// hides the usage string altogether. /// - 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 @@ -64,20 +76,24 @@ public struct CommandConfiguration { /// command. /// - defaultSubcommand: The default command type to run if no subcommand /// is given. - /// - helpNames: The flag names to use for requesting help, simulating - /// a Boolean property named `help`. + /// - 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 + /// are `-h` and `--help`. public init( commandName: String? = nil, abstract: String = "", + usage: String? = nil, discussion: String = "", version: String = "", shouldDisplay: Bool = true, subcommands: [ParsableCommand.Type] = [], defaultSubcommand: ParsableCommand.Type? = nil, - helpNames: NameSpecification = [.short, .long] + helpNames: NameSpecification? = nil ) { self.commandName = commandName self.abstract = abstract + self.usage = usage self.discussion = discussion self.version = version self.shouldDisplay = shouldDisplay @@ -92,16 +108,18 @@ public struct CommandConfiguration { commandName: String? = nil, _superCommandName: String, abstract: String = "", + usage: String? = nil, discussion: String = "", version: String = "", shouldDisplay: Bool = true, subcommands: [ParsableCommand.Type] = [], defaultSubcommand: ParsableCommand.Type? = nil, - helpNames: NameSpecification = [.short, .long] + helpNames: NameSpecification? = nil ) { self.commandName = commandName self._superCommandName = _superCommandName self.abstract = abstract + self.usage = usage self.discussion = discussion self.version = version self.shouldDisplay = shouldDisplay @@ -110,3 +128,28 @@ public struct CommandConfiguration { self.helpNames = helpNames } } + +extension CommandConfiguration { + @available(*, deprecated, message: "Use the memberwise initializer with the usage parameter.") + public init( + commandName: String?, + abstract: String, + discussion: String, + version: String, + shouldDisplay: Bool, + subcommands: [ParsableCommand.Type], + defaultSubcommand: ParsableCommand.Type?, + helpNames: NameSpecification? + ) { + self.init( + commandName: commandName, + abstract: abstract, + usage: "", + discussion: discussion, + version: version, + shouldDisplay: shouldDisplay, + subcommands: subcommands, + defaultSubcommand: defaultSubcommand, + helpNames: helpNames) + } +} diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index a3fdbdab9..061492d5a 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -18,6 +18,8 @@ let _exit: (Int32) -> Never = Darwin.exit #elseif canImport(CRT) import CRT let _exit: (Int32) -> Never = ucrt._exit +#elseif canImport(WASILibc) +import WASILibc #endif /// A type that can be parsed from a program's command-line arguments. @@ -93,8 +95,8 @@ extension ParsableArguments { ) 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 helpCommand as HelpCommand: + throw ParserError.helpRequested(visibility: helpCommand.visibility) case let result as _WrappedParsableCommand: return result.options case var result as Self: @@ -130,15 +132,44 @@ extension ParsableArguments { ) -> String { MessageInfo(error: error, type: self).fullText(for: self) } - + + /// Returns the text of the help screen for this type. + /// + /// - Parameters: + /// - 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. + /// - Returns: The full help screen for this type. + @_disfavoredOverload + @available(*, deprecated, message: "Use helpMessage(includeHidden:columns:) instead.") + public static func helpMessage( + columns: Int? + ) -> String { + helpMessage(includeHidden: false, columns: columns) + } + /// 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. + /// - Parameters: + /// - includeHidden: Include hidden help information in the generated + /// message. + /// - 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. /// - Returns: The full help screen for this type. - public static func helpMessage(columns: Int? = nil) -> String { - HelpGenerator(self).rendered(screenWidth: columns) + public static func helpMessage( + includeHidden: Bool = false, + columns: Int? = nil + ) -> String { + HelpGenerator(self, visibility: includeHidden ? .hidden : .default) + .rendered(screenWidth: columns) + } + + /// Returns the JSON representation of this type. + public static func _dumpHelp() -> String { + DumpHelpGenerator(self).rendered() } /// Returns the exit code for the given error. @@ -208,13 +239,37 @@ extension ParsableArguments { } } +/// 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 _visibility: ArgumentVisibility { get } +} + +extension ArgumentSetProvider { + var _visibility: ArgumentVisibility { .default } } extension ArgumentSet { - init(_ type: ParsableArguments.Type) { - + init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility) { #if DEBUG do { try type._validate() @@ -226,18 +281,26 @@ extension ArgumentSet { let a: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in - guard - var codingKey = child.label, - let parsed = child.value as? ArgumentSetProvider - else { return nil } - - // Property wrappers have underscore-prefixed names - codingKey = String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) + guard var codingKey = child.label else { return nil } - let key = InputKey(rawValue: codingKey) - return parsed.argumentSet(for: key) - } - self.init(sets: a) + if let parsed = child.value as? ArgumentSetProvider { + guard parsed._visibility.isAtLeastAsVisible(as: visibility) + 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 + return ArgumentSet( + ArgumentDefinition(unparsedKey: codingKey, default: nilOrValue(child.value))) + } + } + self.init( + a.joined().filter { $0.help.visibility.isAtLeastAsVisible(as: visibility) }) } } diff --git a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift index bec6c1139..c9ad4b47d 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @@ -22,15 +22,18 @@ public protocol ParsableCommand: ParsableArguments { /// can pass through the wrapped type's name. static var _commandName: String { get } - /// Runs this command. + /// The behavior or functionality of 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. + /// Implement this method in your `ParsableCommand`-conforming type with the + /// functionality that this command represents. + /// + /// This method has a default implementation that prints the help screen for + /// this command. mutating func run() throws } +// MARK: - Default implementations + extension ParsableCommand { public static var _commandName: String { configuration.commandName ?? @@ -40,7 +43,7 @@ extension ParsableCommand { public static var configuration: CommandConfiguration { CommandConfiguration() } - + public mutating func run() throws { throw CleanExit.helpRequest(self) } @@ -75,21 +78,59 @@ extension ParsableCommand { /// 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. + @_disfavoredOverload + @available(*, deprecated, renamed: "helpMessage(for:includeHidden:columns:)") 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) + ) -> String { + helpMessage(for: subcommand, includeHidden: false, columns: 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. + /// 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. + /// - includeHidden: Include hidden help information in the generated + /// message. + /// - 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. + /// - Returns: The full help screen for this type. + public static func helpMessage( + for subcommand: ParsableCommand.Type, + includeHidden: Bool = false, + columns: Int? = nil + ) -> String { + HelpGenerator( + commandStack: CommandParser(self).commandStack(for: subcommand), + visibility: includeHidden ? .hidden : .default) + .rendered(screenWidth: columns) + } + + /// Executes this command, or one of its subcommands, with the given + /// arguments. + /// + /// This method parses an instance of this type, one of its subcommands, or + /// another built-in `ParsableCommand` type, from command-line arguments, + /// and then 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]?) { + +#if DEBUG + if #available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) { + checkAsyncHierarchy(self, root: "\(self)") + } +#endif + do { var command = try parseAsRoot(arguments) try command.run() @@ -98,10 +139,60 @@ extension ParsableCommand { } } - /// 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. + /// Executes this command, or one of its subcommands, with the program's + /// command-line arguments. + /// + /// Instead of calling this method directly, you can add `@main` to the root + /// command for your command-line tool. + /// + /// This method parses an instance of this type, one of its subcommands, or + /// another built-in `ParsableCommand` type, from command-line arguments, + /// and then calls its `run()` method, exiting with a relevant error message + /// if necessary. public static func main() { self.main(nil) } } + +// MARK: - Internal API + +extension ParsableCommand { + /// `true` if this command contains any array arguments that are declared + /// with `.unconditionalRemaining`. + internal static var includesUnconditionalArguments: Bool { + ArgumentSet(self, visibility: .private).contains(where: { + $0.isRepeatingPositional && $0.parsingStrategy == .allRemainingInput + }) + } + + /// `true` if this command's default subcommand contains any array arguments + /// that are declared with `.unconditionalRemaining`. This is `false` if + /// there's no default subcommand. + internal static var defaultIncludesUnconditionalArguments: Bool { + configuration.defaultSubcommand?.includesUnconditionalArguments == true + } + +#if DEBUG + @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) + internal static func checkAsyncHierarchy(_ command: ParsableCommand.Type, root: String) { + for sub in command.configuration.subcommands { + checkAsyncHierarchy(sub, root: root) + + guard sub.configuration.subcommands.isEmpty else { continue } + guard sub is AsyncParsableCommand.Type else { continue } + + fatalError(""" + + -------------------------------------------------------------------- + Asynchronous subcommand of a synchronous root. + + The asynchronous command `\(sub)` is declared as a subcommand of the synchronous root command `\(root)`. + + With this configuration, your asynchronous `run()` method will not be called. To fix this issue, change `\(root)`'s `ParsableCommand` conformance to `AsyncParsableCommand`. + -------------------------------------------------------------------- + + """.wrapped(to: 70)) + } + } +#endif +} diff --git a/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift index 09ddde45d..8784adb29 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift @@ -104,7 +104,7 @@ final class ParsedArgumentsContainer: KeyedDecodingContainerProtocol where K } func decodeNil(forKey key: K) throws -> Bool { - return !contains(key) + return element(forKey: key)?.value == nil } func decode(_ type: T.Type, forKey key: K) throws -> T where T : Decodable { @@ -112,6 +112,23 @@ final class ParsedArgumentsContainer: KeyedDecodingContainerProtocol where K 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() } diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index 1acf74c17..7773b80f6 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -10,25 +10,42 @@ //===----------------------------------------------------------------------===// 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? + + // `ArgumentHelp` members + var abstract: String = "" + var discussion: String = "" + var valueName: String = "" + var visibility: ArgumentVisibility = .default + var defaultValue: String? var keys: [InputKey] var allValues: [String] = [] @@ -36,25 +53,33 @@ struct ArgumentDefinition { 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) { + + init(allValues: [String] = [], 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.allValues = allValues self.isComposite = isComposite + updateArgumentHelp(help: help) } - + init(type: T.Type, options: Options = [], help: ArgumentHelp? = nil, defaultValue: String? = nil, key: InputKey) { self.options = options - self.help = help self.defaultValue = defaultValue self.keys = [key] self.allValues = type.allValueStrings + updateArgumentHelp(help: help) + } + + mutating func updateArgumentHelp(help: ArgumentHelp?) { + self.abstract = help?.abstract ?? "" + self.discussion = help?.discussion ?? "" + self.valueName = help?.valueName ?? "" + self.visibility = help?.visibility ?? .default } } @@ -63,7 +88,7 @@ struct ArgumentDefinition { 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 + case `default` /// Parse the next `SplitArguments.Element.value` case scanningForValue /// Parse the next `SplitArguments.Element` as a value, regardless of its type. @@ -84,22 +109,23 @@ struct ArgumentDefinition { var names: [Name] { switch kind { case .named(let n): return n - case .positional: return [] + case .positional, .default: return [] } } var valueName: String { - return help.help?.valueName - ?? preferredNameForSynopsis?.valueString - ?? help.keys.first?.rawValue.convertedToSnakeCase(separator: "-") - ?? "value" + help.valueName.mapEmpty { + names.preferredName?.valueString + ?? help.keys.first?.rawValue.convertedToSnakeCase(separator: "-") + ?? "value" + } } - + init( kind: Kind, help: Help, completion: CompletionKind, - parsingStrategy: ParsingStrategy = .nextAsValue, + parsingStrategy: ParsingStrategy = .default, update: Update, initial: @escaping Initial = { _, _ in } ) { @@ -116,32 +142,6 @@ struct ArgumentDefinition { } } -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) { @@ -156,6 +156,8 @@ extension ArgumentDefinition: CustomDebugStringConvertible { + " <\(valueName)>" case (.positional, _): return "<\(valueName)>" + case (.default, _): + return "" } } } @@ -163,7 +165,6 @@ extension ArgumentDefinition: CustomDebugStringConvertible { extension ArgumentDefinition { var optional: ArgumentDefinition { var result = self - result.help.options.insert(.isOptional) return result } @@ -194,6 +195,10 @@ extension ArgumentDefinition { return false } } + + var allowsJoinedValue: Bool { + names.contains(where: { $0.allowsJoined }) + } } extension ArgumentDefinition.Kind { diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 606c4b7ed..2eee0a4cc 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -25,7 +25,7 @@ struct ArgumentSet { init(_ arguments: S) where S.Element == ArgumentDefinition { self.content = Array(arguments) self.namePositions = Dictionary( - content.enumerated().flatMap { i, arg in arg.names.map { ($0, i) } }, + content.enumerated().flatMap { i, arg in arg.names.map { ($0.nameToMatch, i) } }, uniquingKeysWith: { first, _ in first }) } @@ -38,6 +38,14 @@ struct ArgumentSet { init(sets: [ArgumentSet]) { self.init(sets.joined()) } + + mutating func append(_ arg: ArgumentDefinition) { + let newPosition = content.count + content.append(arg) + for name in arg.names where namePositions[name.nameToMatch] == nil { + namePositions[name.nameToMatch] = newPosition + } + } } extension ArgumentSet: CustomDebugStringConvertible { @@ -48,9 +56,11 @@ extension ArgumentSet: CustomDebugStringConvertible { } } -extension ArgumentSet: Sequence { - func makeIterator() -> Array.Iterator { - return content.makeIterator() +extension ArgumentSet: RandomAccessCollection { + var startIndex: Int { content.startIndex } + var endIndex: Int { content.endIndex } + subscript(position: Int) -> ArgumentDefinition { + content[position] } } @@ -75,7 +85,7 @@ extension ArgumentSet { } static func updateFlag(key: InputKey, value: Value, origin: InputOrigin, values: inout ParsedValues, hasUpdated: Bool, exclusivity: FlagExclusivity) throws -> Bool { - switch (hasUpdated, exclusivity) { + switch (hasUpdated, exclusivity.base) { case (true, .exclusive): // This value has already been set. if let previous = values.element(forKey: key) { @@ -96,22 +106,31 @@ extension ArgumentSet { } /// 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 : [] + static func flag( + key: InputKey, + name: NameSpecification, + default initialValue: Bool?, + required: Bool, + inversion: FlagInversion, + exclusivity: FlagExclusivity, + help: ArgumentHelp?) -> ArgumentSet + { + let helpOptions: ArgumentDefinition.Help.Options = required ? [] : .isOptional - let help = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: initialValue.map(String.init), key: key, isComposite: true) + 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: help, completion: .default, update: .nullary({ (origin, name, values) in + 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: ArgumentDefinition.Help(options: [.isOptional], key: key), completion: .default, update: .nullary({ (origin, name, values) in + 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]) @@ -136,9 +155,9 @@ extension ArgumentSet { 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) { + init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ArgumentDefinition.ParsingStrategy = .default, parseType type: A.Type, name: NameSpecification, default initial: A?, help: ArgumentHelp?, completion: CompletionKind) { var arg = ArgumentDefinition(key: key, kind: kind, parsingStrategy: parsingStrategy, parser: A.init(argument:), default: initial, completion: completion) - arg.help.help = help + arg.help.updateArgumentHelp(help: help) arg.help.defaultValue = initial.map { "\($0.defaultValueDescription)" } self.init(arg) } @@ -146,46 +165,75 @@ extension ArgumentSet { 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) { - let initialValueCreator: (InputOrigin, inout ParsedValues) throws -> Void - if let initialValue = initial { - initialValueCreator = { origin, values in - values.set(initialValue, forKey: key, inputOrigin: origin) - } - } else { - initialValueCreator = { _, _ in } - } - - self.init(kind: kind, help: ArgumentDefinition.Help(key: key), completion: completion, parsingStrategy: parsingStrategy, update: .unary({ (origin, name, value, values) in + init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .default, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind) { + self.init(key: key, kind: kind, parsingStrategy: parsingStrategy, parser: parser, parseType: type, default: initial, completion: completion, help: ArgumentDefinition.Help(key: key)) + } + + /// Create a unary / argument that parses using the given closure. + init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .default, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind) { + self.init(key: key, kind: kind, parsingStrategy: parsingStrategy, parser: parser, parseType: type, default: initial, completion: completion, help: ArgumentDefinition.Help(type: A.self, key: key)) + } + + private init(key: InputKey, kind: ArgumentDefinition.Kind, parsingStrategy: ParsingStrategy = .default, parser: @escaping (String) -> A?, parseType type: A.Type = A.self, default initial: A?, completion: CompletionKind, help: ArgumentDefinition.Help) { + self.init(kind: kind, help: help, completion: completion, parsingStrategy: parsingStrategy, update: .unary({ (origin, name, value, values) in guard let v = parser(value) else { throw ParserError.unableToParseValue(origin, name, value, forKey: key) } values.set(v, forKey: key, inputOrigin: origin) - }), initial: initialValueCreator) - - help.options.formUnion(ArgumentDefinition.Help.Options(type: type)) - help.defaultValue = initial.map { "\($0)" } + }), initial: { origin, values in + switch kind { + case .default: + values.set(initial, forKey: key, inputOrigin: InputOrigin(element: .defaultValue)) + case .named, .positional: + values.set(initial, forKey: key, inputOrigin: origin) + } + }) + + self.help.options.formUnion(ArgumentDefinition.Help.Options(type: type)) + self.help.defaultValue = initial.map { "\($0)" } if initial != nil { self = self.optional } } } +extension ArgumentDefinition { + /// Creates an argument definition for a property that isn't parsed from the + /// command line. + /// + /// This initializer is used for any property defined on a `ParsableArguments` + /// type that isn't decorated with one of ArgumentParser's property wrappers. + init(unparsedKey: String, default defaultValue: Any?) { + self.init( + key: InputKey(rawValue: unparsedKey), + kind: .default, + parser: { _ in nil }, + default: defaultValue, + completion: .default) + help.updateArgumentHelp(help: .private) + } +} + // MARK: - Parsing from SplitArguments extension ArgumentSet { - /// Parse the given input (`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`. + /// Parse the given input for this set of defined arguments. /// - /// When dealing with commands, this will be called iteratively in order to find - /// the matching command(s). + /// This method will consume only the arguments that it understands. If any + /// arguments are declared to capture all remaining input, or a subcommand + /// is configured as such, parsing stops on the first positional argument or + /// unrecognized dash-prefixed argument. /// - /// - 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 { + /// - Parameter input: The input that needs to be parsed. + /// - Parameter subcommands: Any subcommands of the current command. + /// - Parameter defaultCapturesAll: `true` if the default subcommand has an + /// argument that captures all remaining input. + func lenientParse( + _ input: SplitArguments, + subcommands: [ParsableCommand.Type], + defaultCapturesAll: Bool + ) throws -> ParsedValues { // Create a local, mutable copy of the arguments: - var inputArguments = all + var inputArguments = input func parseValue( _ argument: ArgumentDefinition, @@ -197,11 +245,19 @@ extension ArgumentSet { ) throws { let origin = InputOrigin(elements: [originElement]) switch argument.parsingStrategy { - case .nextAsValue: + case .default: // We need a value for this option. 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 if let (origin2, value) = inputArguments.popNextElementIfValue(after: originElement) { // Use `popNextElementIfValue(after:)` to handle cases where short option // labels are combined @@ -217,6 +273,13 @@ extension ArgumentSet { 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 if let (origin2, value) = inputArguments.popNextValue(after: originElement) { // Use `popNext(after:)` to handle cases where short option // labels are combined @@ -233,6 +296,12 @@ extension ArgumentSet { // 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) @@ -251,6 +320,13 @@ extension ArgumentSet { // 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 @@ -261,14 +337,31 @@ extension ArgumentSet { } 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) + } + + // Clear out the initial origin first, since it can include + // the exploded elements of an options group (see issue #327). + usedOrigins.formUnion(origin) + inputArguments.removeAll(in: origin) + + // Fix incorrect error message + // for @Option array without values (see issue #434). + guard let first = inputArguments.elements.first, + first.isValue + else { + throw ParserError.missingValueForOption(origin, parsed.name) } // ...and then consume the arguments until hitting an option @@ -280,12 +373,21 @@ extension ArgumentSet { } } - var result = ParsedValues(elements: [:], originalInput: all.originalInput) + // If this argument set includes a positional argument that unconditionally + // captures all remaining input, we use a different behavior, where we + // shortcut out at the first sign of a positional argument or unrecognized + // option/flag label. + let capturesAll = defaultCapturesAll || self.contains(where: { arg in + arg.isRepeatingPositional && arg.parsingStrategy == .allRemainingInput + }) + + var result = ParsedValues(elements: [:], originalInput: input.originalInput) var allUsedOrigins = InputOrigin() try setInitialValues(into: &result) // Loop over all arguments: + ArgumentLoop: while let (origin, next) = inputArguments.popNext() { var usedOrigins = InputOrigin() defer { @@ -294,7 +396,22 @@ extension ArgumentSet { } switch next.value { - case .value: + case .value(let argument): + // Special handling for matching subcommand names. We generally want + // parsing to skip over unrecognized input, but if the current + // command or the matched subcommand captures all remaining input, + // then we want to break out of parsing at this point. + if let matchedSubcommand = subcommands.first(where: { $0._commandName == argument }) { + if !matchedSubcommand.includesUnconditionalArguments && defaultCapturesAll { + continue ArgumentLoop + } else if matchedSubcommand.includesUnconditionalArguments { + break ArgumentLoop + } + } + + // If we're capturing all, the first positional value represents the + // start of positional input. + if capturesAll { break ArgumentLoop } // We'll parse positional values later. break case let .option(parsed): @@ -302,8 +419,19 @@ extension ArgumentSet { // 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 } + guard let argument = first(matching: parsed) else + { + // If we're capturing all, an unrecognized option/flag is the start + // of positional input. However, the first time we see an option + // pack (like `-fi`) it looks like a long name with a single-dash + // prefix, which may not match an argument even if its subcomponents + // will match. + if capturesAll && parsed.subarguments.isEmpty { break ArgumentLoop } + + // Otherwise, continue parsing. This option/flag may get picked up + // by a child command. + continue + } switch argument.update { case let .nullary(update): @@ -324,7 +452,7 @@ extension ArgumentSet { // We have parsed all non-positional values at this point. // Next: parse / consume the positional values. - var unusedArguments = all + var unusedArguments = input unusedArguments.removeAll(in: allUsedOrigins) try parsePositionalValues(from: unusedArguments, into: &result) diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index f037f5d61..1446f241b 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -14,7 +14,9 @@ struct CommandError: Error { var parserError: ParserError } -struct HelpRequested: Error {} +struct HelpRequested: Error { + var visibility: ArgumentVisibility +} struct CommandParser { let commandTree: Tree @@ -71,15 +73,35 @@ extension CommandParser { return subcommandNode } - /// Throws a `HelpRequested` error if the user has specified either of the - /// built in help flags. - func checkForBuiltInFlags(_ split: SplitArguments) throws { + /// Throws a `HelpRequested` error if the user has specified any of the + /// built-in flags. + /// + /// - Parameters: + /// - split: The remaining arguments to examine. + /// - requireSoloArgument: `true` if the built-in flag must be the only + /// one remaining for this to catch it. + func checkForBuiltInFlags( + _ split: SplitArguments, + requireSoloArgument: Bool = false + ) throws { + guard !requireSoloArgument || split.count == 1 else { return } + // Look for help flags - guard !split.contains(anyOf: self.commandTree.element.getHelpNames()) else { - throw HelpRequested() + guard !split.contains(anyOf: self.commandStack.getHelpNames(visibility: .default)) else { + throw HelpRequested(visibility: .default) + } + + // Look for help-hidden flags + guard !split.contains(anyOf: self.commandStack.getHelpNames(visibility: .hidden)) else { + throw HelpRequested(visibility: .hidden) } - // Look for --version if any commands in the stack define a version + // Look for dump-help flag + guard !split.contains(Name.long("experimental-dump-help")) else { + throw CommandError(commandStack: commandStack, parserError: .dumpHelpRequested) + } + + // Look for a version flag 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) @@ -118,11 +140,14 @@ extension CommandParser { /// 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) + let commandArguments = ArgumentSet(currentNode.element, visibility: .private) // Parse the arguments, ignoring anything unexpected - let values = try commandArguments.lenientParse(split) - + let values = try commandArguments.lenientParse( + split, + subcommands: currentNode.element.configuration.subcommands, + defaultCapturesAll: currentNode.element.defaultIncludesUnconditionalArguments) + // Decode the values from ParsedValues into the ParsableCommand: let decoder = ArgumentDecoder(values: values, previouslyDecoded: decodedArguments) var decodedResult: ParsableCommand @@ -172,7 +197,7 @@ extension CommandParser { } // Look for the help flag before falling back to a default command. - try checkForBuiltInFlags(split) + try checkForBuiltInFlags(split, requireSoloArgument: true) // No command was found, so fall back to the default subcommand. if let defaultSubcommand = currentNode.element.configuration.defaultSubcommand { @@ -226,8 +251,10 @@ extension CommandParser { } 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 let helpRequest as HelpRequested { + return .success(HelpCommand( + commandStack: commandStack, + visibility: helpRequest.visibility)) } catch { return .failure(CommandError(commandStack: commandStack, parserError: .invalidState)) } @@ -298,7 +325,7 @@ extension CommandParser { let completionValues = Array(args) // Generate the argument set and parse the argument to find in the set - let argset = ArgumentSet(current.element) + let argset = ArgumentSet(current.element, visibility: .private) let parsedArgument = try! parseIndividualArg(argToMatch, at: 0).first! // Look up the specified argument and retrieve its custom completion function diff --git a/Sources/ArgumentParser/Parsing/InputOrigin.swift b/Sources/ArgumentParser/Parsing/InputOrigin.swift index 34d696658..98bfb8582 100644 --- a/Sources/ArgumentParser/Parsing/InputOrigin.swift +++ b/Sources/ArgumentParser/Parsing/InputOrigin.swift @@ -11,13 +11,51 @@ /// Specifies where a given input came from. /// -/// When reading from the command line, a value might originate from multiple indices. +/// When reading from the command line, a value might originate from a single +/// index, multiple indices, or from part of an index. For this command: /// -/// This is usually an index into the `SplitArguments`. -/// In some cases it can be multiple indices. +/// 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 = [] @@ -65,11 +103,21 @@ struct InputOrigin: Equatable, ExpressibleByArrayLiteral { } } +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 index cd940d9ca..3e7b4b3a0 100644 --- a/Sources/ArgumentParser/Parsing/Name.swift +++ b/Sources/ArgumentParser/Parsing/Name.swift @@ -9,16 +9,18 @@ // //===----------------------------------------------------------------------===// -enum Name: Hashable { +enum Name { /// 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) + case short(Character, allowingJoined: Bool = false) /// A name (usually multi-character) prefixed with `-` (1 dash). case longWithSingleDash(String) - +} + +extension Name { init(_ baseName: Substring) { assert(baseName.first == "-", "Attempted to create name for unprefixed argument") if baseName.hasPrefix("--") { @@ -31,12 +33,41 @@ enum Name: Hashable { } } +// 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 + } +} + +extension Name: Hashable { } + +extension Name { + enum Case: Equatable { + case long + case short + case longWithSingleDash + } + + var `case`: Case { + switch self { + case .short: + return .short + case .longWithSingleDash: + return .longWithSingleDash + case .long: + return .long + } + } +} + extension Name { var synopsisString: String { switch self { case .long(let n): return "--\(n)" - case .short(let n): + case .short(let n, _): return "-\(n)" case .longWithSingleDash(let n): return "-\(n)" @@ -47,27 +78,38 @@ extension Name { switch self { case .long(let n): return n - case .short(let n): + case .short(let n, _): return String(n) case .longWithSingleDash(let n): return n } } - - var isShort: Bool { + + var allowsJoined: Bool { switch self { - case .short: - return true + 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 +extension BidirectionalCollection where Element == Name { + var preferredName: Name? { + first { $0.case != .short } ?? first + } + + var partitioned: [Name] { + filter { $0.case == .short } + filter { $0.case != .short } } } diff --git a/Sources/ArgumentParser/Parsing/ParsedValues.swift b/Sources/ArgumentParser/Parsing/ParsedValues.swift index f490c4d43..69ef15cb5 100644 --- a/Sources/ArgumentParser/Parsing/ParsedValues.swift +++ b/Sources/ArgumentParser/Parsing/ParsedValues.swift @@ -29,7 +29,7 @@ struct InputKey: RawRepresentable, Hashable { struct ParsedValues { struct Element { var key: InputKey - var value: Any + var value: Any? /// Where in the input that this came from. var inputOrigin: InputOrigin fileprivate var shouldClearArrayIfParsed = true @@ -45,7 +45,7 @@ struct ParsedValues { } extension ParsedValues { - mutating func set(_ new: Any, forKey key: InputKey, inputOrigin: InputOrigin) { + mutating func set(_ new: Any?, forKey key: InputKey, inputOrigin: InputOrigin) { set(Element(key: key, value: new, inputOrigin: inputOrigin)) } diff --git a/Sources/ArgumentParser/Parsing/ParserError.swift b/Sources/ArgumentParser/Parsing/ParserError.swift index d2814ed57..8756f9d3f 100644 --- a/Sources/ArgumentParser/Parsing/ParserError.swift +++ b/Sources/ArgumentParser/Parsing/ParserError.swift @@ -11,8 +11,9 @@ /// Gets thrown while parsing and will be handled by the error output generation. enum ParserError: Error { - case helpRequested + case helpRequested(visibility: ArgumentVisibility) case versionRequested + case dumpHelpRequested case completionScriptRequested(shell: String?) case completionScriptCustomResponse(String) @@ -38,7 +39,7 @@ enum ParserError: 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 wrongType(Any?, forKey: InputKey) case subcommandNameMismatch case subcommandLevelMismatch(Int, Int) case subcommandLevelMissing(Int) diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift index 4316b11c1..2d08ee6c9 100644 --- a/Sources/ArgumentParser/Parsing/SplitArguments.swift +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -163,9 +163,13 @@ struct SplitArguments { var inputIndex: InputIndex var subIndex: SubIndex = .complete + + var completeIndex: Index { + return Index(inputIndex: inputIndex) + } } - /// The parsed arguments. Onl + /// The parsed arguments. var _elements: [Element] = [] var firstUnused: Int = 0 @@ -176,6 +180,10 @@ struct SplitArguments { var elements: ArraySlice { _elements[firstUnused...] } + + var count: Int { + elements.count + } } extension SplitArguments.Element: CustomDebugStringConvertible { @@ -287,6 +295,25 @@ extension SplitArguments { 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 @@ -425,6 +452,7 @@ extension SplitArguments { 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 diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift new file mode 100644 index 000000000..2f7c9c6c4 --- /dev/null +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -0,0 +1,170 @@ +//===----------------------------------------------------------*- 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 +@_implementationOnly import ArgumentParserToolInfo + +internal struct DumpHelpGenerator { + var toolInfo: ToolInfoV0 + + init(_ type: ParsableArguments.Type) { + self.init(commandStack: [type.asCommand]) + } + + init(commandStack: [ParsableCommand.Type]) { + self.toolInfo = ToolInfoV0(commandStack: commandStack) + } + + func rendered() -> String { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + if #available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) { + encoder.outputFormatting.insert(.sortedKeys) + } + guard let encoded = try? encoder.encode(self.toolInfo) else { return "" } + return String(data: encoded, encoding: .utf8) ?? "" + } +} + +fileprivate extension BidirectionalCollection where Element == ParsableCommand.Type { + /// Returns the ArgumentSet for the last command in this stack, including + /// help and version flags, when appropriate. + func allArguments() -> ArgumentSet { + guard var arguments = self.last.map({ ArgumentSet($0, visibility: .private) }) + else { return ArgumentSet() } + self.versionArgumentDefinition().map { arguments.append($0) } + self.helpArgumentDefinition().map { arguments.append($0) } + return arguments + } +} + +fileprivate extension ArgumentSet { + func mergingCompositeArguments() -> ArgumentSet { + var arguments = ArgumentSet() + var slice = self[...] + while var argument = slice.popFirst() { + if argument.help.isComposite { + // If this argument is composite, we have a group of arguments to + // merge together. + let groupEnd = slice + .firstIndex { $0.help.keys != argument.help.keys } + ?? slice.endIndex + let group = [argument] + slice[.. CommandInfoV0 in + var commandStack = commandStack + commandStack.append(subcommand) + return CommandInfoV0(commandStack: commandStack) + } + let arguments = commandStack + .allArguments() + .mergingCompositeArguments() + .compactMap(ArgumentInfoV0.init) + + self = CommandInfoV0( + superCommands: superCommands, + commandName: command._commandName, + abstract: command.configuration.abstract, + discussion: command.configuration.discussion, + defaultSubcommand: defaultSubcommand, + subcommands: subcommands, + arguments: arguments) + } +} + +fileprivate extension ArgumentInfoV0 { + init?(argument: ArgumentDefinition) { + guard let kind = ArgumentInfoV0.KindV0(argument: argument) else { return nil } + self.init( + kind: kind, + shouldDisplay: argument.help.visibility.base == .default, + isOptional: argument.help.options.contains(.isOptional), + isRepeating: argument.help.options.contains(.isRepeating), + names: argument.names.map(ArgumentInfoV0.NameInfoV0.init), + preferredName: argument.names.preferredName.map(ArgumentInfoV0.NameInfoV0.init), + valueName: argument.valueName, + defaultValue: argument.help.defaultValue, + allValues: argument.help.allValues, + abstract: argument.help.abstract, + discussion: argument.help.discussion) + } +} + +fileprivate extension ArgumentInfoV0.KindV0 { + init?(argument: ArgumentDefinition) { + switch argument.kind { + case .named: + switch argument.update { + case .nullary: + self = .flag + case .unary: + self = .option + } + case .positional: + self = .positional + case .default: + return nil + } + } +} + +fileprivate extension ArgumentInfoV0.NameInfoV0 { + init(name: Name) { + switch name { + case let .long(n): + self.init(kind: .long, name: n) + case let .short(n, _): + self.init(kind: .short, name: String(n)) + case let .longWithSingleDash(n): + self.init(kind: .longWithSingleDash, name: n) + } + } +} diff --git a/Sources/ArgumentParser/Usage/HelpCommand.swift b/Sources/ArgumentParser/Usage/HelpCommand.swift index 81739e245..c79d37983 100644 --- a/Sources/ArgumentParser/Usage/HelpCommand.swift +++ b/Sources/ArgumentParser/Usage/HelpCommand.swift @@ -12,37 +12,54 @@ struct HelpCommand: ParsableCommand { static var configuration = CommandConfiguration( commandName: "help", - abstract: "Show subcommand help information.") + abstract: "Show subcommand help information.", + helpNames: []) + /// Any subcommand names provided after the `help` subcommand. @Argument var subcommands: [String] = [] - private(set) var commandStack: [ParsableCommand.Type] = [] + /// Capture and ignore any extra help flags given by the user. + @Flag(name: [.short, .long, .customLong("help", withSingleDash: true)], help: .private) + var help = false + private(set) var commandStack: [ParsableCommand.Type] = [] + private(set) var visibility: ArgumentVisibility = .default + init() {} mutating func run() throws { - throw CommandError(commandStack: commandStack, parserError: .helpRequested) + throw CommandError( + commandStack: commandStack, + parserError: .helpRequested(visibility: visibility)) } mutating func buildCommandStack(with parser: CommandParser) throws { commandStack = parser.commandStack(for: subcommands) } - - func generateHelp() -> String { - return HelpGenerator(commandStack: commandStack).rendered() + + /// Used for testing. + func generateHelp(screenWidth: Int) -> String { + HelpGenerator( + commandStack: commandStack, + visibility: visibility) + .rendered(screenWidth: screenWidth) } enum CodingKeys: CodingKey { case subcommands + case help } 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))) + self.subcommands = try container.decode([String].self, forKey: .subcommands) + self.help = try container.decode(Bool.self, forKey: .help) } - init(commandStack: [ParsableCommand.Type]) { + init(commandStack: [ParsableCommand.Type], visibility: ArgumentVisibility) { self.commandStack = commandStack - self._subcommands = Argument(_parsedValue: .value(commandStack.map { $0._commandName })) + self.visibility = visibility + self.subcommands = commandStack.map { $0._commandName } + self.help = false } } diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index d3538a8ce..b216aa255 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -12,21 +12,8 @@ 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") - } - } - + static var systemScreenWidth: Int { _terminalSize().width } + struct Section { struct Element: Hashable { var label: String @@ -98,16 +85,16 @@ internal struct HelpGenerator { var commandStack: [ParsableCommand.Type] var abstract: String - var usage: Usage + var usage: String var sections: [Section] var discussionSections: [DiscussionSection] - init(commandStack: [ParsableCommand.Type]) { + init(commandStack: [ParsableCommand.Type], visibility: ArgumentVisibility) { guard let currentCommand = commandStack.last else { fatalError() } - let currentArgSet = ArgumentSet(currentCommand) + let currentArgSet = ArgumentSet(currentCommand, visibility: visibility) self.commandStack = commandStack // Build the tool name and subcommand name from the command configuration @@ -116,10 +103,16 @@ internal struct HelpGenerator { toolName = "\(superName) \(toolName)" } - var usageString = UsageGenerator(toolName: toolName, definition: [currentArgSet]).synopsis - if !currentCommand.configuration.subcommands.isEmpty { - if usageString.last != " " { usageString += " " } - usageString += "" + if let usage = currentCommand.configuration.usage { + self.usage = usage + } else { + var usage = UsageGenerator(toolName: toolName, definition: [currentArgSet]) + .synopsis + if !currentCommand.configuration.subcommands.isEmpty { + if usage.last != " " { usage += " " } + usage += "" + } + self.usage = usage } self.abstract = currentCommand.configuration.abstract @@ -129,95 +122,74 @@ internal struct HelpGenerator { } self.abstract += "\n\(currentCommand.configuration.discussion)" } - - self.usage = Usage(components: [usageString]) - self.sections = HelpGenerator.generateSections(commandStack: commandStack) + + self.sections = HelpGenerator.generateSections(commandStack: commandStack, visibility: visibility) self.discussionSections = [] } - init(_ type: ParsableArguments.Type) { - self.init(commandStack: [type.asCommand]) + init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility) { + self.init(commandStack: [type.asCommand], visibility: visibility) } - static func generateSections(commandStack: [ParsableCommand.Type]) -> [Section] { + private static func generateSections(commandStack: [ParsableCommand.Type], visibility: ArgumentVisibility) -> [Section] { + guard !commandStack.isEmpty else { return [] } + var positionalElements: [Section.Element] = [] var optionElements: [Section.Element] = [] - /// Used to keep track of elements already seen from parent commands. - var alreadySeenElements = Set() - for commandType in commandStack { - let args = Array(ArgumentSet(commandType)) + /// Start with a full slice of the ArgumentSet so we can peel off one or + /// more elements at a time. + var args = commandStack.argumentsForHelp(visibility: visibility)[...] + while let arg = args.popFirst() { + assert(arg.help.visibility.isAtLeastAsVisible(as: visibility)) - 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 - } + let synopsis: String + let description: String + + if arg.help.isComposite { + // If this argument is composite, we have a group of arguments to + // output together. + let groupEnd = args.firstIndex(where: { $0.help.keys != arg.help.keys }) ?? args.endIndex + let groupedArgs = [arg] + args[.. String { - let screenWidth = screenWidth ?? HelpGenerator.systemScreenWidth - return "Usage: \(usage.rendered(screenWidth: screenWidth))" + func usageMessage() -> String { + guard !usage.isEmpty else { return "" } + return "Usage: \(usage.hangingIndentingEachLine(by: 7))" } var includesSubcommands: Bool { @@ -259,7 +231,7 @@ internal struct HelpGenerator { ? "" : "OVERVIEW: \(abstract)".wrapped(to: screenWidth) + "\n\n" - var helpSubcommandMessage: String = "" + var helpSubcommandMessage = "" if includesSubcommands { var names = commandStack.map { $0._commandName } if let superName = commandStack.first!.configuration._superCommandName { @@ -273,24 +245,111 @@ internal struct HelpGenerator { """ } + let renderedUsage = usage.isEmpty + ? "" + : "USAGE: \(usage.hangingIndentingEachLine(by: 7))\n\n" + return """ \(renderedAbstract)\ - USAGE: \(usage.rendered(screenWidth: screenWidth)) - + \(renderedUsage)\ \(renderedSections)\(helpSubcommandMessage) """ } } -internal extension ParsableCommand { - static func getHelpNames() -> [Name] { - return self.configuration - .helpNames +fileprivate extension CommandConfiguration { + static var defaultHelpNames: NameSpecification { [.short, .long] } +} + +fileprivate extension NameSpecification { + /// Generates a list of `Name`s for the help command at any visibility level. + /// + /// If the `default` visibility is used, the help names are returned + /// unmodified. If a non-default visibility is used the short names are + /// removed and the long names (both single and double dash) are appended with + /// the name of the visibility level. After the optional name modification + /// step, the name are returned in descending order. + func generateHelpNames(visibility: ArgumentVisibility) -> [Name] { + self .makeNames(InputKey(rawValue: "help")) + .compactMap { name in + guard visibility.base != .default else { return name } + switch name { + case .long(let helpName): + return .long("\(helpName)-\(visibility.base)") + case .longWithSingleDash(let helpName): + return .longWithSingleDash("\(helpName)-\(visibility)") + case .short: + // Cannot create a non-default help flag from a short name. + return nil + } + } .sorted(by: >) } } +internal extension BidirectionalCollection where Element == ParsableCommand.Type { + /// Returns a list of help names at the request visibility level for the top + /// most ParsableCommand in the command stack with custom helpNames. If the + /// command stack contains no custom help names the default help names. + func getHelpNames(visibility: ArgumentVisibility) -> [Name] { + self.last(where: { $0.configuration.helpNames != nil }) + .map { $0.configuration.helpNames!.generateHelpNames(visibility: visibility) } + ?? CommandConfiguration.defaultHelpNames.generateHelpNames(visibility: visibility) + } + + func getPrimaryHelpName() -> Name? { + getHelpNames(visibility: .default).preferredName + } + + func versionArgumentDefinition() -> ArgumentDefinition? { + guard contains(where: { !$0.configuration.version.isEmpty }) + else { return nil } + return ArgumentDefinition( + kind: .named([.long("version")]), + help: .init(help: "Show the version.", key: InputKey(rawValue: "")), + completion: .default, + update: .nullary({ _, _, _ in }) + ) + } + + func helpArgumentDefinition() -> ArgumentDefinition? { + let names = getHelpNames(visibility: .default) + guard !names.isEmpty else { return nil } + return ArgumentDefinition( + kind: .named(names), + help: .init(help: "Show help information.", key: InputKey(rawValue: "")), + completion: .default, + update: .nullary({ _, _, _ in }) + ) + } + + func dumpHelpArgumentDefinition() -> ArgumentDefinition { + return ArgumentDefinition( + kind: .named([.long("experimental-dump-help")]), + help: .init( + help: ArgumentHelp("Dump help information as JSON."), + key: InputKey(rawValue: "")), + completion: .default, + update: .nullary({ _, _, _ in }) + ) + } + + /// Returns the ArgumentSet for the last command in this stack, including + /// help and version flags, when appropriate. + func argumentsForHelp(visibility: ArgumentVisibility) -> ArgumentSet { + guard var arguments = self.last.map({ ArgumentSet($0, visibility: visibility) }) + else { return ArgumentSet() } + self.versionArgumentDefinition().map { arguments.append($0) } + self.helpArgumentDefinition().map { arguments.append($0) } + + // To add when 'dump-help' is public API: + // arguments.append(self.dumpHelpArgumentDefinition()) + + return arguments + } +} + #if canImport(Glibc) import Glibc func ioctl(_ a: Int32, _ b: Int32, _ p: UnsafeMutableRawPointer) -> Int32 { @@ -304,15 +363,25 @@ import WinSDK #endif func _terminalSize() -> (width: Int, height: Int) { -#if os(Windows) +#if os(WASI) + // WASI doesn't yet support terminal size + return (80, 25) +#elseif os(Windows) var csbi: CONSOLE_SCREEN_BUFFER_INFO = CONSOLE_SCREEN_BUFFER_INFO() - - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) + guard GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) else { + return (80, 25) + } 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) } diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index accf643d6..eff4c0aa3 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -13,7 +13,7 @@ enum MessageInfo { case help(text: String) - case validation(message: String, usage: String) + case validation(message: String, usage: String, help: String) case other(message: String, exitCode: Int32) init(error: Error, type: ParsableArguments.Type) { @@ -27,10 +27,14 @@ enum MessageInfo { // Exit early on built-in requests switch e.parserError { - case .helpRequested: - self = .help(text: HelpGenerator(commandStack: e.commandStack).rendered()) + case .helpRequested(let visibility): + self = .help(text: HelpGenerator(commandStack: e.commandStack, visibility: visibility).rendered()) return - + + case .dumpHelpRequested: + self = .help(text: DumpHelpGenerator(commandStack: e.commandStack).rendered()) + return + case .versionRequested: let versionString = commandStack .map { $0.configuration.version } @@ -69,9 +73,15 @@ enum MessageInfo { parserError = .userValidationError(error) } + var usage = HelpGenerator(commandStack: commandStack, visibility: .default).usageMessage() + let commandNames = commandStack.map { $0._commandName }.joined(separator: " ") - let usage = HelpGenerator(commandStack: commandStack).usageMessage() - + "\n See '\(commandNames) --help' for more information." + if let helpName = commandStack.getPrimaryHelpName() { + if !usage.isEmpty { + usage += "\n" + } + usage += " 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. @@ -79,14 +89,19 @@ enum MessageInfo { if case .userValidationError(let error) = parserError { switch error { case let error as ValidationError: - self = .validation(message: error.message, usage: usage) + self = .validation(message: error.message, usage: usage, help: "") case let error as CleanExit: - switch error { + switch error.base { case .helpRequest(let command): if let command = command { commandStack = CommandParser(type.asCommand).commandStack(for: command) } - self = .help(text: HelpGenerator(commandStack: commandStack).rendered()) + self = .help(text: HelpGenerator(commandStack: commandStack, visibility: .default).rendered()) + case .dumpRequest(let command): + if let command = command { + commandStack = CommandParser(type.asCommand).commandStack(for: command) + } + self = .help(text: DumpHelpGenerator(commandStack: commandStack).rendered()) case .message(let message): self = .help(text: message) } @@ -95,15 +110,21 @@ enum MessageInfo { case let error as LocalizedError where error.errorDescription != nil: self = .other(message: error.errorDescription!, exitCode: EXIT_FAILURE) default: - self = .other(message: String(describing: error), exitCode: EXIT_FAILURE) + 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() + return "\n" + HelpGenerator(commandStack: [type.asCommand], visibility: .default).rendered() }() - let message = ArgumentSet(commandStack.last!).helpMessage(for: parserError) - self = .validation(message: message, usage: usage) + let argumentSet = ArgumentSet(commandStack.last!, visibility: .default) + 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) } @@ -113,7 +134,7 @@ enum MessageInfo { switch self { case .help(text: let text): return text - case .validation(message: let message, usage: _): + case .validation(message: let message, usage: _, help: _): return message case .other(let message, _): return message @@ -124,9 +145,10 @@ enum MessageInfo { switch self { case .help(text: let text): return text - case .validation(message: let message, usage: let usage): + 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 + usage + return errorMessage + helpMessage + usage case .other(let message, _): return message.isEmpty ? "" : "\(args._errorLabel): \(message)" } diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index d8d18d89b..cbb1c7198 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -22,8 +22,10 @@ extension UsageGenerator { self.init(toolName: toolName, definition: definition) } - init(toolName: String, parsable: ParsableArguments) { - self.init(toolName: toolName, definition: ArgumentSet(type(of: parsable))) + init(toolName: String, parsable: ParsableArguments, visibility: ArgumentVisibility) { + self.init( + toolName: toolName, + definition: ArgumentSet(type(of: parsable), visibility: visibility)) } init(toolName: String, definition: [ArgumentSet]) { @@ -36,101 +38,89 @@ extension UsageGenerator { /// /// In `roff`. var synopsis: String { - let definitionSynopsis = definition.synopsis - switch definitionSynopsis.count { + var options = Array(definition) + switch options.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. + options = options.filter { + $0.isPositional || !$0.help.options.contains(.isOptional) + } + // If there are between 1 and 12 options left, print them, otherwise print + // a simplified usage string. + if !options.isEmpty, options.count <= 12 { + let synopsis = options + .map { $0.synopsis } + .joined(separator: " ") + return "\(toolName) [] \(synopsis)" + } return "\(toolName) " default: - return "\(toolName) \(definition.synopsis.joined(separator: " "))" + let synopsis = options + .map { $0.synopsis } + .joined(separator: " ") + return "\(toolName) \(synopsis)" } } } -extension ArgumentSet { - var synopsis: [String] { - return self - .compactMap { $0.synopsis } - } -} - extension ArgumentDefinition { - var synopsisForHelp: String? { - guard help.help?.shouldDisplay != false else { - return nil - } - + var synopsisForHelp: String { switch kind { case .named: - let joinedSynopsisString = partitionedNames + let joinedSynopsisString = names + .partitioned .map { $0.synopsisString } .joined(separator: ", ") - + switch update { case .unary: - return "\(joinedSynopsisString) <\(synopsisValueName ?? "")>" + return "\(joinedSynopsisString) <\(valueName)>" case .nullary: return joinedSynopsisString } case .positional: return "<\(valueName)>" + case .default: + return "" } } - - var unadornedSynopsis: String? { + + var unadornedSynopsis: String { switch kind { case .named: - guard let name = preferredNameForSynopsis else { return nil } - + guard let name = names.preferredName else { + fatalError("preferredName cannot be nil for named arguments") + } + switch update { case .unary: - return "\(name.synopsisString) <\(synopsisValueName ?? "value")>" + return "\(name.synopsisString) <\(valueName)>" 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)]" } + + var synopsis: String { + var synopsis = unadornedSynopsis + if help.options.contains(.isRepeating) { + synopsis += " ..." } - guard !help.options.contains(.isRepeating) else { - var n = self - n.help.options.remove(.isRepeating) - return n.synopsis.flatMap { "\($0) ..." } + if help.options.contains(.isOptional) { + synopsis = "[\(synopsis)]" } - - 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 + return synopsis } } extension ArgumentSet { - func helpMessage(for error: Swift.Error) -> String { - return errorDescription(error: error) ?? "" - } - /// Will generate a descriptive help message if possible. /// /// If no descriptive help message can be generated, `nil` will be returned. @@ -148,6 +138,19 @@ extension ArgumentSet { 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 { @@ -158,7 +161,7 @@ struct ErrorMessageGenerator { extension ErrorMessageGenerator { func makeErrorMessage() -> String? { switch error { - case .helpRequested, .versionRequested, .completionScriptRequested, .completionScriptCustomResponse: + case .helpRequested, .versionRequested, .completionScriptRequested, .completionScriptCustomResponse, .dumpHelpRequested: return nil case .unsupportedShell(let shell?): @@ -208,31 +211,37 @@ extension ErrorMessageGenerator { } } } + + 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) + case .missingValueForOption(_, let n): + return missingValueForOptionHelpMessage(name: n) + case .noValue(let k): + return noValueHelpMessage(key: k) + default: + return nil + } + } } extension ErrorMessageGenerator { func arguments(for key: InputKey) -> [ArgumentDefinition] { - return arguments - .filter { - $0.help.keys.contains(key) - } + arguments + .filter { $0.help.keys.contains(key) } } func help(for key: InputKey) -> ArgumentDefinition.Help? { - return arguments + 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 + arguments + .first { $0.names.contains(name) } + .map { $0.valueName } } } @@ -284,7 +293,7 @@ extension ErrorMessageGenerator { }) if let suggestion = suggestion { - return "Unknown option '\(name.synopsisString)'. Did you mean '\(suggestion.synopsisString)'?" + return "Unknown option '\(name.synopsisString)'. Did you mean '\(suggestion.synopsisString)'?" } return "Unknown option '\(name.synopsisString)'" } @@ -334,12 +343,14 @@ extension ErrorMessageGenerator { func noValueMessage(key: InputKey) -> String? { let args = arguments(for: key) - let possibilities = args.compactMap { - $0.nonOptional.synopsis + let possibilities: [String] = args.compactMap { + $0.help.visibility.base == .default + ? $0.nonOptional.synopsis + : nil } switch possibilities.count { case 0: - return "Missing expected argument" + 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: @@ -348,11 +359,46 @@ extension ErrorMessageGenerator { } } - func unableToParseValueMessage(origin: InputOrigin, name: Name?, value: String, key: InputKey, error: Error?) -> String { + func unableToParseHelpMessage(origin: InputOrigin, name: Name?, value: String, key: InputKey, error: Error?) -> String { + guard let abstract = help(for: key)?.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 missingValueForOptionHelpMessage(name: Name) -> String { + guard let arg = arguments.first(where: { $0.names.contains(name) }) else { + return "" + } + + let help = arg.help.abstract + return "\(name.synopsisString) <\(arg.valueName)> \(help)" + } + + func noValueHelpMessage(key: InputKey) -> String { + guard let abstract = help(for: key)?.abstract else { return "" } + guard let arg = arguments(for: key).first else { return "" } + + if let synopsisString = arg.names.first?.synopsisString { + return "\(synopsisString) <\(arg.valueName)> \(abstract)" + } + return "<\(arg.valueName)> \(abstract)" + } + + func unableToParseValueMessage(origin: InputOrigin, name: Name?, value: String, key: InputKey, error: Error?) -> String { + let argumentValue = arguments(for: key).first + let valueName = argumentValue?.valueName + // We want to make the "best effort" in producing a custom error message. - // We favour `LocalizedError.errorDescription` and fall back to + // We favor `LocalizedError.errorDescription` and fall back to // `CustomStringConvertible`. To opt in, return your custom error message // as the `description` property of `CustomStringConvertible`. let customErrorMessage: String = { @@ -362,10 +408,10 @@ extension ErrorMessageGenerator { case let err?: return ": " + String(describing: err) default: - return "" + return argumentValue?.formattedValueList ?? "" } }() - + switch (name, valueName) { case let (n?, v?): return "The value '\(value)' is invalid for '\(n.synopsisString) <\(v)>'\(customErrorMessage)" @@ -378,3 +424,25 @@ extension ErrorMessageGenerator { } } } + +private extension ArgumentDefinition { + var formattedValueList: String { + if help.allValues.isEmpty { + return "" + } + + if help.allValues.count < 6 { + let quotedValues = help.allValues.map { "'\($0)'" } + let validList: String + if quotedValues.count <= 2 { + validList = quotedValues.joined(separator: " and ") + } else { + validList = quotedValues.dropLast().joined(separator: ", ") + " or \(quotedValues.last!)" + } + return ". Please provide one of \(validList)." + } else { + let bulletValueList = help.allValues.map { " - \($0)" }.joined(separator: "\n") + return ". Please provide one of the following:\n\(bulletValueList)" + } + } +} diff --git a/Sources/ArgumentParser/Utilities/CollectionExtensions.swift b/Sources/ArgumentParser/Utilities/CollectionExtensions.swift new file mode 100644 index 000000000..1f86b27a5 --- /dev/null +++ b/Sources/ArgumentParser/Utilities/CollectionExtensions.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +extension Collection { + func mapEmpty(_ replacement: () -> Self) -> Self { + isEmpty ? replacement() : self + } +} diff --git a/Sources/ArgumentParser/Utilities/SequenceExtensions.swift b/Sources/ArgumentParser/Utilities/SequenceExtensions.swift index f36c1248d..0a0ab01dd 100644 --- a/Sources/ArgumentParser/Utilities/SequenceExtensions.swift +++ b/Sources/ArgumentParser/Utilities/SequenceExtensions.swift @@ -11,7 +11,7 @@ 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. + /// order of the first occurrence of each unique element. func uniquing() -> [Element] { var seen = Set() return self.filter { seen.insert($0).0 } diff --git a/Sources/ArgumentParser/Utilities/StringExtensions.swift b/Sources/ArgumentParser/Utilities/StringExtensions.swift index c402d3e5e..46413052c 100644 --- a/Sources/ArgumentParser/Utilities/StringExtensions.swift +++ b/Sources/ArgumentParser/Utilities/StringExtensions.swift @@ -9,9 +9,16 @@ // //===----------------------------------------------------------------------===// -extension String { +extension StringProtocol where SubSequence == Substring { func wrapped(to columns: Int, wrappingIndent: Int = 0) -> String { let columns = columns - wrappingIndent + guard columns > 0 else { + // Skip wrapping logic if the number of columns is less than 1 in release + // builds and assert in debug builds. + assertionFailure("`columns - wrappingIndent` should be always be greater than 0.") + return "" + } + var result: [Substring] = [] var currentIndex = startIndex @@ -89,7 +96,7 @@ extension String { /// "myURLProperty".convertedToSnakeCase(separator: "-") /// // my-url-property func convertedToSnakeCase(separator: Character = "_") -> String { - guard !isEmpty else { return self } + guard !isEmpty else { return "" } var result = "" // Whether we should append a separator when we see a uppercase character. var separateOnUppercase = true @@ -131,44 +138,105 @@ extension String { let columns = target.count if rows <= 0 || columns <= 0 { - return max(rows, columns) + return Swift.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 + // Trim common prefix and suffix + var selfStartTrim = self.startIndex + var targetStartTrim = target.startIndex + while selfStartTrim < self.endIndex && + targetStartTrim < target.endIndex && + self[selfStartTrim] == target[targetStartTrim] { + self.formIndex(after: &selfStartTrim) + target.formIndex(after: &targetStartTrim) } - for column in 1...columns { - matrix[0][column] = column + + var selfEndTrim = self.endIndex + var targetEndTrim = target.endIndex + + while selfEndTrim > selfStartTrim && + targetEndTrim > targetStartTrim { + let selfIdx = self.index(before: selfEndTrim) + let targetIdx = target.index(before: targetEndTrim) + + guard self[selfIdx] == target[targetIdx] else { + break + } + + selfEndTrim = selfIdx + targetEndTrim = targetIdx + } + + // Equal strings + guard !(selfStartTrim == self.endIndex && + targetStartTrim == target.endIndex) else { + return 0 } - 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 - ) + // After trimming common prefix and suffix, self is empty. + guard selfStartTrim < selfEndTrim else { + return target.distance(from: targetStartTrim, + to: targetEndTrim) + } + + // After trimming common prefix and suffix, target is empty. + guard targetStartTrim < targetEndTrim else { + return distance(from: selfStartTrim, + to: selfEndTrim) + } + + let newSelf = self[selfStartTrim.. 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") - } + let spacer = String(repeating: " ", count: n) + return lines.map { + $0.isEmpty ? $0 : spacer + $0 + }.joined(separator: "\n") + } + + func hangingIndentingEachLine(by n: Int) -> String { + let lines = self.split( + separator: "\n", + maxSplits: 1, + omittingEmptySubsequences: false) + guard lines.count == 2 else { return lines.joined(separator: "") } + return "\(lines[0])\n\(lines[1].indentingEachLine(by: n))" } } diff --git a/Sources/ArgumentParserTestHelpers/TestHelpers.swift b/Sources/ArgumentParserTestHelpers/TestHelpers.swift index 57a3bd01a..d0ccb0589 100644 --- a/Sources/ArgumentParserTestHelpers/TestHelpers.swift +++ b/Sources/ArgumentParserTestHelpers/TestHelpers.swift @@ -9,8 +9,9 @@ // //===----------------------------------------------------------------------===// +@testable import ArgumentParser +import ArgumentParserToolInfo import XCTest -import ArgumentParser // extensions to the ParsableArguments protocol to facilitate XCTestExpectation support public protocol TestableParsableArguments: ParsableArguments { @@ -112,32 +113,91 @@ public func AssertEqualStringsIgnoringTrailingWhitespace(_ string1: String, _ st } public func AssertHelp( - for _: T.Type, equals expected: String, - file: StaticString = #file, line: UInt = #line + _ visibility: ArgumentVisibility, + for _: T.Type, + equals expected: String, + file: StaticString = #file, + line: UInt = #line ) { + let flag: String + let includeHidden: Bool + + switch visibility.base { + case .default: + flag = "--help" + includeHidden = false + case .hidden: + flag = "--help-hidden" + includeHidden = true + case .private: + XCTFail("Should not be called.") + return + } + do { - _ = try T.parse(["-h"]) - XCTFail(file: (file), line: line) + _ = try T.parse([flag]) + XCTFail(file: file, line: line) } catch { let helpString = T.fullMessage(for: error) AssertEqualStringsIgnoringTrailingWhitespace( helpString, expected, file: file, line: line) } - - let helpString = T.helpMessage() + + let helpString = T.helpMessage(includeHidden: includeHidden, columns: nil) 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 + _ visibility: ArgumentVisibility, + for _: T.Type, + root _: U.Type, + equals expected: String, + file: StaticString = #file, + line: UInt = #line ) { - let helpString = U.helpMessage(for: T.self) + let includeHidden: Bool + + switch visibility.base { + case .default: + includeHidden = false + case .hidden: + includeHidden = true + case .private: + XCTFail("Should not be called.") + return + } + + let helpString = U.helpMessage( + for: T.self, includeHidden: includeHidden, columns: nil) AssertEqualStringsIgnoringTrailingWhitespace( helpString, expected, file: file, line: line) } +public func AssertDump( + for _: T.Type, equals expected: String, + file: StaticString = #file, line: UInt = #line +) throws { + do { + _ = try T.parse(["--experimental-dump-help"]) + XCTFail(file: (file), line: line) + } catch { + let dumpString = T.fullMessage(for: error) + try AssertJSONEqualFromString(actual: dumpString, expected: expected, for: ToolInfoV0.self) + } + + try AssertJSONEqualFromString(actual: T._dumpHelp(), expected: expected, for: ToolInfoV0.self) +} + +public func AssertJSONEqualFromString(actual: String, expected: String, for type: T.Type) throws { + let actualJSONData = try XCTUnwrap(actual.data(using: .utf8)) + let actualDumpJSON = try XCTUnwrap(JSONDecoder().decode(type, from: actualJSONData)) + + let expectedJSONData = try XCTUnwrap(expected.data(using: .utf8)) + let expectedDumpJSON = try XCTUnwrap(JSONDecoder().decode(type, from: expectedJSONData)) + XCTAssertEqual(actualDumpJSON, expectedDumpJSON) +} + extension XCTest { public var debugURL: URL { let bundleURL = Bundle(for: type(of: self)).bundleURL @@ -150,12 +210,24 @@ extension XCTest { command: String, expected: String? = nil, exitCode: ExitCode = .success, - file: StaticString = #file, line: UInt = #line) + file: StaticString = #file, line: UInt = #line) throws { - let splitCommand = command.split(separator: " ") - let arguments = splitCommand.dropFirst().map(String.init) - - let commandName = String(splitCommand.first!) + try AssertExecuteCommand( + command: command.split(separator: " ").map(String.init), + expected: expected, + exitCode: exitCode, + file: file, + line: line) + } + + public func AssertExecuteCommand( + command: [String], + expected: String? = nil, + exitCode: ExitCode = .success, + file: StaticString = #file, line: UInt = #line) throws + { + let arguments = Array(command.dropFirst()) + let commandName = String(command.first!) let commandURL = debugURL.appendingPathComponent(commandName) guard (try? commandURL.checkResourceIsReachable()) ?? false else { XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.", @@ -163,6 +235,7 @@ extension XCTest { return } + #if !canImport(Darwin) || os(macOS) let process = Process() if #available(macOS 10.13, *) { process.executableURL = commandURL @@ -188,7 +261,7 @@ extension XCTest { 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) @@ -197,5 +270,85 @@ extension XCTest { } XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: (file), line: line) + #else + throw XCTSkip("Not supported on this platform") + #endif + } + + public func AssertJSONOutputEqual( + command: String, + expected: String, + file: StaticString = #file, line: UInt = #line + ) throws { + + 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 + } + + #if !canImport(Darwin) || os(macOS) + 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 outputString = try XCTUnwrap(String(data: output.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)) + XCTAssertTrue(error.fileHandleForReading.readDataToEndOfFile().isEmpty, "Error occurred with `--experimental-dump-help`") + try AssertJSONEqualFromString(actual: outputString, expected: expected, for: ToolInfoV0.self) + #else + throw XCTSkip("Not supported on this platform") + #endif + } + + public func AssertGenerateManual( + singlePage: Bool, + command: String, + expected: String, + file: StaticString = #file, + line: UInt = #line + ) throws { + let commandURL = debugURL.appendingPathComponent(command) + var command = [ + "generate-manual", commandURL.path, + "--date", "1996-05-12", + "--section", "9", + "--authors", "Jane Appleseed", + "--authors", "", + "--authors", "The Appleseeds", + "--output-directory", "-", + ] + if singlePage { + command.append("--single-page") + } + try AssertExecuteCommand( + command: command, + expected: expected, + exitCode: .success, + file: file, + line: line) } } diff --git a/Sources/ArgumentParserToolInfo/CMakeLists.txt b/Sources/ArgumentParserToolInfo/CMakeLists.txt new file mode 100644 index 000000000..b82adb71d --- /dev/null +++ b/Sources/ArgumentParserToolInfo/CMakeLists.txt @@ -0,0 +1,10 @@ +add_library(ArgumentParserToolInfo STATIC + ToolInfo.swift) +# NOTE: workaround for CMake not setting up include flags yet +set_target_properties(ArgumentParserToolInfo PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) +target_compile_options(ArgumentParserToolInfo PRIVATE + $<$:-enable-testing>) + + +set_property(GLOBAL APPEND PROPERTY ArgumentParser_EXPORTS ArgumentParserToolInfo) diff --git a/Sources/ArgumentParserToolInfo/ToolInfo.swift b/Sources/ArgumentParserToolInfo/ToolInfo.swift new file mode 100644 index 000000000..086f8fcc4 --- /dev/null +++ b/Sources/ArgumentParserToolInfo/ToolInfo.swift @@ -0,0 +1,176 @@ +//===----------------------------------------------------------*- 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 extension Collection { + /// - returns: A non-empty collection or `nil`. + var nonEmpty: Self? { isEmpty ? nil : self } +} + +/// Header used to validate serialization version of an encoded ToolInfo struct. +public struct ToolInfoHeader: Decodable { + /// A sentinel value indicating the version of the ToolInfo struct used to + /// generate the serialized form. + public var serializationVersion: Int + + public init(serializationVersion: Int) { + self.serializationVersion = serializationVersion + } +} + +/// Top-level structure containing serialization version and information for all +/// commands in a tool. +public struct ToolInfoV0: Codable, Hashable { + /// A sentinel value indicating the version of the ToolInfo struct used to + /// generate the serialized form. + public var serializationVersion = 0 + /// Root command of the tool. + public var command: CommandInfoV0 + + public init(command: CommandInfoV0) { + self.command = command + } +} + +/// All information about a particular command, including arguments and +/// subcommands. +public struct CommandInfoV0: Codable, Hashable { + /// Super commands and tools. + public var superCommands: [String]? + + /// Name used to invoke the command. + public var commandName: String + /// Short description of the command's functionality. + public var abstract: String? + /// Extended description of the command's functionality. + public var discussion: String? + + /// Optional name of the subcommand invoked when the command is invoked with + /// no arguments. + public var defaultSubcommand: String? + /// List of nested commands. + public var subcommands: [CommandInfoV0]? + /// List of supported arguments. + public var arguments: [ArgumentInfoV0]? + + public init( + superCommands: [String], + commandName: String, + abstract: String, + discussion: String, + defaultSubcommand: String?, + subcommands: [CommandInfoV0], + arguments: [ArgumentInfoV0] + ) { + self.superCommands = superCommands.nonEmpty + + self.commandName = commandName + self.abstract = abstract.nonEmpty + self.discussion = discussion.nonEmpty + + self.defaultSubcommand = defaultSubcommand?.nonEmpty + self.subcommands = subcommands.nonEmpty + self.arguments = arguments.nonEmpty + } +} + +/// All information about a particular argument, including display names and +/// options. +public struct ArgumentInfoV0: Codable, Hashable { + /// Information about an argument's name. + public struct NameInfoV0: Codable, Hashable { + /// Kind of prefix of an argument's name. + public enum KindV0: String, Codable, Hashable { + /// A multi-character name preceded by two dashes. + case long + /// A single character name preceded by a single dash. + case short + /// A multi-character name preceded by a single dash. + case longWithSingleDash + } + + /// Kind of prefix the NameInfoV0 describes. + public var kind: KindV0 + /// Single or multi-character name of the argument. + public var name: String + + public init(kind: NameInfoV0.KindV0, name: String) { + self.kind = kind + self.name = name + } + } + + /// Kind of argument. + public enum KindV0: String, Codable, Hashable { + /// Argument specified as a bare value on the command line. + case positional + /// Argument specified as a value prefixed by a `--flag` on the command line. + case option + /// Argument specified only as a `--flag` on the command line. + case flag + } + + /// Kind of argument the ArgumentInfo describes. + public var kind: KindV0 + + /// Argument should appear in help displays. + public var shouldDisplay: Bool + /// Argument can be omitted. + public var isOptional: Bool + /// Argument can be specified multiple times. + public var isRepeating: Bool + + /// All names of the argument. + public var names: [NameInfoV0]? + /// The best name to use when referring to the argument in help displays. + public var preferredName: NameInfoV0? + + /// Name of argument's value. + public var valueName: String? + /// Default value of the argument is none is specified on the command line. + public var defaultValue: String? + /// List of all valid values. + public var allValues: [String]? + + /// Short description of the argument's functionality. + public var abstract: String? + /// Extended description of the argument's functionality. + public var discussion: String? + + public init( + kind: KindV0, + shouldDisplay: Bool, + isOptional: Bool, + isRepeating: Bool, + names: [NameInfoV0]?, + preferredName: NameInfoV0?, + valueName: String?, + defaultValue: String?, + allValues: [String]?, + abstract: String?, + discussion: String? + ) { + self.kind = kind + + self.shouldDisplay = shouldDisplay + self.isOptional = isOptional + self.isRepeating = isRepeating + + self.names = names?.nonEmpty + self.preferredName = preferredName + + self.valueName = valueName?.nonEmpty + self.defaultValue = defaultValue?.nonEmpty + self.allValues = allValues?.nonEmpty + + self.abstract = abstract?.nonEmpty + self.discussion = discussion?.nonEmpty + } +} diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index a98ace356..b1772e671 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(ArgumentParser) +add_subdirectory(ArgumentParserToolInfo) if(BUILD_TESTING) add_subdirectory(ArgumentParserTestHelpers) endif() diff --git a/Tests/ArgumentParserEndToEndTests/CMakeLists.txt b/Tests/ArgumentParserEndToEndTests/CMakeLists.txt index 2cd1f497c..d89093b13 100644 --- a/Tests/ArgumentParserEndToEndTests/CMakeLists.txt +++ b/Tests/ArgumentParserEndToEndTests/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(EndToEndTests DefaultsEndToEndTests.swift EnumEndToEndTests.swift FlagsEndToEndTests.swift + JoinedEndToEndTests.swift LongNameWithShortDashEndToEndTests.swift NestedCommandEndToEndTests.swift OptionalEndToEndTests.swift diff --git a/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift index a603d609c..2073afa8b 100644 --- a/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift @@ -69,3 +69,125 @@ extension DefaultSubcommandEndToEndTests { XCTAssertThrowsError(try Main.parseAsRoot(["qux"])) } } + +extension DefaultSubcommandEndToEndTests { + fileprivate struct MyCommand: ParsableCommand { + static var configuration = CommandConfiguration( + subcommands: [Plugin.self, NonDefault.self, Other.self], + defaultSubcommand: Plugin.self + ) + + @OptionGroup + var options: CommonOptions + } + + fileprivate struct CommonOptions: ParsableArguments { + @Flag(name: [.customLong("verbose"), .customShort("v")], + help: "Enable verbose aoutput.") + var verbose = false + } + + fileprivate struct Plugin: ParsableCommand { + @OptionGroup var options: CommonOptions + @Argument var pluginName: String + + @Argument(parsing: .unconditionalRemaining) + var pluginArguments: [String] = [] + } + + fileprivate struct NonDefault: ParsableCommand { + @OptionGroup var options: CommonOptions + @Argument var pluginName: String + + @Argument(parsing: .unconditionalRemaining) + var pluginArguments: [String] = [] + } + + fileprivate struct Other: ParsableCommand { + @OptionGroup var options: CommonOptions + } + + func testRemainingDefaultImplicit() throws { + AssertParseCommand(MyCommand.self, Plugin.self, ["my-plugin"]) { plugin in + XCTAssertEqual(plugin.pluginName, "my-plugin") + XCTAssertEqual(plugin.pluginArguments, []) + XCTAssertEqual(plugin.options.verbose, false) + } + AssertParseCommand(MyCommand.self, Plugin.self, ["my-plugin", "--verbose"]) { plugin in + XCTAssertEqual(plugin.pluginName, "my-plugin") + XCTAssertEqual(plugin.pluginArguments, ["--verbose"]) + XCTAssertEqual(plugin.options.verbose, false) + } + AssertParseCommand(MyCommand.self, Plugin.self, ["--verbose", "my-plugin", "--verbose"]) { plugin in + XCTAssertEqual(plugin.pluginName, "my-plugin") + XCTAssertEqual(plugin.pluginArguments, ["--verbose"]) + XCTAssertEqual(plugin.options.verbose, true) + } + AssertParseCommand(MyCommand.self, Plugin.self, ["my-plugin", "--help"]) { plugin in + XCTAssertEqual(plugin.pluginName, "my-plugin") + XCTAssertEqual(plugin.pluginArguments, ["--help"]) + XCTAssertEqual(plugin.options.verbose, false) + } + } + + func testRemainingDefaultExplicit() throws { + AssertParseCommand(MyCommand.self, Plugin.self, ["plugin", "my-plugin"]) { plugin in + XCTAssertEqual(plugin.pluginName, "my-plugin") + XCTAssertEqual(plugin.pluginArguments, []) + XCTAssertEqual(plugin.options.verbose, false) + } + AssertParseCommand(MyCommand.self, Plugin.self, ["plugin", "my-plugin", "--verbose"]) { plugin in + XCTAssertEqual(plugin.pluginName, "my-plugin") + XCTAssertEqual(plugin.pluginArguments, ["--verbose"]) + XCTAssertEqual(plugin.options.verbose, false) + } + AssertParseCommand(MyCommand.self, Plugin.self, ["--verbose", "plugin", "my-plugin", "--verbose"]) { plugin in + XCTAssertEqual(plugin.pluginName, "my-plugin") + XCTAssertEqual(plugin.pluginArguments, ["--verbose"]) + XCTAssertEqual(plugin.options.verbose, true) + } + AssertParseCommand(MyCommand.self, Plugin.self, ["--verbose", "plugin", "my-plugin", "--help"]) { plugin in + XCTAssertEqual(plugin.pluginName, "my-plugin") + XCTAssertEqual(plugin.pluginArguments, ["--help"]) + XCTAssertEqual(plugin.options.verbose, true) + } + } + + func testRemainingNonDefault() throws { + AssertParseCommand(MyCommand.self, NonDefault.self, ["non-default", "my-plugin"]) { nondef in + XCTAssertEqual(nondef.pluginName, "my-plugin") + XCTAssertEqual(nondef.pluginArguments, []) + XCTAssertEqual(nondef.options.verbose, false) + } + AssertParseCommand(MyCommand.self, NonDefault.self, ["non-default", "my-plugin", "--verbose"]) { nondef in + XCTAssertEqual(nondef.pluginName, "my-plugin") + XCTAssertEqual(nondef.pluginArguments, ["--verbose"]) + XCTAssertEqual(nondef.options.verbose, false) + } + AssertParseCommand(MyCommand.self, NonDefault.self, ["--verbose", "non-default", "my-plugin", "--verbose"]) { nondef in + XCTAssertEqual(nondef.pluginName, "my-plugin") + XCTAssertEqual(nondef.pluginArguments, ["--verbose"]) + XCTAssertEqual(nondef.options.verbose, true) + } + AssertParseCommand(MyCommand.self, NonDefault.self, ["--verbose", "non-default", "my-plugin", "--help"]) { nondef in + XCTAssertEqual(nondef.pluginName, "my-plugin") + XCTAssertEqual(nondef.pluginArguments, ["--help"]) + XCTAssertEqual(nondef.options.verbose, true) + } + } + + func testRemainingDefaultOther() throws { + AssertParseCommand(MyCommand.self, Other.self, ["other"]) { other in + XCTAssertEqual(other.options.verbose, false) + } + AssertParseCommand(MyCommand.self, Other.self, ["other", "--verbose"]) { other in + XCTAssertEqual(other.options.verbose, true) + } + } + + func testRemainingDefaultFailure() { + XCTAssertThrowsError(try MyCommand.parseAsRoot([])) + XCTAssertThrowsError(try MyCommand.parseAsRoot(["--verbose"])) + XCTAssertThrowsError(try MyCommand.parseAsRoot(["plugin", "--verbose", "my-plugin"])) + } +} diff --git a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift index dd850252f..f9b9fd7d5 100644 --- a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift @@ -777,3 +777,19 @@ extension DefaultsEndToEndTests { } } } + +@available(*, deprecated) +fileprivate struct OptionPropertyDeprecatedInit_NoDefault: ParsableArguments { + @Option(completion: .file(), help: "") + var data: String = "test" +} + +extension DefaultsEndToEndTests { + /// Tests that instances created using deprecated initializer with completion and help arguments swapped are constructed and parsed correctly. + @available(*, deprecated) + func testParsing_OptionPropertyDeprecatedInit_NoDefault() { + AssertParse(OptionPropertyDeprecatedInit_NoDefault.self, []) { arguments in + XCTAssertEqual(arguments.data, "test") + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift new file mode 100644 index 000000000..482d3fc47 --- /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, ["debug1", "debug2", "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 index 940d20549..8dedfd5df 100644 --- a/Tests/ArgumentParserEndToEndTests/LongNameWithShortDashEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/LongNameWithShortDashEndToEndTests.swift @@ -107,3 +107,28 @@ extension LongNameWithSingleDashEndToEndTests { XCTAssertThrowsError(try Bar.parse(["--file"])) } } + +extension LongNameWithSingleDashEndToEndTests { + private struct Issue327: ParsableCommand { + @Option(name: .customLong("argWithAnH", withSingleDash: true), + parsing: .upToNextOption) + var args: [String] + } + + func testIssue327() { + AssertParse(Issue327.self, ["-argWithAnH", "03ade86c0", "8f2058e3ade86c84ec5b"]) { issue327 in + XCTAssertEqual(issue327.args, ["03ade86c0", "8f2058e3ade86c84ec5b"]) + } + } + + private struct JoinedItem: ParsableCommand { + @Option(name: .customLong("argWithAnH", withSingleDash: true)) + var arg: String + } + + func testJoinedItem_Issue327() { + AssertParse(JoinedItem.self, ["-argWithAnH=foo"]) { joinedItem in + XCTAssertEqual(joinedItem.arg, "foo") + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift index 38d85de01..a25847a8b 100644 --- a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift @@ -175,18 +175,16 @@ extension RepeatingEndToEndTests { 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 + AssertParse(Qux.self, ["--names", "one", "two", "--verbose", "--names", "three", "--names", "four"]) { qux in XCTAssertTrue(qux.verbose) - XCTAssertTrue(qux.names.isEmpty) + XCTAssertEqual(qux.names, ["one", "two", "three", "four"]) XCTAssertNil(qux.extra) } - AssertParse(Qux.self, ["--names", "--verbose", "three"]) { qux in + AssertParse(Qux.self, ["extra", "--names", "one", "--names", "two", "--verbose", "--names", "three", "four"]) { qux in XCTAssertTrue(qux.verbose) - XCTAssertTrue(qux.names.isEmpty) - XCTAssertEqual(qux.extra, "three") + XCTAssertEqual(qux.names, ["one", "two", "three", "four"]) + XCTAssertEqual(qux.extra, "extra") } AssertParse(Qux.self, ["--names", "one", "two"]) { qux in @@ -217,8 +215,9 @@ extension RepeatingEndToEndTests { 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"])) + XCTAssertThrowsError(try Qux.parse(["--names", "--verbose"])) + XCTAssertThrowsError(try Qux.parse(["--names", "--verbose", "three"])) } } @@ -330,6 +329,7 @@ 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 + @Option var config = "debug" @Argument(parsing: .unconditionalRemaining) var names: [String] = [] } @@ -351,8 +351,8 @@ extension RepeatingEndToEndTests { } AssertParse(Foozle.self, ["one", "two", "three", "--other", "--verbose"]) { foozle in - XCTAssertTrue(foozle.verbose) - XCTAssertEqual(foozle.names, ["one", "two", "three", "--other"]) + XCTAssertFalse(foozle.verbose) + XCTAssertEqual(foozle.names, ["one", "two", "three", "--other", "--verbose"]) } AssertParse(Foozle.self, ["--verbose", "--other", "one", "two", "three"]) { foozle in @@ -382,12 +382,29 @@ extension RepeatingEndToEndTests { XCTAssertEqual(foozle.names, ["-one", "-two", "three"]) } - AssertParse(Foozle.self, ["-one", "-two", "three", "-if"]) { foozle in + AssertParse(Foozle.self, ["--config", "release", "one", "two", "--config", "debug"]) { foozle in + XCTAssertEqual(foozle.config, "release") + XCTAssertEqual(foozle.names, ["one", "two", "--config", "debug"]) + } + + AssertParse(Foozle.self, ["--config", "release", "--config", "debug", "one", "two"]) { foozle in + XCTAssertEqual(foozle.config, "debug") + XCTAssertEqual(foozle.names, ["one", "two"]) + } + + AssertParse(Foozle.self, ["-if", "-one", "-two", "three"]) { foozle in XCTAssertFalse(foozle.verbose) XCTAssertTrue(foozle.useFiles) XCTAssertTrue(foozle.useStandardInput) XCTAssertEqual(foozle.names, ["-one", "-two", "three"]) } + + AssertParse(Foozle.self, ["-one", "-two", "-three", "-if"]) { foozle in + XCTAssertFalse(foozle.verbose) + XCTAssertFalse(foozle.useFiles) + XCTAssertFalse(foozle.useStandardInput) + XCTAssertEqual(foozle.names, ["-one", "-two", "-three", "-if"]) + } } func testParsing_repeatingUnconditionalArgument_Fails() throws { diff --git a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift index c315e3a03..9280f8878 100644 --- a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift @@ -20,35 +20,25 @@ final class SourceCompatEndToEndTests: XCTestCase {} // MARK: - Property Wrapper Initializers fileprivate struct AlmostAllArguments: ParsableArguments { - @Argument(default: 0, help: "") var a: Int @Argument(help: "") var a_newDefaultSyntax: Int = 0 @Argument() var a0: Int @Argument(help: "") var a1: Int - @Argument(default: 0) var a2: Int @Argument var a2_newDefaultSyntax: Int = 0 - @Argument(default: 0, help: "", transform: { _ in 0 }) var b: Int @Argument(help: "", transform: { _ in 0 }) var b_newDefaultSyntax: Int = 0 - @Argument(default: 0) var b1: Int @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(default: 0, transform: { _ in 0 }) var b5: Int @Argument(transform: { _ in 0 }) var b5_newDefaultSyntax: Int = 0 - @Argument(default: 0, help: "") var b6: Int @Argument(help: "") var b6_newDefaultSyntax: Int = 0 - @Argument(default: 0, help: "") var c: Int? @Argument() var c0: Int? @Argument(help: "") var c1: Int? - @Argument(default: 0) var c2: Int? - @Argument(default: 0, help: "", transform: { _ in 0 }) var d: Int? @Argument(help: "") var d2: Int? @Argument(transform: { _ in 0 }) var d3: Int? @Argument(help: "", transform: { _ in 0 }) var d4: Int? - @Argument(default: 0, transform: { _ in 0 }) var d5: Int? @Argument(parsing: .remaining, help: "") var e: [Int] = [1, 2] @Argument(parsing: .remaining, help: "") var e1: [Int] @@ -69,77 +59,49 @@ fileprivate struct AlmostAllArguments: ParsableArguments { } fileprivate struct AllOptions: ParsableArguments { - @Option(name: .long, default: 0, parsing: .next, help: "") var a: Int @Option(name: .long, parsing: .next, help: "") var a_newDefaultSyntax: Int = 0 - @Option(default: 0, parsing: .next, help: "") var a1: Int @Option(parsing: .next, help: "") var a1_newDefaultSyntax: Int = 0 @Option(name: .long, parsing: .next, help: "") var a2: Int - @Option(name: .long, default: 0, help: "") var a3: Int @Option(name: .long, help: "") var a3_newDefaultSyntax: Int = 0 @Option(parsing: .next, help: "") var a4: Int - @Option(default: 0, help: "") var a5: Int @Option(help: "") var a5_newDefaultSyntax: Int = 0 - @Option(default: 0, parsing: .next) var a6: Int @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, default: 0) var a9: Int @Option(name: .long) var a9_newDefaultSyntax: Int = 0 @Option(name: .long) var a10: Int - @Option(default: 0) var a11: Int @Option var a11_newDefaultSyntax: Int = 0 @Option(parsing: .next) var a12: Int @Option(help: "") var a13: Int - @Option(name: .long, default: 0, parsing: .next, help: "") var b: Int? - @Option(default: 0, parsing: .next, help: "") var b1: Int? @Option(name: .long, parsing: .next, help: "") var b2: Int? - @Option(name: .long, default: 0, help: "") var b3: Int? @Option(parsing: .next, help: "") var b4: Int? - @Option(default: 0, help: "") var b5: Int? - @Option(default: 0, parsing: .next) var b6: Int? @Option(name: .long, help: "") var b7: Int? @Option(name: .long, parsing: .next) var b8: Int? - @Option(name: .long, default: 0) var b9: Int? @Option(name: .long) var b10: Int? - @Option(default: 0) var b11: Int? @Option(parsing: .next) var b12: Int? @Option(help: "") var b13: Int? - @Option(name: .long, default: 0, parsing: .next, help: "", transform: { _ in 0 }) var c: Int @Option(name: .long, parsing: .next, help: "", transform: { _ in 0 }) var c_newDefaultSyntax: Int = 0 - @Option(default: 0, parsing: .next, help: "", transform: { _ in 0 }) var c1: Int @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, default: 0, help: "", transform: { _ in 0 }) var c3: Int @Option(name: .long, help: "", transform: { _ in 0 }) var c3_newDefaultSyntax: Int = 0 @Option(parsing: .next, help: "", transform: { _ in 0 }) var c4: Int - @Option(default: 0, help: "", transform: { _ in 0 }) var c5: Int @Option(help: "", transform: { _ in 0 }) var c5_newDefaultSyntax: Int = 0 - @Option(default: 0, parsing: .next, transform: { _ in 0 }) var c6: Int @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, default: 0, transform: { _ in 0 }) var c9: Int @Option(name: .long, transform: { _ in 0 }) var c9_newDefaultSyntax: Int = 0 @Option(name: .long, transform: { _ in 0 }) var c10: Int - @Option(default: 0, transform: { _ in 0 }) var c11: 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, default: 0, parsing: .next, help: "", transform: { _ in 0 }) var d: Int? - @Option(default: 0, parsing: .next, help: "", transform: { _ in 0 }) var d1: Int? @Option(name: .long, parsing: .next, help: "", transform: { _ in 0 }) var d2: Int? - @Option(name: .long, default: 0, help: "", transform: { _ in 0 }) var d3: Int? @Option(parsing: .next, help: "", transform: { _ in 0 }) var d4: Int? - @Option(default: 0, help: "", transform: { _ in 0 }) var d5: Int? - @Option(default: 0, parsing: .next, transform: { _ in 0 }) var d6: 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, default: 0, transform: { _ in 0 }) var d9: Int? @Option(name: .long, transform: { _ in 0 }) var d10: Int? - @Option(default: 0, transform: { _ in 0 }) var d11: Int? @Option(parsing: .next, transform: { _ in 0 }) var d12: Int? @Option(help: "", transform: { _ in 0 }) var d13: Int? @@ -179,13 +141,9 @@ struct AllFlags: ParsableArguments { case one, two, three } - @Flag(name: .long, help: "") var a: Bool @Flag(name: .long, help: "") var a_explicitFalse: Bool = false - @Flag() var a0: Bool @Flag() var a0_explicitFalse: Bool = false - @Flag(name: .long) var a1: Bool @Flag(name: .long) var a1_explicitFalse: Bool = false - @Flag(help: "") var a2: Bool @Flag(help: "") var a2_explicitFalse: Bool = false @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var b: Bool @@ -197,38 +155,22 @@ struct AllFlags: ParsableArguments { @Flag(name: .long, inversion: .prefixedNo) var b6: Bool @Flag(inversion: .prefixedNo) var b7: Bool - @Flag(name: .long, default: false, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var c: Bool @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var c_newDefaultSyntax: Bool = false - @Flag(default: false, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var c1: Bool @Flag(inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var c1_newDefaultSyntax: Bool = false - @Flag(name: .long, default: false, inversion: .prefixedNo, help: "") var c2: Bool @Flag(name: .long, inversion: .prefixedNo, help: "") var c2_newDefaultSyntax: Bool = false - @Flag(name: .long, default: false, inversion: .prefixedNo, exclusivity: .chooseLast) var c3: Bool @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast) var c3_newDefaultSyntax: Bool = false - @Flag(default: false, inversion: .prefixedNo, help: "") var c4: Bool @Flag(inversion: .prefixedNo, help: "") var c4_newDefaultSyntax: Bool = false - @Flag(default: false, inversion: .prefixedNo, exclusivity: .chooseLast) var c5: Bool @Flag(inversion: .prefixedNo, exclusivity: .chooseLast) var c5_newDefaultSyntax: Bool = false - @Flag(name: .long, default: false, inversion: .prefixedNo) var c6: Bool @Flag(name: .long, inversion: .prefixedNo) var c6_newDefaultSyntax: Bool = false - @Flag(default: false, inversion: .prefixedNo) var c7: Bool @Flag(inversion: .prefixedNo) var c7_newDefaultSyntax: Bool = false - @Flag(name: .long, default: nil, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var d: Bool @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var d_implicitNil: Bool - @Flag(default: nil, inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var d1: Bool @Flag(inversion: .prefixedNo, exclusivity: .chooseLast, help: "") var d1_implicitNil: Bool - @Flag(name: .long, default: nil, inversion: .prefixedNo, help: "") var d2: Bool @Flag(name: .long, inversion: .prefixedNo, help: "") var d2_implicitNil: Bool - @Flag(name: .long, default: nil, inversion: .prefixedNo, exclusivity: .chooseLast) var d3: Bool @Flag(name: .long, inversion: .prefixedNo, exclusivity: .chooseLast) var d3_implicitNil: Bool - @Flag(default: nil, inversion: .prefixedNo, help: "") var d4: Bool @Flag(inversion: .prefixedNo, help: "") var d4_implicitNil: Bool - @Flag(default: nil, inversion: .prefixedNo, exclusivity: .chooseLast) var d5: Bool @Flag(inversion: .prefixedNo, exclusivity: .chooseLast) var d5_implicitNil: Bool - @Flag(name: .long, default: nil, inversion: .prefixedNo) var d6: Bool @Flag(name: .long, inversion: .prefixedNo) var d6_implicitNil: Bool - @Flag(default: nil, inversion: .prefixedNo) var d7: Bool @Flag(inversion: .prefixedNo) var d7_implicitNil: Bool @Flag(name: .long, help: "") var e: Int @@ -236,17 +178,13 @@ struct AllFlags: ParsableArguments { @Flag(name: .long) var e1: Int @Flag(help: "") var e2: Int - @Flag(default: .one, exclusivity: .chooseLast, help: "") var f: E @Flag(exclusivity: .chooseLast, help: "") var f_newDefaultSyntax: E = .one @Flag() var f1: E @Flag(exclusivity: .chooseLast, help: "") var f2: E - @Flag(default: .one, help: "") var f3: E @Flag(help: "") var f3_newDefaultSyntax: E = .one - @Flag(default: .one, exclusivity: .chooseLast) var f4: E @Flag(exclusivity: .chooseLast) var f4_newDefaultSyntax: E = .one @Flag(help: "") var f5: E @Flag(exclusivity: .chooseLast) var f6: E - @Flag(default: .one) var f7: E @Flag var f7_newDefaultSyntax: E = .one @Flag(exclusivity: .chooseLast, help: "") var g: E? diff --git a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift index f6edbd677..957eae5fe 100644 --- a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift @@ -40,6 +40,7 @@ fileprivate struct FooOption: Convert, ParsableArguments { 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) }) @@ -52,6 +53,7 @@ fileprivate struct BarOption: Convert, ParsableCommand { 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) }) @@ -69,11 +71,11 @@ extension TransformEndToEndTests { } 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.usageString) + 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.usageString) + AssertFullErrorMessage(FooOption.self, ["--string", "4827"], "Error: The value '4827' is invalid for '--string ': outOfBounds\n" + FooOption.help + FooOption.usageString) } // MARK: Arrays @@ -85,11 +87,11 @@ extension TransformEndToEndTests { } 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.usageString) + 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.usageString) + AssertFullErrorMessage(BarOption.self, ["--strings", "4827", "--strings", "72", "--strings", "99"], "Error: The value '4827' is invalid for '--strings ': outOfBounds\n" + BarOption.help + BarOption.usageString) } } @@ -101,6 +103,7 @@ fileprivate struct FooArgument: Convert, ParsableArguments { 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 @@ -117,6 +120,7 @@ fileprivate struct BarArgument: Convert, ParsableCommand { 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) }) @@ -134,11 +138,11 @@ extension TransformEndToEndTests { } 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.usageString) + 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.usageString) + AssertFullErrorMessage(FooArgument.self, ["4827"], "Error: The value '4827' is invalid for '': outOfBounds\n" + FooArgument.help + FooArgument.usageString) } // MARK: Arrays @@ -150,10 +154,10 @@ extension TransformEndToEndTests { } 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.usageString) + 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.usageString) + 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 000000000..ccf2e6c56 --- /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 index 79e2cda2f..b4222bc3c 100644 --- a/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift @@ -135,7 +135,7 @@ extension ValidationEndToEndTests { } func testCustomErrorValidation() { - // verify that error description is printed if avaiable via LocalizedError + // verify that error description is printed if available via LocalizedError AssertErrorMessage(Foo.self, ["--throw", "Joe"], UserValidationError.userValidationError.errorDescription!) } diff --git a/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift b/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift new file mode 100644 index 000000000..56b54d5c1 --- /dev/null +++ b/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if os(macOS) && swift(>=5.6) + +import XCTest +import ArgumentParserTestHelpers + +final class CountLinesExampleTests: XCTestCase { + func testCountLines() throws { + guard #available(macOS 12, *) else { return } + let testFile = try XCTUnwrap(Bundle.module.url(forResource: "CountLinesTest", withExtension: "txt")) + try AssertExecuteCommand(command: "count-lines \(testFile.path)", expected: "20") + try AssertExecuteCommand(command: "count-lines \(testFile.path) --prefix al", expected: "4") + } + + func testCountLinesHelp() throws { + guard #available(macOS 12, *) else { return } + let helpText = """ + USAGE: count-lines [--prefix ] [--verbose] + + ARGUMENTS: + A file to count lines in. If omitted, counts the + lines of stdin. + + OPTIONS: + --prefix Only count lines with this prefix. + --verbose Include extra information in the output. + -h, --help Show help information. + """ + try AssertExecuteCommand(command: "count-lines -h", expected: helpText) + } +} + +#endif diff --git a/Tests/ArgumentParserExampleTests/CountLinesTest.txt b/Tests/ArgumentParserExampleTests/CountLinesTest.txt new file mode 100644 index 000000000..7733ce90b --- /dev/null +++ b/Tests/ArgumentParserExampleTests/CountLinesTest.txt @@ -0,0 +1,20 @@ +absinthe +actuary +adenoid +affidavit +affluent +agnostic +agoraphobia +albatross +algebra +alkaline +allocution +ampule +antimony +argument +artesian +askance +asterisk +astonishment +asynchronous +axiomatic diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift index 31380b871..d687e2906 100644 --- a/Tests/ArgumentParserExampleTests/MathExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -15,8 +15,8 @@ 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") + try AssertExecuteCommand(command: "math 1 2 3 4 5", expected: "15") + try AssertExecuteCommand(command: "math multiply 1 2 3 4 5", expected: "120") } func testMath_Help() throws { @@ -37,9 +37,9 @@ final class MathExampleTests: XCTestCase { See 'math help ' for detailed help. """ - AssertExecuteCommand(command: "math -h", expected: helpText) - AssertExecuteCommand(command: "math --help", expected: helpText) - AssertExecuteCommand(command: "math help", expected: helpText) + try AssertExecuteCommand(command: "math -h", expected: helpText) + try AssertExecuteCommand(command: "math --help", expected: helpText) + try AssertExecuteCommand(command: "math help", expected: helpText) } func testMath_AddHelp() throws { @@ -57,9 +57,14 @@ final class MathExampleTests: XCTestCase { -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) + try AssertExecuteCommand(command: "math add -h", expected: helpText) + try AssertExecuteCommand(command: "math add --help", expected: helpText) + try AssertExecuteCommand(command: "math help add", expected: helpText) + + // Verify that extra help flags are ignored. + try AssertExecuteCommand(command: "math help add -h", expected: helpText) + try AssertExecuteCommand(command: "math help add -help", expected: helpText) + try AssertExecuteCommand(command: "math help add --help", expected: helpText) } func testMath_StatsMeanHelp() throws { @@ -77,36 +82,42 @@ final class MathExampleTests: XCTestCase { -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) + try AssertExecuteCommand(command: "math stats average -h", expected: helpText) + try AssertExecuteCommand(command: "math stats average --help", expected: helpText) + try 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 [ ...] + 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) + try 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) + try AssertExecuteCommand(command: "math stats quantiles -h", expected: helpText) + try AssertExecuteCommand(command: "math stats quantiles --help", expected: helpText) + try AssertExecuteCommand(command: "math help stats quantiles", expected: helpText) } func testMath_CustomValidation() throws { - AssertExecuteCommand( + try AssertExecuteCommand( command: "math stats average --kind mode", expected: """ Error: Please provide at least one value to calculate the mode. @@ -117,38 +128,38 @@ final class MathExampleTests: XCTestCase { } func testMath_Versions() throws { - AssertExecuteCommand( + try AssertExecuteCommand( command: "math --version", expected: "1.0.0") - AssertExecuteCommand( + try AssertExecuteCommand( command: "math stats --version", expected: "1.0.0") - AssertExecuteCommand( + try AssertExecuteCommand( command: "math stats average --version", expected: "1.5.0-alpha") } func testMath_ExitCodes() throws { - AssertExecuteCommand( + try AssertExecuteCommand( command: "math stats quantiles --test-success-exit-code", expected: "", exitCode: .success) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math stats quantiles --test-failure-exit-code", expected: "", exitCode: .failure) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math stats quantiles --test-validation-exit-code", expected: "", exitCode: .validationFailure) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math stats quantiles --test-custom-exit-code 42", expected: "", exitCode: ExitCode(42)) } func testMath_Fail() throws { - AssertExecuteCommand( + try AssertExecuteCommand( command: "math --foo", expected: """ Error: Unknown option '--foo' @@ -157,10 +168,11 @@ final class MathExampleTests: XCTestCase { """, exitCode: .validationFailure) - AssertExecuteCommand( + try 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. """, @@ -171,29 +183,29 @@ final class MathExampleTests: XCTestCase { // MARK: - Completion Script extension MathExampleTests { - func testMath_CompletionScript() { - AssertExecuteCommand( + func testMath_CompletionScript() throws { + try AssertExecuteCommand( command: "math --generate-completion-script=bash", expected: bashCompletionScriptText) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math --generate-completion-script bash", expected: bashCompletionScriptText) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math --generate-completion-script=zsh", expected: zshCompletionScriptText) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math --generate-completion-script zsh", expected: zshCompletionScriptText) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math --generate-completion-script=fish", expected: fishCompletionScriptText) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math --generate-completion-script fish", expected: fishCompletionScriptText) } - func testMath_CustomCompletion() { - AssertExecuteCommand( + func testMath_CustomCompletion() throws { + try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom", expected: """ hello @@ -201,7 +213,7 @@ extension MathExampleTests { heliotrope """) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom h", expected: """ hello @@ -209,7 +221,7 @@ extension MathExampleTests { heliotrope """) - AssertExecuteCommand( + try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom a", expected: """ aardvark @@ -225,7 +237,7 @@ _math() { cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() - opts="add multiply stats help -h --help" + opts="--version -h --help add multiply stats help" if [[ $COMP_CWORD == "1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return @@ -251,7 +263,7 @@ _math() { COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) } _math_add() { - opts="--hex-output -x -h --help" + opts="--hex-output -x --version -h --help" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return @@ -259,7 +271,7 @@ _math_add() { COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) } _math_multiply() { - opts="--hex-output -x -h --help" + opts="--hex-output -x --version -h --help" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return @@ -267,7 +279,7 @@ _math_multiply() { COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) } _math_stats() { - opts="average stdev quantiles -h --help" + opts="--version -h --help average stdev quantiles" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return @@ -289,7 +301,7 @@ _math_stats() { COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) } _math_stats_average() { - opts="--kind -h --help" + opts="--kind --version -h --help" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return @@ -303,7 +315,7 @@ _math_stats_average() { COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) } _math_stats_stdev() { - opts="-h --help" + opts="--version -h --help" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return @@ -311,18 +323,14 @@ _math_stats_stdev() { COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) } _math_stats_quantiles() { - opts="--test-success-exit-code --test-failure-exit-code --test-validation-exit-code --test-custom-exit-code --file --directory --shell --custom -h --help" + opts="--file --directory --shell --custom --version -h --help" opts="$opts alphabet alligator branch braggart" - opts="$opts $(math ---completion stats quantiles -- customArg "$COMP_WORDS")" + opts="$opts $("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return fi case $prev in - --test-custom-exit-code) - - return - ;; --file) COMPREPLY=( $(compgen -f -- "$cur") ) return @@ -336,14 +344,14 @@ _math_stats_quantiles() { return ;; --custom) - COMPREPLY=( $(compgen -W "$(math ---completion stats quantiles -- --custom "$COMP_WORDS")" -- "$cur") ) + COMPREPLY=( $(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "$cur") ) return ;; esac COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) } _math_help() { - opts="-h --help" + opts="--version" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) return @@ -365,7 +373,8 @@ _math() { integer ret=1 local -a args args+=( - '(-h --help)'{-h,--help}'[Print help information.]' + '--version[Show the version.]' + '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' ) @@ -408,7 +417,8 @@ _math_add() { args+=( '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' ':values:' - '(-h --help)'{-h,--help}'[Print help information.]' + '--version[Show the version.]' + '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S $args[@] && ret=0 @@ -421,7 +431,8 @@ _math_multiply() { args+=( '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' ':values:' - '(-h --help)'{-h,--help}'[Print help information.]' + '--version[Show the version.]' + '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S $args[@] && ret=0 @@ -432,7 +443,8 @@ _math_stats() { integer ret=1 local -a args args+=( - '(-h --help)'{-h,--help}'[Print help information.]' + '--version[Show the version.]' + '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' ) @@ -471,7 +483,8 @@ _math_stats_average() { args+=( '--kind[The kind of average to provide.]:kind:(mean median mode)' ':values:' - '(-h --help)'{-h,--help}'[Print help information.]' + '--version[Show the version.]' + '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S $args[@] && ret=0 @@ -483,7 +496,8 @@ _math_stats_stdev() { local -a args args+=( ':values:' - '(-h --help)'{-h,--help}'[Print help information.]' + '--version[Show the version.]' + '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S $args[@] && ret=0 @@ -497,15 +511,12 @@ _math_stats_quantiles() { ':one-of-four:(alphabet alligator branch braggart)' ':custom-arg:{_custom_completion $_math_commandname ---completion stats quantiles -- customArg $words}' ':values:' - '--test-success-exit-code' - '--test-failure-exit-code' - '--test-validation-exit-code' - '--test-custom-exit-code:test-custom-exit-code:' '--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.]' + '--version[Show the version.]' + '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S $args[@] && ret=0 @@ -517,7 +528,7 @@ _math_help() { local -a args args+=( ':subcommands:' - '(-h --help)'{-h,--help}'[Print help information.]' + '--version[Show the version.]' ) _arguments -w -s -S $args[@] && ret=0 @@ -534,8 +545,8 @@ _math """ private let fishCompletionScriptText = """ -function __fish_math_using_command - set cmd (commandline -opc) +function _swift_math_using_command + set -l cmd (commandline -opc) if [ (count $cmd) -eq (count $argv) ] for i in (seq (count $argv)) if [ $cmd[$i] != $argv[$i] ] @@ -546,28 +557,33 @@ function __fish_math_using_command 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 -l test-success-exit-code -complete -c math -n '__fish_math_using_command math stats quantiles' -f -l test-failure-exit-code -complete -c math -n '__fish_math_using_command math stats quantiles' -f -l test-validation-exit-code -complete -c math -n '__fish_math_using_command math stats quantiles' -f -r -l test-custom-exit-code -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])' +complete -c math -n '_swift_math_using_command math' -f -l version -d 'Show the version.' +complete -c math -n '_swift_math_using_command math' -f -s h -l help -d 'Show help information.' +complete -c math -n '_swift_math_using_command math' -f -a 'add' -d 'Print the sum of the values.' +complete -c math -n '_swift_math_using_command math' -f -a 'multiply' -d 'Print the product of the values.' +complete -c math -n '_swift_math_using_command math' -f -a 'stats' -d 'Calculate descriptive statistics.' +complete -c math -n '_swift_math_using_command math' -f -a 'help' -d 'Show subcommand help information.' +complete -c math -n '_swift_math_using_command math add' -f -l hex-output -s x -d 'Use hexadecimal notation for the result.' +complete -c math -n '_swift_math_using_command math add' -f -s h -l help -d 'Show help information.' +complete -c math -n '_swift_math_using_command math multiply' -f -l hex-output -s x -d 'Use hexadecimal notation for the result.' +complete -c math -n '_swift_math_using_command math multiply' -f -s h -l help -d 'Show help information.' +complete -c math -n '_swift_math_using_command math stats' -f -s h -l help -d 'Show help information.' +complete -c math -n '_swift_math_using_command math stats' -f -a 'average' -d 'Print the average of the values.' +complete -c math -n '_swift_math_using_command math stats' -f -a 'stdev' -d 'Print the standard deviation of the values.' +complete -c math -n '_swift_math_using_command math stats' -f -a 'quantiles' -d 'Print the quantiles of the values (TBD).' +complete -c math -n '_swift_math_using_command math stats' -f -a 'help' -d 'Show subcommand help information.' +complete -c math -n '_swift_math_using_command math stats average' -f -r -l kind -d 'The kind of average to provide.' +complete -c math -n '_swift_math_using_command math stats average --kind' -f -k -a 'mean median mode' +complete -c math -n '_swift_math_using_command math stats average' -f -l version -d 'Show the version.' +complete -c math -n '_swift_math_using_command math stats average' -f -s h -l help -d 'Show help information.' +complete -c math -n '_swift_math_using_command math stats stdev' -f -s h -l help -d 'Show help information.' +complete -c math -n '_swift_math_using_command math stats quantiles' -f -r -l file +complete -c math -n '_swift_math_using_command math stats quantiles --file' -f -a '(for i in *.{txt,md}; echo $i;end)' +complete -c math -n '_swift_math_using_command math stats quantiles' -f -r -l directory +complete -c math -n '_swift_math_using_command math stats quantiles --directory' -f -a '(__fish_complete_directories)' +complete -c math -n '_swift_math_using_command math stats quantiles' -f -r -l shell +complete -c math -n '_swift_math_using_command math stats quantiles --shell' -f -a '(head -100 /usr/share/dict/words | tail -50)' +complete -c math -n '_swift_math_using_command math stats quantiles' -f -r -l custom +complete -c math -n '_swift_math_using_command math stats quantiles --custom' -f -a '(command math ---completion stats quantiles -- --custom (commandline -opc)[1..-1])' +complete -c math -n '_swift_math_using_command math stats quantiles' -f -s h -l help -d 'Show help information.' """ diff --git a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift index 4e6b3d46a..9f0e24566 100644 --- a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift @@ -10,12 +10,25 @@ //===----------------------------------------------------------------------===// import XCTest -import ArgumentParser import ArgumentParserTestHelpers final class RepeatExampleTests: XCTestCase { func testRepeat() throws { - AssertExecuteCommand(command: "repeat hello --count 6", expected: """ + try AssertExecuteCommand(command: "repeat hello", expected: """ + hello + hello + """) + } + + func testRepeat_include_counter() throws { + try AssertExecuteCommand(command: "repeat --include-counter hello", expected: """ + 1: hello + 2: hello + """) + } + + func testRepeat_Count() throws { + try AssertExecuteCommand(command: "repeat hello --count 6", expected: """ hello hello hello @@ -38,12 +51,12 @@ final class RepeatExampleTests: XCTestCase { -h, --help Show help information. """ - AssertExecuteCommand(command: "repeat -h", expected: helpText) - AssertExecuteCommand(command: "repeat --help", expected: helpText) + try AssertExecuteCommand(command: "repeat -h", expected: helpText) + try AssertExecuteCommand(command: "repeat --help", expected: helpText) } func testRepeat_Fail() throws { - AssertExecuteCommand( + try AssertExecuteCommand( command: "repeat", expected: """ Error: Missing expected argument '' @@ -60,25 +73,27 @@ final class RepeatExampleTests: XCTestCase { """, exitCode: .validationFailure) - AssertExecuteCommand( + try AssertExecuteCommand( command: "repeat hello --count", expected: """ Error: Missing value 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( + try 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( + try AssertExecuteCommand( command: "repeat --version hello", expected: """ Error: Unknown option '--version' diff --git a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift index fb3fc138d..32b3629d5 100644 --- a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift @@ -10,12 +10,11 @@ //===----------------------------------------------------------------------===// import XCTest -import ArgumentParser import ArgumentParserTestHelpers final class RollDiceExampleTests: XCTestCase { func testRollDice() throws { - AssertExecuteCommand(command: "roll --times 6") + try AssertExecuteCommand(command: "roll --times 6") } func testRollDice_Help() throws { @@ -31,24 +30,26 @@ final class RollDiceExampleTests: XCTestCase { -h, --help Show help information. """ - AssertExecuteCommand(command: "roll -h", expected: helpText) - AssertExecuteCommand(command: "roll --help", expected: helpText) + try AssertExecuteCommand(command: "roll -h", expected: helpText) + try AssertExecuteCommand(command: "roll --help", expected: helpText) } func testRollDice_Fail() throws { - AssertExecuteCommand( + try AssertExecuteCommand( command: "roll --times", expected: """ Error: Missing value for '--times ' + Help: --times Rolls the dice times. Usage: roll [--times ] [--sides ] [--seed ] [--verbose] See 'roll --help' for more information. """, exitCode: .validationFailure) - AssertExecuteCommand( + try 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. """, diff --git a/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift new file mode 100644 index 000000000..fd9f824a3 --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.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 +// +//===----------------------------------------------------------------------===// + +#if os(macOS) + +import XCTest +import ArgumentParserTestHelpers + +final class CountLinesGenerateManualTests: XCTestCase { + func testCountLines_SinglePageManual() throws { + guard #available(macOS 12, *) else { return } + try AssertGenerateManual(singlePage: true, command: "count-lines", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt COUNT-LINES 9 + .Os + .Sh NAME + .Nm count-lines + .Sh SYNOPSIS + .Nm + .Ar input-file + .Op Fl -prefix + .Op Fl -verbose Ar verbose + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Ar input-file + A file to count lines in. If omitted, counts the lines of stdin. + .It Fl -prefix Ar prefix + Only count lines with this prefix. + .It Fl -verbose + Include extra information in the output. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } + + func testCountLines_MultiPageManual() throws { + guard #available(macOS 12, *) else { return } + try AssertGenerateManual(singlePage: false, command: "count-lines", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt COUNT-LINES 9 + .Os + .Sh NAME + .Nm count-lines + .Sh SYNOPSIS + .Nm + .Ar input-file + .Op Fl -prefix + .Op Fl -verbose Ar verbose + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Ar input-file + A file to count lines in. If omitted, counts the lines of stdin. + .It Fl -prefix Ar prefix + Only count lines with this prefix. + .It Fl -verbose + Include extra information in the output. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } +} + +#endif diff --git a/Tests/ArgumentParserGenerateManualTests/MathGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/MathGenerateManualTests.swift new file mode 100644 index 000000000..34187d8e5 --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/MathGenerateManualTests.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 +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParser +import ArgumentParserTestHelpers + +final class MathGenerateManualTests: XCTestCase { + func testMath_SinglePageManual() throws { + try AssertGenerateManual(singlePage: true, command: "math", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH 9 + .Os + .Sh NAME + .Nm math + .Nd "A utility for performing maths." + .Sh SYNOPSIS + .Nm + .Ar subcommand + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .It Em add + .Bl -tag -width 6n + .It Fl x , -hex-output + Use hexadecimal notation for the result. + .It Ar values... + A group of integers to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .It Em multiply + .Bl -tag -width 6n + .It Fl x , -hex-output + Use hexadecimal notation for the result. + .It Ar values... + A group of integers to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .It Em stats + .Bl -tag -width 6n + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .It Em average + .Bl -tag -width 6n + .It Fl -kind Ar kind + The kind of average to provide. + .It Ar values... + A group of floating-point values to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .It Em stdev + .Bl -tag -width 6n + .It Ar values... + A group of floating-point values to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .It Em quantiles + .Bl -tag -width 6n + .It Ar one-of-four + .It Ar custom-arg + .It Ar values... + A group of floating-point values to operate on. + .It Fl -test-success-exit-code + .It Fl -test-failure-exit-code + .It Fl -test-validation-exit-code + .It Fl -test-custom-exit-code Ar test-custom-exit-code + .It Fl -file Ar file + .It Fl -directory Ar directory + .It Fl -shell Ar shell + .It Fl -custom Ar custom + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .El + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } + + func testMath_MultiPageManual() throws { + try AssertGenerateManual(singlePage: false, command: "math", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH 9 + .Os + .Sh NAME + .Nm math + .Nd "A utility for performing maths." + .Sh SYNOPSIS + .Nm + .Ar subcommand + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh "SEE ALSO" + .Xr math.add 9 , + .Xr math.multiply 9 , + .Xr math.stats 9 + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.ADD 9 + .Os + .Sh NAME + .Nm "math add" + .Nd "Print the sum of the values." + .Sh SYNOPSIS + .Nm + .Op Fl -hex-output Ar hex-output + .Op Ar values... + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl x , -hex-output + Use hexadecimal notation for the result. + .It Ar values... + A group of integers to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.MULTIPLY 9 + .Os + .Sh NAME + .Nm "math multiply" + .Nd "Print the product of the values." + .Sh SYNOPSIS + .Nm + .Op Fl -hex-output Ar hex-output + .Op Ar values... + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl x , -hex-output + Use hexadecimal notation for the result. + .It Ar values... + A group of integers to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.STATS 9 + .Os + .Sh NAME + .Nm "math stats" + .Nd "Calculate descriptive statistics." + .Sh SYNOPSIS + .Nm + .Ar subcommand + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh "SEE ALSO" + .Xr math.stats.average 9 , + .Xr math.stats.quantiles 9 , + .Xr math.stats.stdev 9 + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.STATS.AVERAGE 9 + .Os + .Sh NAME + .Nm "math stats average" + .Nd "Print the average of the values." + .Sh SYNOPSIS + .Nm + .Op Fl -kind + .Op Ar values... + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -kind Ar kind + The kind of average to provide. + .It Ar values... + A group of floating-point values to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.STATS.STDEV 9 + .Os + .Sh NAME + .Nm "math stats stdev" + .Nd "Print the standard deviation of the values." + .Sh SYNOPSIS + .Nm + .Op Ar values... + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Ar values... + A group of floating-point values to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.STATS.QUANTILES 9 + .Os + .Sh NAME + .Nm "math stats quantiles" + .Nd "Print the quantiles of the values (TBD)." + .Sh SYNOPSIS + .Nm + .Op Ar one-of-four + .Op Ar custom-arg + .Op Ar values... + .Op Fl -test-success-exit-code Ar test-success-exit-code + .Op Fl -test-failure-exit-code Ar test-failure-exit-code + .Op Fl -test-validation-exit-code Ar test-validation-exit-code + .Op Fl -test-custom-exit-code + .Op Fl -file + .Op Fl -directory + .Op Fl -shell + .Op Fl -custom + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Ar one-of-four + .It Ar custom-arg + .It Ar values... + A group of floating-point values to operate on. + .It Fl -test-success-exit-code + .It Fl -test-failure-exit-code + .It Fl -test-validation-exit-code + .It Fl -test-custom-exit-code Ar test-custom-exit-code + .It Fl -file Ar file + .It Fl -directory Ar directory + .It Fl -shell Ar shell + .It Fl -custom Ar custom + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } +} diff --git a/Tests/ArgumentParserGenerateManualTests/RepeatGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/RepeatGenerateManualTests.swift new file mode 100644 index 000000000..c4ba9f728 --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/RepeatGenerateManualTests.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------*- 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 + +final class RepeatGenerateManualTests: XCTestCase { + func testMath_SinglePageManual() throws { + try AssertGenerateManual(singlePage: true, command: "repeat", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt REPEAT 9 + .Os + .Sh NAME + .Nm repeat + .Sh SYNOPSIS + .Nm + .Op Fl -count + .Op Fl -include-counter Ar include-counter + .Ar phrase + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -count Ar count + The number of times to repeat 'phrase'. + .It Fl -include-counter + Include a counter with each repetition. + .It Ar phrase + The phrase to repeat. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } + + func testMath_MultiPageManual() throws { + try AssertGenerateManual(singlePage: false, command: "repeat", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt REPEAT 9 + .Os + .Sh NAME + .Nm repeat + .Sh SYNOPSIS + .Nm + .Op Fl -count + .Op Fl -include-counter Ar include-counter + .Ar phrase + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -count Ar count + The number of times to repeat 'phrase'. + .It Fl -include-counter + Include a counter with each repetition. + .It Ar phrase + The phrase to repeat. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } +} diff --git a/Tests/ArgumentParserGenerateManualTests/RollDiceGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/RollDiceGenerateManualTests.swift new file mode 100644 index 000000000..115e46ddf --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/RollDiceGenerateManualTests.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers + +final class RollDiceGenerateManualTests: XCTestCase { + func testRollDice_SinglePageManual() throws { + try AssertGenerateManual(singlePage: true, command: "roll", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt ROLL 9 + .Os + .Sh NAME + .Nm roll + .Sh SYNOPSIS + .Nm + .Op Fl -times + .Op Fl -sides + .Op Fl -seed + .Op Fl -verbose Ar verbose + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -times Ar n + Rolls the dice times. + .It Fl -sides Ar m + Rolls an -sided dice. + .Pp + Use this option to override the default value of a six-sided die. + .It Fl -seed Ar seed + A seed to use for repeatable random generation. + .It Fl v , -verbose + Show all roll results. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } + + func testRollDice_MultiPageManual() throws { + try AssertGenerateManual(singlePage: false, command: "roll", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt ROLL 9 + .Os + .Sh NAME + .Nm roll + .Sh SYNOPSIS + .Nm + .Op Fl -times + .Op Fl -sides + .Op Fl -seed + .Op Fl -verbose Ar verbose + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -times Ar n + Rolls the dice times. + .It Fl -sides Ar m + Rolls an -sided dice. + .Pp + Use this option to override the default value of a six-sided die. + .It Fl -seed Ar seed + A seed to use for repeatable random generation. + .It Fl v , -verbose + Show all roll results. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } +} diff --git a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift index 8f5cc750b..aa52bf873 100644 --- a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift +++ b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift @@ -26,11 +26,11 @@ func getErrorText(_: T.Type, _ arguments: [String]) -> Str } } -func getErrorText(_: T.Type, _ arguments: [String]) -> String { +func getErrorText(_: T.Type, _ arguments: [String], screenWidth: Int) -> String { do { let command = try T.parseAsRoot(arguments) if let helpCommand = command as? HelpCommand { - return helpCommand.generateHelp() + return helpCommand.generateHelp(screenWidth: screenWidth) } else { XCTFail("Didn't generate a help error") return "" @@ -90,7 +90,7 @@ extension HelpTests { func testConfigHelp() throws { XCTAssertEqual( - getErrorText(Package.self, ["help", "config"]).trimmingLines(), + getErrorText(Package.self, ["help", "config"], screenWidth: 80).trimmingLines(), """ USAGE: package config @@ -107,13 +107,10 @@ extension HelpTests { } func testGetMirrorHelp() throws { - HelpGenerator._screenWidthOverride = 80 - defer { HelpGenerator._screenWidthOverride = nil } - XCTAssertEqual( - getErrorText(Package.self, ["help", "config", "get-mirror"]).trimmingLines(), + getErrorText(Package.self, ["help", "config", "get-mirror"], screenWidth: 80).trimmingLines(), """ - USAGE: package config get-mirror + USAGE: package config get-mirror [] --package-url OPTIONS: --build-path @@ -196,8 +193,16 @@ struct CustomHelp: ParsableCommand { extension HelpTests { func testCustomHelpNames() { - let names = CustomHelp.getHelpNames() - XCTAssertEqual(names, [.short("?"), .long("show-help")]) + let helpNames = [CustomHelp.self].getHelpNames(visibility: .default) + XCTAssertEqual(helpNames, [.short("?"), .long("show-help")]) + let helpHiddenNames = [CustomHelp.self].getHelpNames(visibility: .hidden) + XCTAssertEqual(helpHiddenNames, [.long("show-help-hidden")]) + + AssertFullErrorMessage(CustomHelp.self, ["--error"], """ + Error: Unknown option '--error' + Usage: custom-help + See 'custom-help --show-help' for more information. + """) } } @@ -211,8 +216,16 @@ struct NoHelp: ParsableCommand { extension HelpTests { func testNoHelpNames() { - let names = NoHelp.getHelpNames() - XCTAssertEqual(names, []) + let helpNames = [NoHelp.self].getHelpNames(visibility: .default) + XCTAssertEqual(helpNames, []) + let helpHiddenNames = [NoHelp.self].getHelpNames(visibility: .hidden) + XCTAssertEqual(helpHiddenNames, []) + + AssertFullErrorMessage(NoHelp.self, ["--error"], """ + Error: Missing expected argument '--count ' + Help: --count How many florps? + Usage: no-help --count + """) XCTAssertEqual( NoHelp.message(for: CleanExit.helpRequest()).trimmingLines(), @@ -225,3 +238,50 @@ extension HelpTests { """) } } + +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(visibility: .default) + XCTAssertEqual(names, [.short("p"), .long("parent-help")]) + } + + func testSubCommandCustomHelpNames() { + let names = [ + SubCommandCustomHelp.self, + SubCommandCustomHelp.ModifiedHelp.self + ].getHelpNames(visibility: .default) + XCTAssertEqual(names, [.short("s"), .long("subcommand-help")]) + } + + func testInheritImmediateParentHelpNames() { + let names = [ + SubCommandCustomHelp.self, + SubCommandCustomHelp.ModifiedHelp.self, + SubCommandCustomHelp.ModifiedHelp.InheritImmediateParentdHelp.self + ].getHelpNames(visibility: .default) + XCTAssertEqual(names, [.short("s"), .long("subcommand-help")]) + } +} diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift index 4bdf1fa22..ab8b3adc2 100644 --- a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift +++ b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift @@ -41,6 +41,8 @@ extension CompletionScriptTests { @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 { @@ -59,7 +61,6 @@ extension CompletionScriptTests { 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") @@ -123,13 +124,16 @@ extension CompletionScriptTests { } extension CompletionScriptTests { - struct Escaped: ParsableCommand { + struct EscapedCommand: ParsableCommand { @Option(help: #"Escaped chars: '[]\."#) var one: String + + @Argument(completion: .custom { _ in ["d", "e", "f"] }) + var two: String } func testEscaped_Zsh() throws { - XCTAssertEqual(zshEscapedCompletion, Escaped.completionScript(for: .zsh)) + XCTAssertEqual(zshEscapedCompletion, EscapedCommand.completionScript(for: .zsh)) } } @@ -149,7 +153,7 @@ _base() { '--path1:path1:_files' '--path2:path2:_files' '--path3:path3:(a b c)' - '(-h --help)'{-h,--help}'[Print help information.]' + '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S $args[@] && ret=0 @@ -179,7 +183,7 @@ _base() { fi case $prev in --name) - + return ;; --kind) @@ -211,17 +215,18 @@ complete -F _base base """ private let zshEscapedCompletion = """ -#compdef escaped +#compdef escaped-command local context state state_descr line -_escaped_commandname=$words[1] +_escaped_command_commandname=$words[1] typeset -A opt_args -_escaped() { +_escaped-command() { integer ret=1 local -a args args+=( '--one[Escaped chars: '"'"'\\[\\]\\\\.]:one:' - '(-h --help)'{-h,--help}'[Print help information.]' + ':two:{_custom_completion $_escaped_command_commandname ---completion -- two $words}' + '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S $args[@] && ret=0 @@ -234,12 +239,134 @@ _custom_completion() { _describe '' completions } -_escaped +_escaped-command """ private let fishBaseCompletions = """ -function __fish_base_using_command - set cmd (commandline -opc) +function _swift_base_using_command + set -l 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 '_swift_base_using_command base' -f -r -l name -d 'The user\\'s name.' +complete -c base -n '_swift_base_using_command base' -f -r -l kind +complete -c base -n '_swift_base_using_command base --kind' -f -k -a 'one two custom-three' +complete -c base -n '_swift_base_using_command base' -f -r -l other-kind +complete -c base -n '_swift_base_using_command base --other-kind' -f -k -a '1 2 3' +complete -c base -n '_swift_base_using_command base' -f -r -l path1 +complete -c base -n '_swift_base_using_command base --path1' -f -a '(for i in *.{}; echo $i;end)' +complete -c base -n '_swift_base_using_command base' -f -r -l path2 +complete -c base -n '_swift_base_using_command base --path2' -f -a '(for i in *.{}; echo $i;end)' +complete -c base -n '_swift_base_using_command base' -f -r -l path3 +complete -c base -n '_swift_base_using_command base --path3' -f -k -a 'a b c' +complete -c base -n '_swift_base_using_command base' -f -s h -l help -d 'Show help information.' +""" + +// MARK: - Test Hidden Subcommand +struct Parent: ParsableCommand { + static var configuration = CommandConfiguration(subcommands: [HiddenChild.self]) +} + +struct HiddenChild: ParsableCommand { + static var configuration = CommandConfiguration(shouldDisplay: false) +} + +extension CompletionScriptTests { + func testHiddenSubcommand_Zsh() throws { + let script1 = try CompletionsGenerator(command: Parent.self, shell: .zsh) + .generateCompletionScript() + XCTAssertEqual(zshHiddenCompletion, script1) + + let script2 = try CompletionsGenerator(command: Parent.self, shellName: "zsh") + .generateCompletionScript() + XCTAssertEqual(zshHiddenCompletion, script2) + + let script3 = Parent.completionScript(for: .zsh) + XCTAssertEqual(zshHiddenCompletion, script3) + } + + func testHiddenSubcommand_Bash() throws { + let script1 = try CompletionsGenerator(command: Parent.self, shell: .bash) + .generateCompletionScript() + XCTAssertEqual(bashHiddenCompletion, script1) + + let script2 = try CompletionsGenerator(command: Parent.self, shellName: "bash") + .generateCompletionScript() + XCTAssertEqual(bashHiddenCompletion, script2) + + let script3 = Parent.completionScript(for: .bash) + XCTAssertEqual(bashHiddenCompletion, script3) + } + + func testHiddenSubcommand_Fish() throws { + let script1 = try CompletionsGenerator(command: Parent.self, shell: .fish) + .generateCompletionScript() + XCTAssertEqual(fishHiddenCompletion, script1) + + let script2 = try CompletionsGenerator(command: Parent.self, shellName: "fish") + .generateCompletionScript() + XCTAssertEqual(fishHiddenCompletion, script2) + + let script3 = Parent.completionScript(for: .fish) + XCTAssertEqual(fishHiddenCompletion, script3) + } +} + +let zshHiddenCompletion = """ +#compdef parent +local context state state_descr line +_parent_commandname=$words[1] +typeset -A opt_args + +_parent() { + integer ret=1 + local -a args + args+=( + '(-h --help)'{-h,--help}'[Show help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + + +_custom_completion() { + local completions=("${(@f)$($*)}") + _describe '' completions +} + +_parent +""" + +let bashHiddenCompletion = """ +#!/bin/bash + +_parent() { + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + opts="-h --help" + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + +complete -F _parent parent +""" + +let fishHiddenCompletion = """ +function _swift_parent_using_command + set -l cmd (commandline -opc) if [ (count $cmd) -eq (count $argv) ] for i in (seq (count $argv)) if [ $cmd[$i] != $argv[$i] ] @@ -250,15 +377,5 @@ function __fish_base_using_command 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' +complete -c parent -n '_swift_parent_using_command parent' -f -s h -l help -d 'Show help information.' """ diff --git a/Tests/ArgumentParserUnitTests/DumpHelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/DumpHelpGenerationTests.swift new file mode 100644 index 000000000..b05e82656 --- /dev/null +++ b/Tests/ArgumentParserUnitTests/DumpHelpGenerationTests.swift @@ -0,0 +1,1395 @@ +// +// 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 DumpHelpGenerationTests: XCTestCase { + public static let allTests = [ + ("testDumpExampleCommands", testDumpExampleCommands), + ("testDumpA", testDumpA) + ] +} + +extension DumpHelpGenerationTests { + struct A: ParsableCommand { + enum TestEnum: String, CaseIterable, ExpressibleByArgument { + case a, b, c + } + + @Option + var enumeratedOption: TestEnum + + @Option + var noHelpOption: Int + + @Option(help: "int value option") + var intOption: Int + + @Option(help: "int value option with default value") + var intOptionWithDefaultValue: Int = 0 + + @Argument + var arg: Int + + @Argument(help: "argument with help") + var argWithHelp: Int + + @Argument(help: "argument with default value") + var argWithDefaultValue: Int = 1 + } + + public func testDumpA() throws { + try AssertDump(for: A.self, equals: Self.aDumpText) + } + + public func testDumpExampleCommands() throws { + struct TestCase { + let expected: String + let command: String + } + + let testCases: [UInt : TestCase] = [ + #line : .init(expected: Self.mathDumpText, command: "math --experimental-dump-help"), + #line : .init(expected: Self.mathAddDumpText, command: "math add --experimental-dump-help"), + #line : .init(expected: Self.mathMultiplyDumpText, command: "math multiply --experimental-dump-help"), + #line : .init(expected: Self.mathStatsDumpText, command: "math stats --experimental-dump-help") + ] + + try testCases.forEach { line, testCase in + try AssertJSONOutputEqual( + command: testCase.command, + expected: testCase.expected, + line: line) + } + } +} + +extension DumpHelpGenerationTests { + static let aDumpText = """ +{ + "command" : { + "arguments" : [ + { + "allValues" : [ + "a", + "b", + "c" + ], + "isOptional" : false, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "enumerated-option" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "enumerated-option" + }, + "shouldDisplay" : true, + "valueName" : "enumerated-option" + }, + { + "isOptional" : false, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "no-help-option" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "no-help-option" + }, + "shouldDisplay" : true, + "valueName" : "no-help-option" + }, + { + "abstract" : "int value option", + "isOptional" : false, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "int-option" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "int-option" + }, + "shouldDisplay" : true, + "valueName" : "int-option" + }, + { + "abstract" : "int value option with default value", + "defaultValue" : "0", + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "int-option-with-default-value" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "int-option-with-default-value" + }, + "shouldDisplay" : true, + "valueName" : "int-option-with-default-value" + }, + { + "isOptional" : false, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "arg" + }, + { + "abstract" : "argument with help", + "isOptional" : false, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "arg-with-help" + }, + { + "abstract" : "argument with default value", + "defaultValue" : "1", + "isOptional" : true, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "arg-with-default-value" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "a" + }, + "serializationVersion" : 0 +} +""" + + static let mathDumpText: String = """ +{ + "command" : { + "abstract" : "A utility for performing maths.", + "arguments" : [ + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "math", + "subcommands" : [ + { + "abstract" : "Print the sum of the values.", + "arguments" : [ + { + "abstract" : "Use hexadecimal notation for the result.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "hex-output" + }, + { + "kind" : "short", + "name" : "x" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "hex-output" + }, + "shouldDisplay" : true, + "valueName" : "hex-output" + }, + { + "abstract" : "A group of integers to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "add", + "superCommands" : [ + "math" + ] + }, + { + "abstract" : "Print the product of the values.", + "arguments" : [ + { + "abstract" : "Use hexadecimal notation for the result.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "hex-output" + }, + { + "kind" : "short", + "name" : "x" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "hex-output" + }, + "shouldDisplay" : true, + "valueName" : "hex-output" + }, + { + "abstract" : "A group of integers to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "multiply", + "superCommands" : [ + "math" + ] + }, + { + "abstract" : "Calculate descriptive statistics.", + "arguments" : [ + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "stats", + "subcommands" : [ + { + "abstract" : "Print the average of the values.", + "arguments" : [ + { + "abstract" : "The kind of average to provide.", + "allValues" : [ + "mean", + "median", + "mode" + ], + "defaultValue" : "mean", + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "kind" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "kind" + }, + "shouldDisplay" : true, + "valueName" : "kind" + }, + { + "abstract" : "A group of floating-point values to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "average", + "superCommands" : [ + "math", + "stats" + ] + }, + { + "abstract" : "Print the standard deviation of the values.", + "arguments" : [ + { + "abstract" : "A group of floating-point values to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "stdev", + "superCommands" : [ + "math", + "stats" + ] + }, + { + "abstract" : "Print the quantiles of the values (TBD).", + "arguments" : [ + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "one-of-four" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "custom-arg" + }, + { + "abstract" : "A group of floating-point values to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "test-success-exit-code" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "test-success-exit-code" + }, + "shouldDisplay" : false, + "valueName" : "test-success-exit-code" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "test-failure-exit-code" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "test-failure-exit-code" + }, + "shouldDisplay" : false, + "valueName" : "test-failure-exit-code" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "test-validation-exit-code" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "test-validation-exit-code" + }, + "shouldDisplay" : false, + "valueName" : "test-validation-exit-code" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "test-custom-exit-code" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "test-custom-exit-code" + }, + "shouldDisplay" : false, + "valueName" : "test-custom-exit-code" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "file" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "file" + }, + "shouldDisplay" : true, + "valueName" : "file" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "directory" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "directory" + }, + "shouldDisplay" : true, + "valueName" : "directory" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "shell" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "shell" + }, + "shouldDisplay" : true, + "valueName" : "shell" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "custom" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "custom" + }, + "shouldDisplay" : true, + "valueName" : "custom" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "quantiles", + "superCommands" : [ + "math", + "stats" + ] + } + ], + "superCommands" : [ + "math" + ] + } + ] + }, + "serializationVersion" : 0 +} +""" + + static let mathAddDumpText: String = """ +{ + "command" : { + "abstract" : "Print the sum of the values.", + "arguments" : [ + { + "abstract" : "Use hexadecimal notation for the result.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "hex-output" + }, + { + "kind" : "short", + "name" : "x" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "hex-output" + }, + "shouldDisplay" : true, + "valueName" : "hex-output" + }, + { + "abstract" : "A group of integers to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "add", + "superCommands" : [ + "math" + ] + }, + "serializationVersion" : 0 +} +""" + + static let mathMultiplyDumpText: String = """ +{ + "command" : { + "abstract" : "Print the product of the values.", + "arguments" : [ + { + "abstract" : "Use hexadecimal notation for the result.", + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "hex-output" + }, + { + "kind" : "short", + "name" : "x" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "hex-output" + }, + "shouldDisplay" : true, + "valueName" : "hex-output" + }, + { + "abstract" : "A group of integers to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "multiply", + "superCommands" : [ + "math" + ] + }, + "serializationVersion" : 0 +} +""" + + static let mathStatsDumpText: String = """ +{ + "command" : { + "abstract" : "Calculate descriptive statistics.", + "arguments" : [ + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "stats", + "subcommands" : [ + { + "abstract" : "Print the average of the values.", + "arguments" : [ + { + "abstract" : "The kind of average to provide.", + "allValues" : [ + "mean", + "median", + "mode" + ], + "defaultValue" : "mean", + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "kind" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "kind" + }, + "shouldDisplay" : true, + "valueName" : "kind" + }, + { + "abstract" : "A group of floating-point values to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "average", + "superCommands" : [ + "math", + "stats" + ] + }, + { + "abstract" : "Print the standard deviation of the values.", + "arguments" : [ + { + "abstract" : "A group of floating-point values to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "stdev", + "superCommands" : [ + "math", + "stats" + ] + }, + { + "abstract" : "Print the quantiles of the values (TBD).", + "arguments" : [ + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "one-of-four" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "custom-arg" + }, + { + "abstract" : "A group of floating-point values to operate on.", + "isOptional" : true, + "isRepeating" : true, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "values" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "test-success-exit-code" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "test-success-exit-code" + }, + "shouldDisplay" : false, + "valueName" : "test-success-exit-code" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "test-failure-exit-code" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "test-failure-exit-code" + }, + "shouldDisplay" : false, + "valueName" : "test-failure-exit-code" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "test-validation-exit-code" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "test-validation-exit-code" + }, + "shouldDisplay" : false, + "valueName" : "test-validation-exit-code" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "test-custom-exit-code" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "test-custom-exit-code" + }, + "shouldDisplay" : false, + "valueName" : "test-custom-exit-code" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "file" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "file" + }, + "shouldDisplay" : true, + "valueName" : "file" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "directory" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "directory" + }, + "shouldDisplay" : true, + "valueName" : "directory" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "shell" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "shell" + }, + "shouldDisplay" : true, + "valueName" : "shell" + }, + { + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "custom" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "custom" + }, + "shouldDisplay" : true, + "valueName" : "custom" + }, + { + "abstract" : "Show the version.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "long", + "name" : "version" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "version" + }, + "shouldDisplay" : true, + "valueName" : "version" + }, + { + "abstract" : "Show help information.", + "isOptional" : false, + "isRepeating" : false, + "kind" : "flag", + "names" : [ + { + "kind" : "short", + "name" : "h" + }, + { + "kind" : "long", + "name" : "help" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "help" + }, + "shouldDisplay" : true, + "valueName" : "help" + } + ], + "commandName" : "quantiles", + "superCommands" : [ + "math", + "stats" + ] + } + ], + "superCommands" : [ + "math" + ] + }, + "serializationVersion" : 0 +} +""" +} diff --git a/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift b/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift index 20eb687d7..d3cf274fb 100644 --- a/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift +++ b/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift @@ -64,19 +64,77 @@ extension ErrorMessageTests { } } +fileprivate enum Format: String, Equatable, Decodable, ExpressibleByArgument, CaseIterable { + case text + case json + case csv +} + +fileprivate enum Name: String, Equatable, Decodable, ExpressibleByArgument, CaseIterable { + case bruce + case clint + case hulk + case natasha + case steve + case thor + case tony +} + fileprivate struct Foo: ParsableArguments { - enum Format: String, Equatable, Decodable, ExpressibleByArgument { - case text - case json - } @Option(name: [.short, .long]) var format: Format + @Option(name: [.short, .long]) + var name: Name? +} + +fileprivate struct EnumWithFewCasesArrayArgument: ParsableArguments { + @Argument + var formats: [Format] +} + +fileprivate struct EnumWithManyCasesArrayArgument: ParsableArguments { + @Argument + var names: [Name] } 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 '") + AssertErrorMessage(Foo.self, ["--format", "png"], "The value 'png' is invalid for '--format '. Please provide one of 'text', 'json' or 'csv'.") + AssertErrorMessage(Foo.self, ["-f", "png"], "The value 'png' is invalid for '-f '. Please provide one of 'text', 'json' or 'csv'.") + AssertErrorMessage(Foo.self, ["-f", "text", "--name", "loki"], + """ + The value 'loki' is invalid for '--name '. Please provide one of the following: + - bruce + - clint + - hulk + - natasha + - steve + - thor + - tony + """) + AssertErrorMessage(Foo.self, ["-f", "text", "-n", "loki"], + """ + The value 'loki' is invalid for '-n '. Please provide one of the following: + - bruce + - clint + - hulk + - natasha + - steve + - thor + - tony + """) + AssertErrorMessage(EnumWithFewCasesArrayArgument.self, ["png"], "The value 'png' is invalid for ''. Please provide one of 'text', 'json' or 'csv'.") + AssertErrorMessage(EnumWithManyCasesArrayArgument.self, ["loki"], + """ + The value 'loki' is invalid for ''. Please provide one of the following: + - bruce + - clint + - hulk + - natasha + - steve + - thor + - tony + """) } } @@ -175,6 +233,27 @@ extension ErrorMessageTests { } } +// (see issue #434). +private struct EmptyArray: ParsableArguments { + @Option(parsing: .upToNextOption) + var array: [String] = [] + + @Flag(name: [.short, .long]) + var verbose = false +} + +extension ErrorMessageTests { + func testEmptyArrayOption() { + AssertErrorMessage(EmptyArray.self, ["--array"], "Missing value for '--array '") + + AssertErrorMessage(EmptyArray.self, ["--array", "--verbose"], "Missing value for '--array '") + AssertErrorMessage(EmptyArray.self, ["-verbose", "--array"], "Missing value for '--array '") + + AssertErrorMessage(EmptyArray.self, ["--array", "-v"], "Missing value for '--array '") + AssertErrorMessage(EmptyArray.self, ["-v", "--array"], "Missing value for '--array '") + } +} + // MARK: - fileprivate struct Repeat: ParsableArguments { diff --git a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift index 798490686..1895aba92 100644 --- a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift +++ b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift @@ -76,3 +76,15 @@ extension ExitCodeTests { } } } + +// 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 index f794ade79..cc2e3a8e9 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -25,7 +25,7 @@ extension URL: ExpressibleByArgument { } public var defaultValueDescription: String { - self.absoluteString == FileManager.default.currentDirectoryPath + self.path == FileManager.default.currentDirectoryPath && self.isFileURL ? "current directory" : String(describing: self) } @@ -40,7 +40,7 @@ extension HelpGenerationTests { } func testHelp() { - AssertHelp(for: A.self, equals: """ + AssertHelp(.default, for: A.self, equals: """ USAGE: a --name [--title ] OPTIONS: @@ -58,10 +58,11 @@ extension HelpGenerationTests { @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: """ + AssertHelp(.default, for: B.self, equals: """ USAGE: b --name <name> [--title <title>] OPTIONS: @@ -70,6 +71,23 @@ extension HelpGenerationTests { -h, --help Show help information. """) + + AssertHelp(.hidden, for: B.self, equals: """ + USAGE: b --name <name> [--title <title>] [<hidden-name>] [--hidden-title <hidden-title>] [--hidden-flag] [--hidden-inverted-flag] [--no-hidden-inverted-flag] + + ARGUMENTS: + <hidden-name> + + OPTIONS: + --name <name> Your name + --title <title> Your title + --hidden-title <hidden-title> + --hidden-flag + --hidden-inverted-flag/--no-hidden-inverted-flag + (default: true) + -h, --help Show help information. + + """) } struct C: ParsableArguments { @@ -79,7 +97,7 @@ extension HelpGenerationTests { } func testHelpWithDiscussion() { - AssertHelp(for: C.self, equals: """ + AssertHelp(.default, for: C.self, equals: """ USAGE: c --name <name> OPTIONS: @@ -102,7 +120,7 @@ extension HelpGenerationTests { } func testHelpWithDefaultValueButNoDiscussion() { - AssertHelp(for: Issue27.self, equals: """ + AssertHelp(.default, for: Issue27.self, equals: """ USAGE: issue27 [--two <two>] --three <three> [--four <four>] [--five <five>] OPTIONS: @@ -139,9 +157,6 @@ extension HelpGenerationTests { @Option(help: "Your name.") var name: String = "John" - @Option(default: "Winston", help: "Your middle name.") - var middleName: String? - @Option(help: "Your age.") var age: Int = 20 @@ -158,20 +173,18 @@ extension HelpGenerationTests { var degree: Degree = .bachelor @Option(help: "Directory.") - var directory: URL = URL(string: FileManager.default.currentDirectoryPath)! + var directory: URL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) } func testHelpWithDefaultValues() { - AssertHelp(for: D.self, equals: """ - USAGE: d [<occupation>] [--name <name>] [--middle-name <middle-name>] [--age <age>] [--logging <logging>] [--lucky <numbers> ...] [--optional] [--required] [--degree <degree>] [--directory <directory>] + AssertHelp(.default, 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) - --middle-name <middle-name> - Your middle name. (default: Winston) --age <age> Your age. (default: 20) --logging <logging> Whether logging is enabled. (default: false) --lucky <numbers> Your lucky numbers. (default: 7, 14) @@ -215,7 +228,7 @@ extension HelpGenerationTests { } func testHelpWithMutuallyExclusiveFlags() { - AssertHelp(for: E.self, equals: """ + AssertHelp(.default, for: E.self, equals: """ USAGE: e --stats --count --list OPTIONS: @@ -225,7 +238,7 @@ extension HelpGenerationTests { """) - AssertHelp(for: F.self, equals: """ + AssertHelp(.default, for: F.self, equals: """ USAGE: f [-s] [-c] [-l] OPTIONS: @@ -234,7 +247,7 @@ extension HelpGenerationTests { """) - AssertHelp(for: G.self, equals: """ + AssertHelp(.default, for: G.self, equals: """ USAGE: g [--flag] [--no-flag] OPTIONS: @@ -272,7 +285,7 @@ extension HelpGenerationTests { } func testHelpWithSubcommands() { - AssertHelp(for: H.self, equals: """ + AssertHelp(.default, for: H.self, equals: """ USAGE: h <subcommand> OPTIONS: @@ -288,7 +301,7 @@ extension HelpGenerationTests { See 'h help <subcommand>' for detailed help. """) - AssertHelp(for: H.AnotherCommand.self, root: H.self, equals: """ + AssertHelp(.default, 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: @@ -310,7 +323,7 @@ extension HelpGenerationTests { } func testHelpWithVersion() { - AssertHelp(for: I.self, equals: """ + AssertHelp(.default, for: I.self, equals: """ USAGE: i OPTIONS: @@ -326,7 +339,8 @@ extension HelpGenerationTests { } func testOverviewButNoAbstractSpacing() { - let renderedHelp = HelpGenerator(J.self).rendered() + let renderedHelp = HelpGenerator(J.self, visibility: .default) + .rendered() AssertEqualStringsIgnoringTrailingWhitespace(renderedHelp, """ OVERVIEW: test @@ -351,7 +365,7 @@ extension HelpGenerationTests { } func testHelpWithNoValueForArray() { - AssertHelp(for: K.self, equals: """ + AssertHelp(.default, for: K.self, equals: """ USAGE: k [<paths> ...] ARGUMENTS: @@ -371,7 +385,7 @@ extension HelpGenerationTests { } func testHelpWithMultipleCustomNames() { - AssertHelp(for: L.self, equals: """ + AssertHelp(.default, for: L.self, equals: """ USAGE: l [--remote <remote>] OPTIONS: @@ -389,7 +403,7 @@ extension HelpGenerationTests { } func testHelpWithDefaultCommand() { - AssertHelp(for: N.self, equals: """ + AssertHelp(.default, for: N.self, equals: """ USAGE: n <subcommand> OPTIONS: @@ -423,7 +437,7 @@ extension HelpGenerationTests { } func testHelpWithDefaultValueForArray() { - AssertHelp(for: P.self, equals: """ + AssertHelp(.default, for: P.self, equals: """ USAGE: p [-o <o> ...] [<remainder> ...] ARGUMENTS: @@ -435,4 +449,330 @@ extension HelpGenerationTests { """) } + + 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(.default, 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 + -h, -help, --help Show help information. + + """) + } +} + +extension HelpGenerationTests { + private struct optionsToHide: ParsableArguments { + @Flag(help: "Verbose") + var verbose: Bool = false + + @Option(help: "Custom Name") + var customName: String? + + @Option(help: .hidden) + var hiddenOption: String? + + @Argument(help: .private) + var privateArg: String? + } + + @available(*, deprecated) + private struct HideOptionGroupLegacyDriver: 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? + } + + private struct HideOptionGroupDriver: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "driver", abstract: "Demo hiding option groups") + + @OptionGroup(visibility: .hidden) + var hideMe: optionsToHide + + @Option(help: "Time to wait before timeout (in seconds)") + var timeout: Int? + } + + private struct PrivateOptionGroupDriver: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "driver", abstract: "Demo hiding option groups") + + @OptionGroup(visibility: .private) + var hideMe: optionsToHide + + @Option(help: "Time to wait before timeout (in seconds)") + var timeout: Int? + } + + private var helpMessage: String { """ + OVERVIEW: Demo hiding option groups + + USAGE: driver [--timeout <timeout>] + + OPTIONS: + --timeout <timeout> Time to wait before timeout (in seconds) + -h, --help Show help information. + + """ + } + + private var helpHiddenMessage: String { """ + OVERVIEW: Demo hiding option groups + + USAGE: driver [--verbose] [--custom-name <custom-name>] [--hidden-option <hidden-option>] [--timeout <timeout>] + + OPTIONS: + --verbose Verbose + --custom-name <custom-name> + Custom Name + --hidden-option <hidden-option> + --timeout <timeout> Time to wait before timeout (in seconds) + -h, --help Show help information. + + """ + } + + @available(*, deprecated) + func testHidingOptionGroup() throws { + AssertHelp(.default, for: HideOptionGroupLegacyDriver.self, equals: helpMessage) + AssertHelp(.default, for: HideOptionGroupDriver.self, equals: helpMessage) + AssertHelp(.default, for: PrivateOptionGroupDriver.self, equals: helpMessage) + } + + @available(*, deprecated) + func testHelpHiddenShowsDefaultAndHidden() throws { + AssertHelp(.hidden, for: HideOptionGroupLegacyDriver.self, equals: helpHiddenMessage) + AssertHelp(.hidden, for: HideOptionGroupDriver.self, equals: helpHiddenMessage) + + // Note: Private option groups are not visible at `.hidden` help level. + AssertHelp(.hidden, for: PrivateOptionGroupDriver.self, equals: helpMessage) + } +} + +extension HelpGenerationTests { + struct AllValues: ParsableCommand { + enum Manual: Int, ExpressibleByArgument { + case foo + static var allValueStrings = ["bar"] + } + + enum UnspecializedSynthesized: Int, CaseIterable, ExpressibleByArgument { + case one, two + } + + enum SpecializedSynthesized: String, CaseIterable, ExpressibleByArgument { + case apple = "Apple", banana = "Banana" + } + + @Argument var manualArgument: Manual + @Option var manualOption: Manual + + @Argument var unspecializedSynthesizedArgument: UnspecializedSynthesized + @Option var unspecializedSynthesizedOption: UnspecializedSynthesized + + @Argument var specializedSynthesizedArgument: SpecializedSynthesized + @Option var specializedSynthesizedOption: SpecializedSynthesized + } + + func testAllValueStrings() throws { + XCTAssertEqual(AllValues.Manual.allValueStrings, ["bar"]) + XCTAssertEqual(AllValues.UnspecializedSynthesized.allValueStrings, ["one", "two"]) + XCTAssertEqual(AllValues.SpecializedSynthesized.allValueStrings, ["Apple", "Banana"]) + } + + func testAllValues() { + let opts = ArgumentSet(AllValues.self, visibility: .private) + XCTAssertEqual(AllValues.Manual.allValueStrings, opts[0].help.allValues) + XCTAssertEqual(AllValues.Manual.allValueStrings, opts[1].help.allValues) + + XCTAssertEqual(AllValues.UnspecializedSynthesized.allValueStrings, opts[2].help.allValues) + XCTAssertEqual(AllValues.UnspecializedSynthesized.allValueStrings, opts[3].help.allValues) + + XCTAssertEqual(AllValues.SpecializedSynthesized.allValueStrings, opts[4].help.allValues) + XCTAssertEqual(AllValues.SpecializedSynthesized.allValueStrings, opts[5].help.allValues) + } + + struct Q: ParsableArguments { + @Option(help: "Your name") var name: String + @Option(help: "Your title") var title: String? + + @Argument(help: .private) var privateName: String? + @Option(help: .private) var privateTitle: String? + @Flag(help: .private) var privateFlag: Bool = false + @Flag(inversion: .prefixedNo, help: .private) var privateInvertedFlag: Bool = true + } + + func testHelpWithPrivate() { + AssertHelp(.default, for: Q.self, equals: """ + USAGE: q --name <name> [--title <title>] + + OPTIONS: + --name <name> Your name + --title <title> Your title + -h, --help Show help information. + + """) + } +} + +// MARK: - Issue #278 https://github.com/apple/swift-argument-parser/issues/278 + +extension HelpGenerationTests { + private struct ParserBug: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "parserBug", + subcommands: [Sub.self]) + + struct CommonOptions: ParsableCommand { + @Flag(help: "example flag") + var example: Bool = false + } + + struct Sub: ParsableCommand { + @OptionGroup() + var commonOptions: CommonOptions + + @Argument(help: "Non-mandatory argument") + var argument: String? + } + } + + func testIssue278() { + AssertHelp(.default, for: ParserBug.Sub.self, root: ParserBug.self, equals: """ + USAGE: parserBug sub [--example] [<argument>] + + ARGUMENTS: + <argument> Non-mandatory argument + + OPTIONS: + --example example flag + -h, --help Show help information. + + """) + } + + struct CustomUsageShort: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration(usage: """ + example [--verbose] <file-name> + """) + } + + @Argument var file: String + @Flag var verboseMode = false + } + + struct CustomUsageLong: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration(usage: """ + example <file-name> + example --verbose <file-name> + example --help + """) + } + + @Argument var file: String + @Flag var verboseMode = false + } + + struct CustomUsageHidden: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration(usage: "") + } + + @Argument var file: String + @Flag var verboseMode = false + } + + func testCustomUsageHelp() { + XCTAssertEqual(CustomUsageShort.helpMessage(columns: 80), """ + USAGE: example [--verbose] <file-name> + + ARGUMENTS: + <file> + + OPTIONS: + --verbose-mode + -h, --help Show help information. + + """) + + XCTAssertEqual(CustomUsageLong.helpMessage(columns: 80), """ + USAGE: example <file-name> + example --verbose <file-name> + example --help + + ARGUMENTS: + <file> + + OPTIONS: + --verbose-mode + -h, --help Show help information. + + """) + + XCTAssertEqual(CustomUsageHidden.helpMessage(columns: 80), """ + ARGUMENTS: + <file> + + OPTIONS: + --verbose-mode + -h, --help Show help information. + + """) + } + + func testCustomUsageError() { + XCTAssertEqual(CustomUsageShort.fullMessage(for: ValidationError("Test")), """ + Error: Test + Usage: example [--verbose] <file-name> + See 'custom-usage-short --help' for more information. + """) + XCTAssertEqual(CustomUsageLong.fullMessage(for: ValidationError("Test")), """ + Error: Test + Usage: example <file-name> + example --verbose <file-name> + example --help + See 'custom-usage-long --help' for more information. + """) + XCTAssertEqual(CustomUsageHidden.fullMessage(for: ValidationError("Test")), """ + Error: Test + See 'custom-usage-hidden --help' for more information. + """) + } } diff --git a/Tests/ArgumentParserUnitTests/InputOriginTests.swift b/Tests/ArgumentParserUnitTests/InputOriginTests.swift new file mode 100644 index 000000000..a1767922b --- /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 000000000..cf2f2f765 --- /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/ParsableArgumentsValidationTests.swift b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift index 29aa0979f..b7a324352 100644 --- a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift +++ b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift @@ -176,7 +176,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { @Argument var foo: String @Option var bar: String @OptionGroup var options: Options - @Flag var flag: Bool + @Flag var flag = false } func testPositionalArgumentsValidation() throws { diff --git a/Tests/ArgumentParserUnitTests/StringEditDistanceTests.swift b/Tests/ArgumentParserUnitTests/StringEditDistanceTests.swift index 5967c21e8..ffa7ed94f 100644 --- a/Tests/ArgumentParserUnitTests/StringEditDistanceTests.swift +++ b/Tests/ArgumentParserUnitTests/StringEditDistanceTests.swift @@ -23,5 +23,10 @@ extension StringEditDistanceTests { XCTAssertEqual("bar".editDistance(to: "foo"), 3) XCTAssertEqual("bar".editDistance(to: "baz"), 1) XCTAssertEqual("baz".editDistance(to: "bar"), 1) + XCTAssertEqual("friend".editDistance(to: "fresh"), 3) + XCTAssertEqual("friend".editDistance(to: "friend"), 0) + XCTAssertEqual("friend".editDistance(to: "fried"), 1) + XCTAssertEqual("friend".editDistance(to: "friendly"), 2) + XCTAssertEqual("friendly".editDistance(to: "friend"), 2) } } diff --git a/Tests/ArgumentParserUnitTests/StringWrappingTests.swift b/Tests/ArgumentParserUnitTests/StringWrappingTests.swift index 2829160b5..710aa2b77 100644 --- a/Tests/ArgumentParserUnitTests/StringWrappingTests.swift +++ b/Tests/ArgumentParserUnitTests/StringWrappingTests.swift @@ -164,4 +164,41 @@ extension StringWrappingTests { } """) } + + func testIndent() { + XCTAssertEqual( + shortSample.wrapped(to: 40).indentingEachLine(by: 10), + shortSample.wrapped(to: 50, wrappingIndent: 10)) + XCTAssertEqual( + longSample.wrapped(to: 40).indentingEachLine(by: 10), + longSample.wrapped(to: 50, wrappingIndent: 10)) + + XCTAssertEqual("".indentingEachLine(by: 10), "") + XCTAssertEqual("\n".indentingEachLine(by: 10), "\n") + XCTAssertEqual("a\n".indentingEachLine(by: 10), " a\n") + XCTAssertEqual("\na\n".indentingEachLine(by: 10), "\n a\n") + XCTAssertEqual("a\n\nb\n".indentingEachLine(by: 10), + " a\n\n b\n") + XCTAssertEqual("\na\n\nb\n".indentingEachLine(by: 10), + "\n a\n\n b\n") + } + + func testHangingIndent() { + XCTAssertEqual( + shortSample.wrapped(to: 40).hangingIndentingEachLine(by: 10), + String(shortSample.wrapped(to: 50, wrappingIndent: 10).dropFirst(10))) + XCTAssertEqual( + longSample.wrapped(to: 40).hangingIndentingEachLine(by: 10), + String(longSample.wrapped(to: 50, wrappingIndent: 10).dropFirst(10))) + + XCTAssertEqual("".hangingIndentingEachLine(by: 10), "") + XCTAssertEqual("\n".hangingIndentingEachLine(by: 10), "\n") + XCTAssertEqual("a\n".hangingIndentingEachLine(by: 10), "a\n") + XCTAssertEqual("\na\n".hangingIndentingEachLine(by: 10), "\n a\n") + XCTAssertEqual("a\n\nb\n".hangingIndentingEachLine(by: 10), + "a\n\n b\n") + XCTAssertEqual("\na\n\nb\n".hangingIndentingEachLine(by: 10), + "\n a\n\n b\n") + + } } diff --git a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift index c52f2fda2..218861fa1 100644 --- a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift @@ -15,6 +15,17 @@ import XCTest final class UsageGenerationTests: XCTestCase { } +func _testSynopsis<T: ParsableArguments>( + _ type: T.Type, + visibility: ArgumentVisibility = .default, + expected: String, + file: StaticString = #file, + line: UInt = #line +) { + let help = UsageGenerator(toolName: "example", parsable: T(), visibility: visibility) + XCTAssertEqual(help.synopsis, expected, file: file, line: line) +} + // MARK: - extension UsageGenerationTests { @@ -32,8 +43,7 @@ extension UsageGenerationTests { } func testSynopsis() { - let help = UsageGenerator(toolName: "bar", parsable: A()) - XCTAssertEqual(help.synopsis, "bar --first-name <first-name> --title <title>") + _testSynopsis(A.self, expected: "example --first-name <first-name> --title <title>") } struct B: ParsableArguments { @@ -42,8 +52,7 @@ extension UsageGenerationTests { } func testSynopsisWithOptional() { - let help = UsageGenerator(toolName: "bar", parsable: B()) - XCTAssertEqual(help.synopsis, "bar [--first-name <first-name>] [--title <title>]") + _testSynopsis(B.self, expected: "example [--first-name <first-name>] [--title <title>]") } struct C: ParsableArguments { @@ -52,8 +61,7 @@ extension UsageGenerationTests { } func testFlagSynopsis() { - let help = UsageGenerator(toolName: "bar", parsable: C()) - XCTAssertEqual(help.synopsis, "bar [--log] [--verbose ...]") + _testSynopsis(C.self, expected: "example [--log] [--verbose ...]") } struct D: ParsableArguments { @@ -62,8 +70,7 @@ extension UsageGenerationTests { } func testPositionalSynopsis() { - let help = UsageGenerator(toolName: "bar", parsable: D()) - XCTAssertEqual(help.synopsis, "bar <first-name> [<title>]") + _testSynopsis(D.self, expected: "example <first-name> [<title>]") } struct E: ParsableArguments { @@ -78,8 +85,7 @@ extension UsageGenerationTests { } func testSynopsisWithDefaults() { - let help = UsageGenerator(toolName: "bar", parsable: E()) - XCTAssertEqual(help.synopsis, "bar [--name <name>] [--count <count>] [<arg>]") + _testSynopsis(E.self, expected: "example [--name <name>] [--count <count>] [<arg>]") } struct F: ParsableArguments { @@ -88,8 +94,7 @@ extension UsageGenerationTests { } func testSynopsisWithRepeats() { - let help = UsageGenerator(toolName: "bar", parsable: F()) - XCTAssertEqual(help.synopsis, "bar [--name <name> ...] [<name-counts> ...]") + _testSynopsis(F.self, expected: "example [--name <name> ...] [<name-counts> ...]") } struct G: ParsableArguments { @@ -101,8 +106,7 @@ extension UsageGenerationTests { } func testSynopsisWithCustomization() { - let help = UsageGenerator(toolName: "bar", parsable: G()) - XCTAssertEqual(help.synopsis, "bar [--file-path <path>] <user-home-path>") + _testSynopsis(G.self, expected: "example [--file-path <path>] <user-home-path>") } struct H: ParsableArguments { @@ -111,8 +115,8 @@ extension UsageGenerationTests { } func testSynopsisWithHidden() { - let help = UsageGenerator(toolName: "bar", parsable: H()) - XCTAssertEqual(help.synopsis, "bar") + _testSynopsis(H.self, expected: "example") + _testSynopsis(H.self, visibility: .hidden, expected: "example [--first-name <first-name>] [<title>]") } struct I: ParsableArguments { @@ -135,8 +139,7 @@ extension UsageGenerationTests { } func testSynopsisWithDefaultValueAndTransform() { - let help = UsageGenerator(toolName: "bar", parsable: I()) - XCTAssertEqual(help.synopsis, "bar [--color <color>]") + _testSynopsis(I.self, expected: "example [--color <color>]") } struct J: ParsableArguments { @@ -146,8 +149,7 @@ extension UsageGenerationTests { } func testSynopsisWithTransform() { - let help = UsageGenerator(toolName: "bar", parsable: J()) - XCTAssertEqual(help.synopsis, "bar --req <req> [--opt <opt>]") + _testSynopsis(J.self, expected: "example --req <req> [--opt <opt>]") } struct K: ParsableArguments { @@ -158,8 +160,7 @@ extension UsageGenerationTests { } func testSynopsisWithMultipleCustomNames() { - let help = UsageGenerator(toolName: "bar", parsable: K()) - XCTAssertEqual(help.synopsis, "bar [--remote <remote>]") + _testSynopsis(K.self, expected: "example [--remote <remote>]") } struct L: ParsableArguments { @@ -170,7 +171,50 @@ extension UsageGenerationTests { } func testSynopsisWithSingleDashLongNameFirst() { - let help = UsageGenerator(toolName: "bar", parsable: L()) - XCTAssertEqual(help.synopsis, "bar [-remote <remote>]") + _testSynopsis(L.self, expected: "example [-remote <remote>]") + } + + struct M: ParsableArguments { + enum Color: String, EnumerableFlag { + case green, blue, yellow + } + + @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 + + @Flag(inversion: .prefixedEnableDisable) + var optionalBool: Bool? + + @Flag var optionalColor: Color? + + @Option var option: Bool + @Argument var input: String + @Argument var output: String? + } + + func testSynopsisWithTooManyOptions() { + _testSynopsis(M.self, expected: "example [<options>] --option <option> <input> [<output>]") + } + + struct N: ParsableArguments { + @Flag var a: Bool = false + @Flag var b: Bool = false + var title = "defaulted value" + var decode = false + } + + func testNonwrappedValues() { + _testSynopsis(N.self, expected: "example [--a] [--b]") + _testSynopsis(N.self, visibility: .hidden, expected: "example [--a] [--b]") } } diff --git a/Tools/changelog-authors/main.swift b/Tools/changelog-authors/ChangelogAuthors.swift similarity index 65% rename from Tools/changelog-authors/main.swift rename to Tools/changelog-authors/ChangelogAuthors.swift index 3c5231bfb..78cb8590b 100644 --- a/Tools/changelog-authors/main.swift +++ b/Tools/changelog-authors/ChangelogAuthors.swift @@ -2,68 +2,23 @@ // // This source file is part of the Swift Argument Parser open source project // -// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2020-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 // //===----------------------------------------------------------------------===// +#if os(macOS) + 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 { +@main +@available(macOS 12.1, *) +struct ChangelogAuthors: AsyncParsableCommand { static var configuration: CommandConfiguration { CommandConfiguration( abstract: "A helper tool for generating author info for the changelog.", @@ -79,6 +34,9 @@ struct ChangelogAuthors: ParsableCommand { @Argument(help: "The ending point for the comparison.") var endingTag: String? + + @Option(name: [.short, .customLong("repo")], help: "The GitHub repository to search for changes.") + var repository: String = "apple/swift-argument-parser" func validate() throws { func checkTag(_ tag: String) -> Bool { @@ -109,15 +67,22 @@ struct ChangelogAuthors: ParsableCommand { } } + func linkReference(for author: Author) -> String { + """ + [\(author.login)]: \ + https://github.com/\(repository)/commits?author=\(author.login) + """ + } + func references(for authors: [Author]) -> String { authors - .map({ $0.linkReference }) + .map({ linkReference(for: $0) }) .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")") + string: "https://api.github.com/repos/\(repository)/compare/\(startingTag)...\(endingTag ?? "HEAD")") else { print("Couldn't create url string") throw ExitCode.failure @@ -126,8 +91,8 @@ struct ChangelogAuthors: ParsableCommand { return url } - mutating func run() throws { - let data = try Data(contentsOf: try comparisonURL()) + mutating func run() async throws { + let (data, _) = try await URLSession.shared.data(from: try comparisonURL()) let comparison = try JSONDecoder().decode(Comparison.self, from: data) let authors = comparison.commits.map({ $0.author }) .uniqued(by: { $0.login }) @@ -139,5 +104,4 @@ struct ChangelogAuthors: ParsableCommand { } } -ChangelogAuthors.main() - +#endif diff --git a/Tools/changelog-authors/Models.swift b/Tools/changelog-authors/Models.swift new file mode 100644 index 000000000..6e3f4ff5d --- /dev/null +++ b/Tools/changelog-authors/Models.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +// 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 inlineLink: String { + "[\(login)]" + } +} diff --git a/Tools/changelog-authors/Util.swift b/Tools/changelog-authors/Util.swift new file mode 100644 index 000000000..17c8eba90 --- /dev/null +++ b/Tools/changelog-authors/Util.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +// 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 + } +} diff --git a/Tools/generate-manual/AuthorArgument.swift b/Tools/generate-manual/AuthorArgument.swift new file mode 100644 index 000000000..11092ecf9 --- /dev/null +++ b/Tools/generate-manual/AuthorArgument.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +fileprivate extension Character { + static let emailStart: Character = "<" + static let emailEnd: Character = ">" +} + +fileprivate extension Substring { + mutating func collecting(until terminator: (Element) throws -> Bool) rethrows -> String { + let terminatorIndex = try firstIndex(where: terminator) ?? endIndex + let collected = String(self[..<terminatorIndex]) + self = self[terminatorIndex...] + return collected + } + + mutating func next() { + if !isEmpty { removeFirst() } + } +} + +enum AuthorArgument { + case name(name: String) + case email(email: String) + case both(name: String, email: String) +} + +extension AuthorArgument: ExpressibleByArgument { + // parsed as: + // - name: `name` + // - email: `<email>` + // - both: `name<email>` + public init?(argument: String) { + var argument = argument[...] + // collect until the email start character is seen. + let name = argument.collecting(until: { $0 == .emailStart }) + // drop the email start character. + argument.next() + // collect until the email end character is seen. + let email = argument.collecting(until: { $0 == .emailEnd }) + // drop the email end character. + argument.next() + // ensure no collected characters remain. + guard argument.isEmpty else { return nil } + + switch (name.isEmpty, email.isEmpty) { + case (true, true): + return nil + case (false, true): + self = .name(name: name) + case (true, false): + self = .email(email: email) + case (false, false): + self = .both(name: name, email: email) + } + } +} diff --git a/Tools/generate-manual/DSL/ArgumentSynopsis.swift b/Tools/generate-manual/DSL/ArgumentSynopsis.swift new file mode 100644 index 000000000..e57eb42b5 --- /dev/null +++ b/Tools/generate-manual/DSL/ArgumentSynopsis.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct ArgumentSynopsis: MDocComponent { + var argument: ArgumentInfoV0 + + var body: MDocComponent { + if argument.isOptional { + MDocMacro.OptionalCommandLineComponent(arguments: [synopsis]) + } else { + synopsis + } + } + + // ArgumentInfoV0 formatted as MDoc without optional bracket wrapper. + var synopsis: MDocASTNode { + switch argument.kind { + case .positional: + return argument.manualPageDescription + case .option: + // preferredName cannot be nil + let name = argument.preferredName! + return MDocMacro.CommandOption(options: [name.manualPage]) + case .flag: + // preferredName cannot be nil + let name = argument.preferredName! + return MDocMacro.CommandOption(options: [name.manualPage]) + .withUnsafeChildren(nodes: [argument.manualPageValueName]) + } + } +} diff --git a/Tools/generate-manual/DSL/Author.swift b/Tools/generate-manual/DSL/Author.swift new file mode 100644 index 000000000..9ba1f3ef2 --- /dev/null +++ b/Tools/generate-manual/DSL/Author.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct Author: MDocComponent { + var author: AuthorArgument + var trailing: String + + var body: MDocComponent { + switch author { + case let .name(name): + MDocMacro.Author(split: false) + MDocMacro.Author(name: name) + .withUnsafeChildren(nodes: [trailing]) + case let .email(email): + MDocMacro.MailTo(email: email) + .withUnsafeChildren(nodes: [trailing]) + case let .both(name, email): + MDocMacro.Author(split: false) + MDocMacro.Author(name: name) + MDocMacro.BeginAngleBrackets() + MDocMacro.MailTo(email: email) + MDocMacro.EndAngleBrackets() + .withUnsafeChildren(nodes: [trailing]) + } + } +} diff --git a/Tools/generate-manual/DSL/Authors.swift b/Tools/generate-manual/DSL/Authors.swift new file mode 100644 index 000000000..00b363891 --- /dev/null +++ b/Tools/generate-manual/DSL/Authors.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct Authors: MDocComponent { + var authors: [AuthorArgument] + + var body: MDocComponent { + Section(title: "authors") { + if !authors.isEmpty { + "The" + MDocMacro.DocumentName() + "reference was written by" + ForEach(authors) { author, last in + Author(author: author, trailing: last ? "." : ",") + } + } + } + } +} diff --git a/Tools/generate-manual/DSL/Core/Container.swift b/Tools/generate-manual/DSL/Core/Container.swift new file mode 100644 index 000000000..a0651d3dc --- /dev/null +++ b/Tools/generate-manual/DSL/Core/Container.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct Container: MDocComponent { + var ast: [MDocASTNode] { children.flatMap { $0.ast } } + var body: MDocComponent { self } + var children: [MDocComponent] +} diff --git a/Tools/generate-manual/DSL/Core/Empty.swift b/Tools/generate-manual/DSL/Core/Empty.swift new file mode 100644 index 000000000..d681fcca7 --- /dev/null +++ b/Tools/generate-manual/DSL/Core/Empty.swift @@ -0,0 +1,15 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct Empty: MDocComponent { + var ast: [MDocASTNode] { [] } + var body: MDocComponent { self } +} diff --git a/Tools/generate-manual/DSL/Core/ForEach.swift b/Tools/generate-manual/DSL/Core/ForEach.swift new file mode 100644 index 000000000..6593d7c63 --- /dev/null +++ b/Tools/generate-manual/DSL/Core/ForEach.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct ForEach<C>: MDocComponent where C: Collection { + var items: C + var builder: (C.Element, Bool) -> MDocComponent + + init(_ items: C, @MDocBuilder builder: @escaping (C.Element, Bool) -> MDocComponent) { + self.items = items + self.builder = builder + } + + var body: MDocComponent { + guard !items.isEmpty else { return Empty() } + var currentIndex = items.startIndex + var last = false + var components = [MDocComponent]() + repeat { + let item = items[currentIndex] + currentIndex = items.index(after: currentIndex) + last = currentIndex == items.endIndex + components.append(builder(item, last)) + } while !last + return Container(children: components) + } +} diff --git a/Tools/generate-manual/DSL/Core/MDocASTNodeWrapper.swift b/Tools/generate-manual/DSL/Core/MDocASTNodeWrapper.swift new file mode 100644 index 000000000..1f3c2de1c --- /dev/null +++ b/Tools/generate-manual/DSL/Core/MDocASTNodeWrapper.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct MDocASTNodeWrapper: MDocComponent { + var ast: [MDocASTNode] { [node] } + var body: MDocComponent { self } + var node: MDocASTNode +} diff --git a/Tools/generate-manual/DSL/Core/MDocBuilder.swift b/Tools/generate-manual/DSL/Core/MDocBuilder.swift new file mode 100644 index 000000000..2cfafbb4b --- /dev/null +++ b/Tools/generate-manual/DSL/Core/MDocBuilder.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +@resultBuilder +struct MDocBuilder { + static func buildBlock(_ components: MDocComponent...) -> MDocComponent { Container(children: components) } + static func buildArray(_ components: [MDocComponent]) -> MDocComponent { Container(children: components) } + static func buildOptional(_ component: MDocComponent?) -> MDocComponent { component ?? Empty() } + static func buildEither(first component: MDocComponent) -> MDocComponent { component } + static func buildEither(second component: MDocComponent) -> MDocComponent { component } + static func buildExpression(_ expression: MDocComponent) -> MDocComponent { expression } + static func buildExpression(_ expression: MDocASTNode) -> MDocComponent { MDocASTNodeWrapper(node: expression) } +} diff --git a/Tests/LinuxMain.swift b/Tools/generate-manual/DSL/Core/MDocComponent.swift similarity index 60% rename from Tests/LinuxMain.swift rename to Tools/generate-manual/DSL/Core/MDocComponent.swift index 917e33104..12fc67b2f 100644 --- a/Tests/LinuxMain.swift +++ b/Tools/generate-manual/DSL/Core/MDocComponent.swift @@ -2,18 +2,19 @@ // // This source file is part of the Swift Argument Parser open source project // -// Copyright (c) 2020 Apple Inc. and the Swift project authors +// 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 // //===----------------------------------------------------------------------===// -#error(""" +protocol MDocComponent { + var ast: [MDocASTNode] { get } + @MDocBuilder + var body: MDocComponent { get } +} - ----------------------------------------------------- - - Please test with `swift test --enable-test-discovery` - - ----------------------------------------------------- - """) +extension MDocComponent { + var ast: [MDocASTNode] { body.ast } +} diff --git a/Tools/generate-manual/DSL/Document.swift b/Tools/generate-manual/DSL/Document.swift new file mode 100644 index 000000000..dfb0514f9 --- /dev/null +++ b/Tools/generate-manual/DSL/Document.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo +import Foundation + +struct Document: MDocComponent { + var singlePage: Bool + var date: Date + var section: Int + var authors: [AuthorArgument] + var command: CommandInfoV0 + + var body: MDocComponent { + Preamble(date: date, section: section, command: command) + Name(command: command) + Synopsis(command: command) + if singlePage { + SinglePageDescription(command: command) + } else { + MultiPageDescription(command: command) + } + Exit(section: section) + if !singlePage { + SeeAlso(section: section, command: command) + } + Authors(authors: authors) + } +} diff --git a/Tools/generate-manual/DSL/DocumentDate.swift b/Tools/generate-manual/DSL/DocumentDate.swift new file mode 100644 index 000000000..a014ef50d --- /dev/null +++ b/Tools/generate-manual/DSL/DocumentDate.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo +import Foundation + +struct DocumentDate: MDocComponent { + private var month: String + private var day: Int + private var year: Int + + init(date: Date) { + let calendar = Calendar(identifier: .iso8601) + let timeZone = TimeZone(identifier: "UTC")! + let formatter = DateFormatter() + formatter.calendar = calendar + formatter.timeZone = timeZone + formatter.dateFormat = "MMMM" + self.month = formatter.string(from: date) + let components = calendar.dateComponents(in: timeZone, from: date) + self.day = components.day! + self.year = components.year! + } + + var body: MDocComponent { + MDocMacro.DocumentDate(day: day, month: month, year: year) + } +} diff --git a/Tools/generate-manual/DSL/Exit.swift b/Tools/generate-manual/DSL/Exit.swift new file mode 100644 index 000000000..a67a2f38a --- /dev/null +++ b/Tools/generate-manual/DSL/Exit.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct Exit: MDocComponent { + var section: Int + + var body: MDocComponent { + Section(title: "exit status") { + if [1, 6, 8].contains(section) { + MDocMacro.ExitStandard() + } + } + } +} diff --git a/Tools/generate-manual/DSL/List.swift b/Tools/generate-manual/DSL/List.swift new file mode 100644 index 000000000..516dc680a --- /dev/null +++ b/Tools/generate-manual/DSL/List.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct List: MDocComponent { + var content: MDocComponent + + init(@MDocBuilder content: () -> MDocComponent) { + self.content = content() + } + + var body: MDocComponent { + if !content.ast.isEmpty { + MDocMacro.BeginList(style: .tag, width: 6) + content + MDocMacro.EndList() + } + } +} diff --git a/Tools/generate-manual/DSL/MultiPageDescription.swift b/Tools/generate-manual/DSL/MultiPageDescription.swift new file mode 100644 index 000000000..77aa97ef9 --- /dev/null +++ b/Tools/generate-manual/DSL/MultiPageDescription.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct MultiPageDescription: MDocComponent { + var command: CommandInfoV0 + + var body: MDocComponent { + Section(title: "description") { + if let discussion = command.discussion { + discussion + } + + List { + for argument in command.arguments ?? [] { + MDocMacro.ListItem(title: argument.manualPageDescription) + + if let abstract = argument.abstract { + abstract + } + + if argument.abstract != nil, argument.discussion != nil { + MDocMacro.ParagraphBreak() + } + + if let discussion = argument.discussion { + discussion + } + } + } + } + } +} diff --git a/Tools/generate-manual/DSL/Name.swift b/Tools/generate-manual/DSL/Name.swift new file mode 100644 index 000000000..4eacd8207 --- /dev/null +++ b/Tools/generate-manual/DSL/Name.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct Name: MDocComponent { + var command: CommandInfoV0 + + var body: MDocComponent { + Section(title: "name") { + MDocMacro.DocumentName(name: command.manualPageName) + if let abstract = command.abstract { + MDocMacro.DocumentDescription(description: abstract) + } + } + } +} diff --git a/Tools/generate-manual/DSL/Preamble.swift b/Tools/generate-manual/DSL/Preamble.swift new file mode 100644 index 000000000..a75383c0b --- /dev/null +++ b/Tools/generate-manual/DSL/Preamble.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo +import Foundation + +struct Preamble: MDocComponent { + var date: Date + var section: Int + var command: CommandInfoV0 + + var body: MDocComponent { + MDocMacro.Comment("Generated by swift-argument-parser") + DocumentDate(date: date) + MDocMacro.DocumentTitle(title: command.manualPageDocumentTitle, section: section) + MDocMacro.OperatingSystem() + } +} diff --git a/Tools/generate-manual/DSL/Section.swift b/Tools/generate-manual/DSL/Section.swift new file mode 100644 index 000000000..c741b1993 --- /dev/null +++ b/Tools/generate-manual/DSL/Section.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +struct Section: MDocComponent { + var title: String + var content: MDocComponent + + init(title: String, @MDocBuilder content: () -> MDocComponent) { + self.title = title + self.content = content() + } + + var body: MDocComponent { + if !content.ast.isEmpty { + MDocMacro.SectionHeader(title: title.uppercased()) + content + } + } +} diff --git a/Tools/generate-manual/DSL/SeeAlso.swift b/Tools/generate-manual/DSL/SeeAlso.swift new file mode 100644 index 000000000..ac3590297 --- /dev/null +++ b/Tools/generate-manual/DSL/SeeAlso.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct SeeAlso: MDocComponent { + var section: Int + var command: CommandInfoV0 + private var references: [String] { + (command.subcommands ?? []) + .map(\.manualPageTitle) + .sorted() + } + + var body: MDocComponent { + Section(title: "see also") { + ForEach(references) { reference, isLast in + MDocMacro.CrossManualReference(title: reference, section: section) + .withUnsafeChildren(nodes: isLast ? [] : [","]) + } + } + } +} diff --git a/Tools/generate-manual/DSL/SinglePageDescription.swift b/Tools/generate-manual/DSL/SinglePageDescription.swift new file mode 100644 index 000000000..dbbe947a2 --- /dev/null +++ b/Tools/generate-manual/DSL/SinglePageDescription.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct SinglePageDescription: MDocComponent { + var command: CommandInfoV0 + + var body: MDocComponent { + Section(title: "description") { + core + } + } + + @MDocBuilder + var core: MDocComponent { + if let discussion = command.discussion { + discussion + } + + List { + for argument in command.arguments ?? [] { + MDocMacro.ListItem(title: argument.manualPageDescription) + + if let abstract = argument.abstract { + abstract + } + + if argument.abstract != nil, argument.discussion != nil { + MDocMacro.ParagraphBreak() + } + + if let discussion = argument.discussion { + discussion + } + } + + for subcommand in command.subcommands ?? [] { + MDocMacro.ListItem(title: MDocMacro.Emphasis(arguments: [subcommand.commandName])) + SinglePageDescription(command: subcommand).core + } + } + } +} diff --git a/Tools/generate-manual/DSL/Synopsis.swift b/Tools/generate-manual/DSL/Synopsis.swift new file mode 100644 index 000000000..1b978317b --- /dev/null +++ b/Tools/generate-manual/DSL/Synopsis.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +struct Synopsis: MDocComponent { + var command: CommandInfoV0 + + var body: MDocComponent { + Section(title: "synopsis") { + MDocMacro.DocumentName() + + if command.subcommands != nil { + if command.defaultSubcommand != nil { + MDocMacro.BeginOptionalCommandLineComponent() + } + MDocMacro.CommandArgument(arguments: ["subcommand"]) + if command.defaultSubcommand != nil { + MDocMacro.EndOptionalCommandLineComponent() + } + } + for argument in command.arguments ?? [] { + ArgumentSynopsis(argument: argument) + } + } + } +} diff --git a/Tools/generate-manual/Extensions/ArgumentParser+MDoc.swift b/Tools/generate-manual/Extensions/ArgumentParser+MDoc.swift new file mode 100644 index 000000000..77dfd4980 --- /dev/null +++ b/Tools/generate-manual/Extensions/ArgumentParser+MDoc.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo + +extension CommandInfoV0 { + func manualPageFileName(section: Int) -> String { + manualPageTitle + ".\(section)" + } + + var manualPageDocumentTitle: String { + let parts = (superCommands ?? []) + [commandName] + return parts.joined(separator: ".").uppercased() + } + + var manualPageTitle: String { + let parts = (superCommands ?? []) + [commandName] + return parts.joined(separator: ".") + } + + var manualPageName: String { + let parts = (superCommands ?? []) + [commandName] + return parts.joined(separator: " ") + } +} + +extension ArgumentInfoV0 { + // ArgumentInfoV0 value name as MDoc with "..." appended if the argument is + // repeating. + var manualPageValueName: MDocASTNode { + var valueName = valueName ?? "" + if isRepeating { + valueName += "..." + } + // FIXME: MDocMacro.Emphasis? + return MDocMacro.CommandArgument(arguments: [valueName]) + } + + // ArgumentDefinition formatted as MDoc for use in a description section. + var manualPageDescription: MDocASTNode { + // names.partitioned.map(\.manualPage).interspersed(with: ",") + var synopses = (names ?? []).partitioned + .flatMap { [$0.manualPage, ","] } + synopses = synopses.dropLast() + + switch kind { + case .positional: + return manualPageValueName + case .option: + return MDocMacro.CommandOption(options: synopses) + .withUnsafeChildren(nodes: [manualPageValueName]) + case .flag: + return MDocMacro.CommandOption(options: synopses) + } + } +} + +extension ArgumentInfoV0.NameInfoV0 { + // Name formatted as MDoc. + var manualPage: MDocASTNode { + switch kind { + case .long: + return "-\(name)" + case .short: + return name + case .longWithSingleDash: + return name + } + } +} + +extension Array where Element == ParsableCommand.Type { + var commandNames: [String] { + var commandNames = [String]() + if let superName = first?.configuration._superCommandName { + commandNames.append(superName) + } + commandNames.append(contentsOf: map { $0._commandName }) + return commandNames + } +} + +extension BidirectionalCollection where Element == ArgumentInfoV0.NameInfoV0 { + var preferredName: Element? { + first { $0.kind != .short } ?? first + } + + var partitioned: [Element] { + filter { $0.kind == .short } + filter { $0.kind != .short } + } +} diff --git a/Tools/generate-manual/Extensions/Date+ExpressibleByArgument.swift b/Tools/generate-manual/Extensions/Date+ExpressibleByArgument.swift new file mode 100644 index 000000000..cf1709783 --- /dev/null +++ b/Tools/generate-manual/Extensions/Date+ExpressibleByArgument.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import Foundation + +extension Date: ExpressibleByArgument { + // parsed as `yyyy-mm-dd` + public init?(argument: String) { + // ensure the input argument is composed of exactly 3 components separated + // by dashes ('-') + let components = argument.split(separator: "-") + let empty = components.filter { $0.isEmpty } + guard components.count == 3, empty.count == 0 else { return nil } + + // ensure the year component is exactly 4 characters + let _year = components[0] + guard _year.count == 4, let year = Int(_year) else { return nil } + + // ensure the month component is exactly 2 characters + let _month = components[1] + guard _month.count == 2, let month = Int(_month) else { return nil } + + // ensure the day component is exactly 2 characters + let _day = components[2] + guard _day.count == 2, let day = Int(_day) else { return nil } + + // ensure the combination of year, month, day is valid + let dateComponents = DateComponents( + calendar: Calendar(identifier: .iso8601), + timeZone: TimeZone(identifier: "UTC"), + year: year, + month: month, + day: day) + guard dateComponents.isValidDate else { return nil } + guard let date = dateComponents.date else { return nil } + self = date + } +} diff --git a/Tools/generate-manual/Extensions/Process+SimpleAPI.swift b/Tools/generate-manual/Extensions/Process+SimpleAPI.swift new file mode 100644 index 000000000..5e01fcc2e --- /dev/null +++ b/Tools/generate-manual/Extensions/Process+SimpleAPI.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------*- 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 Foundation + +enum SubprocessError: Swift.Error, LocalizedError, CustomStringConvertible { + case missingExecutable(url: URL) + case failedToLaunch(error: Swift.Error) + case nonZeroExitCode(code: Int) + + var description: String { + switch self { + case .missingExecutable(let url): + return "No executable at '\(url.standardizedFileURL.path)'." + case .failedToLaunch(let error): + return "Couldn't run command process. \(error.localizedDescription)" + case .nonZeroExitCode(let code): + return "Process returned non-zero exit code '\(code)'." + } + } + + var errorDescription: String? { description } +} + +func executeCommand( + executable: URL, + arguments: [String] +) throws -> String { + guard (try? executable.checkResourceIsReachable()) ?? false else { + throw SubprocessError.missingExecutable(url: executable) + } + + let process = Process() + if #available(macOS 10.13, *) { + process.executableURL = executable + } else { + process.launchPath = executable.path + } + process.arguments = arguments + + let output = Pipe() + process.standardOutput = output + process.standardError = FileHandle.nullDevice + + if #available(macOS 10.13, *) { + do { + try process.run() + } catch { + throw SubprocessError.failedToLaunch(error: error) + } + } else { + process.launch() + } + let outputData = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + + guard process.terminationStatus == 0 else { + throw SubprocessError.nonZeroExitCode(code: Int(process.terminationStatus)) + } + + let outputActual = String(data: outputData, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) + ?? "" + + return outputActual +} diff --git a/Tools/generate-manual/GenerateManual.swift b/Tools/generate-manual/GenerateManual.swift new file mode 100644 index 000000000..0520c0f44 --- /dev/null +++ b/Tools/generate-manual/GenerateManual.swift @@ -0,0 +1,130 @@ +//===----------------------------------------------------------*- 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 ArgumentParser +import ArgumentParserToolInfo +import Foundation + +@main +struct GenerateManual: ParsableCommand { + enum Error: Swift.Error { + case failedToRunSubprocess(error: Swift.Error) + case unableToParseToolOutput(error: Swift.Error) + case unsupportedDumpHelpVersion(expected: Int, found: Int) + case failedToGenerateManualPages(error: Swift.Error) + } + + static let configuration = CommandConfiguration( + commandName: "generate-manual", + abstract: "Generate a manual for the provided tool.") + + @Argument(help: "Tool to generate manual for.") + var tool: String + + @Flag(help: "Generate a single page with information for all subcommands.") + var singlePage = false + + @Option(name: .long, help: "Override the creation date of the manual. Format: 'yyyy-mm-dd'.") + var date: Date = Date() + + @Option(name: .long, help: "Section of the manual.") + var section: Int = 1 + + @Option(name: .long, help: "Names and/or emails of the tool's authors. Format: 'name<email>'.") + var authors: [AuthorArgument] = [] + + @Option(name: .shortAndLong, help: "Directory to save generated manual. Use '-' for stdout.") + var outputDirectory: String + + func validate() throws { + // Only man pages 1 through 9 are valid. + if !(1...9).contains(section) { + throw ValidationError("Invalid manual section passed to --section") + } + + if outputDirectory != "-" { + // outputDirectory must already exist, `GenerateManual` will not create it. + var objcBool: ObjCBool = true + guard FileManager.default.fileExists(atPath: outputDirectory, isDirectory: &objcBool) else { + throw ValidationError("Output directory \(outputDirectory) does not exist") + } + + guard objcBool.boolValue else { + throw ValidationError("Output directory \(outputDirectory) is not a directory") + } + } + } + + func run() throws { + let data: Data + do { + let tool = URL(fileURLWithPath: tool) + let output = try executeCommand(executable: tool, arguments: ["--experimental-dump-help"]) + data = output.data(using: .utf8) ?? Data() + } catch { + throw Error.failedToRunSubprocess(error: error) + } + + do { + let toolInfoThin = try JSONDecoder().decode(ToolInfoHeader.self, from: data) + guard toolInfoThin.serializationVersion == 0 else { + throw Error.unsupportedDumpHelpVersion( + expected: 0, + found: toolInfoThin.serializationVersion) + } + } catch { + throw Error.unableToParseToolOutput(error: error) + } + + let toolInfo: ToolInfoV0 + do { + toolInfo = try JSONDecoder().decode(ToolInfoV0.self, from: data) + } catch { + throw Error.unableToParseToolOutput(error: error) + } + + do { + if outputDirectory == "-" { + try generatePages(from: toolInfo.command, savingTo: nil) + } else { + try generatePages( + from: toolInfo.command, + savingTo: URL(fileURLWithPath: outputDirectory)) + } + } catch { + throw Error.failedToGenerateManualPages(error: error) + } + } + + func generatePages(from command: CommandInfoV0, savingTo directory: URL?) throws { + let document = Document( + singlePage: singlePage, + date: date, + section: section, + authors: authors, + command: command) + let page = document.ast.map { $0.serialized() }.joined(separator: "\n") + + if let directory = directory { + let fileName = command.manualPageFileName(section: section) + let outputPath = directory.appendingPathComponent(fileName) + try page.write(to: outputPath, atomically: false, encoding: .utf8) + } else { + print(page) + } + + if !singlePage { + for subcommand in command.subcommands ?? [] { + try generatePages(from: subcommand, savingTo: directory) + } + } + } +} diff --git a/Tools/generate-manual/MDoc/MDocASTNode.swift b/Tools/generate-manual/MDoc/MDocASTNode.swift new file mode 100644 index 000000000..177cdaf2c --- /dev/null +++ b/Tools/generate-manual/MDoc/MDocASTNode.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// `MDocASTNode` represents a single abstract syntax tree node in an `mdoc` +/// document. `mdoc` is a semantic markup language for formatting manual pages. +/// +/// See: https://mandoc.bsd.lv/man/mdoc.7.html for more information. +public protocol MDocASTNode { + /// `_serialized` is an implementation detail and should not be used directly. + /// Please use `serialized` instead. + func _serialized(context: MDocSerializationContext) -> String +} + +extension MDocASTNode { + /// `serialized` Serializes an MDocASTNode and children into its string + /// representation for use with other tools. + public func serialized() -> String { + _serialized(context: MDocSerializationContext()) + } +} + +extension Int: MDocASTNode { + public func _serialized(context: MDocSerializationContext) -> String { + "\(self)" + } +} + +extension String: MDocASTNode { + public func _serialized(context: MDocSerializationContext) -> String { + context.macroLine + ? self.escapedMacroArgument() + : self.escapedTextLine() + } +} diff --git a/Tools/generate-manual/MDoc/MDocMacro.swift b/Tools/generate-manual/MDoc/MDocMacro.swift new file mode 100644 index 000000000..82b413639 --- /dev/null +++ b/Tools/generate-manual/MDoc/MDocMacro.swift @@ -0,0 +1,1510 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +//===--------------------------------------------------------*- openbsd -*-===// +// +// This source file contains descriptions of mandoc syntax tree nodes derived +// from their original descriptions in the mandoc source found here: +// https://github.com/openbsd/src/blob/master/share/man/man7/mdoc.7 +// +// $Id: LICENSE,v 1.22 2021/09/19 11:02:09 schwarze Exp $ +// +// With the exceptions noted below, all non-trivial files contained +// in the mandoc toolkit are protected by the Copyright of the following +// developers: +// +// Copyright (c) 2008-2012, 2014 Kristaps Dzonsons <kristaps@bsd.lv> +// Copyright (c) 2010-2021 Ingo Schwarze <schwarze@openbsd.org> +// Copyright (c) 1999, 2004, 2017 Marc Espie <espie@openbsd.org> +// Copyright (c) 2009, 2010, 2011, 2012 Joerg Sonnenberger <joerg@netbsd.org> +// Copyright (c) 2013 Franco Fichtner <franco@lastsummer.de> +// Copyright (c) 2014 Baptiste Daroussin <bapt@freebsd.org> +// Copyright (c) 2016 Ed Maste <emaste@freebsd.org> +// Copyright (c) 2017 Michael Stapelberg <stapelberg@debian.org> +// Copyright (c) 2017 Anthony Bentley <bentley@openbsd.org> +// Copyright (c) 1998, 2004, 2010, 2015 Todd C. Miller <Todd.Miller@courtesan.com> +// Copyright (c) 2008, 2017 Otto Moerbeek <otto@drijf.net> +// Copyright (c) 2004 Ted Unangst <tedu@openbsd.org> +// Copyright (c) 1994 Christos Zoulas <christos@netbsd.org> +// Copyright (c) 2003, 2007, 2008, 2014 Jason McIntyre <jmc@openbsd.org> +// +// See the individual files for information about who contributed +// to which file during which years. +// +// +// The mandoc distribution as a whole is distributed by its developers +// under the following license: +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// +// The following files included from outside sources are protected by +// other people's Copyright and are distributed under various 2-clause +// and 3-clause BSD licenses; see these individual files for details. +// +// soelim.c, soelim.1: +// Copyright (c) 2014 Baptiste Daroussin <bapt@freebsd.org> +// +// compat_err.c, compat_fts.c, compat_fts.h, +// compat_getsubopt.c, compat_strcasestr.c, compat_strsep.c, +// man.1: +// Copyright (c) 1989,1990,1993,1994 The Regents of the University of California +// +// compat_stringlist.c, compat_stringlist.h: +// Copyright (c) 1994 Christos Zoulas <christos@netbsd.org> +// +// See https://mandoc.bsd.lv/LICENSE for license information +// +//===----------------------------------------------------------------------===// + +fileprivate extension Array { + mutating func append(optional newElement: Element?) { + if let newElement = newElement { + append(newElement) + } + } +} + +/// `MDocMacroProtocol` defines the properties required to serialize a +/// strongly-typed mdoc macro to the raw format. +public protocol MDocMacroProtocol: MDocASTNode { + /// The underlying `mdoc` macro string; used during serialization. + static var kind: String { get } + /// The arguments passed to the underlying `mdoc` macro; used during + /// serialization. + var arguments: [MDocASTNode] { get set } +} + +extension MDocMacroProtocol { + /// Append unchecked arguments to a `MDocMacroProtocol`. + public func withUnsafeChildren(nodes: [MDocASTNode]) -> Self { + var copy = self + copy.arguments.append(contentsOf: nodes) + return copy + } +} + +extension MDocMacroProtocol { + public func _serialized(context: MDocSerializationContext) -> String { + var result = "" + + // Prepend a dot if we aren't already in a macroLine context + if !context.macroLine { + result += "." + } + result += Self.kind + + if !arguments.isEmpty { + var context = context + context.macroLine = true + + result += " " + result += arguments + .map { $0._serialized(context: context) } + .joined(separator: " ") + } + return result + } +} + +/// `MDocMacro` is a namespace for types conforming to ``MDocMacroProtocol``. +public enum MDocMacro { + /// Comment placed inline in the manual page. + /// + /// Comment are not displayed by tools consuming serialized manual pages. + /// + /// __Example Usage__: + /// ```swift + /// Comment("WIP: History section...") + /// ``` + public struct Comment: MDocMacroProtocol { + public static let kind = #"\""# + public var arguments: [MDocASTNode] + /// Creates a new `Comment` macro. + /// + /// - Parameters: + /// - comment: A string to insert as an inline comment. + public init(_ comment: String) { + self.arguments = [comment] + } + } + + // MARK: - Document preamble and NAME section macros + + /// Document date displayed in the manual page footer. + /// + /// This must be the first macro in any `mdoc` document. + /// + /// __Example Usage__: + /// ```swift + /// DocumentDate(day: 9, month: "September", year: 2014) + /// ``` + public struct DocumentDate: MDocMacroProtocol { + public static let kind = "Dd" + public var arguments: [MDocASTNode] + /// Creates a new `DocumentDate` macro. + /// + /// - Parameters: + /// - day: An integer number day of the month the manual was written. + /// - month: The full English month name the manual was written. + /// - year: The four digit year the manual was written. + public init(day: Int, month: String, year: Int) { + arguments = [month, "\(day),", year] + } + } + + /// Document title displayed in the manual page header. + /// + /// This must be the second macro in any `mdoc` document. + /// + /// __Example Usage__: + /// ```swift + /// DocumentTitle(title: "swift", section: 1) + /// DocumentTitle(title: "swift", section: 1, arch: "arm64e") + /// ``` + public struct DocumentTitle: MDocMacroProtocol { + public static let kind = "Dt" + public var arguments: [MDocASTNode] + /// Creates a new `DocumentTitle` macro. + /// + /// - Parameters: + /// - title: The document's title or name. By convention the title should + /// be all caps. + /// - section: The manual section. The section should match the manual + /// page's file extension. Must be one of the following values: + /// 1. General Commands + /// 2. System Calls + /// 3. Library Functions + /// 4. Device Drivers + /// 5. File Formats + /// 6. Games + /// 7. Miscellaneous Information + /// 8. System Manager's Manual + /// 9. Kernel Developer's Manual + /// - arch: The machine architecture the manual page applies to, for + /// example: `alpha`, `i386`, `x86_64` or `arm64e`. + public init(title: String, section: Int, arch: String? = nil) { + precondition((1...9).contains(section)) + self.arguments = [title, section] + self.arguments.append(optional: arch) + } + } + + /// Operating system and version displayed in the manual page footer. + /// + /// This must be the third macro in any `mdoc` document. + /// + /// __Example Usage__: + /// ```swift + /// OperatingSystem() + /// OperatingSystem(name: "macOS") + /// OperatingSystem(name: "macOS", version: "10.13") + /// ``` + public struct OperatingSystem: MDocMacroProtocol { + public static let kind = "Os" + public var arguments: [MDocASTNode] + /// Creates a new `OperatingSystem` macro. + /// + /// - Note: The `version` parameter must not be specified without the `name` + /// parameter. + /// + /// - Parameters: + /// - name: The operating system the manual page contents is valid for. + /// Omitting `name` is recommended and will result in the user's + /// operating system name being used. + /// - version: The version the of the operating system specified by `name` + /// the manual page contents is valid for. Omitting `version` is + /// recommended. + public init(name: String? = nil, version: String? = nil) { + precondition(!(name == nil && version != nil)) + self.arguments = [] + self.arguments.append(optional: name) + self.arguments.append(optional: version) + } + } + + /// The name of the manual page. + /// + /// The first use of ``DocumentName`` is typically in the "NAME" section. The + /// name provided to the created ``DocumentName`` will be remembered and + /// subsequent uses of the ``DocumentName`` can omit the name argument. + /// + /// - Note: Manual pages in sections 1, 6, and 8 may use the name of command + /// or feature documented in the manual page as the name. + /// + /// In sections 2, 3, and 9 use the ``FunctionName`` macro instead of the + /// ``DocumentName`` macro to indicate the name of the document. + /// + /// __Example Usage__: + /// ```swift + /// SectionHeader(title: "SYNOPSIS") + /// DocumentName(name: "swift") + /// OptionalCommandLineComponent(arguments: CommandArgument(arguments: ["h"])) + /// ``` + public struct DocumentName: MDocMacroProtocol { + public static let kind = "Nm" + public var arguments: [MDocASTNode] + /// Creates a new `DocumentName` macro. + /// + /// - Parameters: + /// - name: The name of the manual page. + public init(name: String? = nil) { + self.arguments = [] + self.arguments.append(optional: name) + } + } + + /// Single line description of the manual page. + /// + /// This must be the last macro in the "NAME" section `mdoc` document and + /// should not appear in any other section. + /// + /// __Example Usage__: + /// ```swift + /// DocumentDescription(description: "Safe, fast, and expressive general-purpose programming language") + /// ``` + public struct DocumentDescription: MDocMacroProtocol { + public static let kind = "Nd" + public var arguments: [MDocASTNode] + /// Creates a new `DocumentDescription` macro. + /// + /// - Parameters: + /// - description: The description of the manual page. + public init(description: String) { + self.arguments = [description] + } + } + + // MARK: - Sections and cross references + + /// Start a new manual section. + /// + /// See [Manual Structure](http://mandoc.bsd.lv/man/mdoc.7.html#MANUAL_STRUCTURE) + /// for a list of standard sections. Custom sections should be avoided though + /// can be used. + /// + /// - Note: Section names should be unique so they can be referenced using a + /// ``SectionReference``. + /// + /// __Example Usage__: + /// ```swift + /// SectionHeader(title: "NAME") + /// ``` + public struct SectionHeader: MDocMacroProtocol { + public static let kind = "Sh" + public var arguments: [MDocASTNode] + /// Creates a new `SectionHeader` macro. + /// + /// - Parameters: + /// - title: The title of the section. + public init(title: String) { + self.arguments = [title] + } + } + + /// Start a new manual subsection. + /// + /// There is no standard naming convention of subsections. + /// + /// - Note: Subsection names should be unique so they can be referenced using + /// a ``SectionReference``. + /// + /// __Example Usage__: + /// ```swift + /// SubsectionHeader(title: "DETAILS") + /// ``` + public struct SubsectionHeader: MDocMacroProtocol { + public static let kind = "Ss" + public var arguments: [MDocASTNode] + /// Creates a new `SubsectionHeader` macro. + /// + /// - Parameters: + /// - title: The title of the subsection. + public init(title: String) { + self.arguments = [title] + } + } + + /// Reference a section or subsection in the same manual page. + /// + /// The section or subsection title must exactly match the title passed to + /// ``SectionReference``. + /// + /// __Example Usage__: + /// ```swift + /// SectionReference(title: "NAME") + /// ``` + public struct SectionReference: MDocMacroProtocol { + public static let kind = "Sx" + public var arguments: [MDocASTNode] + /// Creates a new `SectionReference` macro. + /// + /// - Parameters: + /// - title: The title of the section or subsection to reference. + public init(title: String) { + self.arguments = [title] + } + } + + /// Reference another manual page. + /// + /// __Example Usage__: + /// ```swift + /// CrossManualReference(title: "swift", section: 1) + /// ``` + public struct CrossManualReference: MDocMacroProtocol { + public static let kind = "Xr" + public var arguments: [MDocASTNode] + /// Creates a new `CrossManualReference` macro. + /// + /// - Parameters: + /// - title: The title of the section or subsection to reference. + public init(title: String, section: Int) { + precondition((1...9).contains(section)) + self.arguments = [title, section] + } + } + + /// Whitespace break between paragaphs. + /// + /// Breaks should not be inserted immeediately before or after + /// ``SectionHeader``, ``SubsectionHeader``, and ``BeginList`` macros. + public struct ParagraphBreak: MDocMacroProtocol { + public static let kind = "Pp" + public var arguments: [MDocASTNode] + /// Creates a new `ParagraphBreak` macro. + public init() { + self.arguments = [] + } + } + + // MARK: - Displays and lists + + // Display block: -type [-offset width] [-compact]. + // TODO: "Ed" + + // Indented display (one line). + // TODO: "D1" + + // Indented literal display (one line). + // TODO: "Dl" + + // In-line literal display: ‘text’. + // TODO: "Ql" + + // FIXME: Documentation + /// Open a list scope. + /// + /// Closed by an ``EndList`` macro. + /// + /// Lists are made of ``ListItem``s which are displayed in a variety of styles + /// depending on the ``ListStyle`` used to create the list scope. + /// List scopes can be nested in other list scopes, however nesting `.column` + /// and `ListStyle.enum` lists is not recommended as they may display inconsistently + /// between tools. + /// + /// __Example Usage__: + /// ```swift + /// BeginList(style: .tag, width: 6) + /// ListItem(title: "Hello, Swift!") + /// "Welcome to the Swift programming language." + /// ListItem(title: "Goodbye!") + /// EndList() + /// ``` + public struct BeginList: MDocMacroProtocol { + /// Enumeration of styles supported by the ``BeginList`` macro. + public enum ListStyle: String { + /// A bulleted list. + /// + /// Item titles should not be provided, instead item bodies are displayed + /// indented from a preceding bullet point using the specified width. + case bullet + // TODO: case column + // /// A columnated list. + // case column + /// A dashed list. + /// + /// Identical to `.bullet` except dashes precede each item. + case dash + /// An unindented list without newlines following important item titles + /// without macro parsing. + /// + /// Identical to `.inset` except item titles are displayed with importance + /// and are not parsed for macros. `.diag` is typically used in the + /// "DIAGNOSTICS" section with errors as the item titles. + case diag + /// An enumerated list. + /// + /// Identical to `.bullet` except increasing numbers starting at 1 precede + /// each item. + case `enum` + /// An indented list without joined item titles and bodies. + /// + /// Identical to `.tag` except item bodies always on the line after the + /// item title. + case hang + /// Alias for `.dash`. + case hyphen + /// An unindented list without newlines following item titles. + /// + /// Identical to `.ohang` except item titles are not followed by newlines. + case inset + /// An unindented list without item titles. + /// + /// Identical to `.ohang` except item titles should not be provided and + /// are not displayed. + case item + /// An unindented list. + /// + /// Item titles are displayed on a single line, with unindented item + /// bodies on the succeeding lines. + case ohang + /// An indented list. + /// + /// Item titles are displayed on a single line with item bodies indented + /// using the specified width on succeeding lines. If the item title is + /// shorter than the indentation width, item bodies are displayed on the + /// same as the title. + case tag + } + public static let kind = "Bl" + public var arguments: [MDocASTNode] + /// Creates a new `BeginList` macro. + /// + /// - Parameters: + /// - style: Display style. + /// - width: Number of characters to indent item bodies from titles. + /// - offset: Number of characters to indent both the item titles and bodies. + /// - compact: Disable vertical spacing between list items. + public init(style: ListStyle, width: Int? = nil, offset: Int? = nil, compact: Bool = false) { + self.arguments = ["-\(style)"] + switch style { + case .bullet, .dash, .`enum`, .hang, .hyphen, .tag: + if let width = width { + self.arguments.append(contentsOf: ["-width", "\(width)n"]) + } + case /*.column, */.diag, .inset, .item, .ohang: + assert(width == nil, "`width` should be nil for style: \(style)") + } + if let offset = offset { + self.arguments.append(contentsOf: ["-offset", "\(offset)n"]) + } + if compact { + self.arguments.append(contentsOf: ["-compact"]) + } + } + } + + /// A list item. + /// + /// ``ListItem`` begins a list item scope continuing until another + /// ``ListItem`` is encountered or the enclosing list scope is closed by + /// ``EndList``. ``ListItem``s may include a title if the the enclosing list + /// scope was constructed with one of the following styles: + /// - `.bullet` + /// - `.dash` + /// - `.enum` + /// - `.hang` + /// - `.hyphen` + /// - `.tag` + /// + /// __Example Usage__: + /// ```swift + /// BeginList(style: .tag, width: 6) + /// ListItem(title: "Hello, Swift!") + /// "Welcome to the Swift programming language." + /// ListItem(title: "Goodbye!") + /// EndList() + /// ``` + public struct ListItem: MDocMacroProtocol { + public static let kind = "It" + public var arguments: [MDocASTNode] + /// Creates a new `ListItem` macro. + /// + /// - Parameters: + /// - title: List item title, only valid depending on the ``ListStyle``. + public init(title: MDocASTNode? = nil) { + arguments = [] + arguments.append(optional: title) + } + } + + // Table cell separator in Bl -column lists. + // TODO: "Ta" + + /// Close a list scope opened by a ``BeginList`` macro. + public struct EndList: MDocMacroProtocol { + public static let kind = "El" + public var arguments: [MDocASTNode] + /// Creates a new `EndList` macro. + public init() { + self.arguments = [] + } + } + + // Bibliographic block (references). + // TODO: "Re" + + // MARK: Spacing control + + /// Text without a trailing space. + /// + /// __Example Usage__: + /// ```swift + /// WithoutTrailingSpace(text: "swift") + /// ``` + public struct WithoutTrailingSpace: MDocMacroProtocol { + public static let kind = "Pf" + public var arguments: [MDocASTNode] + /// Creates a new `WithoutTrailingSpace` macro. + /// + /// - Parameters: + /// - text: The text to display without a trailing space. + public init(text: String) { + self.arguments = [text] + } + } + + /// Text without a leading space. + /// + /// __Example Usage__: + /// ```swift + /// WithoutLeadingSpace(text: "swift") + /// ``` + public struct WithoutLeadingSpace: MDocMacroProtocol { + public static let kind = "Ns" + public var arguments: [MDocASTNode] + /// Creates a new `WithoutLeadingSpace` macro. + /// + /// - Parameters: + /// - text: The text to display without a trailing space. + public init(text: String) { + self.arguments = [text] + } + } + + /// An apostrophe without leading and trailing spaces. + /// + /// __Example Usage__: + /// ```swift + /// Apostrophe() + /// ``` + public struct Apostrophe: MDocMacroProtocol { + public static let kind = "Ap" + public var arguments: [MDocASTNode] + /// Creates a new `Apostrophe` macro. + public init() { + self.arguments = [] + } + } + + // TODO: HorizontalSpacing + // /// Switch horizontal spacing mode: [on | off]. + // public struct HorizontalSpacing: MDocMacroProtocol { + // public static let kind = "Sm" + // public var arguments: [MDocASTNode] + // public init() { + // self.arguments = [] + // } + // } + + // Keep block: -words. + // TODO: "Ek" + + // MARK: - Semantic markup for command-line utilities + + /// Command-line flags and options. + /// + /// Displays a hyphen (`-`) before each argument. ``CommandOption`` is + /// typically used in the "SYNOPSIS" and "DESCRIPTION" sections when listing + /// and describing options in a manual page. + /// + /// __Example Usage__: + /// ```swift + /// CommandOption(arguments: ["-version"]) + /// .withUnsafeChildren(CommandArgument(arguments: "version")) + /// ``` + public struct CommandOption: MDocMacroProtocol { + public static let kind = "Fl" + public var arguments: [MDocASTNode] + /// Creates a new `CommandOption` macro. + /// + /// - Parameters: + /// - arguments: Command-line flags and options. + public init(options: [MDocASTNode]) { + self.arguments = options + } + } + + /// Command-line modifiers. + /// + /// ``CommandModifier`` is typically used to denote strings exactly passed as + /// arguments, if and only if, ``CommandOption`` is not appropriate. + /// ``CommandModifier`` can also be used to specify configuration options and + /// keys. + /// + /// __Example Usage__: + /// ```swift + /// CommandModifier(modifiers: ["Configuration File"]) + /// .withUnsafeChildren(nodes: [FilePath(path: "$HOME/.swiftpm")]) + /// ``` + public struct CommandModifier: MDocMacroProtocol { + public static let kind = "Cm" + public var arguments: [MDocASTNode] + /// Creates a new `CommandModifier` macro. + /// + /// - Parameters: + /// - modifiers: Command-line modifiers. + public init(modifiers: [MDocASTNode]) { + self.arguments = modifiers + } + } + + /// Command-line placeholders. + /// + /// ``CommandArgument`` displays emphasized placeholders for command-line + /// flags, options and arguments. Flag and option names must use + /// ``CommandOption`` or `CommandModifier` macros. If no arguments are + /// provided to ``CommandArgument``, the string `"file ..."` is used. + /// + /// __Example Usage__: + /// ```swift + /// CommandArgument() + /// CommandArgument(arguments: [arg1, ",", arg2, "."]) + /// CommandOption(arguments: ["-version"]) + /// .withUnsafeChildren(CommandArgument(arguments: "version")) + /// ``` + public struct CommandArgument: MDocMacroProtocol { + public static let kind = "Ar" + public var arguments: [MDocASTNode] + /// Creates a new `CommandArgument` macro. + /// + /// - Parameters: + /// - arguments: Command-line argument placeholders. + public init(arguments: [MDocASTNode]) { + self.arguments = arguments + } + } + + /// Single-line optional command-line components. + /// + /// Displays the arguments in `[squareBrackets]`. + /// ``OptionalCommandLineComponent`` is typically used in the "SYNOPSIS" + /// section. + /// + /// __Example Usage__: + /// ```swift + /// SectionHeader(title: "SYNOPSIS") + /// DocumentName(name: "swift") + /// OptionalCommandLineComponent(arguments: CommandArgument(arguments: ["h"])) + /// ``` + public struct OptionalCommandLineComponent: MDocMacroProtocol { + public static let kind = "Op" + public var arguments: [MDocASTNode] + /// Creates a new `OptionalCommandLineComponent` macro. + /// + /// - Parameters: + /// - arguments: Command-line components to enclose. + public init(arguments: [MDocASTNode]) { + self.arguments = arguments + } + } + + /// Begin a multi-line optional command-line comment scope. + /// + /// Displays the scope contents in `[squareBrackets]`. + /// ``BeginOptionalCommandLineComponent`` is typically used in the "SYNOPSIS" + /// section. + /// + /// Closed by an ``EndOptionalCommandLineComponent`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginOptionalCommandLineComponent() + /// "Hello, Swift!" + /// EndOptionalCommandLineComponent() + /// ``` + public struct BeginOptionalCommandLineComponent: MDocMacroProtocol { + public static let kind = "Oo" + public var arguments: [MDocASTNode] + /// Creates a new `BeginOptionalCommandLineComponent` macro. + public init() { + self.arguments = [] + } + } + + /// Close a ```BeginOptionalCommandLineComponent``` block. + public struct EndOptionalCommandLineComponent: MDocMacroProtocol { + public static let kind = "Oc" + public var arguments: [MDocASTNode] + /// Creates a new `EndOptionalCommandLineComponent` macro. + public init() { + self.arguments = [] + } + } + + /// An interactive command. + /// + /// ``InteractiveCommand`` is similar to ``CommandModifier`` but should be used + /// to describe commands instead of arguments. For example, + /// ``InteractiveCommand`` can be used to describe the commands to editors + /// like `emacs` and `vim` or shells like `bash` or `fish`. + /// + /// __Example Usage__: + /// ```swift + /// InteractiveCommand(name: "print") + /// InteractiveCommand(name: "save") + /// InteractiveCommand(name: "quit") + /// ``` + public struct InteractiveCommand: MDocMacroProtocol { + public static let kind = "Ic" + public var arguments: [MDocASTNode] + /// Creates a new `InteractiveCommand` macro. + /// + /// - Parameters: + /// - name: Name of the interactive command. + public init(name: String) { + self.arguments = [name] + } + } + + /// An environment variable. + /// + /// __Example Usage__: + /// ```swift + /// EnvironmentVariable(variable: "DISPLAY") + /// EnvironmentVariable(variable: "PATH") + /// ``` + public struct EnvironmentVariable: MDocMacroProtocol { + public static let kind = "Ev" + public var arguments: [MDocASTNode] + /// Creates a new `EnvironmentVariable` macro. + /// + /// - Parameters: + /// - name: Name of the environment variable. + public init(name: String) { + self.arguments = [name] + } + } + + /// A file path. + /// + /// __Example Usage__: + /// ```swift + /// FilePath() + /// FilePath(path: "/usr/bin/swift") + /// FilePath(path: "/usr/share/man/man1/swift.1") + /// ``` + public struct FilePath: MDocMacroProtocol { + public static let kind = "Pa" + public var arguments: [MDocASTNode] + /// Creates a new `FilePath` macro. + /// + /// - Parameters: + /// - path: An optional absolute or relative path or a file or directory. + /// Tilde (`~`) will be used, if no path is used. + public init(path: String? = nil) { + self.arguments = [] + self.arguments.append(optional: path) + } + } + + // MARK: - Semantic markup for function libraries + + // Function library (one argument). + // TODO: "Lb" + + // Include file (one argument). + // TODO: "In" + + // Other preprocessor directive (>0 arguments). + // TODO: "Fd" + + // Function type (>0 arguments). + // TODO: "Ft" + + // Function block: funcname. + // TODO: "Fc" + + // Function name: funcname [argument ...]. + // TODO: "Fn" + + // Function argument (>0 arguments). + // TODO: "Fa" + + // Variable type (>0 arguments). + // TODO: "Vt" + + // Variable name (>0 arguments). + // TODO: "Va" + + /// Defined variable or preprocessor constant (>0 arguments). + // TODO: "Dv" + + /// Error constant (>0 arguments). + // TODO: "Er" + + /// Environmental variable (>0 arguments). + // TODO: "Ev" + + // MARK: - Various semantic markup + + /// An author's name. + /// + /// ``Author`` can be used to designate any author. Specifying an author of + /// the manual page itself should only occur in the "AUTHORS" section. + /// + /// ``Author`` also controls the display mode of authors. In the split mode, + /// a new-line will be inserted before each author, otherwise authors will + /// appear inline with other macros and text. Outside of the "AUTHORS" + /// section, the default display mode is unsplit. The display mode is reset at + /// the start of the "AUTHORS" section. In the "AUTHORS" section, the first + /// use of ``Author`` will use the unsplit mode and subsequent uses with use + /// the split mode. This behavior can be overridden by inserting an author + /// display mode macro before the normal author macro. + /// + /// __Example Usage__: + /// ```swift + /// Author(split: false) + /// Author(name: "Rauhul Varma") + /// ``` + public struct Author: MDocMacroProtocol { + public static let kind = "An" + public var arguments: [MDocASTNode] + /// Creates a new `Author` macro. + /// + /// - Parameters: + /// - name: The author name to display. + public init(name: String) { + self.arguments = [name] + } + /// Creates a new `Author` macro. + /// + /// - Parameters: + /// - split: The split display mode to use for subsequent uses of + /// ``Author``. + public init(split: Bool) { + self.arguments = [split ? "-split" : "-nosplit"] + } + } + + /// A website hyperlink. + /// + /// __Example Usage__: + /// ```swift + /// Hyperlink(url: "http://swift.org") + /// Hyperlink(url: "http://swift.org", displayText: "Programming in Swift") + /// ``` + public struct Hyperlink: MDocMacroProtocol { + public static let kind = "Lk" + public var arguments: [MDocASTNode] + /// Creates a new `Hyperlink` macro. + /// + /// - Parameters: + /// - url: The website address to link. + /// - displayText: Optional title text accompanying the url. + public init(url: String, displayText: String? = nil) { + self.arguments = [url] + self.arguments.append(optional: displayText) + } + } + + /// An email hyperlink. + /// + /// __Example Usage__: + /// ```swift + /// MailTo(email: "swift+evolution-discuss@forums.swift.org") + /// ``` + public struct MailTo: MDocMacroProtocol { + public static let kind = "Mt" + public var arguments: [MDocASTNode] + /// Creates a new `MailTo` macro. + /// + /// - Parameters: + /// - email: The email address to link. + public init(email: String) { + self.arguments = [email] + } + } + +// TODO: KernelConfiguration +// /// Kernel configuration declaration (>0 arguments). +// public struct KernelConfiguration: MDocMacroProtocol { +// public static let kind = "Cd" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + +// TODO: MemoryAddress +// /// Memory address (>0 arguments). +// public struct MemoryAddress: MDocMacroProtocol { +// public static let kind = "Ad" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + +// TODO: MathematicalSymbol +// /// Mathematical symbol (>0 arguments). +// public struct MathematicalSymbol: MDocMacroProtocol { +// public static let kind = "Ms" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + + // MARK: - Physical markup + + /// Emphasize single-line text. + /// + /// ``Emphasis`` should only be used when no other semantic macros are + /// appropriate. ``Emphasis`` is used to express "emphasis"; for example: + /// ``Emphasis`` can be used to highlight technical terms and placeholders, + /// except when they appear in syntactic elements. ``Emphasis`` should not be + /// conflated with "importance" which should be expressed using ``Boldface``. + /// + /// - Note: Emphasizes text is usually italicized. If the output program does + /// not support italicizing text, it is underlined instead. + /// + /// __Example Usage__: + /// ```swift + /// Emphasis(arguments: ["Hello", ", "Swift!"]) + /// ``` + public struct Emphasis: MDocMacroProtocol { + public static let kind = "Em" + public var arguments: [MDocASTNode] + /// Creates a new `Emphasis` macro. + /// + /// - Parameters: + /// - arguments: Text to emphasize. + public init(arguments: [MDocASTNode]) { + self.arguments = arguments + } + } + + /// Embolden single-line text. + /// + /// ``Boldface`` should only be used when no other semantic macros are + /// appropriate. ``Boldface`` is used to express "importance"; for example: + /// ``Boldface`` can be used to highlight required arguments and exact text. + /// ``Boldface`` should not be conflated with "emphasis" which + /// should be expressed using ``Emphasis``. + /// + /// __Example Usage__: + /// ```swift + /// Boldface(arguments: ["Hello,", " Swift!"]) + /// ``` + public struct Boldface: MDocMacroProtocol { + public static let kind = "Sy" + public var arguments: [MDocASTNode] + /// Creates a new `Boldface` macro. + /// + /// - Parameters: + /// - arguments: Text to embolden. + public init(arguments: [MDocASTNode]) { + self.arguments = arguments + } + } + + /// Reset the font style, set by a single-line text macro. + /// + /// __Example Usage__: + /// ```swift + /// Boldface(arguments: ["Hello,"]) + /// .withUnsafeChildren(nodes: [NormalText(), " Swift!"]) + /// ``` + public struct NormalText: MDocMacroProtocol { + public static let kind = "No" + public var arguments: [MDocASTNode] + /// Creates a new `NormalText` macro. + public init() { + self.arguments = [] + } + } + + /// Open a font scope with a font style. + /// + /// Closed by a ``EndFont`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginFont(style: .boldface) + /// "Hello, Swift!" + /// EndFont() + /// ``` + public struct BeginFont: MDocMacroProtocol { + /// Enumeration of font styles supported by `mdoc`. + public enum FontStyle { + /// Italic font style. + case emphasis + /// Typewriter font style. + /// + /// `literal` should not be used because it is visually identical to + /// normal text. + case literal + /// Bold font style. + case boldface + } + + public static let kind = "Bf" + public var arguments: [MDocASTNode] + /// Creates a new `BeginFont` macro. + /// + /// - Parameters: + /// - style: The style of font scope the macro opens. + public init(style: FontStyle) { + switch style { + case .emphasis: + self.arguments = ["-emphasis"] + case .literal: + self.arguments = ["-literal"] + case .boldface: + self.arguments = ["-symbolic"] + } + } + } + + /// Close a font scope opened by a ``BeginFont`` macro. + public struct EndFont: MDocMacroProtocol { + public static let kind = "Ef" + public var arguments: [MDocASTNode] + /// Creates a new `EndFont` macro. + public init() { + self.arguments = [] + } + } + + // MARK: - Physical enclosures + + /// Open a scope enclosed by `“typographic”` double-quotes. + /// + /// Closed by a ``EndTypographicDoubleQuotes`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginTypographicDoubleQuotes() + /// "Hello, Swift!" + /// EndTypographicDoubleQuotes() + /// ``` + public struct BeginTypographicDoubleQuotes: MDocMacroProtocol { + public static let kind = "Do" + public var arguments: [MDocASTNode] + /// Creates a new `BeginTypographicDoubleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginTypographicDoubleQuotes`` macro. + public struct EndTypographicDoubleQuotes: MDocMacroProtocol { + public static let kind = "Dc" + public var arguments: [MDocASTNode] + /// Creates a new `EndTypographicDoubleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `"typewriter"` double-quotes. + /// + /// Closed by a ``EndTypewriterDoubleQuotes`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginTypewriterDoubleQuotes() + /// "Hello, Swift!" + /// EndTypewriterDoubleQuotes() + /// ``` + public struct BeginTypewriterDoubleQuotes: MDocMacroProtocol { + public static let kind = "Qo" + public var arguments: [MDocASTNode] + /// Creates a new `BeginTypewriterDoubleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginTypewriterDoubleQuotes`` macro. + public struct EndTypewriterDoubleQuotes: MDocMacroProtocol { + public static let kind = "Qc" + public var arguments: [MDocASTNode] + /// Creates a new `EndTypewriterDoubleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `'single'` quotes. + /// + /// Closed by a ``EndSingleQuotes`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginSingleQuotes() + /// "Hello, Swift!" + /// EndSingleQuotes() + /// ``` + public struct BeginSingleQuotes: MDocMacroProtocol { + public static let kind = "So" + public var arguments: [MDocASTNode] + /// Creates a new `BeginSingleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginSingleQuotes`` macro. + public struct EndSingleQuotes: MDocMacroProtocol { + public static let kind = "Sc" + public var arguments: [MDocASTNode] + /// Creates a new `EndSingleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `(parentheses)`. + /// + /// Closed by a ``EndParentheses`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginParentheses() + /// "Hello, Swift!" + /// EndParentheses() + /// ``` + public struct BeginParentheses: MDocMacroProtocol { + public static let kind = "Po" + public var arguments: [MDocASTNode] + /// Creates a new `BeginParentheses` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginParentheses`` macro. + public struct EndParentheses: MDocMacroProtocol { + public static let kind = "Pc" + public var arguments: [MDocASTNode] + /// Creates a new `EndParentheses` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `[squareBrackets]`. + /// + /// Closed by a ``EndSquareBrackets`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginSquareBrackets() + /// "Hello, Swift!" + /// EndSquareBrackets() + /// ``` + public struct BeginSquareBrackets: MDocMacroProtocol { + public static let kind = "Bo" + public var arguments: [MDocASTNode] + /// Creates a new `BeginSquareBrackets` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginSquareBrackets`` macro. + public struct EndSquareBrackets: MDocMacroProtocol { + public static let kind = "Bc" + public var arguments: [MDocASTNode] + /// Creates a new `EndSquareBrackets` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `{curlyBraces}`. + /// + /// Closed by a ``EndCurlyBraces`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginCurlyBraces() + /// "Hello, Swift!" + /// EndCurlyBraces() + /// ``` + public struct BeginCurlyBraces: MDocMacroProtocol { + public static let kind = "Bro" + public var arguments: [MDocASTNode] + /// Creates a new `BeginCurlyBraces` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginCurlyBraces`` macro. + public struct EndCurlyBraces: MDocMacroProtocol { + public static let kind = "Brc" + public var arguments: [MDocASTNode] + /// Creates a new `EndCurlyBraces` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `<angleBrackets>`. + /// + /// Closed by a ``EndAngleBrackets`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginAngleBrackets() + /// "Hello, Swift!" + /// EndAngleBrackets() + /// ``` + public struct BeginAngleBrackets: MDocMacroProtocol { + public static let kind = "Ao" + public var arguments: [MDocASTNode] + /// Creates a new `BeginAngleBrackets` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginAngleBrackets`` macro. + public struct EndAngleBrackets: MDocMacroProtocol { + public static let kind = "Ac" + public var arguments: [MDocASTNode] + /// Creates a new `EndAngleBrackets` macro. + public init() { + self.arguments = [] + } + } + + // TODO: GenericEnclosure + // /// Enclose another element generically. + // case genericEnclosure(MDocLowLevelASTNode) + + // MARK: - Text production + + /// Display a standard line about the exit code of specified utilities. + /// + /// This macro indicates the specified utilities exit 0 on success and other + /// values on failure. ``ExitStandard``` should be only included in the + /// "EXIT STATUS" section. + /// + /// ``ExitStandard`` should only be used in sections 1, 6, and 8. + public struct ExitStandard: MDocMacroProtocol { + public static let kind = "Ex" + public var arguments: [MDocASTNode] + /// Creates a new `ExitStandard` macro. + /// + /// - Parameters: + /// - utilities: A list of utilities the exit standard applies to. If no + /// utilities are specified the document's name set by ``DocumentName`` + /// is used. + public init(utilities: [String] = []) { + self.arguments = ["-std"] + utilities + } + } + +// TODO: ReturnStandard +// /// Insert a standard sentence regarding a function call's return value of 0 on success and -1 on error, with the errno libc global variable set on error. +// /// +// /// If function is not specified, the document's name set by ``DocumentName`` is used. Multiple function arguments are treated as separate functions. +// public struct ReturnStandard: MDocMacroProtocol { +// public static let kind = "Rv" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + +// TODO: StandardsReference +// /// Reference to a standards document (one argument). +// public struct StandardsReference: MDocMacroProtocol { +// public static let kind = "St" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + + /// Display a formatted version of AT&T UNIX. + /// + /// __Example Usage__: + /// ```swift + /// AttUnix() + /// AttUnix(version: "V.1") + /// ``` + public struct AttUnix: MDocMacroProtocol { + public static let kind = "At" + public var arguments: [MDocASTNode] + /// Creates a new `AttUnix` macro. + /// + /// - Parameters: + /// - version: The version of Att Unix to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + /// `version` should be one of the following values; + /// - `v[1-7] | 32v` - A version of AT&T UNIX. + /// - `III` - AT&T System III UNIX. + /// - `V | V.[1-4]` - A version of AT&T System V UNIX. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted variant and version of BSD. + /// + /// __Example Usage__: + /// ```swift + /// BSD() + /// BSD(name: "Ghost") + /// BSD(name: "Ghost", version: "21.04.27") + /// ``` + public struct BSD: MDocMacroProtocol { + public static let kind = "Bx" + public var arguments: [MDocASTNode] + /// Creates a new `BSD` macro. + /// + /// - Note: The `version` parameter must not be specified without + /// the `name` parameter. + /// + /// - Parameters: + /// - name: The name of the BSD variant to stylize. + /// - version: The version `name` to stylize. Omitting `version` + /// will result in an unversioned OS being displayed. + public init(name: String? = nil, version: String? = nil) { + precondition(!(name == nil && version != nil)) + self.arguments = [] + self.arguments.append(optional: name) + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of BSD/OS. + /// + /// __Example Usage__: + /// ```swift + /// BSDOS() + /// BSDOS(version: "5.1") + /// ``` + public struct BSDOS: MDocMacroProtocol { + public static let kind = "Bsx" + public var arguments: [MDocASTNode] + /// Creates a new `BSDOS` macro. + /// + /// - Parameters: + /// - version: The version of BSD/OS to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of NetBSD. + /// + /// __Example Usage__: + /// ```swift + /// NetBSD() + /// NetBSD(version: "9.2") + /// ``` + public struct NetBSD: MDocMacroProtocol { + public static let kind = "Nx" + public var arguments: [MDocASTNode] + /// Creates a new `NetBSD` macro. + /// + /// - Parameters: + /// - version: The version of NetBSD to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of FreeBSD. + /// + /// __Example Usage__: + /// ```swift + /// FreeBSD() + /// FreeBSD(version: "13.0") + /// ``` + public struct FreeBSD: MDocMacroProtocol { + public static let kind = "Fx" + public var arguments: [MDocASTNode] + /// Creates a new `FreeBSD` macro. + /// + /// - Parameters: + /// - version: The version of FreeBSD to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of OpenBSD. + /// + /// __Example Usage__: + /// ```swift + /// OpenBSD() + /// OpenBSD(version: "6.9") + /// ``` + public struct OpenBSD: MDocMacroProtocol { + public static let kind = "Ox" + public var arguments: [MDocASTNode] + /// Creates a new `OpenBSD` macro. + /// + /// - Parameters: + /// - version: The version of OpenBSD to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of DragonFly. + /// + /// __Example Usage__: + /// ```swift + /// DragonFly() + /// DragonFly(version: "6.0") + /// ``` + public struct DragonFly: MDocMacroProtocol { + public static let kind = "Dx" + public var arguments: [MDocASTNode] + /// Creates a new `DragonFly` macro. + /// + /// - Parameters: + /// - version: The version of DragonFly to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } +} diff --git a/Tools/generate-manual/MDoc/MDocSerializationContext.swift b/Tools/generate-manual/MDoc/MDocSerializationContext.swift new file mode 100644 index 000000000..8619adf8a --- /dev/null +++ b/Tools/generate-manual/MDoc/MDocSerializationContext.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +/// The context needed to serialize an AST Node to its string representation. +public struct MDocSerializationContext { + var macroLine: Bool = false + + public init() { } +} diff --git a/Tools/generate-manual/MDoc/String+Escaping.swift b/Tools/generate-manual/MDoc/String+Escaping.swift new file mode 100644 index 000000000..e87e3bfd8 --- /dev/null +++ b/Tools/generate-manual/MDoc/String+Escaping.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +// Escaping rules +// https://mandoc.bsd.lv/mdoc/intro/escaping.html +extension String { + func escapedMacroArgument() -> String { + var escaped = "" + var containsBlankCharacter = false + + // TODO: maybe drop `where character.isASCII` clause + for character in self where character.isASCII { + switch character { + case " ": + escaped.append(character) + containsBlankCharacter = true + + // backslashes: + // To output a backslash, use the escape sequence `\e`. Never use the escape sequence `\\` in any context. + case #"\"#: + escaped += #"\e"# + + // double quotes in macro arguments: + // If a macro argument needs to contain a double quote character, write it as “\(dq”. No escaping is needed on text input lines. + case "\"": + escaped += #"\(dq"# + + default: + // Custom addition: + // newlines in macro arguments: + // If a macro argument contains a newline character, replace it with a blank character. + if character.isNewline { + escaped.append(" ") + containsBlankCharacter = true + } else { + escaped.append(character) + } + } + } + + // FIXME: + // macro names as macro arguments: + // If the name of another mdoc(7) macro occurs as an argument on an mdoc(7) macro line, the former macro is called, and any remaining arguments are passed to it. To prevent this call and instead render the name of the former macro literally, prepend the name with a zero-width space (‘\&’). See the MACRO SYNTAX section of the mdoc(7) manual for details. + + // blanks in macro arguments + // If a macro argument needs to contain a blank character, enclose the whole argument in double quotes. For example, this often occurs with Fa macros. See the MACRO SYNTAX in the roff(7) manual for details. + if escaped.isEmpty || containsBlankCharacter { + return "\"\(escaped)\"" + } + + return escaped + } + + func escapedTextLine() -> String { + var escaped = "" + var atBeginning = true + + // TODO: maybe drop `where character.isASCII` clause + for character in self where character.isASCII { + switch (character, atBeginning) { + + // backslashes: + // To output a backslash, use the escape sequence `\e`. Never use the escape sequence `\\` in any context. + case (#"\"#, _): + escaped += #"\e"# + atBeginning = false + + // dots and apostrophes at the beginning of text lines: + // If a text input line needs to begin with a dot (`.`) or apostrophe (`'`), prepend a zero-width space (`\&`) to prevent the line from being mistaken for a macro line. Never use the escape sequence `\.` in any context. + case (".", true), ("'", true): + escaped += #"\&"# + escaped.append(character) + atBeginning = false + + // blank characters at the beginning of text lines: + // If a text input line needs to begin with a blank character (` `) and no line break is desired before that line, prepend a zero-width space (`\&`). + case (" ", true): + escaped += #"\&"# + escaped.append(character) + + default: + escaped.append(character) + atBeginning = false + } + } + + return escaped + } +} diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 066f122c9..2b2ac76c6 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -8,8 +8,13 @@ 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}" STREQUAL "aarch64") - set("${result_var_name}" "aarch64" 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") @@ -18,10 +23,12 @@ function(get_swift_host_arch result_var_name) 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 "armv7l") - set("${result_var_name}" "armv7" 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")