Skip to content

Commit eee7edc

Browse files
committed
Provide async command support
Adds a new `AsyncParsableCommand` protocol, which provides a `static func main() async` entry point and can call through to the root command's or a subcommand's asynchronous `run()` method. For this asynchronous execution, the root command must conform to `AsyncParsableCommand`, but its subcommands can be a mix of asynchronous and synchronous commands. Due to an issue in Swift 5.5, you can only use `@main` on an `AsyncParsableCommand` root command starting in Swift 5.6. This change also includes a workaround for clients that are using Swift 5.5. Declare a separate type that conforms to `AsyncMainProtocol` and add the `@main` attribute to that type. ``` @main enum Main: AsyncMain { typealias Command = <#command#> } ```
1 parent 5540737 commit eee7edc

File tree

14 files changed

+318
-143
lines changed

14 files changed

+318
-143
lines changed

Examples/count-lines/CountLines.swift

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
#if os(macOS)
13+
14+
import ArgumentParser
15+
import Foundation
16+
17+
struct CountLines: AsyncParsableCommand {
18+
@Argument(
19+
help: "A file to count lines in. If omitted, counts the lines of stdin.",
20+
completion: .file(), transform: URL.init(fileURLWithPath:))
21+
var inputFile: URL?
22+
23+
@Option(help: "Only count lines with this prefix.")
24+
var prefix: String?
25+
26+
@Flag(help: "Include extra information in the output.")
27+
var verbose = false
28+
}
29+
30+
extension CountLines {
31+
var fileHandle: FileHandle {
32+
get throws {
33+
guard let inputFile = inputFile else {
34+
return .standardInput
35+
}
36+
return try FileHandle(forReadingFrom: inputFile)
37+
}
38+
}
39+
40+
func printCount(_ count: Int) {
41+
guard verbose else {
42+
print(count)
43+
return
44+
}
45+
46+
if let filename = inputFile?.lastPathComponent {
47+
print("Lines in '\(filename)'", terminator: "")
48+
} else {
49+
print("Lines from stdin", terminator: "")
50+
}
51+
52+
if let prefix = prefix {
53+
print(", prefixed by '\(prefix)'", terminator: "")
54+
}
55+
56+
print(": \(count)")
57+
}
58+
59+
mutating func run() async throws {
60+
let countAllLines = prefix == nil
61+
62+
let lineCount = try await fileHandle.bytes.lines.reduce(0) { count, line in
63+
if countAllLines || line.starts(with: prefix!) {
64+
return count + 1
65+
} else {
66+
return count
67+
}
68+
}
69+
70+
printCount(lineCount)
71+
}
72+
}
73+
74+
#if swift(>=5.6)
75+
@main extension CountLines {}
76+
#else
77+
@main struct AsyncMain: AsyncMainProtocol {
78+
typealias Command = CountLines
79+
}
80+
#endif
81+
82+
#else
83+
84+
@main enum Main {
85+
static func main() {
86+
print("Unsupported on this platform.")
87+
}
88+
}
89+
90+
#endif

Examples/math/main.swift renamed to Examples/math/Math.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import ArgumentParser
1313

14+
@main
1415
struct Math: ParsableCommand {
1516
// Customize your command's help and subcommands by implementing the
1617
// `configuration` property.
@@ -242,5 +243,3 @@ func customCompletion(_ s: [String]) -> [String] {
242243
? ["aardvark", "aaaaalbert"]
243244
: ["hello", "helicopter", "heliotrope"]
244245
}
245-
246-
Math.main()

Examples/repeat/main.swift renamed to Examples/repeat/Repeat.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import ArgumentParser
1313

14+
@main
1415
struct Repeat: ParsableCommand {
1516
@Option(help: "The number of times to repeat 'phrase'.")
1617
var count: Int?
@@ -33,5 +34,3 @@ struct Repeat: ParsableCommand {
3334
}
3435
}
3536
}
36-
37-
Repeat.main()

Package.swift

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.2
1+
// swift-tools-version:5.5
22
//===----------------------------------------------------------*- swift -*-===//
33
//
44
// This source file is part of the Swift Argument Parser open source project
@@ -12,8 +12,9 @@
1212

1313
import PackageDescription
1414

15-
var package = Package(
15+
let package = Package(
1616
name: "swift-argument-parser",
17+
platforms: [.macOS(.v12)],
1718
products: [
1819
.library(
1920
name: "ArgumentParser",
@@ -23,48 +24,54 @@ var package = Package(
2324
targets: [
2425
.target(
2526
name: "ArgumentParser",
26-
dependencies: ["ArgumentParserToolInfo"]),
27+
dependencies: ["ArgumentParserToolInfo"],
28+
exclude: ["CMakeLists.txt"]),
2729
.target(
2830
name: "ArgumentParserTestHelpers",
29-
dependencies: ["ArgumentParser", "ArgumentParserToolInfo"]),
31+
dependencies: ["ArgumentParser", "ArgumentParserToolInfo"],
32+
exclude: ["CMakeLists.txt"]),
3033
.target(
3134
name: "ArgumentParserToolInfo",
32-
dependencies: []),
35+
dependencies: [],
36+
exclude: ["CMakeLists.txt"]),
3337

34-
.target(
38+
.executableTarget(
3539
name: "roll",
3640
dependencies: ["ArgumentParser"],
3741
path: "Examples/roll"),
38-
.target(
42+
.executableTarget(
3943
name: "math",
4044
dependencies: ["ArgumentParser"],
4145
path: "Examples/math"),
42-
.target(
46+
.executableTarget(
4347
name: "repeat",
4448
dependencies: ["ArgumentParser"],
4549
path: "Examples/repeat"),
50+
.executableTarget(
51+
name: "count-lines",
52+
dependencies: ["ArgumentParser"],
53+
path: "Examples/count-lines"),
4654

47-
.target(
55+
.executableTarget(
4856
name: "changelog-authors",
4957
dependencies: ["ArgumentParser"],
5058
path: "Tools/changelog-authors"),
5159

5260
.testTarget(
5361
name: "ArgumentParserEndToEndTests",
54-
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]),
62+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
63+
exclude: ["CMakeLists.txt"]),
5564
.testTarget(
5665
name: "ArgumentParserUnitTests",
57-
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]),
66+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
67+
exclude: ["CMakeLists.txt"]),
68+
.testTarget(
69+
name: "ArgumentParserPackageManagerTests",
70+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
71+
exclude: ["CMakeLists.txt"]),
5872
.testTarget(
5973
name: "ArgumentParserExampleTests",
60-
dependencies: ["ArgumentParserTestHelpers"]),
74+
dependencies: ["ArgumentParserTestHelpers"],
75+
resources: [.copy("CountLinesTest.txt")]),
6176
]
6277
)
63-
64-
#if swift(>=5.2)
65-
// Skip if < 5.2 to avoid issue with nested type synthesized 'CodingKeys'
66-
package.targets.append(
67-
.testTarget(
68-
name: "ArgumentParserPackageManagerTests",
69-
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]))
70-
#endif

[email protected]

Lines changed: 0 additions & 71 deletions
This file was deleted.

Sources/ArgumentParser/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ add_library(ArgumentParser
1313
"Parsable Properties/Option.swift"
1414
"Parsable Properties/OptionGroup.swift"
1515

16+
"Parsable Types/AsyncParsableCommand.swift"
1617
"Parsable Types/CommandConfiguration.swift"
1718
"Parsable Types/EnumerableFlag.swift"
1819
"Parsable Types/ExpressibleByArgument.swift"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
/// A type that can be executed as part of a nested tree of commands.
13+
public protocol AsyncParsableCommand: ParsableCommand {
14+
mutating func run() async throws
15+
}
16+
17+
extension AsyncParsableCommand {
18+
public static func main() async {
19+
do {
20+
var command = try parseAsRoot()
21+
if var asyncCommand = command as? AsyncParsableCommand {
22+
try await asyncCommand.run()
23+
} else {
24+
try command.run()
25+
}
26+
} catch {
27+
exit(withError: error)
28+
}
29+
}
30+
}
31+
32+
@available(
33+
swift, deprecated: 5.6,
34+
message: "Use @main directly on your root `AsyncParsableCommand` type.")
35+
public protocol AsyncMainProtocol {
36+
associatedtype Command: ParsableCommand
37+
}
38+
39+
@available(swift, deprecated: 5.6)
40+
extension AsyncMainProtocol {
41+
public static func main() async {
42+
do {
43+
var command = try Command.parseAsRoot()
44+
if var asyncCommand = command as? AsyncParsableCommand {
45+
try await asyncCommand.run()
46+
} else {
47+
try command.run()
48+
}
49+
} catch {
50+
Command.exit(withError: error)
51+
}
52+
}
53+
}
54+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
#if os(macOS)
13+
14+
import XCTest
15+
import ArgumentParserTestHelpers
16+
17+
final class CountLinesExampleTests: XCTestCase {
18+
func testCountLines() throws {
19+
let testFile = try XCTUnwrap(Bundle.module.url(forResource: "CountLinesTest", withExtension: "txt"))
20+
try AssertExecuteCommand(command: "count-lines \(testFile.path)", expected: "20")
21+
try AssertExecuteCommand(command: "count-lines \(testFile.path) --prefix al", expected: "4")
22+
}
23+
24+
func testCountLinesHelp() throws {
25+
let helpText = """
26+
USAGE: count-lines <input-file> [--prefix <prefix>] [--verbose]
27+
28+
ARGUMENTS:
29+
<input-file> A file to count lines in. If omitted, counts the
30+
lines of stdin.
31+
32+
OPTIONS:
33+
--prefix <prefix> Only count lines with this prefix.
34+
--verbose Include extra information in the output.
35+
-h, --help Show help information.
36+
"""
37+
try AssertExecuteCommand(command: "count-lines -h", expected: helpText)
38+
}
39+
}
40+
41+
#endif

0 commit comments

Comments
 (0)