apple / swift-argument-parser

Straightforward, type-safe argument parsing for Swift
Apache License 2.0
3.33k stars 319 forks source link

Unexpected “Asynchronous subcommand of a synchronous root” #536

Open fboundp opened 1 year ago

fboundp commented 1 year ago

When attempting to create an application with a AsyncParsableCommand root and AsyncParsableCommand subcommands, a fatal error is thrown at runtime “Asynchronous subcommand of a synchronous root.”

ArgumentParser version: 1.2.0 Swift version: swift-driver version: 1.62.8 Apple Swift version 5.7 (swiftlang-5.7.0.127.4 clang-1400.0.29.50) Target: arm64-apple-macosx13.0

Checklist

Steps to Reproduce

source:

import protocol ArgumentParser.AsyncParsableCommand
import struct ArgumentParser.CommandConfiguration

@main
struct asyncissue: AsyncParsableCommand {
  static var configuration = CommandConfiguration(subcommands: [Cmd1.self])

  struct Cmd1: AsyncParsableCommand {
    mutating func run() async throws {
      print("Hello")
    }
  }
}

Expected behavior

When resulting command is run, help with a subcommand would be displayed.

Actual behavior

ArgumentParser/ParsableCommand.swift:184: Fatal error: 
--------------------------------------------------------------------
Asynchronous subcommand of a synchronous root.

The asynchronous command `Cmd1` is declared as a subcommand of the
synchronous root command `asyncissue`.

With this configuration, your asynchronous `run()` method will not be
called. To fix this issue, change `asyncissue`'s `ParsableCommand`
conformance to `AsyncParsableCommand`.
--------------------------------------------------------------------

zsh: trace trap  swift run
fboundp commented 1 year ago

Adding @available(macOS 10.15, *) to the definition of struct asyncissue seems to fix this.

natecook1000 commented 1 year ago

@fboundp Thanks for the report and your research into this! I think the best resolution here is to add the workaround as a suggestion in the error message.

TiagoMaiaL commented 1 year ago

I've tried reproducing this issue, but it seems to be working correctly here.

@main struct Root: AsyncParsableCommand { static var configuration = CommandConfiguration(subcommands: [Sub.self])

struct Sub: AsyncParsableCommand {
    mutating func run() async throws {
        print("Hello World")
    }
}

}



I've tried reproducing it from both `main` and `1.2.0`.
timwredwards commented 1 year ago

I'm experiencing this issue using main, and with the annotation added.

Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
Target: arm64-apple-macosx13.0
natecook1000 commented 1 year ago

547 should improve the error message when this happens, so that at least it's describing the problem accurately.

@timwredwards Can you provide some more detail on your project and what you're seeing?

calebhailey commented 1 year ago

Not sure if I'm seeing this same thing or something related, but I just went through a migration from ParsableCommand to AsyncParsableCommand and had the following experience:

Then it seemed that no matter what I did I couldn't resolve this error. I tried adding the annotation to my struct as follows:

@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
struct ExampleCLI: AsyncParsableCommand {}

and I tried setting platforms in Package. as follows:

let package: Package = Package(
  name: "example",
  platforms: [
      .macOS(.v13),
      .macCatalyst(.v13),
      .iOS(.v13),
      .tvOS(.v13),
      .watchOS(.v6),
  ]
}

...and still no dice.

Then I found this issue and noticed that some folks were having success with the @available annotation, however in the snippets that were shared here I observed that most people are setting the @main annotation on the root command (as recommended by the docs). However, I have been using a different struct as my entry point, like this:

// CLI Entrypoint
@main
struct CLI {

    static var stdin: String?
    static func main() {
        // check for STDIN before parsing arguments
        // usage: 
        //     if let stdin: String = CLI.stdin {
        //         print("stdin: \(stdin)")
        //     }
        self.stdin = standardInput()
        ExampleCLI.main()
    }
}

As soon as I set the @main annotation on my root command, along with the @available annotation as suggested by the error message, I was able to resolve the error. This seems like a bug, and also related to this issue, but I could also be doing something super obviously wrong.

I hope this helps! 😊

PS: I'm doing this whole root command wrapper struct as a dance around capturing standard input and stripping trailing - (hyphen) characters (e.g. echo "helloworld" | examplecli --dosomething -), which ArgumentParser seems to complain about. My standardInput() function looks like this:

// FeatherDB CLI STDIN Helper
func standardInput() -> String? {
    // Only read STDIN if the last argument is "-"
    guard CommandLine.arguments.last == "-" else {
        return nil
    }

    // Remove the trailing "-" argument to avoid tripping up ArgumentParser
    CommandLine.arguments.removeLast()

    // Read & return STDIN
    var input: String = String()
    while let line: String = readLine() {
        input += line
    }
    return input
}
calebhailey commented 1 year ago

@natecook1000 👋 thanks for your work on ArgumentParser! Coming from a background in developing golang CLI tools – which has a robust ecosystem of stdlib & OSS libraries for CLI argument parsing – I've found it to be quite impressive. It has almost everything I could ask for!

Any suggestions RE: the behavior I described above? Should I open a separate issue for what I'm seeing, or do you think the behavior I observed is related to this issue?

Thanks in advance for your time! 😊