apple / swift-argument-parser

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

Fails to parse arguments if a subcommand has an option with `remaining` parsing strategy and one of the remaining arguments matches the top-level command #623

Closed ahoppen closed 3 months ago

ahoppen commented 5 months ago

Consider the following

import ArgumentParser

@main
struct asfdsdafjlk: AsyncParsableCommand {
  static let configuration = CommandConfiguration(subcommands: [MySubcommand.self])

  @Option(name: [.customLong("configuration"), .customShort("c")])
  var buildConfiguration: String = "x"

  mutating func run() throws {}
}

struct MySubcommand: AsyncParsableCommand {
  static var configuration: CommandConfiguration = CommandConfiguration(commandName: "sub")

  @Option(parsing: .remaining)
  var compilerArgs: [String]

  func run() async throws {
    print(compilerArgs)
  }
}

When running the executable with sub --compiler-args -abc xxx, the argument parser prints

Error: Missing expected argument '--compiler-args <compiler-args> …'

I would have expected it to print ["-abc", "xxx"] as the value for compilerArgs.

Removing customShort("c") from the top-level command makes the arguments parse as expected.

My expectation would be that if a subcommand is specified, the top level arguments should not influence argument parsing of the subcommand in any way.

natecook1000 commented 3 months ago

This is behaving as designed, as perplexing as that may be. ArgumentParser supports providing a parent command's either before or after the subcommand name, so these two commands parse in the same way:

$ command --parent-flag sub --sub-flag
$ command sub --parent-flag --sub-flag

I'd recommend extracting the arguments from your parent command into a ParsableArguments type, and then including that in your leaf node commands (hindsight would indicate that this should be the required design for the library):

@main
struct asfdsdafjlk: AsyncParsableCommand {
  static let configuration = CommandConfiguration(subcommands: [MySubcommand.self])
}

struct CommonArguments: ParsableArguments {
  @Option(name: [.customLong("configuration"), .customShort("c")])
  var buildConfiguration: String = "x"
}

struct MySubcommand: AsyncParsableCommand {
  static var configuration: CommandConfiguration = CommandConfiguration(commandName: "sub")

  @Option(parsing: .remaining)
  var compilerArgs: [String]

  @OptionGroup
  var commonArgs: CommonArguments

  func run() async throws {
    print(compilerArgs)
  }
}