Skip to content

Commit

Permalink
Add built-in support for --version flag (apple#102)
Browse files Browse the repository at this point in the history
* Add built-in support for --version flag

* Test that command-defined --version overrides the built-in.

* Document the `version:` parameter in CommandConfiguration

* Include --version in the generated help.
  • Loading branch information
natecook1000 authored Mar 30, 2020
1 parent 023730b commit 31799bc
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 12 deletions.
6 changes: 5 additions & 1 deletion Examples/math/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ struct Math: ParsableCommand {
// Optional abstracts and discussions are used for help output.
abstract: "A utility for performing maths.",

// Commands can define a version for automatic '--version' support.
version: "1.0.0",

// Pass an array to `subcommands` to set up a nested tree of subcommands.
// With language support for type-level introspection, this could be
// provided by automatically finding nested `ParsableCommand` types.
Expand Down Expand Up @@ -89,7 +92,8 @@ extension Math {
extension Math.Statistics {
struct Average: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Print the average of the values.")
abstract: "Print the average of the values.",
version: "1.5.0-alpha")

enum Kind: String, ExpressibleByArgument {
case mean, median, mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public struct CommandConfiguration {
/// display.
public var discussion: String

/// Version information for this command.
public var version: String

/// A Boolean value indicating whether this command should be shown in
/// the extended help display.
public var shouldDisplay: Bool
Expand All @@ -45,6 +48,9 @@ public struct CommandConfiguration {
/// the name of the command type to hyphen-separated lowercase words.
/// - abstract: A one-line description of the command.
/// - discussion: A longer description of the command.
/// - version: The version number for this command. When you provide a
/// non-empty string, the arguemnt parser prints it if the user provides
/// a `--version` flag.
/// - shouldDisplay: A Boolean value indicating whether the command
/// should be shown in the extended help display.
/// - subcommands: An array of the types that define subcommands for the
Expand All @@ -57,6 +63,7 @@ public struct CommandConfiguration {
commandName: String? = nil,
abstract: String = "",
discussion: String = "",
version: String = "",
shouldDisplay: Bool = true,
subcommands: [ParsableCommand.Type] = [],
defaultSubcommand: ParsableCommand.Type? = nil,
Expand All @@ -65,6 +72,7 @@ public struct CommandConfiguration {
self.commandName = commandName
self.abstract = abstract
self.discussion = discussion
self.version = version
self.shouldDisplay = shouldDisplay
self.subcommands = subcommands
self.defaultSubcommand = defaultSubcommand
Expand Down
28 changes: 24 additions & 4 deletions Sources/ArgumentParser/Parsing/CommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,26 @@ extension CommandParser {

/// Throws a `HelpRequested` error if the user has specified either of the
/// built in help flags.
func checkForHelpFlag(_ split: SplitArguments) throws {
func checkForBuiltInFlags(_ split: SplitArguments) throws {
// Look for help flags
guard !split.contains(anyOf: self.commandTree.element.getHelpNames()) else {
throw HelpRequested()
}

// Look for --version if any commands in the stack define a version
if commandStack.contains(where: { !$0.configuration.version.isEmpty }) {
guard !split.contains(Name.long("version")) else {
throw CommandError(commandStack: commandStack, parserError: .versionRequested)
}
}
}

/// Returns the last parsed value if there are no remaining unused arguments.
///
/// If there are remaining arguments or if no commands have been parsed,
/// this throws an error.
fileprivate func extractLastParsedValue(_ split: SplitArguments) throws -> ParsableCommand {
try checkForHelpFlag(split)
try checkForBuiltInFlags(split)

// We should have used up all arguments at this point:
guard split.isEmpty else {
Expand Down Expand Up @@ -124,7 +132,7 @@ extension CommandParser {
} catch let error {
// If decoding this command failed, see if they were asking for
// help before propagating that parsing failure.
try checkForHelpFlag(split)
try checkForBuiltInFlags(split)
throw error
}

Expand Down Expand Up @@ -152,7 +160,7 @@ extension CommandParser {
}

// Look for the help flag before falling back to a default command.
try checkForHelpFlag(split)
try checkForBuiltInFlags(split)

// No command was found, so fall back to the default subcommand.
if let defaultSubcommand = currentNode.element.configuration.defaultSubcommand {
Expand Down Expand Up @@ -238,6 +246,18 @@ extension CommandParser {
}

extension SplitArguments {
func contains(_ needle: Name) -> Bool {
self.elements.contains {
switch $0.element {
case .option(.name(let name)),
.option(.nameWithValue(let name, _)):
return name == needle
default:
return false
}
}
}

func contains(anyOf names: [Name]) -> Bool {
self.elements.contains {
switch $0.element {
Expand Down
1 change: 1 addition & 0 deletions Sources/ArgumentParser/Parsing/ParserError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
/// Gets thrown while parsing and will be handled by the error output generation.
enum ParserError: Error {
case helpRequested
case versionRequested
case notImplemented
case invalidState
case unknownOption(InputOrigin.Element, Name)
Expand Down
1 change: 0 additions & 1 deletion Sources/ArgumentParser/Usage/HelpCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ struct HelpCommand: ParsableCommand {
self._subcommands = Argument(_parsedValue: .value(commandStack.map { $0._commandName }))
}
}

6 changes: 5 additions & 1 deletion Sources/ArgumentParser/Usage/HelpGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ internal struct HelpGenerator {
}
}

if commandStack.contains(where: { !$0.configuration.version.isEmpty }) {
optionElements.append(.init(label: "--version", abstract: "Show the version."))
}

let helpLabels = commandStack
.first!
.getHelpNames()
Expand All @@ -198,7 +202,7 @@ internal struct HelpGenerator {
if !helpLabels.isEmpty {
optionElements.append(.init(label: helpLabels, abstract: "Show help information."))
}

let subcommandElements: [Section.Element] =
commandStack.last!.configuration.subcommands.compactMap { command in
guard command.configuration.shouldDisplay else { return nil }
Expand Down
13 changes: 12 additions & 1 deletion Sources/ArgumentParser/Usage/MessageInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,20 @@ enum MessageInfo {
case let e as CommandError:
commandStack = e.commandStack
parserError = e.parserError
if case .helpRequested = e.parserError {

switch e.parserError {
case .helpRequested:
self = .help(text: HelpGenerator(commandStack: e.commandStack).rendered)
return
case .versionRequested:
let versionString = commandStack
.map { $0.configuration.version }
.last(where: { !$0.isEmpty })
?? "Unspecified version"
self = .help(text: versionString)
return
default:
break
}
case let e as ParserError:
commandStack = [type.asCommand]
Expand Down
7 changes: 5 additions & 2 deletions Sources/ArgumentParser/Usage/UsageGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ struct ErrorMessageGenerator {
extension ErrorMessageGenerator {
func makeErrorMessage() -> String? {
switch error {
case .helpRequested:
return nil
case .versionRequested:
return nil

case .notImplemented:
return notImplementedMessage
case .invalidState:
Expand All @@ -194,8 +199,6 @@ extension ErrorMessageGenerator {
return noValueMessage(key: k)
case .unableToParseValue(let o, let n, let v, forKey: let k):
return unableToParseValueMessage(origin: o, name: n, value: v, key: k)
case .helpRequested:
return nil
case .invalidOption(let str):
return "Invalid option: \(str)"
case .nonAlphanumericShortOption(let c):
Expand Down
26 changes: 26 additions & 0 deletions Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,29 @@ extension SubcommandEndToEndTests {
}
}

// MARK: Version flags

private struct A: ParsableCommand {
static var configuration = CommandConfiguration(
version: "1.0.0",
subcommands: [HasVersionFlag.self, NoVersionFlag.self])

struct HasVersionFlag: ParsableCommand {
@Flag() var version: Bool
}

struct NoVersionFlag: ParsableCommand {
@Flag() var hello: Bool
}
}

extension SubcommandEndToEndTests {
func testParsingVersionFlags() throws {
AssertErrorMessage(A.self, ["--version"], "1.0.0")
AssertErrorMessage(A.self, ["no-version-flag", "--version"], "1.0.0")

AssertParseCommand(A.self, A.HasVersionFlag.self, ["has-version-flag", "--version"]) { cmd in
XCTAssertTrue(cmd.version)
}
}
}
16 changes: 16 additions & 0 deletions Tests/ArgumentParserExampleTests/MathExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class MathExampleTests: XCTestCase {
USAGE: math <subcommand>
OPTIONS:
--version Show the version.
-h, --help Show help information.
SUBCOMMANDS:
Expand All @@ -50,6 +51,7 @@ final class MathExampleTests: XCTestCase {
OPTIONS:
-x, --hex-output Use hexadecimal notation for the result.
--version Show the version.
-h, --help Show help information.
"""

Expand All @@ -69,6 +71,7 @@ final class MathExampleTests: XCTestCase {
OPTIONS:
--kind <kind> The kind of average to provide. (default: mean)
--version Show the version.
-h, --help Show help information.
"""

Expand All @@ -87,6 +90,7 @@ final class MathExampleTests: XCTestCase {
<values> A group of floating-point values to operate on.
OPTIONS:
--version Show the version.
-h, --help Show help information.
"""

Expand All @@ -108,6 +112,18 @@ final class MathExampleTests: XCTestCase {
""",
exitCode: .validationFailure)
}

func testMath_Versions() throws {
AssertExecuteCommand(
command: "math --version",
expected: "1.0.0")
AssertExecuteCommand(
command: "math stats --version",
expected: "1.0.0")
AssertExecuteCommand(
command: "math stats average --version",
expected: "1.5.0-alpha")
}

func testMath_ExitCodes() throws {
AssertExecuteCommand(
Expand Down
10 changes: 9 additions & 1 deletion Tests/ArgumentParserExampleTests/RepeatExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ final class RepeatExampleTests: XCTestCase {
Usage: repeat [--count <count>] [--include-counter] <phrase>
""",
exitCode: .validationFailure)

AssertExecuteCommand(
command: "repeat hello --count",
expected: """
Expand All @@ -66,5 +66,13 @@ final class RepeatExampleTests: XCTestCase {
Usage: repeat [--count <count>] [--include-counter] <phrase>
""",
exitCode: .validationFailure)

AssertExecuteCommand(
command: "repeat --version hello",
expected: """
Error: Unknown option '--version'
Usage: repeat [--count <count>] [--include-counter] <phrase>
""",
exitCode: .validationFailure)
}
}
33 changes: 32 additions & 1 deletion Tests/ArgumentParserUnitTests/ExitCodeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ final class ExitCodeTests: XCTestCase {
extension ExitCodeTests {
struct A: ParsableArguments {}
struct E: Error {}

struct C: ParsableCommand {
static var configuration = CommandConfiguration(version: "v1")
}

func testExitCodes() {
XCTAssertEqual(ExitCode.failure, A.exitCode(for: E()))
XCTAssertEqual(ExitCode.validationFailure, A.exitCode(for: ValidationError("")))
Expand All @@ -31,6 +34,20 @@ extension ExitCodeTests {
} catch {
XCTAssertEqual(ExitCode.success, A.exitCode(for: error))
}

do {
_ = try A.parse(["--version"])
XCTFail("Didn't throw unrecognized --version error.")
} catch {
XCTAssertEqual(ExitCode.validationFailure, A.exitCode(for: error))
}

do {
_ = try C.parse(["--version"])
XCTFail("Didn't throw version request error.")
} catch {
XCTAssertEqual(ExitCode.success, C.exitCode(for: error))
}
}

func testExitCode_Success() {
Expand All @@ -43,5 +60,19 @@ extension ExitCodeTests {
} catch {
XCTAssertTrue(A.exitCode(for: error).isSuccess)
}

do {
_ = try A.parse(["--version"])
XCTFail("Didn't throw unrecognized --version error.")
} catch {
XCTAssertFalse(A.exitCode(for: error).isSuccess)
}

do {
_ = try C.parse(["--version"])
XCTFail("Didn't throw version request error.")
} catch {
XCTAssertTrue(C.exitCode(for: error).isSuccess)
}
}
}
16 changes: 16 additions & 0 deletions Tests/ArgumentParserUnitTests/HelpGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,20 @@ extension HelpGenerationTests {
""")
}

struct I: ParsableCommand {
static var configuration = CommandConfiguration(version: "1.0.0")
}

func testHelpWithVersion() {
AssertHelp(for: I.self, equals: """
USAGE: i
OPTIONS:
--version Show the version.
-h, --help Show help information.
""")

}
}

0 comments on commit 31799bc

Please sign in to comment.