Skip to content

Commit ea3c17f

Browse files
committed
Refactor bash completions to use ToolInfoV0.
Signed-off-by: Ross Goldberg <[email protected]>
1 parent d1ffc6c commit ea3c17f

File tree

8 files changed

+312
-234
lines changed

8 files changed

+312
-234
lines changed

Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift

+194-200
Large diffs are not rendered by default.

Sources/ArgumentParser/Completions/CompletionsGenerator.swift

+62-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12+
#if swift(>=6.0)
13+
internal import ArgumentParserToolInfo
14+
#else
15+
import ArgumentParserToolInfo
16+
#endif
17+
1218
/// A shell for which the parser can generate a completion script.
1319
public struct CompletionShell: RawRepresentable, Hashable, CaseIterable {
1420
public var rawValue: String
@@ -136,7 +142,7 @@ struct CompletionsGenerator {
136142
case .zsh:
137143
return [command].zshCompletionScript
138144
case .bash:
139-
return [command].bashCompletionScript
145+
return ToolInfoV0(commandStack: [command]).bashCompletionScript
140146
case .fish:
141147
return [command].fishCompletionScript
142148
default:
@@ -215,3 +221,58 @@ extension String {
215221
replacingOccurrences(of: "-", with: "_")
216222
}
217223
}
224+
225+
extension CommandInfoV0 {
226+
var commandContext: [String] {
227+
(superCommands ?? []) + [commandName]
228+
}
229+
230+
var initialCommand: String {
231+
superCommands?.first ?? commandName
232+
}
233+
234+
var positionalArguments: [ArgumentInfoV0] {
235+
(arguments ?? []).filter { $0.kind == .positional }
236+
}
237+
238+
var completionFunctionName: String {
239+
"_" + commandContext.joined(separator: "_")
240+
}
241+
242+
var completionFunctionPrefix: String {
243+
"__\(initialCommand)"
244+
}
245+
}
246+
247+
extension ArgumentInfoV0 {
248+
/// Returns a string with the arguments for the callback to generate custom
249+
/// completions for this argument.
250+
func commonCustomCompletionCall(command: CommandInfoV0) -> String {
251+
let subcommandNames =
252+
command.commandContext.dropFirst().map { "\($0) " }.joined()
253+
254+
let argumentName: String
255+
switch kind {
256+
case .positional:
257+
if let index = command.positionalArguments.firstIndex(of: self) {
258+
argumentName = "positional@\(index)"
259+
} else {
260+
argumentName = "---"
261+
}
262+
default:
263+
argumentName = preferredName?.commonCompletionSynopsisString() ?? "---"
264+
}
265+
return "---completion \(subcommandNames)-- \(argumentName)"
266+
}
267+
}
268+
269+
extension ArgumentInfoV0.NameInfoV0 {
270+
func commonCompletionSynopsisString() -> String {
271+
switch kind {
272+
case .long:
273+
return "--\(name)"
274+
case .short, .longWithSingleDash:
275+
return "-\(name)"
276+
}
277+
}
278+
}

Sources/ArgumentParser/Parsing/ArgumentSet.swift

+8
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,14 @@ extension ArgumentSet {
235235
) -> ArgumentDefinition? {
236236
first(where: { $0.help.keys.contains(key) })
237237
}
238+
239+
func positional(
240+
at index: Int
241+
) -> ArgumentDefinition? {
242+
let positionals = content.filter { $0.isPositional }
243+
guard positionals.count > index else { return nil }
244+
return positionals[index]
245+
}
238246
}
239247

240248
/// A parser for a given input and set of arguments defined by the given

Sources/ArgumentParser/Parsing/CommandParser.swift

+22-7
Original file line numberDiff line numberDiff line change
@@ -414,14 +414,29 @@ extension CommandParser {
414414
}
415415
try customComplete(matchedArgument, forArguments: Array(args))
416416

417-
case .value(let str):
418-
guard
419-
let key = InputKey(fullPathString: str),
420-
let matchedArgument = argset.firstPositional(withKey: key)
421-
else {
422-
throw ParserError.invalidState
417+
case .value(let value):
418+
// Legacy completion script generators use internal key paths to identify
419+
// positional args, e.g. optionGroupA.optionGroupB.property. Newer
420+
// generators based on ToolInfo use the `positional@<index>` syntax which
421+
// avoids leaking implementation details of the tool.
422+
let toolInfoPrefix = "positional@"
423+
if value.hasPrefix(toolInfoPrefix) {
424+
guard
425+
let index = Int(value.dropFirst(toolInfoPrefix.count)),
426+
let matchedArgument = argset.positional(at: index)
427+
else {
428+
throw ParserError.invalidState
429+
}
430+
try customComplete(matchedArgument, forArguments: Array(args))
431+
} else {
432+
guard
433+
let key = InputKey(fullPathString: value),
434+
let matchedArgument = argset.firstPositional(withKey: key)
435+
else {
436+
throw ParserError.invalidState
437+
}
438+
try customComplete(matchedArgument, forArguments: Array(args))
423439
}
424-
try customComplete(matchedArgument, forArguments: Array(args))
425440

426441
case .terminator:
427442
throw ParserError.invalidState

Sources/ArgumentParser/Usage/DumpHelpGenerator.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ extension BidirectionalCollection where Element == ParsableCommand.Type {
5353
}
5454

5555
extension ToolInfoV0 {
56-
fileprivate init(commandStack: [ParsableCommand.Type]) {
56+
init(commandStack: [ParsableCommand.Type]) {
5757
self.init(command: CommandInfoV0(commandStack: commandStack))
5858
// FIXME: This is a hack to inject the help command into the tool info
5959
// instead we should try to lift this into the parseable command tree

Sources/ArgumentParserToolInfo/ToolInfo.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,9 @@ public struct ArgumentInfoV0: Codable, Hashable {
215215
/// Mapping of valid values to descriptions of the value.
216216
public var allValueDescriptions: [String: String]?
217217

218-
/// The type of completion to use for an argument or option.
218+
/// The type of completion to use for an argument or an option value.
219219
///
220-
/// `nil` if the tool use use the default completion kind.
220+
/// `nil` if the tool uses the default completion kind.
221221
public var completionKind: CompletionKindV0?
222222

223223
/// Short description of the argument's functionality.

Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash

+2-2
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,11 @@ _math_stats_quantiles() {
267267
return
268268
;;
269269
2)
270-
__math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- customArg "${COMP_CWORD}" "$(__math_cursor_index_in_current_word)")"
270+
__math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- positional@1 "${COMP_CWORD}" "$(__math_cursor_index_in_current_word)")"
271271
return
272272
;;
273273
3)
274-
__math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- customDeprecatedArg)"
274+
__math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- positional@2)"
275275
return
276276
;;
277277
esac

Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash

+21-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/bash
22

3-
__base_test_cursor_index_in_current_word() {
3+
__base-test_cursor_index_in_current_word() {
44
local remaining="${COMP_LINE}"
55

66
local word
@@ -33,7 +33,7 @@ __base_test_cursor_index_in_current_word() {
3333
# - options: remove options for this (sub)command that are already on the command line
3434
# - positional_number: set to the current positional number
3535
# - unparsed_words: remove all flags, options, and option values for this (sub)command
36-
__base_test_offer_flags_options() {
36+
__base-test_offer_flags_options() {
3737
local -ir positional_count="${1}"
3838
positional_number=0
3939

@@ -125,14 +125,14 @@ __base_test_offer_flags_options() {
125125
fi
126126
}
127127

128-
__base_test_add_completions() {
128+
__base-test_add_completions() {
129129
local completion
130130
while IFS='' read -r completion; do
131131
COMPREPLY+=("${completion}")
132132
done < <(IFS=$'\n' compgen "${@}" -- "${cur}")
133133
}
134134

135-
__base_test_custom_complete() {
135+
__base-test_custom_complete() {
136136
if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then
137137
local -ar words=("${COMP_WORDS[@]}")
138138
else
@@ -142,7 +142,7 @@ __base_test_custom_complete() {
142142
"${COMP_WORDS[0]}" "${@}" "${words[@]}"
143143
}
144144

145-
_base_test() {
145+
_base-test() {
146146
trap "$(shopt -p);$(shopt -po)" RETURN
147147
shopt -s extglob
148148
set +o history +o posix
@@ -160,31 +160,31 @@ _base_test() {
160160

161161
local -a flags=(--one --two --three --kind-counter -h --help)
162162
local -a options=(--name --kind --other-kind --path1 --path2 --path3 --rep1 -r --rep2)
163-
__base_test_offer_flags_options 2
163+
__base-test_offer_flags_options 2
164164

165165
# Offer option value completions
166166
case "${prev}" in
167167
--name)
168168
return
169169
;;
170170
--kind)
171-
__base_test_add_completions -W 'one'$'\n''two'$'\n''custom-three'
171+
__base-test_add_completions -W 'one'$'\n''two'$'\n''custom-three'
172172
return
173173
;;
174174
--other-kind)
175-
__base_test_add_completions -W 'b1_bash'$'\n''b2_bash'$'\n''b3_bash'
175+
__base-test_add_completions -W 'b1_bash'$'\n''b2_bash'$'\n''b3_bash'
176176
return
177177
;;
178178
--path1)
179-
__base_test_add_completions -f
179+
__base-test_add_completions -f
180180
return
181181
;;
182182
--path2)
183-
__base_test_add_completions -f
183+
__base-test_add_completions -f
184184
return
185185
;;
186186
--path3)
187-
__base_test_add_completions -W 'c1_bash'$'\n''c2_bash'$'\n''c3_bash'
187+
__base-test_add_completions -W 'c1_bash'$'\n''c2_bash'$'\n''c3_bash'
188188
return
189189
;;
190190
--rep1)
@@ -198,11 +198,11 @@ _base_test() {
198198
# Offer positional completions
199199
case "${positional_number}" in
200200
1)
201-
__base_test_add_completions -W "$(__base_test_custom_complete ---completion -- argument "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")"
201+
__base-test_add_completions -W "$(__base-test_custom_complete ---completion -- positional@0 "${COMP_CWORD}" "$(__base-test_cursor_index_in_current_word)")"
202202
return
203203
;;
204204
2)
205-
__base_test_add_completions -W "$(__base_test_custom_complete ---completion -- nested.nestedArgument "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")"
205+
__base-test_add_completions -W "$(__base-test_custom_complete ---completion -- positional@1 "${COMP_CWORD}" "$(__base-test_cursor_index_in_current_word)")"
206206
return
207207
;;
208208
esac
@@ -214,7 +214,7 @@ _base_test() {
214214
case "${subcommand}" in
215215
sub-command|escaped-command|help)
216216
# Offer subcommand argument completions
217-
"_base_test_${subcommand}"
217+
"_base-test_${subcommand}"
218218
;;
219219
*)
220220
# Offer subcommand completions
@@ -223,16 +223,16 @@ _base_test() {
223223
esac
224224
}
225225

226-
_base_test_sub_command() {
226+
_base-test_sub-command() {
227227
flags=(-h --help)
228228
options=()
229-
__base_test_offer_flags_options 0
229+
__base-test_offer_flags_options 0
230230
}
231231

232-
_base_test_escaped_command() {
232+
_base-test_escaped-command() {
233233
flags=(-h --help)
234234
options=(--one)
235-
__base_test_offer_flags_options 1
235+
__base-test_offer_flags_options 1
236236

237237
# Offer option value completions
238238
case "${prev}" in
@@ -244,14 +244,14 @@ _base_test_escaped_command() {
244244
# Offer positional completions
245245
case "${positional_number}" in
246246
1)
247-
__base_test_add_completions -W "$(__base_test_custom_complete ---completion escaped-command -- two "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")"
247+
__base-test_add_completions -W "$(__base-test_custom_complete ---completion escaped-command -- positional@0 "${COMP_CWORD}" "$(__base-test_cursor_index_in_current_word)")"
248248
return
249249
;;
250250
esac
251251
}
252252

253-
_base_test_help() {
253+
_base-test_help() {
254254
:
255255
}
256256

257-
complete -o filenames -F _base_test base-test
257+
complete -o filenames -F _base-test base-test

0 commit comments

Comments
 (0)