Skip to content

Commit d66d015

Browse files
authored
Fix zsh/bash completions for arguments in option groups with a custom completion (#648)
* Added conversion for InputKey to/from fullPathString. * Updated custom completions to use the IndexKey.fullPathString. This resolves an issue where custom completion for arguments in an OptionGroup would fail to match the argument. It was caused by: - the completion script only using the name of the argument (instead of the full path) - the CommandParser looking for the matching argument by comparing a name only IndexKey with the “full” IndexKeys * Updated BashCompletionsGenerator to use customCompletionCall. The zsh completions already uses this function. The function’s implementation is the same as what the BashCompletionsGenerator is doing. This removes the duplicated logic. * Updated completion tests to include nested arguments with custom completions. * Switched to using the split method from the stdlib. Prevously was using .components(seperatedBy:) from Foundation. * Updated the fish completions to include arguments
1 parent 81ac872 commit d66d015

File tree

8 files changed

+93
-49
lines changed

8 files changed

+93
-49
lines changed

Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,8 @@ struct BashCompletionsGenerator {
144144
case .shellCommand(let command):
145145
return "$(\(command))"
146146
case .custom:
147-
// Generate a call back into the command to retrieve a completions list
148-
let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ")
149-
// TODO: Make this work for @Arguments
150-
let argumentName = arg.names.preferredName?.synopsisString
151-
?? arg.help.keys.first?.name ?? "---"
152-
153147
return """
154-
$("${COMP_WORDS[0]}" ---completion \(subcommandNames) -- \(argumentName) "${COMP_WORDS[@]}")
148+
$("${COMP_WORDS[0]}" \(arg.customCompletionCall(commands)) "${COMP_WORDS[@]}")
155149
"""
156150
}
157151
}

Sources/ArgumentParser/Completions/CompletionsGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ extension ArgumentDefinition {
8888
func customCompletionCall(_ commands: [ParsableCommand.Type]) -> String {
8989
let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ")
9090
let argumentName = names.preferredName?.synopsisString
91-
?? self.help.keys.first?.name ?? "---"
91+
?? self.help.keys.first?.fullPathString ?? "---"
9292
return "---completion \(subcommandNames) -- \(argumentName)"
9393
}
9494
}

Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ struct FishCompletionsGenerator {
55
preprocessorFunction(commandName: programName),
66
helperFunction(commandName: programName)
77
]
8-
let completions = generateCompletions(commandChain: [programName], [type])
8+
let completions = generateCompletions([type])
99

1010
return helperFunctions.joined(separator: "\n\n") + "\n\n" + completions.joined(separator: "\n")
1111
}
@@ -14,10 +14,10 @@ struct FishCompletionsGenerator {
1414
// MARK: - Private functions
1515

1616
extension FishCompletionsGenerator {
17-
private static func generateCompletions(commandChain: [String], _ commands: [ParsableCommand.Type]) -> [String] {
17+
private static func generateCompletions(_ commands: [ParsableCommand.Type]) -> [String] {
1818
let type = commands.last!
1919
let isRootCommand = commands.count == 1
20-
let programName = commandChain[0]
20+
let programName = commands[0]._commandName
2121
var subcommands = type.configuration.subcommands
2222
.filter { $0.configuration.shouldDisplay }
2323

@@ -27,7 +27,7 @@ extension FishCompletionsGenerator {
2727

2828
let helperFunctionName = helperFunctionName(commandName: programName)
2929

30-
var prefix = "complete -c \(programName) -n '\(helperFunctionName) \"\(commandChain.joined(separator: separator))\""
30+
var prefix = "complete -c \(programName) -n '\(helperFunctionName) \"\(commands.map { $0._commandName }.joined(separator: separator))\""
3131
if !subcommands.isEmpty {
3232
prefix += " \"\(subcommands.map { $0._commandName }.joined(separator: separator))\""
3333
}
@@ -45,50 +45,53 @@ extension FishCompletionsGenerator {
4545

4646
let argumentCompletions = commands
4747
.argumentsForHelp(visibility: .default)
48-
.compactMap { $0.argumentSegments(commandChain) }
48+
.compactMap { $0.argumentSegments(commands) }
4949
.map { $0.joined(separator: " ") }
5050
.map { complete(suggestion: $0) }
5151

5252
let completionsFromSubcommands = subcommands.flatMap { subcommand in
53-
generateCompletions(commandChain: commandChain + [subcommand._commandName], [subcommand])
53+
generateCompletions(commands + [subcommand])
5454
}
5555

5656
return completionsFromSubcommands + argumentCompletions + subcommandCompletions
5757
}
5858
}
5959

6060
extension ArgumentDefinition {
61-
fileprivate func argumentSegments(_ commandChain: [String]) -> [String]? {
62-
guard help.visibility.base == .default,
63-
!names.isEmpty
61+
fileprivate func argumentSegments(_ commands: [ParsableCommand.Type]) -> [String]? {
62+
guard help.visibility.base == .default
6463
else { return nil }
65-
66-
var results = names.map{ $0.asFishSuggestion }
67-
64+
65+
var results: [String] = []
66+
67+
if !names.isEmpty {
68+
results += names.map{ $0.asFishSuggestion }
69+
}
70+
6871
if !help.abstract.isEmpty {
6972
results += ["-d '\(help.abstract.fishEscape())'"]
7073
}
71-
72-
if isNullary {
73-
return results
74-
}
75-
74+
7675
switch completion.kind {
77-
case .default: return results
76+
case .default where names.isEmpty:
77+
return nil
78+
case .default:
79+
break
7880
case .list(let list):
79-
return results + ["-r -f -k -a '\(list.joined(separator: " "))'"]
81+
results += ["-r -f -k -a '\(list.joined(separator: " "))'"]
8082
case .file(let extensions):
8183
let pattern = "*.{\(extensions.joined(separator: ","))}"
82-
return results + ["-r -f -a '(for i in \(pattern); echo $i;end)'"]
84+
results += ["-r -f -a '(for i in \(pattern); echo $i;end)'"]
8385
case .directory:
84-
return results + ["-r -f -a '(__fish_complete_directories)'"]
86+
results += ["-r -f -a '(__fish_complete_directories)'"]
8587
case .shellCommand(let shellCommand):
86-
return results + ["-r -f -a '(\(shellCommand))'"]
88+
results += ["-r -f -a '(\(shellCommand))'"]
8789
case .custom:
88-
let program = commandChain[0]
89-
let subcommands = commandChain.dropFirst().joined(separator: " ")
90-
return results + ["-r -f -a '(command \(program) ---completion \(subcommands) -- --custom (commandline -opc)[1..-1])'"]
90+
let commandName = commands.first!._commandName
91+
results += ["-r -f -a '(command \(commandName) \(customCompletionCall(commands)) (commandline -opc)[1..-1])'"]
9192
}
93+
94+
return results
9295
}
9396
}
9497

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,9 @@ extension ArgumentSet {
191191
}
192192

193193
func firstPositional(
194-
named name: String
194+
withKey key: InputKey
195195
) -> ArgumentDefinition? {
196-
let key = InputKey(name: name, parent: nil)
197-
return first(where: { $0.help.keys.contains(key) })
196+
first(where: { $0.help.keys.contains(key) })
198197
}
199198
}
200199

Sources/ArgumentParser/Parsing/CommandParser.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,8 @@ extension CommandParser {
343343
completionFunction = f
344344

345345
case .value(let str):
346-
guard let matchedArgument = argset.firstPositional(named: str),
346+
guard let key = InputKey(fullPathString: str),
347+
let matchedArgument = argset.firstPositional(withKey: key),
347348
case .custom(let f) = matchedArgument.completion.kind
348349
else { throw ParserError.invalidState }
349350
completionFunction = f

Sources/ArgumentParser/Parsing/InputKey.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,23 @@ struct InputKey: Hashable {
5151

5252
extension InputKey: CustomStringConvertible {
5353
var description: String {
54-
fullPath.joined(separator: ".")
54+
fullPathString
55+
}
56+
}
57+
58+
extension InputKey {
59+
private static var separator: Character { "." }
60+
61+
var fullPathString: String {
62+
fullPath.joined(separator: .init(Self.separator))
63+
}
64+
65+
init?(fullPathString: String) {
66+
let fullPath = fullPathString.split(separator: Self.separator).map(String.init)
67+
68+
guard let name = fullPath.last else { return nil }
69+
70+
self.name = name
71+
self.path = fullPath.dropLast()
5572
}
5673
}

Tests/ArgumentParserExampleTests/MathExampleTests.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -608,23 +608,30 @@ function _swift_math_using_command
608608
end
609609
610610
complete -c math -n \'_swift_math_using_command \"math add\"\' -l hex-output -s x -d \'Use hexadecimal notation for the result.\'
611+
complete -c math -n \'_swift_math_using_command \"math add\"\' -l version -d \'Show the version.\'
611612
complete -c math -n \'_swift_math_using_command \"math add\"\' -s h -l help -d \'Show help information.\'
612613
complete -c math -n \'_swift_math_using_command \"math multiply\"\' -l hex-output -s x -d \'Use hexadecimal notation for the result.\'
614+
complete -c math -n \'_swift_math_using_command \"math multiply\"\' -l version -d \'Show the version.\'
613615
complete -c math -n \'_swift_math_using_command \"math multiply\"\' -s h -l help -d \'Show help information.\'
614616
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -l kind -d \'The kind of average to provide.\' -r -f -k -a \'mean median mode\'
615617
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -l version -d \'Show the version.\'
616618
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -s h -l help -d \'Show help information.\'
619+
complete -c math -n \'_swift_math_using_command \"math stats stdev\"\' -l version -d \'Show the version.\'
617620
complete -c math -n \'_swift_math_using_command \"math stats stdev\"\' -s h -l help -d \'Show help information.\'
621+
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -r -f -k -a \'alphabet alligator branch braggart\'
622+
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -r -f -a \'(command math ---completion stats quantiles -- customArg (commandline -opc)[1..-1])\'
618623
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l file -r -f -a \'(for i in *.{txt,md}; echo $i;end)\'
619624
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l directory -r -f -a \'(__fish_complete_directories)\'
620625
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l shell -r -f -a \'(head -100 /usr/share/dict/words | tail -50)\'
621626
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l custom -r -f -a \'(command math ---completion stats quantiles -- --custom (commandline -opc)[1..-1])\'
627+
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l version -d \'Show the version.\'
622628
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -s h -l help -d \'Show help information.\'
623-
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -s h -l help -d \'Show help information.\'
624-
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'average\' -d \'Print the average of the values.\'
625-
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'stdev\' -d \'Print the standard deviation of the values.\'
626-
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'quantiles\' -d \'Print the quantiles of the values (TBD).\'
627-
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'help\' -d \'Show subcommand help information.\'
629+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -l version -d \'Show the version.\'
630+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -s h -l help -d \'Show help information.\'
631+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -f -a \'average\' -d \'Print the average of the values.\'
632+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -f -a \'stdev\' -d \'Print the standard deviation of the values.\'
633+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -f -a \'quantiles\' -d \'Print the quantiles of the values (TBD).\'
634+
complete -c math -n \'_swift_math_using_command \"math help\"\' -l version -d \'Show the version.\'
628635
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -l version -d \'Show the version.\'
629636
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -s h -l help -d \'Show help information.\'
630637
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -f -a \'add\' -d \'Print the sum of the values.\'

Tests/ArgumentParserUnitTests/CompletionScriptTests.swift

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ extension CompletionScriptTests {
3333
case one, two, three = "custom-three"
3434
}
3535

36+
struct NestedArguments: ParsableArguments {
37+
@Argument(completion: .custom { _ in ["t", "u", "v"] })
38+
var nestedArgument: String
39+
}
40+
3641
struct Base: ParsableCommand {
3742
static let configuration = CommandConfiguration(
3843
commandName: "base-test",
@@ -53,12 +58,15 @@ extension CompletionScriptTests {
5358

5459
@Option() var rep1: [String]
5560
@Option(name: [.short, .long]) var rep2: [String]
56-
57-
struct SubCommand: ParsableCommand {
58-
static let configuration = CommandConfiguration(
59-
commandName: "sub-command"
60-
)
61-
}
61+
62+
@Argument(completion: .custom { _ in ["q", "r", "s"] }) var argument: String
63+
@OptionGroup var nested: NestedArguments
64+
65+
struct SubCommand: ParsableCommand {
66+
static let configuration = CommandConfiguration(
67+
commandName: "sub-command"
68+
)
69+
}
6270
}
6371

6472
func testBase_Zsh() throws {
@@ -111,6 +119,13 @@ extension CompletionScriptTests {
111119

112120
@Option(name: .customShort("z"), completion: .custom { _ in ["x", "y", "z"] })
113121
var three: String
122+
123+
@OptionGroup var nested: NestedArguments
124+
125+
struct NestedArguments: ParsableArguments {
126+
@Argument(completion: .custom { _ in ["g", "h", "i"] })
127+
var four: String
128+
}
114129
}
115130

116131
func verifyCustomOutput(
@@ -134,8 +149,10 @@ extension CompletionScriptTests {
134149
try verifyCustomOutput("--one", expectedOutput: "a\nb\nc")
135150
try verifyCustomOutput("two", expectedOutput: "d\ne\nf")
136151
try verifyCustomOutput("-z", expectedOutput: "x\ny\nz")
152+
try verifyCustomOutput("nested.four", expectedOutput: "g\nh\ni")
137153

138154
XCTAssertThrowsError(try verifyCustomOutput("--bad", expectedOutput: ""))
155+
XCTAssertThrowsError(try verifyCustomOutput("four", expectedOutput: ""))
139156
}
140157
}
141158

@@ -175,6 +192,8 @@ _base-test() {
175192
'*--kind-counter'
176193
'*--rep1:rep1:'
177194
'*'{-r,--rep2}':rep2:'
195+
':argument:{_custom_completion $_base_test_commandname ---completion -- argument $words}'
196+
':nested-argument:{_custom_completion $_base_test_commandname ---completion -- nested.nestedArgument $words}'
178197
'(-h --help)'{-h,--help}'[Show help information.]'
179198
'(-): :->command'
180199
'(-)*:: :->arg'
@@ -243,6 +262,8 @@ _base_test() {
243262
prev="${COMP_WORDS[COMP_CWORD-1]}"
244263
COMPREPLY=()
245264
opts="--name --kind --other-kind --path1 --path2 --path3 --one --two --three --kind-counter --rep1 -r --rep2 -h --help sub-command help"
265+
opts="$opts $("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")"
266+
opts="$opts $("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")"
246267
if [[ $COMP_CWORD == "1" ]]; then
247268
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
248269
return
@@ -401,6 +422,8 @@ complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-comman
401422
complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l kind-counter
402423
complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l rep1
403424
complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -s r -l rep2
425+
complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -r -f -a '(command base-test ---completion -- argument (commandline -opc)[1..-1])'
426+
complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -r -f -a '(command base-test ---completion -- nested.nestedArgument (commandline -opc)[1..-1])'
404427
complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -s h -l help -d 'Show help information.'
405428
complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -f -a 'sub-command' -d ''
406429
complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -f -a 'help' -d 'Show subcommand help information.'

0 commit comments

Comments
 (0)