I am using ArgumentParser
package for command-line parsing and want to use it with an async
API of Swift concurrency:
struct Foo: ParsableCommand {
@Argument(
help: "File to be parsed. If not present, parses stdin.",
transform: URL.init(fileURLWithPath:)
)
var file: URL?
mutating func run() async throws {
let handle: FileHandle
if let file {
handle = try .init(forReadingFrom: file)
} else {
handle = .standardInput
}
for try await line in handle.bytes.lines {
// do something with each line
}
try handle.close()
}
}
But when I do this, I always see the “USAGE” text:
USAGE: foo [<file>]
ARGUMENTS:
<file> File to be parsed. If not present, parses stdin.
OPTIONS:
-h, --help Show help information.
I get no compilation errors, but I always see the “USAGE” text whether I supply a parameter or not.
The issue is the use of ParsableCommand
in conjunction with run() async throws
.
Use AsyncParsableCommand
instead. As its documentation says:
To use
async
/await
code in your commands’run()
method implementations, follow these steps:
- For the root command in your command-line tool, declare conformance to
AsyncParsableCommand
, whether or not that command uses asynchronous code.- Apply the
@main
attribute to the root command. (Note: If your root command is in amain.Swift
file, rename the file to the name of the command.)- For any command that needs to use asynchronous code, declare conformance to
AsyncParsableCommand
and mark therun()
method asasync
. No changes are needed for subcommands that don’t use asynchronous code.
Unfortunately, while the broader documentation makes an occasional reference to supporting async
, you have to dig into the code samples (specifically, count-lines
) or pour through the entire class library to stumble across AsyncParsableCommand
.
So, use AsyncParsableCommand
with async
rendition of run
:
import ArgumentParser
@main
struct Foo: AsyncParsableCommand {
@Argument(
help: "File to be parsed. If not present, parses stdin.",
transform: URL.init(fileURLWithPath:)
)
var file: URL?
mutating func run() async throws {
let handle: FileHandle
if let file {
handle = try .init(forReadingFrom: file)
} else {
handle = .standardInput
}
for try await line in handle.bytes.lines {
// do something with each line
}
try handle.close()
}
}
But, unfortunately, if you accidentally use async
rendition of run
with ParsableCommand
, it compiles without error but just produces the “USAGE” text whenever you run it, without any diagnostic information about why it is not working.
In short, ParsableCommand
requires a non-async
rendition of run
. The async
rendition requires AsyncParsableCommand
.