Search code examples
swiftswift-concurrencyswift-argumentparser

Using ArgumentParser with Swift concurrency


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.


Solution

  • 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:

    1. For the root command in your command-line tool, declare conformance to AsyncParsableCommand, whether or not that command uses asynchronous code.
    2. Apply the @main attribute to the root command. (Note: If your root command is in a main.Swift file, rename the file to the name of the command.)
    3. For any command that needs to use asynchronous code, declare conformance to AsyncParsableCommand and mark the run() method as async. 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.