From 53555a04503c175eaffcf587e4b8c380a7c41a5c Mon Sep 17 00:00:00 2001 From: Sergey Petrachkov Date: Thu, 5 Nov 2020 22:37:36 +0300 Subject: [PATCH 001/114] Support Exit codes from thrown CustomNSError conformers (#244) Introduce customnserror support, so exit code is calculated correctly, resolves #243. --- .../ArgumentParser/Usage/MessageInfo.swift | 13 +++++ .../ExitCodeTests.swift | 50 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index accf643d6..4b8baf699 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -62,6 +62,17 @@ enum MessageInfo { self.init(error: CommandError(commandStack: [type.asCommand], parserError: e), type: type) return + case let e as CustomNSError: + // Send CustomNSError back through the CommandError path + self.init( + error: CommandError( + commandStack: [type.asCommand], + parserError: .userValidationError(e) + ), + type: type + ) + return + default: commandStack = [type.asCommand] // if the error wasn't one of our two Error types, wrap it as a userValidationError @@ -92,6 +103,8 @@ enum MessageInfo { } case let error as ExitCode: self = .other(message: "", exitCode: error.rawValue) + case let error as CustomNSError: + self = .other(message: error.localizedDescription, exitCode: Int32(error.errorCode)) case let error as LocalizedError where error.errorDescription != nil: self = .other(message: error.errorDescription!, exitCode: EXIT_FAILURE) default: diff --git a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift index 798490686..e6dca823e 100644 --- a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift +++ b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift @@ -76,3 +76,53 @@ extension ExitCodeTests { } } } + +// MARK: - CustomNSError tests + +extension ExitCodeTests { + enum MyCustomNSError: CustomNSError { + case myFirstCase + case mySecondCase + + var errorCode: Int { + switch self { + case .myFirstCase: + return 101 + case .mySecondCase: + return 102 + } + } + + var errorUserInfo: [String : Any] { + switch self { + case .myFirstCase: + return [NSLocalizedDescriptionKey: "My first case localized description"] + case .mySecondCase: + return [:] + } + } + } + + struct CheckFirstCustomNSErrorCommand: ParsableCommand { + + @Option + var errorCase: Int + + func run() throws { + switch errorCase { + case 101: + throw MyCustomNSError.myFirstCase + default: + throw MyCustomNSError.mySecondCase + } + } + } + + func testCustomErrorCodeForTheFirstCase() { + XCTAssertEqual(CheckFirstCustomNSErrorCommand.exitCode(for: MyCustomNSError.myFirstCase), ExitCode(rawValue: 101)) + } + + func testCustomErrorCodeForTheSecondCase() { + XCTAssertEqual(CheckFirstCustomNSErrorCommand.exitCode(for: MyCustomNSError.mySecondCase), ExitCode(rawValue: 102)) + } +} From 4273ad222e6c51969e8585541f9da5187ad94e47 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 10 Dec 2020 22:02:33 +0000 Subject: [PATCH 002/114] Add support for Apple Silicon in SwiftSupport.cmake (#257) This matches the behavior of [a similar `elseif` branch in the Swift toolchain itself](https://github.com/apple/swift/blob/b80e4bf8f866be26a4108bd92f97b2a182662642/CMakeLists.txt#L594). --- cmake/modules/SwiftSupport.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 066f122c9..0f62fa094 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -10,6 +10,8 @@ function(get_swift_host_arch result_var_name) 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}" STREQUAL "arm64") + set("${result_var_name}" "aarch64" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64") set("${result_var_name}" "powerpc64" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64le") From 41f5fe52a34b2c7b9938996387fabfaca21918bd Mon Sep 17 00:00:00 2001 From: Drew McCormack Date: Tue, 5 Jan 2021 17:22:31 +0100 Subject: [PATCH 003/114] Gave a more descriptive error message for when a non-argument variable causes a parsing failure. (#256) --- Sources/ArgumentParser/Usage/UsageGenerator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index d8d18d89b..05b5538a3 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -339,7 +339,7 @@ extension ErrorMessageGenerator { } 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: From 9564d61b08a5335ae0a36f789a7d71493eacadfc Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Fri, 15 Jan 2021 23:46:48 -0600 Subject: [PATCH 004/114] Update changelog for 0.3.2 release (#261) --- CHANGELOG.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23488ce13..8abe0fc65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,26 @@ package updates, you can specify your package dependency using --- +## [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 @@ -346,7 +366,8 @@ 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/0.3.2...HEAD +[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,6 +384,7 @@ This changelog's format is based on [Keep a Changelog](https://keepachangelog.co [#65]: https://github.com/apple/swift-argument-parser/pull/65 +[#256]: https://github.com/apple/swift-argument-parser/pull/256 @@ -372,10 +394,14 @@ This changelog's format is based on [Keep a Changelog](https://keepachangelog.co [BradLarson]: https://github.com/apple/swift-argument-parser/commits?author=BradLarson [buttaface]: https://github.com/apple/swift-argument-parser/commits?author=buttaface [compnerd]: https://github.com/apple/swift-argument-parser/commits?author=compnerd +[CypherPoet]: https://github.com/apple/swift-argument-parser/commits?author=CypherPoet +[damuellen]: https://github.com/apple/swift-argument-parser/commits?author=damuellen [dduan]: https://github.com/apple/swift-argument-parser/commits?author=dduan +[drewmccormack]: https://github.com/apple/swift-argument-parser/commits?author=drewmccormack [elliottwilliams]: https://github.com/apple/swift-argument-parser/commits?author=elliottwilliams [erica]: https://github.com/apple/swift-argument-parser/commits?author=erica [glessard]: https://github.com/apple/swift-argument-parser/commits?author=glessard +[gmittert]: https://github.com/apple/swift-argument-parser/commits?author=gmittert [griffin-stewie]: https://github.com/apple/swift-argument-parser/commits?author=griffin-stewie [iainsmith]: https://github.com/apple/swift-argument-parser/commits?author=iainsmith [ibrahimoktay]: https://github.com/apple/swift-argument-parser/commits?author=ibrahimoktay @@ -386,14 +412,17 @@ This changelog's format is based on [Keep a Changelog](https://keepachangelog.co [kennyyork]: https://github.com/apple/swift-argument-parser/commits?author=kennyyork [klaaspieter]: https://github.com/apple/swift-argument-parser/commits?author=klaaspieter [Lantua]: https://github.com/apple/swift-argument-parser/commits?author=Lantua +[MaxDesiatov]: https://github.com/apple/swift-argument-parser/commits?author=MaxDesiatov [miguelangel-dev]: https://github.com/apple/swift-argument-parser/commits?author=miguelangel-dev [MPLew-is]: https://github.com/apple/swift-argument-parser/commits?author=MPLew-is [natecook1000]: https://github.com/apple/swift-argument-parser/commits?author=natecook1000 [NicFontana]: https://github.com/apple/swift-argument-parser/commits?author=NicFontana [owenv]: https://github.com/apple/swift-argument-parser/commits?author=owenv +[pegasuze]: https://github.com/apple/swift-argument-parser/commits?author=pegasuze [rjstelling]: https://github.com/apple/swift-argument-parser/commits?author=rjstelling [Sajjon]: https://github.com/apple/swift-argument-parser/commits?author=Sajjon [schlagelk]: https://github.com/apple/swift-argument-parser/commits?author=schlagelk +[SergeyPetrachkov]: https://github.com/apple/swift-argument-parser/commits?author=SergeyPetrachkov [sgl0v]: https://github.com/apple/swift-argument-parser/commits?author=sgl0v [sharplet]: https://github.com/apple/swift-argument-parser/commits?author=sharplet [sjavora]: https://github.com/apple/swift-argument-parser/commits?author=sjavora From d80c0172d8f5a1f707e8471bc0551f9833adbbfe Mon Sep 17 00:00:00 2001 From: Md Abir Hasan Zoha Date: Sat, 16 Jan 2021 11:53:39 +0600 Subject: [PATCH 005/114] Add custom helpNames support for Subcommand (#251) If helpNames is not modified, Subcommand will inherit helpNames from its immediate parent. The helpNames is generated from `commandStack: [ParsableCommand.Type]`. `getHelpNames()` extension method of `Array` is order sensitive and assumes that the element of `commandStack` at indexed `i` is the parent of the element at indexed `i+1` --- Documentation/04 Customizing Help.md | 28 ++++++++++++ .../Parsable Types/CommandConfiguration.swift | 11 ++--- .../Parsing/CommandParser.swift | 2 +- .../ArgumentParser/Usage/HelpGenerator.swift | 26 ++++++++--- .../HelpTests.swift | 45 ++++++++++++++++++- 5 files changed, 97 insertions(+), 15 deletions(-) diff --git a/Documentation/04 Customizing Help.md b/Documentation/04 Customizing Help.md index a3e11c58a..bc3b320bd 100644 --- a/Documentation/04 Customizing Help.md +++ b/Documentation/04 Customizing Help.md @@ -153,6 +153,34 @@ OPTIONS: -?, --help Show help information. ``` +If you don't provide alternative help names for Subcommand then it will inherit help names from it's immediate parent. + +```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 + } +} +``` + +When running the command, `-h` matches the short name of the `host` property, and `-?` displays the help screen. + +``` +% parent child -h 192.0.0.0 +... +% parent child -? +USAGE: parent child --host + +OPTIONS: + -h, --host The host the server will run on. + -?, --help Show help information. +``` + ## Hiding Arguments and Commands You may want to suppress features under development or experimental flags from the generated help screen. You can hide an argument or a subcommand by passing `shouldDisplay: false` to the property wrapper or `CommandConfiguration` initializers, respectively. diff --git a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift index a06a0d272..63326e2ee 100644 --- a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift +++ b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift @@ -45,7 +45,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. /// @@ -64,8 +64,9 @@ 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. If `helpNames` + /// is `nil`, the flag names are derived by simulating a Boolean property + /// named `help`. public init( commandName: String? = nil, abstract: String = "", @@ -74,7 +75,7 @@ public struct CommandConfiguration { shouldDisplay: Bool = true, subcommands: [ParsableCommand.Type] = [], defaultSubcommand: ParsableCommand.Type? = nil, - helpNames: NameSpecification = [.short, .long] + helpNames: NameSpecification? = nil ) { self.commandName = commandName self.abstract = abstract @@ -97,7 +98,7 @@ public struct CommandConfiguration { shouldDisplay: Bool = true, subcommands: [ParsableCommand.Type] = [], defaultSubcommand: ParsableCommand.Type? = nil, - helpNames: NameSpecification = [.short, .long] + helpNames: NameSpecification? = nil ) { self.commandName = commandName self._superCommandName = _superCommandName diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index f037f5d61..6aa81e2f3 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -75,7 +75,7 @@ extension CommandParser { /// built in help flags. func checkForBuiltInFlags(_ split: SplitArguments) throws { // Look for help flags - guard !split.contains(anyOf: self.commandTree.element.getHelpNames()) else { + guard !split.contains(anyOf: self.commandStack.getHelpNames()) else { throw HelpRequested() } diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index d3538a8ce..9c4023c1a 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -210,7 +210,6 @@ internal struct HelpGenerator { } let helpLabels = commandStack - .first! .getHelpNames() .map { $0.synopsisString } .joined(separator: ", ") @@ -282,12 +281,25 @@ internal struct HelpGenerator { } } -internal extension ParsableCommand { - static func getHelpNames() -> [Name] { - return self.configuration - .helpNames - .makeNames(InputKey(rawValue: "help")) - .sorted(by: >) +fileprivate extension CommandConfiguration { + static var defaultHelpNames: NameSpecification { [.short, .long] } +} + +fileprivate extension NameSpecification { + func generateHelpNames() -> [Name] { + return self.makeNames(InputKey(rawValue: "help")).sorted(by: >) + } +} + +internal extension Array where Element == ParsableCommand.Type { + func getHelpNames() -> [Name] { + if(count == 0){ + return CommandConfiguration.defaultHelpNames.generateHelpNames() + } else if let helpNames = self.last!.configuration.helpNames { + return helpNames.generateHelpNames() + } else { + return self.dropLast().getHelpNames() + } } } diff --git a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift index 8f5cc750b..e5c5ad432 100644 --- a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift +++ b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift @@ -196,7 +196,7 @@ struct CustomHelp: ParsableCommand { extension HelpTests { func testCustomHelpNames() { - let names = CustomHelp.getHelpNames() + let names = [CustomHelp.self].getHelpNames() XCTAssertEqual(names, [.short("?"), .long("show-help")]) } } @@ -211,7 +211,7 @@ struct NoHelp: ParsableCommand { extension HelpTests { func testNoHelpNames() { - let names = NoHelp.getHelpNames() + let names = [NoHelp.self].getHelpNames() XCTAssertEqual(names, []) XCTAssertEqual( @@ -225,3 +225,44 @@ extension HelpTests { """) } } + +struct SubCommandCustomHelp: ParsableCommand { + static var configuration = CommandConfiguration ( + helpNames: [.customShort("p"), .customLong("parrent-help")] + ) + + struct InheritHelp: ParsableCommand { + + } + + struct ModifiedHelp: ParsableCommand { + static var configuration = CommandConfiguration ( + helpNames: [.customShort("s"), .customLong("subcommand-help")] + ) + + struct InheritImmediateParentdHelp: ParsableCommand { + + } + } +} + +extension HelpTests { + func testSubCommandInheritHelpNames() { + let names = [SubCommandCustomHelp.self, SubCommandCustomHelp.InheritHelp.self].getHelpNames() + XCTAssertEqual(names, [.short("p"), .long("parrent-help")]) + } + + func testSubCommandCustomHelpNames() { + let names = [SubCommandCustomHelp.self, SubCommandCustomHelp.ModifiedHelp.self].getHelpNames() + XCTAssertEqual(names, [.short("s"), .long("subcommand-help")]) + } + + func testInheritImmediateParentHelpNames() { + let names = [ + SubCommandCustomHelp.self, + SubCommandCustomHelp.ModifiedHelp.self, + SubCommandCustomHelp.ModifiedHelp.InheritImmediateParentdHelp.self + ].getHelpNames() + XCTAssertEqual(names, [.short("s"), .long("subcommand-help")]) + } +} From 2a4664a40b8156f113512f17c4e272cfa84b20e0 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Sat, 16 Jan 2021 00:18:23 -0600 Subject: [PATCH 006/114] Add support for joined short options (#240) This supports joined arguments, like '-Ddebug' or '-v4'. Joined arguments need to be explicitly declared as `.customShort("D", allowingJoined: true)`. --- .../FishCompletionsGenerator.swift | 4 +- .../NameSpecification.swift | 40 +++- .../Parsing/ArgumentDefinition.swift | 4 + .../ArgumentParser/Parsing/ArgumentSet.swift | 35 +++- .../ArgumentParser/Parsing/InputOrigin.swift | 17 ++ Sources/ArgumentParser/Parsing/Name.swift | 24 ++- .../Parsing/SplitArguments.swift | 24 +++ .../CMakeLists.txt | 1 + .../JoinedEndToEndTests.swift | 193 ++++++++++++++++++ 9 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index c95455ad7..ed5e3ee67 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -78,7 +78,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 +89,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)" diff --git a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift index 77fa74ed3..15414faeb 100644 --- a/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift +++ b/Sources/ArgumentParser/Parsable Properties/NameSpecification.swift @@ -25,6 +25,11 @@ public struct NameSpecification: ExpressibleByArrayLiteral { /// To create a single-dash argument, pass `true` as `withSingleDash`. Note /// that combining single-dash options and options with short, /// single-character names can lead to ambiguities for the user. + /// + /// - Parameters: + /// - name: The name of the option or flag. + /// - withSingleDash: A Boolean value indicating whether to use a single + /// dash as the prefix. If `false`, the name has a double-dash prefix. case customLong(_ name: String, withSingleDash: Bool = false) /// Use the first character of the property's name as a short option label. @@ -35,8 +40,16 @@ public struct NameSpecification: ExpressibleByArrayLiteral { /// 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. + case customShort(_ char: Character, allowingJoined: Bool = false) } var elements: [Element] @@ -62,6 +75,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 +92,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 @@ -100,8 +126,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) } } } diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index 1acf74c17..75dbefdc1 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -194,6 +194,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..315e20931 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 }) } @@ -202,6 +202,13 @@ extension ArgumentSet { if let value = parsed.value { // This was `--foo=bar` style: try update(origin, parsed.name, value, &result) + } else if argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) + { + // Found a joined argument + let origins = origin.inserting(origin2) + try update(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) } else if let (origin2, value) = inputArguments.popNextElementIfValue(after: originElement) { // Use `popNextElementIfValue(after:)` to handle cases where short option // labels are combined @@ -217,6 +224,12 @@ extension ArgumentSet { if let value = parsed.value { // This was `--foo=bar` style: try update(origin, parsed.name, value, &result) + } else if argument.allowsJoinedValue, + let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { + // Found a joined argument + let origins = origin.inserting(origin2) + try update(origins, parsed.name, String(value), &result) + usedOrigins.formUnion(origins) } else if let (origin2, value) = inputArguments.popNextValue(after: originElement) { // Use `popNext(after:)` to handle cases where short option // labels are combined @@ -233,6 +246,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 +270,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 @@ -269,6 +295,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 arguments until hitting an option diff --git a/Sources/ArgumentParser/Parsing/InputOrigin.swift b/Sources/ArgumentParser/Parsing/InputOrigin.swift index 34d696658..7e8e194a5 100644 --- a/Sources/ArgumentParser/Parsing/InputOrigin.swift +++ b/Sources/ArgumentParser/Parsing/InputOrigin.swift @@ -18,6 +18,23 @@ struct InputOrigin: Equatable, ExpressibleByArrayLiteral { enum Element: Comparable, Hashable { case argumentIndex(SplitArguments.Index) + + var baseIndex: Int? { + switch self { + case .argumentIndex(let i): + return i.inputIndex.rawValue + } + } + + var subIndex: Int? { + switch self { + case .argumentIndex(let i): + switch i.subIndex { + case .complete: return nil + case .sub(let n): return n + } + } + } } private var _elements: Set = [] diff --git a/Sources/ArgumentParser/Parsing/Name.swift b/Sources/ArgumentParser/Parsing/Name.swift index cd940d9ca..1859c573a 100644 --- a/Sources/ArgumentParser/Parsing/Name.swift +++ b/Sources/ArgumentParser/Parsing/Name.swift @@ -15,7 +15,7 @@ enum Name: Hashable { /// 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) @@ -36,7 +36,7 @@ extension Name { 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,7 +47,7 @@ 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 @@ -62,6 +62,24 @@ extension Name { return false } } + + var allowsJoined: Bool { + switch self { + case .short(_, let allowingJoined): + return allowingJoined + default: + return false + } + } + + /// The instance to match against user input -- this always has + /// `allowingJoined` as `false`, since that's the way input is parsed. + var nameToMatch: Name { + switch self { + case .long, .longWithSingleDash: return self + case .short(let c, _): return .short(c) + } + } } // short argument names based on the synopsisString diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift index 4316b11c1..94a6541fb 100644 --- a/Sources/ArgumentParser/Parsing/SplitArguments.swift +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -163,6 +163,10 @@ struct SplitArguments { var inputIndex: InputIndex var subIndex: SubIndex = .complete + + var completeIndex: Index { + return Index(inputIndex: inputIndex) + } } /// The parsed arguments. Onl @@ -287,6 +291,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 +448,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/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/JoinedEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift new file mode 100644 index 000000000..09a786152 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift @@ -0,0 +1,193 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +final class JoinedEndToEndTests: XCTestCase { +} + +// MARK: - + +fileprivate struct Foo: ParsableArguments { + @Option(name: .customShort("f")) + var file = "" + + @Option(name: .customShort("d", allowingJoined: true)) + var debug = "" + + @Flag(name: .customLong("fdi", withSingleDash: true)) + var fdi = false +} + +extension JoinedEndToEndTests { + func testSingleValueParsing() throws { + AssertParse(Foo.self, []) { foo in + XCTAssertEqual(foo.file, "") + XCTAssertEqual(foo.debug, "") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-f", "file", "-d=Debug"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-f", "file", "-d", "Debug"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-f", "file", "-dDebug"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-dDebug", "-f", "file"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-dDebug"]) { foo in + XCTAssertEqual(foo.file, "") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-fd", "file", "Debug"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, false) + } + + AssertParse(Foo.self, ["-fd", "file", "Debug", "-fdi"]) { foo in + XCTAssertEqual(foo.file, "file") + XCTAssertEqual(foo.debug, "Debug") + XCTAssertEqual(foo.fdi, true) + } + + AssertParse(Foo.self, ["-fdi"]) { foo in + XCTAssertEqual(foo.file, "") + XCTAssertEqual(foo.debug, "") + XCTAssertEqual(foo.fdi, true) + } + } + + func testSingleValueParsing_Fails() throws { + XCTAssertThrowsError(try Foo.parse(["-f", "-d"])) + XCTAssertThrowsError(try Foo.parse(["-f", "file", "-d"])) + XCTAssertThrowsError(try Foo.parse(["-fd", "file"])) + XCTAssertThrowsError(try Foo.parse(["-fdDebug", "file"])) + XCTAssertThrowsError(try Foo.parse(["-fFile"])) + } +} + +// MARK: - + +fileprivate struct Bar: ParsableArguments { + @Option(name: .customShort("D", allowingJoined: true)) + var debug: [String] = [] +} + +extension JoinedEndToEndTests { + func testArrayValueParsing() throws { + AssertParse(Bar.self, []) { bar in + XCTAssertEqual(bar.debug, []) + } + + AssertParse(Bar.self, ["-Ddebug1"]) { bar in + XCTAssertEqual(bar.debug, ["debug1"]) + } + + AssertParse(Bar.self, ["-Ddebug1", "-Ddebug2", "-Ddebug3"]) { bar in + XCTAssertEqual(bar.debug, ["debug1", "debug2", "debug3"]) + } + + AssertParse(Bar.self, ["-D", "debug1", "-Ddebug2", "-D", "debug3"]) { bar in + XCTAssertEqual(bar.debug, ["debug1", "debug2", "debug3"]) + } + } + + func testArrayValueParsing_Fails() throws { + XCTAssertThrowsError(try Bar.parse(["-D"])) + XCTAssertThrowsError(try Bar.parse(["-Ddebug1", "debug2"])) + } +} + +// MARK: - + +fileprivate struct Baz: ParsableArguments { + @Option(name: .customShort("D", allowingJoined: true), parsing: .upToNextOption) + var debug: [String] = [] + + @Flag var verbose = false +} + +extension JoinedEndToEndTests { + func testArrayUpToNextParsing() throws { + AssertParse(Baz.self, []) { baz in + XCTAssertEqual(baz.debug, []) + } + + AssertParse(Baz.self, ["-Ddebug1", "debug2"]) { baz in + XCTAssertEqual(baz.debug, ["debug1", "debug2"]) + XCTAssertEqual(baz.verbose, false) + } + + AssertParse(Baz.self, ["-Ddebug1", "debug2", "--verbose"]) { baz in + XCTAssertEqual(baz.debug, ["debug1", "debug2"]) + XCTAssertEqual(baz.verbose, true) + } + + AssertParse(Baz.self, ["-Ddebug1", "debug2", "-Ddebug3", "debug4"]) { baz in + XCTAssertEqual(baz.debug, ["debug3", "debug4"]) + } + } + + func testArrayUpToNextParsing_Fails() throws { + XCTAssertThrowsError(try Baz.parse(["-D", "--other"])) + XCTAssertThrowsError(try Baz.parse(["-Ddebug", "--other"])) + XCTAssertThrowsError(try Baz.parse(["-Ddebug", "--other"])) + XCTAssertThrowsError(try Baz.parse(["-Ddebug", "debug", "--other"])) + } +} + +// MARK: - + +fileprivate struct Qux: ParsableArguments { + @Option(name: .customShort("D", allowingJoined: true), parsing: .remaining) + var debug: [String] = [] +} + +extension JoinedEndToEndTests { + func testArrayRemainingParsing() throws { + AssertParse(Qux.self, []) { qux in + XCTAssertEqual(qux.debug, []) + } + + AssertParse(Qux.self, ["-Ddebug1", "debug2"]) { qux in + XCTAssertEqual(qux.debug, ["debug1", "debug2"]) + } + + AssertParse(Qux.self, ["-Ddebug1", "debug2", "-Ddebug3", "debug4", "--other"]) { qux in + XCTAssertEqual(qux.debug, ["debug1", "debug2", "-Ddebug3", "debug4", "--other"]) + } + } + + func testArrayRemainingParsing_Fails() throws { + XCTAssertThrowsError(try Baz.parse(["--other", "-Ddebug", "debug"])) + } +} From 9fe9374a1fbf1f2364b7e4cbc8dfd4e970723f32 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Sat, 16 Jan 2021 00:47:11 -0600 Subject: [PATCH 007/114] Remove deprecated property wrapper initializers (#262) --- .../Parsable Properties/Argument.swift | 85 ------------- .../Parsable Properties/Flag.swift | 115 ------------------ .../Parsable Properties/Option.swift | 101 --------------- .../SourceCompatEndToEndTests.swift | 62 ---------- .../HelpGenerationTests.swift | 7 +- .../ParsableArgumentsValidationTests.swift | 2 +- 6 files changed, 2 insertions(+), 370 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index 69a55ab41..dd8b120ae 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -95,35 +95,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: @@ -234,28 +205,6 @@ extension Argument { }) } - @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 +233,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: diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index d7d313fd3..0c61c6540 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -173,26 +173,6 @@ 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: @@ -226,68 +206,6 @@ extension Flag where Value == Bool { }) } - /// 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. @@ -404,39 +322,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`. diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index c4851cffa..72f0153af 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -106,41 +106,6 @@ 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\"`)") - public init( - name: NameSpecification = .long, - default initial: Value?, - parsing parsingStrategy: SingleValueParsingStrategy = .next, - help: ArgumentHelp? = nil - ) { - self.init( - name: name, - initial: initial, - parsingStrategy: parsingStrategy, - 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 `Option` with a default value such as: @@ -338,30 +303,6 @@ extension Option { }) } - @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 - return ArgumentSet(arg.optional) - }) - } - /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic. /// /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. @@ -395,48 +336,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: 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/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index f794ade79..4f143180e 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -139,9 +139,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 @@ -163,15 +160,13 @@ extension HelpGenerationTests { func testHelpWithDefaultValues() { AssertHelp(for: D.self, equals: """ - USAGE: d [] [--name ] [--middle-name ] [--age ] [--logging ] [--lucky ...] [--optional] [--required] [--degree ] [--directory ] + USAGE: d [] [--name ] [--age ] [--logging ] [--lucky ...] [--optional] [--required] [--degree ] [--directory ] ARGUMENTS: Your occupation. (default: --) OPTIONS: --name Your name. (default: John) - --middle-name - Your middle name. (default: Winston) --age Your age. (default: 20) --logging Whether logging is enabled. (default: false) --lucky Your lucky numbers. (default: 7, 14) 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 { From 75c4dcd2e7cd0878aa1d29e6b493a8abcd65d753 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Sat, 16 Jan 2021 02:32:24 -0600 Subject: [PATCH 008/114] Use the correct help flag in error messages (#263) --- Documentation/04 Customizing Help.md | 4 ++-- .../Parsable Types/CommandConfiguration.swift | 7 ++++--- .../ArgumentParser/Usage/HelpGenerator.swift | 17 +++++++++-------- Sources/ArgumentParser/Usage/MessageInfo.swift | 7 +++++-- .../ArgumentParser/Usage/UsageGenerator.swift | 2 +- .../HelpTests.swift | 15 +++++++++++++-- 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/Documentation/04 Customizing Help.md b/Documentation/04 Customizing Help.md index bc3b320bd..f50546cdc 100644 --- a/Documentation/04 Customizing Help.md +++ b/Documentation/04 Customizing Help.md @@ -153,7 +153,7 @@ OPTIONS: -?, --help Show help information. ``` -If you don't provide alternative help names for Subcommand then it will inherit help names from it's immediate parent. +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 { @@ -168,7 +168,7 @@ struct Parent: ParsableCommand { } ``` -When running the command, `-h` matches the short name of the `host` property, and `-?` displays the help screen. +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 diff --git a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift index 63326e2ee..ea3b8b3af 100644 --- a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift +++ b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift @@ -64,9 +64,10 @@ 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. If `helpNames` - /// is `nil`, the flag names are derived by 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 + /// `-h` and `--help`. public init( commandName: String? = nil, abstract: String = "", diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index 9c4023c1a..39c041c40 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -291,15 +291,16 @@ fileprivate extension NameSpecification { } } -internal extension Array where Element == ParsableCommand.Type { +internal extension BidirectionalCollection where Element == ParsableCommand.Type { func getHelpNames() -> [Name] { - if(count == 0){ - return CommandConfiguration.defaultHelpNames.generateHelpNames() - } else if let helpNames = self.last!.configuration.helpNames { - return helpNames.generateHelpNames() - } else { - return self.dropLast().getHelpNames() - } + return self.last(where: { $0.configuration.helpNames != nil }) + .map { $0.configuration.helpNames!.generateHelpNames() } + ?? CommandConfiguration.defaultHelpNames.generateHelpNames() + } + + func getPrimaryHelpName() -> Name? { + let names = getHelpNames() + return names.first(where: { !$0.isShort }) ?? names.first } } diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index 4b8baf699..b1abeebd5 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -80,9 +80,12 @@ enum MessageInfo { parserError = .userValidationError(error) } + var usage = HelpGenerator(commandStack: commandStack).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() { + usage += "\n See '\(commandNames) \(helpName.synopsisString)' for more information." + } // Parsing errors and user-thrown validation errors have the usage // string attached. Other errors just get the error message. diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index 05b5538a3..1e61ec392 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -118,7 +118,7 @@ extension ArgumentDefinition { } var preferredNameForSynopsis: Name? { - names.first{ !$0.isShort } ?? names.first + names.first { !$0.isShort } ?? names.first } var synopsisValueName: String? { diff --git a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift index e5c5ad432..536f31171 100644 --- a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift +++ b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift @@ -198,6 +198,12 @@ extension HelpTests { func testCustomHelpNames() { let names = [CustomHelp.self].getHelpNames() XCTAssertEqual(names, [.short("?"), .long("show-help")]) + + AssertFullErrorMessage(CustomHelp.self, ["--error"], """ + Error: Unknown option '--error' + Usage: custom-help + See 'custom-help --show-help' for more information. + """) } } @@ -214,6 +220,11 @@ extension HelpTests { let names = [NoHelp.self].getHelpNames() XCTAssertEqual(names, []) + AssertFullErrorMessage(NoHelp.self, ["--error"], """ + Error: Missing expected argument '--count ' + Usage: no-help --count + """) + XCTAssertEqual( NoHelp.message(for: CleanExit.helpRequest()).trimmingLines(), """ @@ -228,7 +239,7 @@ extension HelpTests { struct SubCommandCustomHelp: ParsableCommand { static var configuration = CommandConfiguration ( - helpNames: [.customShort("p"), .customLong("parrent-help")] + helpNames: [.customShort("p"), .customLong("parent-help")] ) struct InheritHelp: ParsableCommand { @@ -249,7 +260,7 @@ struct SubCommandCustomHelp: ParsableCommand { extension HelpTests { func testSubCommandInheritHelpNames() { let names = [SubCommandCustomHelp.self, SubCommandCustomHelp.InheritHelp.self].getHelpNames() - XCTAssertEqual(names, [.short("p"), .long("parrent-help")]) + XCTAssertEqual(names, [.short("p"), .long("parent-help")]) } func testSubCommandCustomHelpNames() { From 605a2330c5643bc12b6e4efab726b372c38b5c5d Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 11 Feb 2021 10:41:46 -0600 Subject: [PATCH 009/114] Suppress hidden arguments from completion scripts (#271) --- Examples/math/main.swift | 14 +++++------- .../BashCompletionsGenerator.swift | 6 ++++- .../FishCompletionsGenerator.swift | 2 ++ .../Completions/ZshCompletionsGenerator.swift | 2 ++ .../MathExampleTests.swift | 22 +++++++------------ .../CompletionScriptTests.swift | 2 ++ 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Examples/math/main.swift b/Examples/math/main.swift index 06110e1db..5e40f684c 100644 --- a/Examples/math/main.swift +++ b/Examples/math/main.swift @@ -186,10 +186,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 +206,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 { diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 3ec9e54ef..c3153645d 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -182,7 +182,11 @@ struct BashCompletionsGenerator { extension ArgumentDefinition { /// Returns the different completion names for this argument. fileprivate func bashCompletionWords() -> [String] { - names.map { $0.synopsisString } +// print(help.help?.shouldDisplay != false, names.map { $0.synopsisString }) + + return help.help?.shouldDisplay == false + ? [] + : names.map { $0.synopsisString } } /// Returns the bash completions that can follow this argument's `--name`. diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index ed5e3ee67..aeb8a028d 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -99,6 +99,8 @@ extension Name { extension ArgumentDefinition { fileprivate func argumentSegments(_ commandChain: [String]) -> [([String], String)] { + guard help.help?.shouldDisplay != false else { return [] } + var results = [([String], String)]() var formattedFlags = [String]() var flags = [String]() diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 82a5c8123..aee012edd 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -134,6 +134,8 @@ extension ArgumentDefinition { } func zshCompletionString(_ commands: [ParsableCommand.Type]) -> String? { + guard help.help?.shouldDisplay != false else { return nil } + var inputs: String switch update { case .unary: diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift index 31380b871..d324e7659 100644 --- a/Tests/ArgumentParserExampleTests/MathExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -86,12 +86,18 @@ final class MathExampleTests: XCTestCase { 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. """ @@ -311,7 +317,7 @@ _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 -h --help" opts="$opts alphabet alligator branch braggart" opts="$opts $(math ---completion stats quantiles -- customArg "$COMP_WORDS")" if [[ $COMP_CWORD == "$1" ]]; then @@ -319,10 +325,6 @@ _math_stats_quantiles() { return fi case $prev in - --test-custom-exit-code) - - return - ;; --file) COMPREPLY=( $(compgen -f -- "$cur") ) return @@ -497,10 +499,6 @@ _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}' @@ -558,10 +556,6 @@ complete -c math -n '__fish_math_using_command math stats' -f -a 'quantiles' -d 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 diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift index 4bdf1fa22..da327d00f 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 { From 26114e5f963c8ab038632bda0ae6dec506fb1434 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 11 Feb 2021 10:53:39 -0600 Subject: [PATCH 010/114] Remove extraneous debugging statement (#273) --- .../ArgumentParser/Completions/BashCompletionsGenerator.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index c3153645d..809421fff 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -182,8 +182,6 @@ struct BashCompletionsGenerator { extension ArgumentDefinition { /// Returns the different completion names for this argument. fileprivate func bashCompletionWords() -> [String] { -// print(help.help?.shouldDisplay != false, names.map { $0.synopsisString }) - return help.help?.shouldDisplay == false ? [] : names.map { $0.synopsisString } From e99a8ef488533e3b331535902843230d2566d4ed Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 15 Feb 2021 15:28:13 -0600 Subject: [PATCH 011/114] Allow variable properties in parsable types (#268) * Allow variable properties in parsable types This captures the default value for non-parsable properties when building the ArgumentSet, which in turn get set as initial values before decoding. --- .../FishCompletionsGenerator.swift | 2 +- .../Parsable Types/ParsableArguments.swift | 30 ++++++++----- .../Parsing/ArgumentDefinition.swift | 16 ++++++- .../ArgumentParser/Parsing/InputOrigin.swift | 31 ++++++++++++-- .../ArgumentParser/Usage/UsageGenerator.swift | 4 ++ .../SimpleEndToEndTests.swift | 42 +++++++++++++++++++ 6 files changed, 110 insertions(+), 15 deletions(-) diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index aeb8a028d..e3266fd59 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -105,7 +105,7 @@ extension ArgumentDefinition { var formattedFlags = [String]() var flags = [String]() switch self.kind { - case .positional: + case .positional, .default: break case .named(let names): flags = names.map { $0.asFishSuggestion } diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index a3fdbdab9..2520c8752 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -226,17 +226,27 @@ 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 } + guard var codingKey = child.label 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) - } + if let parsed = child.value as? ArgumentSetProvider { + // Property wrappers have underscore-prefixed names + codingKey = String(codingKey.first == "_" + ? codingKey.dropFirst(1) + : codingKey.dropFirst(0)) + let key = InputKey(rawValue: codingKey) + return parsed.argumentSet(for: key) + } else { + // Save a non-wrapped property as is + var definition = ArgumentDefinition( + key: InputKey(rawValue: codingKey), + kind: .default, + parser: { _ in nil }, + default: child.value, + completion: .default) + definition.help.help = .hidden + return ArgumentSet(definition) + } + } self.init(sets: a) } } diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index 75dbefdc1..f544df44c 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -10,19 +10,31 @@ //===----------------------------------------------------------------------===// 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 { @@ -84,7 +96,7 @@ struct ArgumentDefinition { var names: [Name] { switch kind { case .named(let n): return n - case .positional: return [] + case .positional, .default: return [] } } @@ -156,6 +168,8 @@ extension ArgumentDefinition: CustomDebugStringConvertible { + " <\(valueName)>" case (.positional, _): return "<\(valueName)>" + case (.default, _): + return "" } } } diff --git a/Sources/ArgumentParser/Parsing/InputOrigin.swift b/Sources/ArgumentParser/Parsing/InputOrigin.swift index 7e8e194a5..e4b7bd763 100644 --- a/Sources/ArgumentParser/Parsing/InputOrigin.swift +++ b/Sources/ArgumentParser/Parsing/InputOrigin.swift @@ -11,16 +11,35 @@ /// 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 sinlge +/// 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 } @@ -28,6 +47,8 @@ struct InputOrigin: Equatable, ExpressibleByArrayLiteral { var subIndex: Int? { switch self { + case .defaultValue: + return nil case .argumentIndex(let i): switch i.subIndex { case .complete: return nil @@ -87,6 +108,10 @@ extension InputOrigin.Element { 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/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index 1e61ec392..1f377f916 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -75,6 +75,8 @@ extension ArgumentDefinition { } case .positional: return "<\(valueName)>" + case .default: + return "" } } @@ -91,6 +93,8 @@ extension ArgumentDefinition { } case .positional: return "<\(valueName)>" + case .default: + return "" } } diff --git a/Tests/ArgumentParserEndToEndTests/SimpleEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SimpleEndToEndTests.swift index c8fd2ef4f..ee482060d 100644 --- a/Tests/ArgumentParserEndToEndTests/SimpleEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SimpleEndToEndTests.swift @@ -115,3 +115,45 @@ extension SimpleEndToEndTests { XCTAssertThrowsError(try Baz.parse(["--name", "--format", "Bar", "Foo"])) } } + +// 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 SimpleEndToEndTests { + 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"])) + } +} From 267558bb4dd72742352bea9293c00f8c149c911f Mon Sep 17 00:00:00 2001 From: Kenny <11343005+schlagelk@users.noreply.github.com> Date: Mon, 15 Feb 2021 16:17:13 -0700 Subject: [PATCH 012/114] =?UTF-8?q?Beautify=20`NSError`=20cases=20when=20t?= =?UTF-8?q?hrown=20=F0=9F=92=84=20=20(#272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/ArgumentParser/Usage/MessageInfo.swift | 6 +++++- Tests/ArgumentParserUnitTests/ExitCodeTests.swift | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index b1abeebd5..a04deb814 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -111,7 +111,11 @@ 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 = { diff --git a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift index e6dca823e..2cc43f0cb 100644 --- a/Tests/ArgumentParserUnitTests/ExitCodeTests.swift +++ b/Tests/ArgumentParserUnitTests/ExitCodeTests.swift @@ -125,4 +125,12 @@ extension ExitCodeTests { func testCustomErrorCodeForTheSecondCase() { XCTAssertEqual(CheckFirstCustomNSErrorCommand.exitCode(for: MyCustomNSError.mySecondCase), ExitCode(rawValue: 102)) } + + 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") + } } From 5bfb39ac07561d05f23eb4f9ed48237674a1416e Mon Sep 17 00:00:00 2001 From: Karoy Lorentey Date: Tue, 16 Feb 2021 07:30:31 -0800 Subject: [PATCH 013/114] Generate useful synopsis for commands with many options (#275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ArgumentParser is configured not to emit detailed synopsis when it would contain more than a dozen entries. This makes sense; however, eliding all information makes the synopsis rather useless. While commands may have dozens of options, in most cases, only a few of them are required — so we can keep the synopsis short but still useful by only displaying the required parts. * Include all positional arguments in shortened synopsis --- .../ArgumentParser/Usage/UsageGenerator.swift | 11 +++++++++ .../HelpTests.swift | 2 +- .../UsageGenerationTests.swift | 24 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift index 1f377f916..b0b90eb6d 100644 --- a/Sources/ArgumentParser/Usage/UsageGenerator.swift +++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift @@ -41,6 +41,17 @@ extension UsageGenerator { case 0: return toolName case let x where x > 12: + // When we have too many options, keep required and positional arguments, + // but discard the rest. + let synopsis: [String] = definition.compactMap { argument in + guard argument.isPositional || !argument.help.options.contains(.isOptional) else { + return nil + } + return argument.synopsis + } + if !synopsis.isEmpty, synopsis.count <= 12 { + return "\(toolName) [] \(synopsis.joined(separator: " "))" + } return "\(toolName) " default: return "\(toolName) \(definition.synopsis.joined(separator: " "))" diff --git a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift index 536f31171..ff47ea33c 100644 --- a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift +++ b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift @@ -113,7 +113,7 @@ extension HelpTests { XCTAssertEqual( getErrorText(Package.self, ["help", "config", "get-mirror"]).trimmingLines(), """ - USAGE: package config get-mirror + USAGE: package config get-mirror [] --package-url OPTIONS: --build-path diff --git a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift index c52f2fda2..bf50b4b33 100644 --- a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift @@ -173,4 +173,28 @@ extension UsageGenerationTests { let help = UsageGenerator(toolName: "bar", parsable: L()) XCTAssertEqual(help.synopsis, "bar [-remote ]") } + + struct M: ParsableArguments { + @Flag var a: Bool = false + @Flag var b: Bool = false + @Flag var c: Bool = false + @Flag var d: Bool = false + @Flag var e: Bool = false + @Flag var f: Bool = false + @Flag var g: Bool = false + @Flag var h: Bool = false + @Flag var i: Bool = false + @Flag var j: Bool = false + @Flag var k: Bool = false + @Flag var l: Bool = false + @Option var option: Bool + @Argument var input: String + @Argument var output: String? + } + + func testSynopsisWithTooManyOptions() { + let help = UsageGenerator(toolName: "foo", parsable: M()) + XCTAssertEqual(help.synopsis, + "foo [] --option