Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions Sources/ArgumentParser/Parsable Properties/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,17 @@ public struct ValidationError: Error, CustomStringConvertible {
/// If you're printing custom errors messages yourself, you can throw this error
/// to specify the exit code without adding any additional output to standard
/// out or standard error.
public struct ExitCode: Error {
var code: Int32
public struct ExitCode: Error, RawRepresentable, Hashable {
/// The exit code represented by this instance.
public var rawValue: Int32

/// Creates a new `ExitCode` with the given code.
public init(_ code: Int32) {
self.code = code
self.rawValue = code
}

public init(rawValue: Int32) {
self.init(rawValue)
}

/// An exit code that indicates successful completion of a command.
Expand All @@ -53,6 +58,12 @@ public struct ExitCode: Error {

/// An exit code that indicates that the user provided invalid input.
public static let validationFailure = ExitCode(EX_USAGE)

/// A Boolean value indicating whether this exit code represents the
/// successful completion of a command.
public var isSuccess: Bool {
self == Self.success
}
}

/// An error type that represents a clean (i.e. non-error state) exit of the
Expand Down
17 changes: 15 additions & 2 deletions Sources/ArgumentParser/Parsable Types/ParsableArguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@ extension ParsableArguments {
MessageInfo(error: error, type: self).fullText
}

/// Returns the exit code for the given error.
///
/// The returned code is the same exit code that is used if `error` is passed
/// to `exit(withError:)`.
///
/// - Parameter error: An error to generate an exit code for.
/// - Returns: The exit code for `error`.
public static func exitCode(
for error: Error
) -> ExitCode {
MessageInfo(error: error, type: self).exitCode
}

/// Terminates execution with a message and exit code that is appropriate
/// for the given error.
///
Expand All @@ -139,7 +152,7 @@ extension ParsableArguments {
withError error: Error? = nil
) -> Never {
guard let error = error else {
_exit(ExitCode.success.code)
_exit(ExitCode.success.rawValue)
}

let messageInfo = MessageInfo(error: error, type: self)
Expand All @@ -150,7 +163,7 @@ extension ParsableArguments {
print(messageInfo.fullText, to: &standardError)
}
}
_exit(messageInfo.exitCode)
_exit(messageInfo.exitCode.rawValue)
}

/// Parses a new instance of this type from command-line arguments or exits
Expand Down
10 changes: 5 additions & 5 deletions Sources/ArgumentParser/Usage/MessageInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ enum MessageInfo {
self = .help(text: message)
}
case let error as ExitCode:
self = .other(message: "", exitCode: error.code)
self = .other(message: "", exitCode: error.rawValue)
case let error as LocalizedError where error.errorDescription != nil:
self = .other(message: error.errorDescription!, exitCode: EXIT_FAILURE)
default:
Expand Down Expand Up @@ -106,11 +106,11 @@ enum MessageInfo {
}
}

var exitCode: Int32 {
var exitCode: ExitCode {
switch self {
case .help: return ExitCode.success.code
case .validation: return ExitCode.validationFailure.code
case .other(_, let exitCode): return exitCode
case .help: return ExitCode.success
case .validation: return ExitCode.validationFailure
case .other(_, let code): return ExitCode(code)
}
}
}
4 changes: 2 additions & 2 deletions Sources/ArgumentParserTestHelpers/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ extension XCTest {
public func AssertExecuteCommand(
command: String,
expected: String? = nil,
exitCode: Int32 = 0,
exitCode: ExitCode = .success,
file: StaticString = #file, line: UInt = #line)
{
let splitCommand = command.split(separator: " ")
Expand Down Expand Up @@ -172,6 +172,6 @@ extension XCTest {
AssertEqualStringsIgnoringTrailingWhitespace(expected, errorActual + outputActual, file: file, line: line)
}

XCTAssertEqual(process.terminationStatus, exitCode, file: file, line: line)
XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: file, line: line)
}
}
15 changes: 8 additions & 7 deletions Tests/ArgumentParserExampleTests/MathExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//===----------------------------------------------------------------------===//

import XCTest
import ArgumentParser
import ArgumentParserTestHelpers

final class MathExampleTests: XCTestCase {
Expand Down Expand Up @@ -105,26 +106,26 @@ final class MathExampleTests: XCTestCase {
Error: Please provide at least one value to calculate the mode.
Usage: math stats average [--kind <kind>] [<values> ...]
""",
exitCode: EX_USAGE)
exitCode: .validationFailure)
}

func testMath_ExitCodes() throws {
AssertExecuteCommand(
command: "math stats quantiles --test-success-exit-code",
expected: "",
exitCode: EXIT_SUCCESS)
exitCode: .success)
AssertExecuteCommand(
command: "math stats quantiles --test-failure-exit-code",
expected: "",
exitCode: EXIT_FAILURE)
exitCode: .failure)
AssertExecuteCommand(
command: "math stats quantiles --test-validation-exit-code",
expected: "",
exitCode: EX_USAGE)
exitCode: .validationFailure)
AssertExecuteCommand(
command: "math stats quantiles --test-custom-exit-code 42",
expected: "",
exitCode: 42)
exitCode: ExitCode(42))
}

func testMath_Fail() throws {
Expand All @@ -134,14 +135,14 @@ final class MathExampleTests: XCTestCase {
Error: Unknown option '--foo'
Usage: math add [--hex-output] [<values> ...]
""",
exitCode: EX_USAGE)
exitCode: .validationFailure)

AssertExecuteCommand(
command: "math ZZZ",
expected: """
Error: The value 'ZZZ' is invalid for '<values>'
Usage: math add [--hex-output] [<values> ...]
""",
exitCode: EX_USAGE)
exitCode: .validationFailure)
}
}
7 changes: 4 additions & 3 deletions Tests/ArgumentParserExampleTests/RepeatExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//===----------------------------------------------------------------------===//

import XCTest
import ArgumentParser
import ArgumentParserTestHelpers

final class RepeatExampleTests: XCTestCase {
Expand Down Expand Up @@ -48,22 +49,22 @@ final class RepeatExampleTests: XCTestCase {
Error: Missing expected argument '<phrase>'
Usage: repeat [--count <count>] [--include-counter] <phrase>
""",
exitCode: EX_USAGE)
exitCode: .validationFailure)

AssertExecuteCommand(
command: "repeat hello --count",
expected: """
Error: Missing value for '--count <count>'
Usage: repeat [--count <count>] [--include-counter] <phrase>
""",
exitCode: EX_USAGE)
exitCode: .validationFailure)

AssertExecuteCommand(
command: "repeat hello --count ZZZ",
expected: """
Error: The value 'ZZZ' is invalid for '--count <count>'
Usage: repeat [--count <count>] [--include-counter] <phrase>
""",
exitCode: EX_USAGE)
exitCode: .validationFailure)
}
}
5 changes: 3 additions & 2 deletions Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//===----------------------------------------------------------------------===//

import XCTest
import ArgumentParser
import ArgumentParserTestHelpers

final class RollDiceExampleTests: XCTestCase {
Expand Down Expand Up @@ -41,14 +42,14 @@ final class RollDiceExampleTests: XCTestCase {
Error: Missing value for '--times <n>'
Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
""",
exitCode: EX_USAGE)
exitCode: .validationFailure)

AssertExecuteCommand(
command: "roll --times ZZZ",
expected: """
Error: The value 'ZZZ' is invalid for '--times <n>'
Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
""",
exitCode: EX_USAGE)
exitCode: .validationFailure)
}
}
47 changes: 47 additions & 0 deletions Tests/ArgumentParserUnitTests/ExitCodeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

import XCTest
@testable import ArgumentParser

final class ExitCodeTests: XCTestCase {
}

// MARK: -

extension ExitCodeTests {
struct A: ParsableArguments {}
struct E: Error {}

func testExitCodes() {
XCTAssertEqual(ExitCode.failure, A.exitCode(for: E()))
XCTAssertEqual(ExitCode.validationFailure, A.exitCode(for: ValidationError("")))

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

func testExitCode_Success() {
XCTAssertFalse(A.exitCode(for: E()).isSuccess)
XCTAssertFalse(A.exitCode(for: ValidationError("")).isSuccess)

do {
_ = try A.parse(["-h"])
XCTFail("Didn't throw help request error.")
} catch {
XCTAssertTrue(A.exitCode(for: error).isSuccess)
}
}
}