-
Notifications
You must be signed in to change notification settings - Fork 307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support an async
entry point for commands
#404
Changes from all commits
eee7edc
d664ed7
0463e7e
882c922
610dff8
5ade2c1
723868a
31e6061
c2e364c
3cad8ef
1830d73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
// swift-tools-version:5.2 | ||
// swift-tools-version:5.5 | ||
//===----------------------------------------------------------*- swift -*-===// | ||
// | ||
// This source file is part of the Swift Argument Parser open source project | ||
|
@@ -14,6 +14,7 @@ import PackageDescription | |
|
||
var package = Package( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @natecook1000 can we take this opportunity to make the indentation 2 spaces? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh sure. It should be 4 in code samples and example code and 2 everywhere else. |
||
name: "swift-argument-parser", | ||
platforms: [.macOS(.v10_15), .macCatalyst(.v13), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], | ||
products: [ | ||
.library( | ||
name: "ArgumentParser", | ||
|
@@ -23,48 +24,58 @@ var package = Package( | |
targets: [ | ||
.target( | ||
name: "ArgumentParser", | ||
dependencies: ["ArgumentParserToolInfo"]), | ||
dependencies: ["ArgumentParserToolInfo"], | ||
exclude: ["CMakeLists.txt"]), | ||
.target( | ||
name: "ArgumentParserTestHelpers", | ||
dependencies: ["ArgumentParser", "ArgumentParserToolInfo"]), | ||
dependencies: ["ArgumentParser", "ArgumentParserToolInfo"], | ||
exclude: ["CMakeLists.txt"]), | ||
.target( | ||
name: "ArgumentParserToolInfo", | ||
dependencies: []), | ||
dependencies: [], | ||
exclude: ["CMakeLists.txt"]), | ||
|
||
.target( | ||
.executableTarget( | ||
name: "roll", | ||
dependencies: ["ArgumentParser"], | ||
path: "Examples/roll"), | ||
.target( | ||
.executableTarget( | ||
name: "math", | ||
dependencies: ["ArgumentParser"], | ||
path: "Examples/math"), | ||
.target( | ||
.executableTarget( | ||
name: "repeat", | ||
dependencies: ["ArgumentParser"], | ||
path: "Examples/repeat"), | ||
|
||
.target( | ||
name: "changelog-authors", | ||
dependencies: ["ArgumentParser"], | ||
path: "Tools/changelog-authors"), | ||
|
||
.testTarget( | ||
name: "ArgumentParserEndToEndTests", | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]), | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], | ||
exclude: ["CMakeLists.txt"]), | ||
.testTarget( | ||
name: "ArgumentParserUnitTests", | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"]), | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], | ||
exclude: ["CMakeLists.txt"]), | ||
.testTarget( | ||
name: "ArgumentParserPackageManagerTests", | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], | ||
exclude: ["CMakeLists.txt"]), | ||
.testTarget( | ||
name: "ArgumentParserExampleTests", | ||
dependencies: ["ArgumentParserTestHelpers"]), | ||
dependencies: ["ArgumentParserTestHelpers"], | ||
resources: [.copy("CountLinesTest.txt")]), | ||
] | ||
) | ||
|
||
#if swift(>=5.2) | ||
// Skip if < 5.2 to avoid issue with nested type synthesized 'CodingKeys' | ||
package.targets.append( | ||
.testTarget( | ||
name: "ArgumentParserPackageManagerTests", | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"])) | ||
#if swift(>=5.6) && os(macOS) | ||
package.targets.append(contentsOf: [ | ||
.executableTarget( | ||
name: "count-lines", | ||
dependencies: ["ArgumentParser"], | ||
path: "Examples/count-lines"), | ||
.executableTarget( | ||
name: "changelog-authors", | ||
dependencies: ["ArgumentParser"], | ||
path: "Tools/changelog-authors"), | ||
]) | ||
#endif |
This file was deleted.
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) | ||
} | ||
} | ||
} | ||
|
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this PR ends up changing the minimum supported Swift version from 5.2 to 5.5, that will be a breaking change for any consumers who support compiling with Swift < 5.5. Is the intention to ship this as part of a 2.0 release or is breaking Swift version support not considered a semver breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question, @jpsim! The plan is to ship this change as part of the 1.1.0 release. My understanding is that this isn't a source breaking change, since clients using older versions of Swift will just continue to get version 1.0.3. That said, I do have the wrong platform setting below — will update that. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why we don't wrap the new stuff in
#if swift(>=5.5)
, and in turn keep support for older Swift versions?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ffried Not to say that you shouldn't, what are your use cases where you need to support older swift versions like 5.2?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rauhul I don't have any myself. I was just wondering in case this is considered a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That might be a convincing enough reason to upgrade. IMO we (the community + apple package owners) should come up with some guidelines for version support. I think NIO announced they will support only the latest 2 swift versions. (Though I might be mis-remembering).
@Lukasa what is swift-nio's policy for swift version support and do you consider dropping support for a swift version a breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are proposing to support the latest Swift version and the two versions prior. We do not consider dropping Swift versions to be a breaking change, because as @natecook1000 says, SwiftPM takes the tools version into account when resolving what version of a package will be used. Users silently get the last version that supported their Swift version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a big surprise to me since it means SwiftPM needs to check out multiple versions of a dependency in order to identify which on is the last to be compatible with the current swift version. Until today I thought SwiftPM just looked at the git tags to determine which version of a dependency to resolve.
If this is the case, then I have no objections to versioning this with 1.1.0.
@rauhul it'd be great if there were guidelines for Swift version support for Swift library authors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jpsim This seems like a good conversation to start on the swift forums, I'm 100% certain people have opinions... (for better or worse)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A fun irony here is that we discussed the tools version change, but I missed that this also added a
platforms
stanza, and while changing the tools version isn't breaking, changing the platforms stanza very much is.