Skip to content

Commit ddc828f

Browse files
authored
Add an API for converting an error to an exit code (#79)
* Add an API for converting an error to an exit code * Make ExitCode more useful as a value type * Update tests to use ExitCode values * Typo fix * Add a test for ExitCode.isSuccess * Switch to just using ExitCode for tests
1 parent 9e77589 commit ddc828f

File tree

8 files changed

+98
-24
lines changed

8 files changed

+98
-24
lines changed

Sources/ArgumentParser/Parsable Properties/Errors.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,17 @@ public struct ValidationError: Error, CustomStringConvertible {
3737
/// If you're printing custom errors messages yourself, you can throw this error
3838
/// to specify the exit code without adding any additional output to standard
3939
/// out or standard error.
40-
public struct ExitCode: Error {
41-
var code: Int32
40+
public struct ExitCode: Error, RawRepresentable, Hashable {
41+
/// The exit code represented by this instance.
42+
public var rawValue: Int32
4243

4344
/// Creates a new `ExitCode` with the given code.
4445
public init(_ code: Int32) {
45-
self.code = code
46+
self.rawValue = code
47+
}
48+
49+
public init(rawValue: Int32) {
50+
self.init(rawValue)
4651
}
4752

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

5459
/// An exit code that indicates that the user provided invalid input.
5560
public static let validationFailure = ExitCode(EX_USAGE)
61+
62+
/// A Boolean value indicating whether this exit code represents the
63+
/// successful completion of a command.
64+
public var isSuccess: Bool {
65+
self == Self.success
66+
}
5667
}
5768

5869
/// An error type that represents a clean (i.e. non-error state) exit of the

Sources/ArgumentParser/Parsable Types/ParsableArguments.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ extension ParsableArguments {
125125
MessageInfo(error: error, type: self).fullText
126126
}
127127

128+
/// Returns the exit code for the given error.
129+
///
130+
/// The returned code is the same exit code that is used if `error` is passed
131+
/// to `exit(withError:)`.
132+
///
133+
/// - Parameter error: An error to generate an exit code for.
134+
/// - Returns: The exit code for `error`.
135+
public static func exitCode(
136+
for error: Error
137+
) -> ExitCode {
138+
MessageInfo(error: error, type: self).exitCode
139+
}
140+
128141
/// Terminates execution with a message and exit code that is appropriate
129142
/// for the given error.
130143
///
@@ -139,7 +152,7 @@ extension ParsableArguments {
139152
withError error: Error? = nil
140153
) -> Never {
141154
guard let error = error else {
142-
_exit(ExitCode.success.code)
155+
_exit(ExitCode.success.rawValue)
143156
}
144157

145158
let messageInfo = MessageInfo(error: error, type: self)
@@ -150,7 +163,7 @@ extension ParsableArguments {
150163
print(messageInfo.fullText, to: &standardError)
151164
}
152165
}
153-
_exit(messageInfo.exitCode)
166+
_exit(messageInfo.exitCode.rawValue)
154167
}
155168

156169
/// Parses a new instance of this type from command-line arguments or exits

Sources/ArgumentParser/Usage/MessageInfo.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ enum MessageInfo {
6262
self = .help(text: message)
6363
}
6464
case let error as ExitCode:
65-
self = .other(message: "", exitCode: error.code)
65+
self = .other(message: "", exitCode: error.rawValue)
6666
case let error as LocalizedError where error.errorDescription != nil:
6767
self = .other(message: error.errorDescription!, exitCode: EXIT_FAILURE)
6868
default:
@@ -106,11 +106,11 @@ enum MessageInfo {
106106
}
107107
}
108108

109-
var exitCode: Int32 {
109+
var exitCode: ExitCode {
110110
switch self {
111-
case .help: return ExitCode.success.code
112-
case .validation: return ExitCode.validationFailure.code
113-
case .other(_, let exitCode): return exitCode
111+
case .help: return ExitCode.success
112+
case .validation: return ExitCode.validationFailure
113+
case .other(_, let code): return ExitCode(code)
114114
}
115115
}
116116
}

Sources/ArgumentParserTestHelpers/TestHelpers.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ extension XCTest {
136136
public func AssertExecuteCommand(
137137
command: String,
138138
expected: String? = nil,
139-
exitCode: Int32 = 0,
139+
exitCode: ExitCode = .success,
140140
file: StaticString = #file, line: UInt = #line)
141141
{
142142
let splitCommand = command.split(separator: " ")
@@ -172,6 +172,6 @@ extension XCTest {
172172
AssertEqualStringsIgnoringTrailingWhitespace(expected, errorActual + outputActual, file: file, line: line)
173173
}
174174

175-
XCTAssertEqual(process.terminationStatus, exitCode, file: file, line: line)
175+
XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: file, line: line)
176176
}
177177
}

Tests/ArgumentParserExampleTests/MathExampleTests.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
import XCTest
13+
import ArgumentParser
1314
import ArgumentParserTestHelpers
1415

1516
final class MathExampleTests: XCTestCase {
@@ -105,26 +106,26 @@ final class MathExampleTests: XCTestCase {
105106
Error: Please provide at least one value to calculate the mode.
106107
Usage: math stats average [--kind <kind>] [<values> ...]
107108
""",
108-
exitCode: EX_USAGE)
109+
exitCode: .validationFailure)
109110
}
110111

111112
func testMath_ExitCodes() throws {
112113
AssertExecuteCommand(
113114
command: "math stats quantiles --test-success-exit-code",
114115
expected: "",
115-
exitCode: EXIT_SUCCESS)
116+
exitCode: .success)
116117
AssertExecuteCommand(
117118
command: "math stats quantiles --test-failure-exit-code",
118119
expected: "",
119-
exitCode: EXIT_FAILURE)
120+
exitCode: .failure)
120121
AssertExecuteCommand(
121122
command: "math stats quantiles --test-validation-exit-code",
122123
expected: "",
123-
exitCode: EX_USAGE)
124+
exitCode: .validationFailure)
124125
AssertExecuteCommand(
125126
command: "math stats quantiles --test-custom-exit-code 42",
126127
expected: "",
127-
exitCode: 42)
128+
exitCode: ExitCode(42))
128129
}
129130

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

139140
AssertExecuteCommand(
140141
command: "math ZZZ",
141142
expected: """
142143
Error: The value 'ZZZ' is invalid for '<values>'
143144
Usage: math add [--hex-output] [<values> ...]
144145
""",
145-
exitCode: EX_USAGE)
146+
exitCode: .validationFailure)
146147
}
147148
}

Tests/ArgumentParserExampleTests/RepeatExampleTests.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
import XCTest
13+
import ArgumentParser
1314
import ArgumentParserTestHelpers
1415

1516
final class RepeatExampleTests: XCTestCase {
@@ -48,22 +49,22 @@ final class RepeatExampleTests: XCTestCase {
4849
Error: Missing expected argument '<phrase>'
4950
Usage: repeat [--count <count>] [--include-counter] <phrase>
5051
""",
51-
exitCode: EX_USAGE)
52+
exitCode: .validationFailure)
5253

5354
AssertExecuteCommand(
5455
command: "repeat hello --count",
5556
expected: """
5657
Error: Missing value for '--count <count>'
5758
Usage: repeat [--count <count>] [--include-counter] <phrase>
5859
""",
59-
exitCode: EX_USAGE)
60+
exitCode: .validationFailure)
6061

6162
AssertExecuteCommand(
6263
command: "repeat hello --count ZZZ",
6364
expected: """
6465
Error: The value 'ZZZ' is invalid for '--count <count>'
6566
Usage: repeat [--count <count>] [--include-counter] <phrase>
6667
""",
67-
exitCode: EX_USAGE)
68+
exitCode: .validationFailure)
6869
}
6970
}

Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
import XCTest
13+
import ArgumentParser
1314
import ArgumentParserTestHelpers
1415

1516
final class RollDiceExampleTests: XCTestCase {
@@ -41,14 +42,14 @@ final class RollDiceExampleTests: XCTestCase {
4142
Error: Missing value for '--times <n>'
4243
Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
4344
""",
44-
exitCode: EX_USAGE)
45+
exitCode: .validationFailure)
4546

4647
AssertExecuteCommand(
4748
command: "roll --times ZZZ",
4849
expected: """
4950
Error: The value 'ZZZ' is invalid for '--times <n>'
5051
Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
5152
""",
52-
exitCode: EX_USAGE)
53+
exitCode: .validationFailure)
5354
}
5455
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
@testable import ArgumentParser
14+
15+
final class ExitCodeTests: XCTestCase {
16+
}
17+
18+
// MARK: -
19+
20+
extension ExitCodeTests {
21+
struct A: ParsableArguments {}
22+
struct E: Error {}
23+
24+
func testExitCodes() {
25+
XCTAssertEqual(ExitCode.failure, A.exitCode(for: E()))
26+
XCTAssertEqual(ExitCode.validationFailure, A.exitCode(for: ValidationError("")))
27+
28+
do {
29+
_ = try A.parse(["-h"])
30+
XCTFail("Didn't throw help request error.")
31+
} catch {
32+
XCTAssertEqual(ExitCode.success, A.exitCode(for: error))
33+
}
34+
}
35+
36+
func testExitCode_Success() {
37+
XCTAssertFalse(A.exitCode(for: E()).isSuccess)
38+
XCTAssertFalse(A.exitCode(for: ValidationError("")).isSuccess)
39+
40+
do {
41+
_ = try A.parse(["-h"])
42+
XCTFail("Didn't throw help request error.")
43+
} catch {
44+
XCTAssertTrue(A.exitCode(for: error).isSuccess)
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)