9
9
//
10
10
//===----------------------------------------------------------------------===//
11
11
12
+ import ArgumentParserToolInfo
13
+
12
14
struct BashCompletionsGenerator {
13
15
/// Generates a Bash completion script for the given command.
14
16
static func generateCompletionScript( _ type: ParsableCommand . Type ) -> String {
17
+ return ToolInfoV0 ( commandStack: [ type] ) . bashCompletionScript ( )
18
+ }
19
+ }
20
+
21
+ extension ToolInfoV0 {
22
+ fileprivate func bashCompletionScript( ) -> String {
15
23
// TODO: Add a check to see if the command is installed where we expect?
16
- let initialFunctionName = [ type ] . completionFunctionName ( ) . makeSafeFunctionName
17
24
return """
18
- #!/bin/bash
25
+ #!/bin/bash
26
+
27
+ \( self . command. bashCompletionFunction ( ) )
28
+
29
+ complete -F \( self . command. bashCompletionFunctionName ( ) ) \( self . command. commandName)
30
+ """
31
+ }
32
+ }
19
33
20
- \( generateCompletionFunction ( [ type] ) )
34
+ extension CommandInfoV0 {
35
+ fileprivate func bashCommandContext( ) -> [ String ] {
36
+ return ( self . superCommands ?? [ ] ) + [ self . commandName]
37
+ }
21
38
22
- complete -F \( initialFunctionName ) \( type . _commandName )
23
- """
39
+ fileprivate func bashCompletionFunctionName ( ) -> String {
40
+ return " _ " + self . bashCommandContext ( ) . joined ( separator : " _ " ) . makeSafeFunctionName
24
41
}
25
42
26
- /// Generates a Bash completion function for the last command in the given list.
27
- fileprivate static func generateCompletionFunction( _ commands: [ ParsableCommand . Type ] ) -> String {
28
- let type = commands. last!
29
- let functionName = commands. completionFunctionName ( ) . makeSafeFunctionName
30
-
43
+ /// Generates a Bash completion function.
44
+ fileprivate func bashCompletionFunction( ) -> String {
45
+ let functionName = self . bashCompletionFunctionName ( )
46
+
31
47
// The root command gets a different treatment for the parsing index.
32
- let isRootCommand = commands . count == 1
48
+ let isRootCommand = ( self . superCommands ?? [ ] ) . count == 0
33
49
let dollarOne = isRootCommand ? " 1 " : " $1 "
34
50
let subcommandArgument = isRootCommand ? " 2 " : " $(($1+1)) "
35
51
36
52
// Include 'help' in the list of subcommands for the root command.
37
- var subcommands = type. configuration. subcommands
38
- . filter { $0. configuration. shouldDisplay }
39
- if !subcommands. isEmpty && isRootCommand {
40
- subcommands. append ( HelpCommand . self)
41
- }
53
+ let subcommands = ( self . subcommands ?? [ ] )
54
+ . filter { $0. shouldDisplay }
42
55
43
56
// Generate the words that are available at the "top level" of this
44
57
// command — these are the dash-prefixed names of options and flags as well
45
58
// as all the subcommand names.
46
- let completionWords = generateArgumentWords ( commands)
47
- + subcommands. map { $0. _commandName }
48
-
59
+ let completionKeys = self . bashCompletionKeys ( ) + subcommands. map { $0. commandName }
60
+
49
61
// Generate additional top-level completions — these are completion lists
50
62
// or custom function-based word lists from positional arguments.
51
- let additionalCompletions = generateArgumentCompletions ( commands )
52
-
63
+ let additionalCompletions = self . bashPositionalCompletions ( )
64
+
53
65
// Start building the resulting function code.
54
66
var result = " \( functionName) () { \n "
55
67
@@ -69,7 +81,7 @@ struct BashCompletionsGenerator {
69
81
70
82
// Start by declaring a local var for the top-level completions.
71
83
// Return immediately if the completion matching hasn't moved further.
72
- result += " opts= \" \( completionWords . joined ( separator: " " ) ) \" \n "
84
+ result += " opts= \" \( completionKeys . joined ( separator: " " ) ) \" \n "
73
85
for line in additionalCompletions {
74
86
result += " opts= \" $opts \( line) \" \n "
75
87
}
@@ -84,7 +96,7 @@ struct BashCompletionsGenerator {
84
96
85
97
// Generate the case pattern-matching statements for option values.
86
98
// If there aren't any, skip the case block altogether.
87
- let optionHandlers = generateOptionHandlers ( commands )
99
+ let optionHandlers = self . bashOptionCompletions ( ) . joined ( separator : " \n " )
88
100
if !optionHandlers. isEmpty {
89
101
result += """
90
102
case $prev in
@@ -100,8 +112,8 @@ struct BashCompletionsGenerator {
100
112
result += " case ${COMP_WORDS[ \( dollarOne) ]} in \n "
101
113
for subcommand in subcommands {
102
114
result += """
103
- ( \( subcommand. _commandName ) )
104
- \( functionName) _ \( subcommand. _commandName ) \( subcommandArgument)
115
+ ( \( subcommand. commandName ) )
116
+ \( functionName) _ \( subcommand. commandName ) \( subcommandArgument)
105
117
return
106
118
;;
107
119
@@ -120,77 +132,100 @@ struct BashCompletionsGenerator {
120
132
121
133
return result +
122
134
subcommands
123
- . map { generateCompletionFunction ( commands + [ $0 ] ) }
124
- . joined ( )
135
+ . map { $0 . bashCompletionFunction ( ) }
136
+ . joined ( )
125
137
}
126
138
127
139
/// Returns the option and flag names that can be top-level completions.
128
- fileprivate static func generateArgumentWords( _ commands: [ ParsableCommand . Type ] ) -> [ String ] {
129
- commands
130
- . argumentsForHelp ( visibility: . default)
131
- . flatMap { $0. bashCompletionWords ( ) }
140
+ fileprivate func bashCompletionKeys( ) -> [ String ] {
141
+ var result = [ String] ( )
142
+ for argument in self . arguments ?? [ ] {
143
+ // Skip hidden arguments.
144
+ guard argument. shouldDisplay else { continue }
145
+ result. append ( contentsOf: argument. bashCompletionKeys ( ) )
146
+ }
147
+ return result
132
148
}
133
149
134
150
/// Returns additional top-level completions from positional arguments.
135
151
///
136
152
/// These consist of completions that are defined as `.list` or `.custom`.
137
- fileprivate static func generateArgumentCompletions( _ commands: [ ParsableCommand . Type ] ) -> [ String ] {
138
- ArgumentSet ( commands. last!, visibility: . default, parent: nil )
139
- . compactMap { arg -> String ? in
140
- guard arg. isPositional else { return nil }
141
-
142
- switch arg. completion. kind {
143
- case . default, . file, . directory:
144
- return nil
145
- case . list( let list) :
146
- return list. joined ( separator: " " )
147
- case . shellCommand( let command) :
148
- return " $( \( command) ) "
149
- case . custom:
150
- return """
151
- $( " ${COMP_WORDS[0]} " \( arg. customCompletionCall ( commands) ) " ${COMP_WORDS[@]} " )
152
- """
153
- }
154
- }
153
+ fileprivate func bashPositionalCompletions( ) -> [ String ] {
154
+ var result = [ String] ( )
155
+ for argument in self . arguments ?? [ ] {
156
+ // Skip hidden arguments.
157
+ guard argument. shouldDisplay else { continue }
158
+ // Only select positional arguments.
159
+ guard argument. kind == . positional else { continue }
160
+ // Skip if no completions.
161
+ guard let completionValues = argument. bashPositionalCompletionValues ( command: self ) else { continue }
162
+ result. append ( completionValues)
163
+ }
164
+ return result
155
165
}
156
166
157
167
/// Returns the case-matching statements for supplying completions after an option or flag.
158
- fileprivate static func generateOptionHandlers( _ commands: [ ParsableCommand . Type ] ) -> String {
159
- ArgumentSet ( commands. last!, visibility: . default, parent: nil )
160
- . compactMap { arg -> String ? in
161
- let words = arg. bashCompletionWords ( )
162
- if words. isEmpty { return nil }
163
-
164
- // Flags don't take a value, so we don't provide follow-on completions.
165
- if arg. isNullary { return nil }
166
-
167
- return """
168
- \( arg. bashCompletionWords ( ) . joined ( separator: " | " ) ) )
169
- \( arg. bashValueCompletion ( commands) . indentingEachLine ( by: 4 ) )
168
+ fileprivate func bashOptionCompletions( ) -> [ String ] {
169
+ var result = [ String] ( )
170
+ for argument in self . arguments ?? [ ] {
171
+ // Skip hidden arguments.
172
+ guard argument. shouldDisplay else { continue }
173
+ // Flags don't take a value, so we don't provide follow-on completions.
174
+ guard argument. kind != . flag else { continue }
175
+ // Skip if no keys.
176
+ let keys = argument. bashCompletionKeys ( )
177
+ guard !keys. isEmpty else { continue }
178
+ // Skip if no completions.
179
+ guard let completionValues = argument. bashOptionCompletionValues ( command: self ) else { continue }
180
+ result. append ( """
181
+ \( keys. joined ( separator: " | " ) ) )
182
+ \( completionValues. indentingEachLine ( by: 4 ) )
170
183
return
171
184
;;
172
- """
173
- }
174
- . joined ( separator : " \n " )
185
+ """ )
186
+ }
187
+ return result
175
188
}
176
189
}
177
190
178
- extension ArgumentDefinition {
191
+ extension ArgumentInfoV0 {
179
192
/// Returns the different completion names for this argument.
180
- fileprivate func bashCompletionWords( ) -> [ String ] {
181
- return help. visibility. base == . default
182
- ? names. map { $0. synopsisString }
183
- : [ ]
193
+ fileprivate func bashCompletionKeys( ) -> [ String ] {
194
+ return ( self . names ?? [ ] ) . map { $0. commonCompletionSynopsisString ( ) }
195
+ }
196
+
197
+ // FIXME: determine if this can be combined with bashOptionCompletionValues
198
+ fileprivate func bashPositionalCompletionValues(
199
+ command: CommandInfoV0
200
+ ) -> String ? {
201
+ precondition ( self . kind == . positional)
202
+
203
+ switch self . completionKind {
204
+ case . none, . file, . directory:
205
+ // FIXME: this doesn't work
206
+ return nil
207
+ case . list( let list) :
208
+ return list. joined ( separator: " " )
209
+ case . shellCommand( let command) :
210
+ return " $( \( command) ) "
211
+ case . custom:
212
+ // Generate a call back into the command to retrieve a completions list
213
+ return #"$("${COMP_WORDS[0]}" \#( self . commonCustomCompletionCall ( command: command) ) "${COMP_WORDS[@]}")"#
214
+ }
184
215
}
185
216
186
217
/// Returns the bash completions that can follow this argument's `--name`.
187
218
///
188
219
/// Uses bash-completion for file and directory values if available.
189
- fileprivate func bashValueCompletion( _ commands: [ ParsableCommand . Type ] ) -> String {
190
- switch completion. kind {
191
- case . default:
192
- return " "
193
-
220
+ fileprivate func bashOptionCompletionValues(
221
+ command: CommandInfoV0
222
+ ) -> String ? {
223
+ precondition ( self . kind == . option)
224
+
225
+ switch self . completionKind {
226
+ case . none:
227
+ return nil
228
+
194
229
case . file( let extensions) where extensions. isEmpty:
195
230
return """
196
231
if declare -F _filedir >/dev/null; then
@@ -203,7 +238,7 @@ extension ArgumentDefinition {
203
238
case . file( let extensions) :
204
239
var safeExts = extensions. map { String ( $0. flatMap { $0 == " ' " ? [ " \\ " , " ' " ] : [ $0] } ) }
205
240
safeExts. append ( contentsOf: safeExts. map { $0. uppercased ( ) } )
206
-
241
+
207
242
return """
208
243
if declare -F _filedir >/dev/null; then
209
244
\( safeExts. map { " _filedir ' \( $0) ' " } . joined ( separator: " \n " ) )
@@ -224,22 +259,16 @@ extension ArgumentDefinition {
224
259
COMPREPLY=( $(compgen -d -- " $cur " ) )
225
260
fi
226
261
"""
227
-
262
+
228
263
case . list( let list) :
229
264
return #"COMPREPLY=( $(compgen -W " \#( list. joined ( separator: " " ) ) " -- "$cur") )"#
230
-
265
+
231
266
case . shellCommand( let command) :
232
267
return " COMPREPLY=( $( \( command) ) ) "
233
-
268
+
234
269
case . custom:
235
270
// Generate a call back into the command to retrieve a completions list
236
- return #"COMPREPLY=( $(compgen -W "$("${COMP_WORDS[0]}" \#( customCompletionCall ( commands ) ) "${COMP_WORDS[@]}")" -- "$cur") )"#
271
+ return #"COMPREPLY=( $(compgen -W "$("${COMP_WORDS[0]}" \#( self . commonCustomCompletionCall ( command : command ) ) "${COMP_WORDS[@]}")" -- "$cur") )"#
237
272
}
238
273
}
239
274
}
240
-
241
- extension String {
242
- var makeSafeFunctionName : String {
243
- self . replacingOccurrences ( of: " - " , with: " _ " )
244
- }
245
- }
0 commit comments