Skip to content

Commit 2244788

Browse files
committed
Overhaul generated fish completion scripts:
Allow positionals & subcommands on the same (sub)command. Don't use `-r` for complete calls for positionals. Use `-\(r)fka ''` for positionals or option values with no completion candidates. Allow option_specs elements to contain spaces. Explicitly scope variables. Rename functions. Prevent odd characters in (sub)command names from breaking the script in some places. Prevent missing data from breaking if tests. Signed-off-by: Ross Goldberg <[email protected]>
1 parent 1ed398f commit 2244788

File tree

3 files changed

+221
-224
lines changed

3 files changed

+221
-224
lines changed

Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift

Lines changed: 83 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -17,69 +17,67 @@ extension [ParsableCommand.Type] {
1717
// - first is guaranteed non-empty in the one place where this computed var is used.
1818
let commandName = first!._commandName
1919
return """
20-
function \(commandsAndPositionalsFunctionName) -S
21-
switch $positionals[1]
20+
function \(shouldOfferCompletionsForFunctionName) -a expected_commands -a expected_positional_index
21+
set -f unparsed_tokens (\(tokensFunctionName) -pc)
22+
set -f positional_index 0
23+
set -f commands
24+
25+
switch $unparsed_tokens[1]
2226
\(commandCases)
23-
case '*'
24-
set commands $positionals[1]
25-
set -e positionals[1]
2627
end
27-
end
2828
29-
function \(commandsAndPositionalsFunctionName)_helper -S -a argparse_options
30-
set -l option_specs $argv[2..]
31-
set -a commands $positionals[1]
32-
set -e positionals[1]
33-
if test -z $argparse_options
34-
argparse -n "$commands" $option_specs -- $positionals 2> /dev/null
35-
set positionals $argv
36-
else
37-
argparse (string split -- '\(separator)' $argparse_options) -n "$commands" $option_specs -- $positionals 2> /dev/null
38-
set positionals $argv
39-
end
29+
test "$commands" = "$expected_commands" -a \\( -z "$expected_positional_index" -o "$expected_positional_index" -eq "$positional_index" \\)
4030
end
4131
4232
function \(tokensFunctionName)
43-
if test (string split -m 1 -f 1 -- . $FISH_VERSION) -gt 3
33+
if test "$(string split -m 1 -f 1 -- . "$FISH_VERSION")" -gt 3
4434
commandline --tokens-raw $argv
4535
else
4636
commandline -o $argv
4737
end
4838
end
4939
50-
function \(usingCommandFunctionName) -a expected_commands
51-
set commands
52-
set positionals (\(tokensFunctionName) -pc)
53-
\(commandsAndPositionalsFunctionName)
54-
test "$commands" = $expected_commands
55-
end
40+
function \(parseSubcommandFunctionName) -S
41+
argparse -s r -- $argv
42+
set -f positional_count $argv[1]
43+
set -f option_specs $argv[2..]
5644
57-
function \(positionalIndexFunctionName)
58-
set positionals (\(tokensFunctionName) -pc)
59-
\(commandsAndPositionalsFunctionName)
60-
math (count $positionals) + 1
45+
set -a commands $unparsed_tokens[1]
46+
set -e unparsed_tokens[1]
47+
48+
set positional_index 0
49+
50+
while true
51+
argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null
52+
set unparsed_tokens $argv
53+
set positional_index (math $positional_index + 1)
54+
if test (count $unparsed_tokens) -eq 0 -o \\( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \\)
55+
return 0
56+
end
57+
set -e unparsed_tokens[1]
58+
end
6159
end
6260
6361
function \(completeDirectoriesFunctionName)
64-
set token (commandline -t)
62+
set -f token (commandline -t)
6563
string match -- '*/' $token
66-
set subdirs $token*/
64+
set -f subdirs $token*/
6765
printf '%s\\n' $subdirs
6866
end
6967
7068
function \(customCompletionFunctionName)
7169
set -x \(CompletionShell.shellEnvironmentVariableName) fish
7270
set -x \(CompletionShell.shellVersionEnvironmentVariableName) $FISH_VERSION
7371
74-
set tokens (\(tokensFunctionName) -p)
75-
if test -z (\(tokensFunctionName) -t)
76-
set index (count (\(tokensFunctionName) -pc))
77-
set tokens $tokens[..$index] \\'\\' $tokens[$(math $index + 1)..]
72+
set -f tokens (\(tokensFunctionName) -p)
73+
if test -z "$(\(tokensFunctionName) -t)"
74+
set -f index (count (\(tokensFunctionName) -pc))
75+
set -f tokens $tokens[..$index] \\'\\' $tokens[$(math $index + 1)..]
7876
end
7977
command $tokens[1] $argv $tokens
8078
end
8179
82-
complete -c \(commandName) -f
80+
complete -c '\(commandName)' -f
8381
\(completions.joined(separator: "\n"))
8482
"""
8583
}
@@ -90,9 +88,7 @@ extension [ParsableCommand.Type] {
9088
// Precondition: last is guaranteed to be non-empty
9189
return """
9290
case '\(last!._commandName)'
93-
\(commandsAndPositionalsFunctionName)_helper '\(
94-
subcommands.isEmpty ? "" : "-s"
95-
)' \(
91+
\(parseSubcommandFunctionName) \(positionalArgumentCountArguments) \(
9692
completableArguments
9793
.compactMap(\.optionSpec)
9894
.map { "'\($0.fishEscapeForSingleQuotedString())'" }
@@ -102,7 +98,7 @@ extension [ParsableCommand.Type] {
10298
? ""
10399
: """
104100
105-
switch $positionals[1]
101+
switch $unparsed_tokens[1]
106102
\(subcommands.map { (self + [$0]).commandCases }.joined(separator: "\n"))
107103
end
108104
"""
@@ -114,64 +110,48 @@ extension [ParsableCommand.Type] {
114110
private var completions: [String] {
115111
// swift-format-ignore: NeverForceUnwrap
116112
// Precondition: first is guaranteed to be non-empty
117-
let commandName = first!._commandName
118113
let prefix = """
119-
complete -c \(commandName)\
120-
-n '\(usingCommandFunctionName)\
114+
complete -c '\(first!._commandName)'\
115+
-n '\(shouldOfferCompletionsForFunctionName)\
121116
"\(map { $0._commandName }.joined(separator: separator))"
122117
"""
123118

124119
let subcommands = subcommands
125120

126-
func complete(suggestion: String, extraTests: [String] = []) -> String {
127-
"\(prefix)\(extraTests.map { ";\($0)" }.joined())' \(suggestion)"
128-
}
129-
130-
let subcommandCompletions: [String] = subcommands.map { subcommand in
131-
complete(
132-
suggestion:
133-
"-fa '\(subcommand._commandName)' -d '\(subcommand.configuration.abstract.fishEscapeForSingleQuotedString())'"
134-
)
135-
}
136-
137121
var positionalIndex = 0
138122

139123
let argumentCompletions =
140124
completableArguments
141125
.map { (arg: ArgumentDefinition) in
142-
complete(
143-
suggestion: argumentSegments(arg).joined(separator: separator),
144-
extraTests: arg.isPositional
145-
? [
146-
"""
147-
and test (\(positionalIndexFunctionName)) \
148-
-eq \({
149-
positionalIndex += 1
150-
return positionalIndex
151-
}())
152-
"""
153-
]
154-
: []
155-
)
126+
"""
127+
\(prefix)\(arg.isPositional
128+
? """
129+
\({
130+
positionalIndex += 1
131+
return " \(positionalIndex)"
132+
}())
133+
"""
134+
: ""
135+
)' \(argumentSegments(arg).joined(separator: separator))
136+
"""
156137
}
157138

158-
let completionsFromSubcommands = subcommands.flatMap { subcommand in
159-
(self + [subcommand]).completions
160-
}
139+
positionalIndex += 1
161140

162141
return
163-
completionsFromSubcommands + argumentCompletions + subcommandCompletions
142+
argumentCompletions
143+
+ subcommands.map { subcommand in
144+
"\(prefix) \(positionalIndex)' -fa '\(subcommand._commandName)' -d '\(subcommand.configuration.abstract.fishEscapeForSingleQuotedString())'"
145+
}
146+
+ subcommands.flatMap { subcommand in
147+
(self + [subcommand]).completions
148+
}
164149
}
165150

166151
private var subcommands: Self {
167-
guard
168-
let command = last,
169-
ArgumentSet(command, visibility: .default, parent: nil)
170-
.filter(\.isPositional).isEmpty
171-
else {
172-
return []
173-
}
174-
var subcommands = command.configuration.subcommands
152+
// swift-format-ignore: NeverForceUnwrap
153+
// Precondition: last is guaranteed to be non-empty
154+
var subcommands = last!.configuration.subcommands
175155
.filter { $0.configuration.shouldDisplay }
176156
if count == 1 {
177157
subcommands.addHelpSubcommandIfMissing()
@@ -205,19 +185,24 @@ extension [ParsableCommand.Type] {
205185
}
206186
}
207187

188+
let r = arg.isPositional ? "" : "r"
189+
208190
switch arg.completion.kind {
209191
case .default:
192+
if case .unary = arg.update {
193+
results += ["-\(r)fka ''"]
194+
}
210195
break
211196
case .list(let list):
212-
results += ["-rfka '\(list.joined(separator: separator))'"]
197+
results += ["-\(r)fka '\(list.joined(separator: separator))'"]
213198
case .file(let extensions):
214199
switch extensions.count {
215200
case 0:
216-
results += ["-rF"]
201+
results += ["-\(r)F"]
217202
case 1:
218203
results += [
219204
"""
220-
-rfa '(\
205+
-\(r)fa '(\
221206
for p in (string match -e -- \\'*/\\' (commandline -t);or printf \\n)*.\\'\(extensions.map { $0.fishEscapeForSingleQuotedString(iterationCount: 2) }.joined())\\';printf %s\\n $p;end;\
222207
__fish_complete_directories (commandline -t) \\'\\'\
223208
)'
@@ -226,51 +211,52 @@ extension [ParsableCommand.Type] {
226211
default:
227212
results += [
228213
"""
229-
-rfa '(\
230-
set exts \(extensions.map { "\\'\($0.fishEscapeForSingleQuotedString(iterationCount: 2))\\'" }.joined(separator: separator));\
214+
-\(r)fa '(\
215+
set -l exts \(extensions.map { "\\'\($0.fishEscapeForSingleQuotedString(iterationCount: 2))\\'" }.joined(separator: separator));\
231216
for p in (string match -e -- \\'*/\\' (commandline -t);or printf \\n)*.{$exts};printf %s\\n $p;end;\
232217
__fish_complete_directories (commandline -t) \\'\\'\
233218
)'
234219
"""
235220
]
236221
}
237222
case .directory:
238-
results += ["-rfa '(\(completeDirectoriesFunctionName))'"]
223+
results += ["-\(r)fa '(\(completeDirectoriesFunctionName))'"]
239224
case .shellCommand(let shellCommand):
240-
results += ["-rfka '(\(shellCommand))'"]
225+
results += ["-\(r)fka '(\(shellCommand))'"]
241226
case .custom:
242227
results += [
243228
"""
244-
-rfka '(\(customCompletionFunctionName) \(arg.customCompletionCall(self)))'
229+
-\(r)fka '(\(customCompletionFunctionName) \(arg.customCompletionCall(self)))'
245230
"""
246231
]
247232
}
248233

249234
return results
250235
}
251236

252-
private var commandsAndPositionalsFunctionName: String {
253-
// swift-format-ignore: NeverForceUnwrap
254-
// Precondition: first is guaranteed to be non-empty
255-
"_swift_\(first!._commandName)_commands_and_positionals"
237+
var positionalArgumentCountArguments: String {
238+
let positionalArguments = positionalArguments
239+
return """
240+
\(positionalArguments.contains(where: { $0.isRepeatingPositional }) ? "-r " : "")\(positionalArguments.count)
241+
"""
256242
}
257243

258-
private var tokensFunctionName: String {
244+
private var shouldOfferCompletionsForFunctionName: String {
259245
// swift-format-ignore: NeverForceUnwrap
260246
// Precondition: first is guaranteed to be non-empty
261-
"_swift_\(first!._commandName)_tokens"
247+
"_swift_\(first!._commandName)_should_offer_completions_for"
262248
}
263249

264-
private var usingCommandFunctionName: String {
250+
private var tokensFunctionName: String {
265251
// swift-format-ignore: NeverForceUnwrap
266252
// Precondition: first is guaranteed to be non-empty
267-
"_swift_\(first!._commandName)_using_command"
253+
"_swift_\(first!._commandName)_tokens"
268254
}
269255

270-
private var positionalIndexFunctionName: String {
256+
private var parseSubcommandFunctionName: String {
271257
// swift-format-ignore: NeverForceUnwrap
272258
// Precondition: first is guaranteed to be non-empty
273-
"_swift_\(first!._commandName)_positional_index"
259+
"_swift_\(first!._commandName)_parse_subcommand"
274260
}
275261

276262
private var completeDirectoriesFunctionName: String {

0 commit comments

Comments
 (0)