Shared `@OptionGroup` flag is rejected after a subcommand when the root has a passthrough `defaultSubcommand`
I maintainer di solito rispondono entro 3 giorni
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 1-2 giorni
- Idoneità per principianti
- 48/100
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
mainbranch of this package - I've searched for existing GitHub issues
Steps to Reproduce
- Create a package:
mkdir -p repro/Sources/tool && cd repro
- 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")])
]
)
- 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()
- 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.
-
The child never reads its own flag. In
OptionGroup.init(from:), if a parent already decodedGlobalOptions, the child reuses that value. SoTagnever decodes its own--debug. The token is never marked as used. At the end,CommandParserfinds it left over and throws "Unknown option". -
The root stops reading early. The default subcommand captures passthrough arguments. Because of that, the root's
LenientParserstops at the first word that isn't one of its own subcommands. Here, that word istag. So the root never reaches--debugeither.
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
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di apple/swift-argument-parser
-
documentation
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
apple/swift-argument-parser#1007 ·
I maintainer di solito rispondono entro 3 giorni
-
Tool tests fail under the Swift Build build system because `generate-manual` & `generate-docc-reference` are not builtForse già presa @rgoldberg l’ha presa 23 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
apple/swift-argument-parser#995 · 1 commento ·
I maintainer di solito rispondono entro 3 giorni
-
Consider making `String.wrapped()` and `Platform.terminalWidth` publicForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
apple/swift-argument-parser#973 · 1 commento ·
I maintainer di solito rispondono entro 3 giorni
-
Include hidden args in completion scriptsForse già presa @Galileo927 l’ha presa 182 giorni fa. Apertagood first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
apple/swift-argument-parser#733 · 7 commenti ·
I maintainer di solito rispondono entro 3 giorni
-
additive change enhancement
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
apple/swift-argument-parser#992 · 1 assegnatario ·
I maintainer di solito rispondono entro 3 giorni
Tutte le issue di apple/swift-argument-parser
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
Ebullioscopic/Atoll#930 ·
I maintainer di solito rispondono entro 2 giorni
-
area:development bug triage:needs-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
vburojevic/hermes#123 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 70/100
jaywcjlove/awesome-mac#4631 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
Muesli-HQ/muesli#764 · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
Dynamic Island: "Tools" and "Files" are the Quick panel and the Shelf under other namesForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
vorssaint/vorssaint-utils#4162 ·
I maintainer di solito rispondono entro 2 giorni