Skip to content

Commit 52bff59

Browse files
committed
Duplicative approach to async support
This duplicates ParsableCommand in an attempt to provide async command support in a non-source-breaking way. It's only partially functional, as it would need a solution for AsyncParsableCommands to provide a subcommand tree with other async commands, while CommandConfig only supports the extant, non-async ParsableCommand type. Issues that I'm working around: - Switching ParsableCommand to have async main() and run() methods is source-breaking for any code that calls either method manually. Calling main() manually, in particular, was the documented way to use ArgumentParser for a long time, so this breakage would be significant. - Adding an AsyncParsableCommand protocol that extends ParsableCommand causes problems because overload resolution of `static func main` seems problematic. When a type conforms to `AsyncParsableCommand` and receives both an async and non-async `main` function (via protocol extension or any other means I can find), the compiler treats them as ambiguous and rejects the @main attribute. Not currently supported: - Command configuration in AsyncParsableCommand, including subcommands and help information.
1 parent e146504 commit 52bff59

File tree

8 files changed

+176
-142
lines changed

8 files changed

+176
-142
lines changed

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: 21 additions & 19 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,49 @@ 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(
51+
.executableTarget(
4852
name: "changelog-authors",
4953
dependencies: ["ArgumentParser"],
5054
path: "Tools/changelog-authors"),
5155

5256
.testTarget(
5357
name: "ArgumentParserEndToEndTests",
54-
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]),
58+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
59+
exclude: ["CMakeLists.txt"]),
5560
.testTarget(
5661
name: "ArgumentParserUnitTests",
57-
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]),
62+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
63+
exclude: ["CMakeLists.txt"]),
64+
.testTarget(
65+
name: "ArgumentParserPackageManagerTests",
66+
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
67+
exclude: ["CMakeLists.txt"]),
5868
.testTarget(
5969
name: "ArgumentParserExampleTests",
6070
dependencies: ["ArgumentParserTestHelpers"]),
6171
]
6272
)
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.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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: ParsableArguments {
14+
/// Runs this command.
15+
///
16+
/// After implementing this method, you can run your command-line
17+
/// application by calling the static `main()` method on the root type.
18+
/// This method has a default implementation that prints help text
19+
/// for this command.
20+
mutating func run() async throws
21+
}
22+
23+
extension AsyncParsableCommand {
24+
public mutating func run() async throws {
25+
throw CleanExit.helpRequest(Self.asCommand)
26+
}
27+
}
28+
29+
// MARK: - API
30+
31+
extension AsyncParsableCommand {
32+
/// Parses an instance of this type, or one of its subcommands, from
33+
/// command-line arguments.
34+
///
35+
/// - Parameter arguments: An array of arguments to use for parsing. If
36+
/// `arguments` is `nil`, this uses the program's command-line arguments.
37+
/// - Returns: A new instance of this type, one of its subcommands, or a
38+
/// command type internal to the `ArgumentParser` library.
39+
public static func parseAsRoot(
40+
_ arguments: [String]? = nil
41+
) throws -> AsyncParsableCommand {
42+
var parser = CommandParser(self.asCommand)
43+
let arguments = arguments ?? Array(CommandLine.arguments.dropFirst())
44+
return try parser.parse(arguments: arguments).get() as! AsyncParsableCommand
45+
}
46+
47+
/// Returns the text of the help screen for the given subcommand of this
48+
/// command.
49+
///
50+
/// - Parameters:
51+
/// - subcommand: The subcommand to generate the help screen for.
52+
/// `subcommand` must be declared in the subcommand tree of this
53+
/// command.
54+
/// - columns: The column width to use when wrapping long line in the
55+
/// help screen. If `columns` is `nil`, uses the current terminal
56+
/// width, or a default value of `80` if the terminal width is not
57+
/// available.
58+
public static func helpMessage(
59+
for subcommand: ParsableCommand.Type,
60+
columns: Int? = nil
61+
) -> String {
62+
let stack = CommandParser(self.asCommand).commandStack(for: subcommand)
63+
return HelpGenerator(commandStack: stack).rendered(screenWidth: columns)
64+
}
65+
66+
/// Parses an instance of this type, or one of its subcommands, from
67+
/// the given arguments and calls its `run()` method, exiting with a
68+
/// relevant error message if necessary.
69+
///
70+
/// - Parameter arguments: An array of arguments to use for parsing. If
71+
/// `arguments` is `nil`, this uses the program's command-line arguments.
72+
public static func main(_ arguments: [String]?) async {
73+
do {
74+
var command = try parseAsRoot(arguments)
75+
try await command.run()
76+
} catch {
77+
exit(withError: error)
78+
}
79+
}
80+
81+
/// Parses an instance of this type, or one of its subcommands, from
82+
/// command-line arguments and calls its `run()` method, exiting with a
83+
/// relevant error message if necessary.
84+
public static func main() async {
85+
await self.main(nil)
86+
}
87+
}

Tools/changelog-authors/main.swift renamed to Tools/changelog-authors/ChangelogAuthors.swift

Lines changed: 5 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift Argument Parser open source project
44
//
5-
// Copyright (c) 2020 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2020-2021 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -12,50 +12,10 @@
1212
import ArgumentParser
1313
import Foundation
1414

15-
// MARK: GitHub API response modeling
16-
17-
struct Comparison: Codable {
18-
var commits: [Commit]
19-
}
20-
21-
struct Commit: Codable {
22-
var sha: String
23-
var author: Author
24-
}
25-
26-
struct Author: Codable {
27-
var login: String
28-
var htmlURL: String
29-
30-
enum CodingKeys: String, CodingKey {
31-
case login
32-
case htmlURL = "html_url"
33-
}
34-
35-
var inlineLink: String {
36-
"[\(login)]"
37-
}
38-
}
39-
40-
// MARK: Helpers
41-
42-
extension Sequence {
43-
func uniqued<T: Hashable>(by transform: (Element) throws -> T) rethrows -> [Element] {
44-
var seen: Set<T> = []
45-
var result: [Element] = []
46-
47-
for element in self {
48-
if try seen.insert(transform(element)).inserted {
49-
result.append(element)
50-
}
51-
}
52-
return result
53-
}
54-
}
55-
5615
// MARK: Command
5716

58-
struct ChangelogAuthors: ParsableCommand {
17+
@main
18+
struct ChangelogAuthors: AsyncParsableCommand {
5919
static var configuration: CommandConfiguration {
6020
CommandConfiguration(
6121
abstract: "A helper tool for generating author info for the changelog.",
@@ -128,8 +88,8 @@ struct ChangelogAuthors: ParsableCommand {
12888
return url
12989
}
13090

131-
mutating func run() throws {
132-
let data = try Data(contentsOf: try comparisonURL())
91+
mutating func run() async throws {
92+
let (data, _) = try await URLSession.shared.data(from: try comparisonURL())
13393
let comparison = try JSONDecoder().decode(Comparison.self, from: data)
13494
let authors = comparison.commits.map({ $0.author })
13595
.uniqued(by: { $0.login })
@@ -140,6 +100,3 @@ struct ChangelogAuthors: ParsableCommand {
140100
print(references(for: authors))
141101
}
142102
}
143-
144-
ChangelogAuthors.main()
145-

Tools/changelog-authors/Models.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2021 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+
// MARK: GitHub API response modeling
13+
14+
struct Comparison: Codable {
15+
var commits: [Commit]
16+
}
17+
18+
struct Commit: Codable {
19+
var sha: String
20+
var author: Author
21+
}
22+
23+
struct Author: Codable {
24+
var login: String
25+
var htmlURL: String
26+
27+
enum CodingKeys: String, CodingKey {
28+
case login
29+
case htmlURL = "html_url"
30+
}
31+
32+
var inlineLink: String {
33+
"[\(login)]"
34+
}
35+
}

0 commit comments

Comments
 (0)