Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support an
async
entry point for commands (#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#> } ```
- Loading branch information
1 parent
357c419
commit 1141ed1
Showing
15 changed files
with
298 additions
and
146 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
//===----------------------------------------------------------*- 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 ArgumentParser | ||
import Foundation | ||
|
||
@main | ||
struct CountLines: AsyncParsableCommand { | ||
@Argument( | ||
help: "A file to count lines in. If omitted, counts the lines of stdin.", | ||
completion: .file(), transform: URL.init(fileURLWithPath:)) | ||
var inputFile: URL? | ||
|
||
@Option(help: "Only count lines with this prefix.") | ||
var prefix: String? | ||
|
||
@Flag(help: "Include extra information in the output.") | ||
var verbose = false | ||
} | ||
|
||
extension CountLines { | ||
var fileHandle: FileHandle { | ||
get throws { | ||
guard let inputFile = inputFile else { | ||
return .standardInput | ||
} | ||
return try FileHandle(forReadingFrom: inputFile) | ||
} | ||
} | ||
|
||
func printCount(_ count: Int) { | ||
guard verbose else { | ||
print(count) | ||
return | ||
} | ||
|
||
if let filename = inputFile?.lastPathComponent { | ||
print("Lines in '\(filename)'", terminator: "") | ||
} else { | ||
print("Lines from stdin", terminator: "") | ||
} | ||
|
||
if let prefix = prefix { | ||
print(", prefixed by '\(prefix)'", terminator: "") | ||
} | ||
|
||
print(": \(count)") | ||
} | ||
|
||
mutating func run() async throws { | ||
guard #available(macOS 12, *) else { | ||
print("'count-lines' isn't supported on this platform.") | ||
return | ||
} | ||
|
||
let countAllLines = prefix == nil | ||
let lineCount = try await fileHandle.bytes.lines.reduce(0) { count, line in | ||
if countAllLines || line.starts(with: prefix!) { | ||
return count + 1 | ||
} else { | ||
return count | ||
} | ||
} | ||
|
||
printCount(lineCount) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
Sources/ArgumentParser/Parsable Types/AsyncParsableCommand.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
//===----------------------------------------------------------*- 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
/// A type that can be executed as part of a nested tree of commands. | ||
public protocol AsyncParsableCommand: ParsableCommand { | ||
mutating func run() async throws | ||
} | ||
|
||
extension AsyncParsableCommand { | ||
public static func main() async { | ||
do { | ||
var command = try parseAsRoot() | ||
if var asyncCommand = command as? AsyncParsableCommand { | ||
try await asyncCommand.run() | ||
} else { | ||
try command.run() | ||
} | ||
} catch { | ||
exit(withError: error) | ||
} | ||
} | ||
} | ||
|
||
@available( | ||
swift, deprecated: 5.6, | ||
message: "Use @main directly on your root `AsyncParsableCommand` type.") | ||
public protocol AsyncMainProtocol { | ||
associatedtype Command: ParsableCommand | ||
} | ||
|
||
@available(swift, deprecated: 5.6) | ||
extension AsyncMainProtocol { | ||
public static func main() async { | ||
do { | ||
var command = try Command.parseAsRoot() | ||
if var asyncCommand = command as? AsyncParsableCommand { | ||
try await asyncCommand.run() | ||
} else { | ||
try command.run() | ||
} | ||
} catch { | ||
Command.exit(withError: error) | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
//===----------------------------------------------------------*- 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
#if os(macOS) && swift(>=5.6) | ||
|
||
import XCTest | ||
import ArgumentParserTestHelpers | ||
|
||
final class CountLinesExampleTests: XCTestCase { | ||
func testCountLines() throws { | ||
guard #available(macOS 12, *) else { return } | ||
let testFile = try XCTUnwrap(Bundle.module.url(forResource: "CountLinesTest", withExtension: "txt")) | ||
try AssertExecuteCommand(command: "count-lines \(testFile.path)", expected: "20") | ||
try AssertExecuteCommand(command: "count-lines \(testFile.path) --prefix al", expected: "4") | ||
} | ||
|
||
func testCountLinesHelp() throws { | ||
guard #available(macOS 12, *) else { return } | ||
let helpText = """ | ||
USAGE: count-lines <input-file> [--prefix <prefix>] [--verbose] | ||
ARGUMENTS: | ||
<input-file> A file to count lines in. If omitted, counts the | ||
lines of stdin. | ||
OPTIONS: | ||
--prefix <prefix> Only count lines with this prefix. | ||
--verbose Include extra information in the output. | ||
-h, --help Show help information. | ||
""" | ||
try AssertExecuteCommand(command: "count-lines -h", expected: helpText) | ||
} | ||
} | ||
|
||
#endif |
Oops, something went wrong.