Skip to content

Commit 1141ed1

Browse files
authored
Support an async entry point for commands (apple#404)
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 357c419 commit 1141ed1

File tree

15 files changed

+298
-146
lines changed

15 files changed

+298
-146
lines changed

Examples/count-lines/CountLines.swift

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

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: 32 additions & 21 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
@@ -14,6 +14,7 @@ import PackageDescription
1414

1515
var package = Package(
1616
name: "swift-argument-parser",
17+
platforms: [.macOS(.v10_15), .macCatalyst(.v13), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
1718
products: [
1819
.library(
1920
name: "ArgumentParser",
@@ -23,48 +24,58 @@ 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"),
4650

47-
.target(
48-
name: "changelog-authors",
49-
dependencies: ["ArgumentParser"],
50-
path: "Tools/changelog-authors"),
51-
5251
.testTarget(
5352
name: "ArgumentParserEndToEndTests",
54-
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]),
53+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
54+
exclude: ["CMakeLists.txt"]),
5555
.testTarget(
5656
name: "ArgumentParserUnitTests",
57-
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]),
57+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
58+
exclude: ["CMakeLists.txt"]),
59+
.testTarget(
60+
name: "ArgumentParserPackageManagerTests",
61+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
62+
exclude: ["CMakeLists.txt"]),
5863
.testTarget(
5964
name: "ArgumentParserExampleTests",
60-
dependencies: ["ArgumentParserTestHelpers"]),
65+
dependencies: ["ArgumentParserTestHelpers"],
66+
resources: [.copy("CountLinesTest.txt")]),
6167
]
6268
)
6369

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+
#if swift(>=5.6) && os(macOS)
71+
package.targets.append(contentsOf: [
72+
.executableTarget(
73+
name: "count-lines",
74+
dependencies: ["ArgumentParser"],
75+
path: "Examples/count-lines"),
76+
.executableTarget(
77+
name: "changelog-authors",
78+
dependencies: ["ArgumentParser"],
79+
path: "Tools/changelog-authors"),
80+
])
7081
#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
@@ -14,6 +14,7 @@ add_library(ArgumentParser
1414
"Parsable Properties/Option.swift"
1515
"Parsable Properties/OptionGroup.swift"
1616

17+
"Parsable Types/AsyncParsableCommand.swift"
1718
"Parsable Types/CommandConfiguration.swift"
1819
"Parsable Types/EnumerableFlag.swift"
1920
"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+

Sources/ArgumentParserTestHelpers/TestHelpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ extension XCTest {
249249

250250
let outputData = output.fileHandleForReading.readDataToEndOfFile()
251251
let outputActual = String(data: outputData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
252-
252+
253253
let errorData = error.fileHandleForReading.readDataToEndOfFile()
254254
let errorActual = String(data: errorData, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines)
255255

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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) && swift(>=5.6)
13+
14+
import XCTest
15+
import ArgumentParserTestHelpers
16+
17+
final class CountLinesExampleTests: XCTestCase {
18+
func testCountLines() throws {
19+
guard #available(macOS 12, *) else { return }
20+
let testFile = try XCTUnwrap(Bundle.module.url(forResource: "CountLinesTest", withExtension: "txt"))
21+
try AssertExecuteCommand(command: "count-lines \(testFile.path)", expected: "20")
22+
try AssertExecuteCommand(command: "count-lines \(testFile.path) --prefix al", expected: "4")
23+
}
24+
25+
func testCountLinesHelp() throws {
26+
guard #available(macOS 12, *) else { return }
27+
let helpText = """
28+
USAGE: count-lines <input-file> [--prefix <prefix>] [--verbose]
29+
30+
ARGUMENTS:
31+
<input-file> A file to count lines in. If omitted, counts the
32+
lines of stdin.
33+
34+
OPTIONS:
35+
--prefix <prefix> Only count lines with this prefix.
36+
--verbose Include extra information in the output.
37+
-h, --help Show help information.
38+
"""
39+
try AssertExecuteCommand(command: "count-lines -h", expected: helpText)
40+
}
41+
}
42+
43+
#endif

0 commit comments

Comments
 (0)