Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions Sources/ArgumentParser/Parsable Properties/OptionGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,32 @@ public struct OptionGroup<Value: ParsableArguments>: Decodable, ParsedWrapper {
}

public init(from _decoder: Decoder) throws {
if let d = _decoder as? SingleValueDecoder,
let value = try? d.previousValue(Value.self)
{
self.init(_parsedValue: .value(value))
if let d = _decoder as? SingleValueDecoder {
let previousValue = try? d.previousValue(Value.self)
if let previousValue, !d.underlying.allowsOptionGroupOverrides {
self.init(_parsedValue: .value(previousValue))
} else {
let previousOrigins = d.underlying.usedOrigins
do {
try self.init(_decoder: _decoder)
} catch {
guard
let previousValue,
d.underlying.usedOrigins == previousOrigins
else { throw error }
self.init(_parsedValue: .value(previousValue))
}

if d.underlying.usedOrigins != previousOrigins {
d.saveValue(wrappedValue)
} else if let previousValue {
self = OptionGroup(_parsedValue: .value(previousValue))
} else {
d.saveValue(wrappedValue)
}
}
} else {
try self.init(_decoder: _decoder)
if let d = _decoder as? SingleValueDecoder {
d.saveValue(wrappedValue)
}
}

do {
Expand Down
17 changes: 14 additions & 3 deletions Sources/ArgumentParser/Parsing/ArgumentDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,21 @@ struct DecodedArguments {

/// A decoder that decodes from parsed command-line arguments.
final class ArgumentDecoder: Decoder {
init(values: ParsedValues, previouslyDecoded: [DecodedArguments] = []) {
init(
values: ParsedValues, previouslyDecoded: [DecodedArguments] = [],
allowsOptionGroupOverrides: Bool = true
) {
self.values = values
self.previouslyDecoded = previouslyDecoded
self.allowsOptionGroupOverrides = allowsOptionGroupOverrides
self.usedOrigins = InputOrigin()
}

let values: ParsedValues
var usedOrigins: InputOrigin
var nextCommandIndex = 0
var previouslyDecoded: [DecodedArguments] = []
let allowsOptionGroupOverrides: Bool

var codingPath: [CodingKey] = []

Expand Down Expand Up @@ -217,8 +222,14 @@ struct SingleValueDecoder: Decoder {
}

func saveValue<T: ParsableArguments>(_ value: T, type: T.Type = T.self) {
underlying.previouslyDecoded.append(
DecodedArguments(type: type, value: value))
if let index = underlying.previouslyDecoded.firstIndex(where: {
$0.type == type
}) {
underlying.previouslyDecoded[index].value = value
} else {
underlying.previouslyDecoded.append(
DecodedArguments(type: type, value: value))
}
}

struct SingleValueContainer: SingleValueDecodingContainer {
Expand Down
11 changes: 10 additions & 1 deletion Sources/ArgumentParser/Parsing/CommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ extension CommandParser {
// Decode the values from ParsedValues into the ParsableCommand:
let decoder = ArgumentDecoder(
values: values,
previouslyDecoded: decodedArguments
previouslyDecoded: decodedArguments,
allowsOptionGroupOverrides:
!currentNode.element.includesPassthroughArguments
)
var decodedResult: ParsableCommand
do {
Expand All @@ -232,6 +234,13 @@ extension CommandParser {
split.removeAll(in: decoder.usedOrigins)

// Save the decoded results to add to the next command.
for previous in decoder.previouslyDecoded {
if let index = decodedArguments.firstIndex(where: {
$0.type == previous.type
}) {
decodedArguments[index].value = previous.value
}
}
let newDecodedValues = decoder.previouslyDecoded
.filter { prev in
!decodedArguments.contains(where: { $0.type == prev.type })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,46 @@ extension DefaultSubcommandEndToEndTests {
}
}

extension DefaultSubcommandEndToEndTests {
struct RootWithSharedOptionAndPassthroughDefault: ParsableCommand {
static let configuration = CommandConfiguration(
subcommands: [Image.self, PassthroughPlugin.self],
defaultSubcommand: PassthroughPlugin.self
)

@OptionGroup var global: SharedDebugOption
}

struct SharedDebugOption: ParsableArguments {
@Flag var debug = false
}

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

@OptionGroup var global: SharedDebugOption

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

struct PassthroughPlugin: ParsableCommand {
@Argument(parsing: .captureForPassthrough) var arguments: [String] = []
}

@Test func sharedOptionAfterNestedPositionalArgument() {
expectParseCommand(
RootWithSharedOptionAndPassthroughDefault.self,
Image.Tag.self,
["image", "tag", "item", "--debug"]
) { tag in
#expect(tag.global.debug)
}
}
}

extension DefaultSubcommandEndToEndTests {
struct NestedDefaultSubcommandHelp: ParsableCommand {
static let configuration = CommandConfiguration(
Expand Down