Shared `@OptionGroup` flag is rejected after a subcommand when the root has a passthrough `defaultSubcommand`

Aperta
#994 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 3 giorni

@sean0407 ci sta già lavorando.

Dal 23/10/2026.

  • #998 di @sean0407 — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
1-2 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
swift
Ambito
cli

Direzione di ricerca

Start by reading Sources/ArgumentParser/Parsable Properties/OptionGroup.swift, Parsing/ArgumentDecoder.swift, and Parsing/CommandParser.swift, then reproduce with the supplied Swift package and command. The proposed patch spans three files and changes how option groups are decoded and saved across subcommands. Done means the shared flag works after a subcommand with a passthrough default subcommand, existing passthrough behavior is preserved, and the remainingDefaultFailure assertion is resolved.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

A flag from a shared @OptionGroup shows up in a subcommand's help. Passing it after the subcommand fails with this error:

Error: Unknown option '--debug'. Did you mean '--debug'?

This affects apple/container. See apple/container#360.

ArgumentParser version: main (efd239f), also 1.8.2
Swift version: Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)

Checklist
  • If possible, I've reproduced the issue using the main branch of this package
  • I've searched for existing GitHub issues
Steps to Reproduce
  1. Create a package:
mkdir -p repro/Sources/tool && cd repro
  1. Save this as Package.swift:
// swift-tools-version:5.9
import PackageDescription

let package = Package(
  name: "repro",
  platforms: [.macOS(.v13)],
  dependencies: [
    .package(url: "https://github.com/apple/swift-argument-parser", branch: "main")
  ],
  targets: [
    .executableTarget(
      name: "tool",
      dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")])
  ]
)
  1. Save this as Sources/tool/main.swift:
import ArgumentParser

// A flag shared by every command.
struct GlobalOptions: ParsableArguments {
  @Flag var debug = false
}

struct Tool: ParsableCommand {
  static let configuration = CommandConfiguration(
    subcommands: [Image.self, Plugin.self],
    defaultSubcommand: Plugin.self
  )

  @OptionGroup var global: GlobalOptions
}

struct Image: ParsableCommand {
  static let configuration = CommandConfiguration(subcommands: [Tag.self])

  @OptionGroup var global: GlobalOptions
}

struct Tag: ParsableCommand {
  @OptionGroup var global: GlobalOptions
  @Argument var name: String

  func run() {
    print("tag \(name), debug: \(global.debug)")
  }
}

// Catches unknown commands, like a plugin dispatcher.
struct Plugin: ParsableCommand {
  @Argument(parsing: .captureForPassthrough) var arguments: [String] = []

  func run() {
    print("plugin \(arguments)")
  }
}

Tool.main()
  1. Build and run:
swift build
.build/debug/tool image tag --help
.build/debug/tool image tag foo --debug
Expected behavior

The help lists --debug, so this should work:

$ tool image tag --help
USAGE: tool image tag [--debug] <name>

$ tool image tag foo --debug
tag foo, debug: true
Actual behavior
$ tool image tag foo --debug
Error: Unknown option '--debug'. Did you mean '--debug'?
Usage: tool image tag [--debug] <name>

It works if --debug comes before tag:

$ tool --debug image tag foo
tag foo, debug: true

It also works if you remove defaultSubcommand: Plugin.self.

Why it happens

There are two causes.

  1. The child never reads its own flag. In OptionGroup.init(from:), if a parent already decoded GlobalOptions, the child reuses that value. So Tag never decodes its own --debug. The token is never marked as used. At the end, CommandParser finds it left over and throws "Unknown option".

  2. The root stops reading early. The default subcommand captures passthrough arguments. Because of that, the root's LenientParser stops at the first word that isn't one of its own subcommands. Here, that word is tag. So the root never reaches --debug either.

Nothing consumes the flag, so parsing fails.

A side effect: the error depends on subcommand names. If Image had a list subcommand and the root also had list, then tool image list --debug would work. The root would scan past list and consume the flag itself.

Proposed fix

When a child reuses a parent's option group, it should still decode its own copy first. If that copy consumed any input, the child keeps it. It also saves the copy so later subcommands see the same value. Otherwise, the child falls back to the parent's value, as it does today.

Patch (3 files, +20 −8)
diff --git Sources/ArgumentParser/Parsable Properties/OptionGroup.swift Sources/ArgumentParser/Parsable Properties/OptionGroup.swift
--- Sources/ArgumentParser/Parsable Properties/OptionGroup.swift	
+++ Sources/ArgumentParser/Parsable Properties/OptionGroup.swift	
@@ -52,5 +52,15 @@ public struct OptionGroup<Value: ParsableArguments>: Decodable, ParsedWrapper {
       let value = try? d.previousValue(Value.self)
     {
-      self.init(_parsedValue: .value(value))
+      // Keep this level's own values if it consumed any input.
+      let before = d.underlying.usedOrigins
+      if let own = try? Self(_decoder: _decoder),
+        d.underlying.usedOrigins != before
+      {
+        self = own
+        d.saveValue(own.wrappedValue)
+      } else {
+        d.underlying.usedOrigins = before
+        self.init(_parsedValue: .value(value))
+      }
     } else {
       try self.init(_decoder: _decoder)
diff --git Sources/ArgumentParser/Parsing/ArgumentDecoder.swift Sources/ArgumentParser/Parsing/ArgumentDecoder.swift
--- Sources/ArgumentParser/Parsing/ArgumentDecoder.swift
+++ Sources/ArgumentParser/Parsing/ArgumentDecoder.swift
@@ -218,6 +218,12 @@ struct SingleValueDecoder: Decoder {
 
   func saveValue<T: ParsableArguments>(_ value: T, type: T.Type = T.self) {
-    underlying.previouslyDecoded.append(
-      DecodedArguments(type: type, value: value))
+    let saved = DecodedArguments(type: type, value: value)
+    if let i = underlying.previouslyDecoded.firstIndex(where: {
+      $0.type == type
+    }) {
+      underlying.previouslyDecoded[i] = saved
+    } else {
+      underlying.previouslyDecoded.append(saved)
+    }
   }
 
diff --git Sources/ArgumentParser/Parsing/CommandParser.swift Sources/ArgumentParser/Parsing/CommandParser.swift
--- Sources/ArgumentParser/Parsing/CommandParser.swift
+++ Sources/ArgumentParser/Parsing/CommandParser.swift
@@ -233,9 +233,5 @@ extension CommandParser {
 
     // Save the decoded results to add to the next command.
-    let newDecodedValues = decoder.previouslyDecoded
-      .filter { prev in
-        !decodedArguments.contains(where: { $0.type == prev.type })
-      }
-    decodedArguments.append(contentsOf: newDecodedValues)
+    decodedArguments = decoder.previouslyDecoded
     decodedArguments.append(
       DecodedArguments(type: currentNode.element, value: decodedResult)

With this patch:

$ tool image tag foo --debug     ->  tag foo, debug: true
$ tool image --debug tag foo     ->  tag foo, debug: true
$ tool --debug image tag foo     ->  tag foo, debug: true
$ tool my-plugin --debug         ->  plugin ["my-plugin", "--debug"]   (unchanged)

In apple/container, it fixes --debug on all 58 subcommands. No container changes are needed.

Question

All tests pass except one assertion in DefaultSubcommandEndToEndTests.remainingDefaultFailure:

#expect(throws: (any Error).self) {
  try MyCommand.parseAsRoot(["plugin", "--verbose", "my-plugin"])
}

With the patch, this parses. Plugin reads its own --verbose, and verbose is true.

remainingDefaultOther already expects ["other", "--verbose"] to work.
So I think the new result is the correct one. But this assertion has been there since #397, so I'd like to check.
Is the error intended here?

If the direction looks right, I'm happy to open a PR with tests.

Lingua principale
Swift
Stelle
3.8k
Fork
436
Merge medio
4g 11h
PR unite (30g)
2

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di apple/swift-argument-parser

Tutte le issue di apple/swift-argument-parser

Issue simili

Altre issue su Swift

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.