Skip to content

Commit 3af071f

Browse files
authored
add control of exclusivity for Bool Flags when inversion is enabled (#62)
1 parent d8fb6bd commit 3af071f

File tree

4 files changed

+52
-27
lines changed

4 files changed

+52
-27
lines changed

Sources/ArgumentParser/Parsable Properties/Flag.swift

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,17 @@ extension Flag where Value == Optional<Bool> {
129129
/// - name: A specification for what names are allowed for this flag.
130130
/// - inversion: The method for converting this flags name into an on/off
131131
/// pair.
132+
/// - exclusivity: The behavior to use when an on/off pair of flags is
133+
/// specified.
132134
/// - help: Information about how to use this flag.
133135
public init(
134136
name: NameSpecification = .long,
135137
inversion: FlagInversion,
138+
exclusivity: FlagExclusivity = .chooseLast,
136139
help: ArgumentHelp? = nil
137140
) {
138141
self.init(_parsedValue: .init { key in
139-
.flag(key: key, name: name, default: nil, inversion: inversion, help: help)
142+
.flag(key: key, name: name, default: nil, inversion: inversion, exclusivity: exclusivity, help: help)
140143
})
141144
}
142145
}
@@ -188,15 +191,18 @@ extension Flag where Value == Bool {
188191
/// - initial: The default value for this flag.
189192
/// - inversion: The method for converting this flag's name into an on/off
190193
/// pair.
194+
/// - exclusivity: The behavior to use when an on/off pair of flags is
195+
/// specified.
191196
/// - help: Information about how to use this flag.
192197
public init(
193198
name: NameSpecification = .long,
194199
default initial: Bool? = false,
195200
inversion: FlagInversion,
201+
exclusivity: FlagExclusivity = .chooseLast,
196202
help: ArgumentHelp? = nil
197203
) {
198204
self.init(_parsedValue: .init { key in
199-
.flag(key: key, name: name, default: initial, inversion: inversion, help: help)
205+
.flag(key: key, name: name, default: initial, inversion: inversion, exclusivity: exclusivity, help: help)
200206
})
201207
}
202208
}
@@ -248,17 +254,7 @@ extension Flag where Value: CaseIterable, Value: RawRepresentable, Value.RawValu
248254
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in
249255
// TODO: We should catch duplicate flags that hit a single part of
250256
// an exclusive argument set in the value parsing, not here.
251-
let previous = values.element(forKey: key)
252-
switch (hasUpdated, previous, exclusivity) {
253-
case (true, let p?, .exclusive):
254-
// This value has already been set.
255-
throw ParserError.duplicateExclusiveValues(previous: p.inputOrigin, duplicate: origin, originalInput: values.originalInput)
256-
case (false, _, _), (_, _, .chooseLast):
257-
values.set(value, forKey: key, inputOrigin: origin)
258-
default:
259-
break
260-
}
261-
hasUpdated = true
257+
hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
262258
}))
263259
}
264260
return exclusivity == .exclusive
@@ -293,13 +289,9 @@ extension Flag {
293289
let caseKey = InputKey(rawValue: value.rawValue)
294290
let help = ArgumentDefinition.Help(options: .isOptional, help: help, key: key)
295291
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in
296-
if hasUpdated && exclusivity == .exclusive {
297-
throw ParserError.unexpectedExtraValues([(origin, String(describing: value))])
298-
}
299-
if !hasUpdated || exclusivity == .chooseLast {
300-
values.set(value, forKey: key, inputOrigin: origin)
301-
}
302-
hasUpdated = true
292+
// TODO: We should catch duplicate flags that hit a single part of
293+
// an exclusive argument set in the value parsing, not here.
294+
hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
303295
}))
304296
}
305297
return exclusivity == .exclusive

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,39 @@ extension ArgumentSet {
169169
})
170170
return ArgumentSet(arg)
171171
}
172+
173+
static func updateFlag(key: InputKey, value: Any, origin: InputOrigin, values: inout ParsedValues, hasUpdated: Bool, exclusivity: FlagExclusivity) throws -> Bool {
174+
let previous = values.element(forKey: key)
175+
switch (hasUpdated, previous, exclusivity) {
176+
case(true, let previous?, .exclusive):
177+
// This value has already been set.
178+
throw ParserError.duplicateExclusiveValues(previous: previous.inputOrigin, duplicate: origin, originalInput: values.originalInput)
179+
case (false, _, _), (_, _, .chooseLast):
180+
values.set(value, forKey: key, inputOrigin: origin)
181+
return true
182+
default:
183+
return hasUpdated
184+
}
185+
}
172186

173187
/// Creates an argument set for a pair of inverted Boolean flags.
174-
static func flag(key: InputKey, name: NameSpecification, default initialValue: Bool?, inversion: FlagInversion, help: ArgumentHelp?) -> ArgumentSet {
188+
static func flag(key: InputKey, name: NameSpecification, default initialValue: Bool?, inversion: FlagInversion, exclusivity: FlagExclusivity, help: ArgumentHelp?) -> ArgumentSet {
175189
// The flag is required if initialValue is `nil`, otherwise it's optional
176190
let helpOptions: ArgumentDefinition.Help.Options = initialValue != nil ? .isOptional : []
177191

178192
let help = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: initialValue.map(String.init), key: key)
179193
let (enableNames, disableNames) = inversion.enableDisableNamePair(for: key, name: name)
180-
181-
let enableArg = ArgumentDefinition(kind: .named(enableNames), help: help, update: .nullary({ (origin, name, values) in
182-
values.set(true, forKey: key, inputOrigin: origin)
194+
195+
var hasUpdated = false
196+
let enableArg = ArgumentDefinition(kind: .named(enableNames),help: help, update: .nullary({ (origin, name, values) in
197+
hasUpdated = try ArgumentSet.updateFlag(key: key, value: true, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
183198
}), initial: { origin, values in
184199
if let initialValue = initialValue {
185200
values.set(initialValue, forKey: key, inputOrigin: origin)
186201
}
187202
})
188203
let disableArg = ArgumentDefinition(kind: .named(disableNames), help: ArgumentDefinition.Help(options: [.isOptional], key: key), update: .nullary({ (origin, name, values) in
189-
values.set(false, forKey: key, inputOrigin: origin)
204+
hasUpdated = try ArgumentSet.updateFlag(key: key, value: false, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
190205
}), initial: { _, _ in })
191206
return ArgumentSet(exclusive: [enableArg, disableArg])
192207
}

Tests/EndToEndTests/FlagsEndToEndTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ fileprivate struct Bar: ParsableArguments {
2727

2828
@Flag(inversion: .prefixedNo)
2929
var extattr2: Bool?
30+
31+
@Flag(inversion: .prefixedEnableDisable, exclusivity: .chooseFirst)
32+
var logging: Bool
3033
}
3134

3235
extension FlagsEndToEndTests {
@@ -74,6 +77,13 @@ extension FlagsEndToEndTests {
7477
AssertParse(Bar.self, ["--extattr", "--no-extattr", "--extattr"]) { options in
7578
XCTAssertEqual(options.extattr, true)
7679
}
80+
AssertParse(Bar.self, ["--enable-logging"]) { options in
81+
XCTAssertEqual(options.logging, true)
82+
}
83+
// Can't test this yet, because .chooseFirst flags don't work
84+
// AssertParse(Bar.self, ["--enable-logging", "--disable-logging"]) { options in
85+
// XCTAssertEqual(options.logging, false)
86+
// }
7787
}
7888
}
7989

Tests/UnitTests/ErrorMessageTests.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,24 @@ extension ErrorMessageTests {
137137

138138
private enum OutputBehaviour: String, CaseIterable { case stats, count, list }
139139
private struct Options: ParsableArguments {
140-
@Flag(name: .shortAndLong, help: "Program output")
140+
@Flag(name: .shortAndLong, default: .list, help: "Program output")
141141
var behaviour: OutputBehaviour
142142

143-
@Flag() var bool: Bool
143+
@Flag(inversion: .prefixedNo, exclusivity: .exclusive) var bool: Bool
144+
}
145+
private struct OptOptions: ParsableArguments {
146+
@Flag(name: .short, help: "Program output")
147+
var behaviour: OutputBehaviour?
144148
}
145149

146150
extension ErrorMessageTests {
147151
func testDuplicateFlags() {
148152
AssertErrorMessage(Options.self, ["--list", "--bool", "-s"], "Value to be set with flag \'-s\' had already been set with flag \'--list\'")
149153
AssertErrorMessage(Options.self, ["-cbl"], "Value to be set with flag \'l\' in \'-cbl\' had already been set with flag \'c\' in \'-cbl\'")
150154
AssertErrorMessage(Options.self, ["-bc", "--stats", "-l"], "Value to be set with flag \'--stats\' had already been set with flag \'c\' in \'-bc\'")
155+
156+
AssertErrorMessage(Options.self, ["--no-bool", "--bool"], "Value to be set with flag \'--bool\' had already been set with flag \'--no-bool\'")
157+
158+
AssertErrorMessage(OptOptions.self, ["-cbl"], "Value to be set with flag \'l\' in \'-cbl\' had already been set with flag \'c\' in \'-cbl\'")
151159
}
152160
}

0 commit comments

Comments
 (0)