@optique/core 1.1.13-dev.2823 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/completion.cjs +1639 -237
- package/dist/completion.js +1639 -237
- package/dist/constructs.cjs +7 -1
- package/dist/constructs.d.cts +2 -0
- package/dist/constructs.d.ts +2 -0
- package/dist/constructs.js +7 -1
- package/dist/internal/joined-file-suggestion.cjs +73 -0
- package/dist/internal/joined-file-suggestion.js +70 -0
- package/dist/modifiers.cjs +13 -8
- package/dist/modifiers.js +13 -8
- package/dist/primitives.cjs +19 -22
- package/dist/primitives.js +19 -22
- package/dist/suggestion.cjs +3 -1
- package/dist/suggestion.js +3 -1
- package/package.json +2 -2
package/dist/completion.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatMessage } from "./message.js";
|
|
2
|
+
import { getJoinedFileSuggestion } from "./internal/joined-file-suggestion.js";
|
|
2
3
|
|
|
3
4
|
//#region src/completion.ts
|
|
4
5
|
/**
|
|
@@ -46,6 +47,11 @@ function encodeFilePrefix(suggestion) {
|
|
|
46
47
|
if (prefix === "" || !(suggestion.pattern ?? "").startsWith(prefix) || sanitizeForTransport(prefix) !== prefix) return "";
|
|
47
48
|
return `:${encodePattern(prefix)}`;
|
|
48
49
|
}
|
|
50
|
+
/** Escapes literal records that could otherwise be read as Bash directives. */
|
|
51
|
+
function encodeBashLiteral(value) {
|
|
52
|
+
const text = sanitizeForTransport(value);
|
|
53
|
+
return /^(?:__FILE__|__JOINED_FILE__|__LITERAL__):/.test(text) ? `__LITERAL__:${text}` : text;
|
|
54
|
+
}
|
|
49
55
|
function encodeExtensions(extensions) {
|
|
50
56
|
return extensions?.map((ext) => ext.replace(/^\./, "")).join(",") ?? "";
|
|
51
57
|
}
|
|
@@ -62,6 +68,17 @@ function sanitizeForTransport(text) {
|
|
|
62
68
|
return text.replace(/[\t\n\r\0]/g, " ");
|
|
63
69
|
}
|
|
64
70
|
/**
|
|
71
|
+
* Escapes literal values that would otherwise be interpreted as transport
|
|
72
|
+
* directives. Escape the escape prefix itself so decoding is unambiguous.
|
|
73
|
+
* @param text The literal suggestion text.
|
|
74
|
+
* @returns The sanitized and, if necessary, escaped literal text.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
function encodeLiteral(text) {
|
|
78
|
+
const sanitized = sanitizeForTransport(text);
|
|
79
|
+
return sanitized.startsWith("__FILE__:") || sanitized.startsWith("__LITERAL__:") ? `__LITERAL__:${sanitized}` : sanitized;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
65
82
|
* The Bash shell completion generator.
|
|
66
83
|
* @since 0.6.0
|
|
67
84
|
*/
|
|
@@ -73,32 +90,296 @@ const bash = {
|
|
|
73
90
|
return `
|
|
74
91
|
function _${programName} () {
|
|
75
92
|
COMPREPLY=()
|
|
76
|
-
local
|
|
93
|
+
local __raw_current="\${COMP_WORDS[COMP_CWORD]}"
|
|
94
|
+
local current="" __quote="" __char __next __index __quote_offset=0
|
|
95
|
+
local __file_mode=0
|
|
96
|
+
if [[ $(type -t compopt) == builtin ]]; then
|
|
97
|
+
compopt +o filenames 2>/dev/null || true
|
|
98
|
+
fi
|
|
99
|
+
# Dequote the shell word without evaluating filename contents. Readline
|
|
100
|
+
# returns escaped words after Tab, and users may start with an open quote.
|
|
101
|
+
for ((__index=0; __index<\${#__raw_current}; __index++)); do
|
|
102
|
+
__char="\${__raw_current:__index:1}"
|
|
103
|
+
if [[ -z "$__quote" ]]; then
|
|
104
|
+
case "$__char" in
|
|
105
|
+
\\\\) ((++__index)); current+="\${__raw_current:__index:1}" ;;
|
|
106
|
+
\\'|\\") __quote="$__char"; __quote_offset=\${#current} ;;
|
|
107
|
+
*) current+="$__char" ;;
|
|
108
|
+
esac
|
|
109
|
+
elif [[ "$__quote" == "'" ]]; then
|
|
110
|
+
if [[ "$__char" == "'" ]]; then
|
|
111
|
+
__quote=""
|
|
112
|
+
else
|
|
113
|
+
current+="$__char"
|
|
114
|
+
fi
|
|
115
|
+
else
|
|
116
|
+
case "$__char" in
|
|
117
|
+
\\") __quote="" ;;
|
|
118
|
+
\\\\)
|
|
119
|
+
__next="\${__raw_current:__index+1:1}"
|
|
120
|
+
case "$__next" in
|
|
121
|
+
\\$|\\\`|\\"|\\\\)
|
|
122
|
+
((++__index)); current+="$__next" ;;
|
|
123
|
+
*) current+="$__char" ;;
|
|
124
|
+
esac ;;
|
|
125
|
+
*) current+="$__char" ;;
|
|
126
|
+
esac
|
|
127
|
+
fi
|
|
128
|
+
done
|
|
129
|
+
local __tilde_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
|
|
130
|
+
local __tilde_glob_pattern='^(~[a-zA-Z0-9_.+-]*)(/.*)?$'
|
|
77
131
|
local prev=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
132
|
+
# Bash splits unquoted '=' at its default word breaks. Reassemble only
|
|
133
|
+
# adjacent fragments, including further word breaks inside a joined value,
|
|
134
|
+
# leaving whitespace-separated arguments intact.
|
|
135
|
+
local __plain_current="$current" __remove="" __word __joined=0
|
|
136
|
+
local __raw_joined_current="$__raw_current"
|
|
137
|
+
local -a __raw_offsets=() __raw_quotes=()
|
|
138
|
+
if [[ \${COMP_LINE+x} ]]; then
|
|
139
|
+
local -a __words=()
|
|
140
|
+
local __rest="$COMP_LINE" __previous="" __space __valid=1 __i __j=0
|
|
141
|
+
local __shell_quote="" __shell_escape=0 __boundary=0 __previous_boundary=0 __scan __symbol __shell_dollar=0
|
|
142
|
+
# Reconstruct the prefix at the cursor, leaving any suffix to Readline.
|
|
143
|
+
if [[ \${COMP_POINT+x} ]]; then
|
|
144
|
+
__rest="\${COMP_LINE:0:COMP_POINT}"
|
|
145
|
+
fi
|
|
146
|
+
for ((__i=0; __i<=COMP_CWORD; __i++)); do
|
|
147
|
+
__word="\${COMP_WORDS[__i]}"
|
|
148
|
+
__space=0
|
|
149
|
+
while [[ "$__rest" == [[:space:]]* ]]; do
|
|
150
|
+
__space=1
|
|
151
|
+
__rest="\${__rest:1}"
|
|
152
|
+
done
|
|
153
|
+
if (( __i == COMP_CWORD && \${#__rest} < \${#__word} )); then
|
|
154
|
+
__word="\${__word:0:\${#__rest}}"
|
|
155
|
+
fi
|
|
156
|
+
if [[ "\${__rest:0:\${#__word}}" != "$__word" ]]; then
|
|
157
|
+
__valid=0
|
|
158
|
+
break
|
|
159
|
+
fi
|
|
160
|
+
# Quoted or escaped operators belong to words; bare operators separate
|
|
161
|
+
# shell syntax even when no whitespace surrounds a redirection.
|
|
162
|
+
__boundary=0
|
|
163
|
+
for ((__scan=0; __scan<\${#__word}; __scan++)); do
|
|
164
|
+
__symbol="\${__word:__scan:1}"
|
|
165
|
+
if (( __shell_escape )); then
|
|
166
|
+
__shell_escape=0
|
|
167
|
+
elif [[ "$__shell_quote" == ansi ]]; then
|
|
168
|
+
if [[ "$__symbol" == '\\' ]]; then __shell_escape=1
|
|
169
|
+
elif [[ "$__symbol" == "'" ]]; then __shell_quote=""; fi
|
|
170
|
+
elif [[ "$__shell_quote" == "'" ]]; then
|
|
171
|
+
[[ "$__symbol" == "'" ]] && __shell_quote=""
|
|
172
|
+
elif [[ "$__symbol" == '\\' ]]; then
|
|
173
|
+
__shell_escape=1
|
|
174
|
+
elif [[ -n "$__shell_quote" ]]; then
|
|
175
|
+
[[ "$__symbol" == "$__shell_quote" ]] && __shell_quote=""
|
|
176
|
+
else
|
|
177
|
+
case "$__symbol" in
|
|
178
|
+
"'")
|
|
179
|
+
if (( __shell_dollar )); then __shell_quote=ansi
|
|
180
|
+
else __shell_quote="'"; fi ;;
|
|
181
|
+
'"') __shell_quote='"' ;;
|
|
182
|
+
'$') __shell_dollar=1 ;;
|
|
183
|
+
'>'|'<'|'|'|'&'|';'|'('|')') __boundary=1 ;;
|
|
184
|
+
esac
|
|
185
|
+
fi
|
|
186
|
+
[[ "$__symbol" == '$' && -z "$__shell_quote" ]] || __shell_dollar=0
|
|
187
|
+
done
|
|
188
|
+
if (( __i > 1 && !__space && !__boundary && !__previous_boundary )) &&
|
|
189
|
+
{ [[ -n "$__word" && -z "\${__word//=/}" ]] ||
|
|
190
|
+
[[ -n "$__previous" && -z "\${__previous//=/}" ]] ||
|
|
191
|
+
[[ "\${__words[__j-1]}" == [-/+]* ]] ||
|
|
192
|
+
[[ "$__word" == =* && "\${__words[__j-1]}" == [-/+]* ]]; }; then
|
|
193
|
+
__words[__j-1]+="$__word"
|
|
194
|
+
else
|
|
195
|
+
__words[__j]="$__word"
|
|
196
|
+
((++__j))
|
|
197
|
+
fi
|
|
198
|
+
__rest="\${__rest:\${#__word}}"
|
|
199
|
+
__previous="$__word"
|
|
200
|
+
__previous_boundary=$__boundary
|
|
201
|
+
done
|
|
202
|
+
if (( __valid )); then
|
|
203
|
+
current="\${__words[__j-1]}"
|
|
204
|
+
__raw_joined_current="$current"
|
|
205
|
+
prev=("\${__words[@]:1:__j-2}")
|
|
206
|
+
# Bash passes the fragment Readline will replace as the second argument,
|
|
207
|
+
# which is empty when the cursor follows a word-break character.
|
|
208
|
+
if [[ \${2+x} ]]; then
|
|
209
|
+
__remove="\${current:0:\${#current}-\${#2}}"
|
|
210
|
+
elif [[ -n "$__raw_current" && -z "\${__raw_current//=/}" ]]; then
|
|
211
|
+
__remove="$current"
|
|
212
|
+
else
|
|
213
|
+
__remove="\${current:0:\${#current}-\${#__raw_current}}"
|
|
214
|
+
fi
|
|
215
|
+
# Decode joined shell words without evaluating substitutions. Keep quote
|
|
216
|
+
# state across the reassembled fragments and decode the preserved prefix
|
|
217
|
+
# separately so trimming uses the same representation as suggestions.
|
|
218
|
+
__words[__j]="$__remove"
|
|
219
|
+
local __decoded __decoded_option=0 __before_quote __join_quote __char __next __k __ansi __part __digits __code __join_quote_offset
|
|
220
|
+
for ((__i=1; __i<=__j; __i++)); do
|
|
221
|
+
__word="\${__words[__i]}"
|
|
222
|
+
__decoded=""; __join_quote=""; __join_quote_offset=0
|
|
223
|
+
for ((__k=0; __k<\${#__word}; __k++)); do
|
|
224
|
+
__char="\${__word:__k:1}"
|
|
225
|
+
__before_quote="$__join_quote"
|
|
226
|
+
if [[ -z "$__join_quote" ]]; then
|
|
227
|
+
case "$__char" in
|
|
228
|
+
'\\') ((++__k)); __decoded+="\${__word:__k:1}" ;;
|
|
229
|
+
'$')
|
|
230
|
+
if [[ "\${__word:__k+1:1}" == "'" ]]; then
|
|
231
|
+
__join_quote=ansi; __join_quote_offset=\${#__decoded}; __ansi=""; ((++__k))
|
|
232
|
+
elif [[ "\${__word:__k+1:1}" == '"' ]]; then
|
|
233
|
+
__join_quote='"'; __join_quote_offset=\${#__decoded}; ((++__k))
|
|
234
|
+
else __decoded+="$__char"; fi ;;
|
|
235
|
+
"'"|'"') __join_quote="$__char"; __join_quote_offset=\${#__decoded} ;;
|
|
236
|
+
*) __decoded+="$__char" ;;
|
|
237
|
+
esac
|
|
238
|
+
elif [[ "$__join_quote" == ansi ]]; then
|
|
239
|
+
if [[ "$__char" == "'" ]]; then
|
|
240
|
+
printf -v __part '%b' "$__ansi"
|
|
241
|
+
__decoded+="$__part"; __join_quote=""
|
|
242
|
+
elif [[ "$__char" == '\\' ]]; then
|
|
243
|
+
((++__k)); __next="\${__word:__k:1}"
|
|
244
|
+
case "$__next" in
|
|
245
|
+
[0-7])
|
|
246
|
+
__digits="$__next"
|
|
247
|
+
while (( \${#__digits} < 3 )) && [[ "\${__word:__k+1:1}" == [0-7] ]]; do
|
|
248
|
+
((++__k)); __digits+="\${__word:__k:1}"
|
|
249
|
+
done
|
|
250
|
+
__ansi+='\\0'"$__digits" ;;
|
|
251
|
+
c)
|
|
252
|
+
__next="\${__word:__k+1:1}"
|
|
253
|
+
if [[ -n "$__next" && "$__next" != "'" ]]; then
|
|
254
|
+
((++__k)); printf -v __code '%d' "'$__next"
|
|
255
|
+
if [[ "$__next" == '?' ]]; then __code=127
|
|
256
|
+
else __code=$((__code & 31)); fi
|
|
257
|
+
printf -v __part '\\\\%03o' "$__code"
|
|
258
|
+
__ansi+="$__part"
|
|
259
|
+
else __ansi+='\\\\c'; fi ;;
|
|
260
|
+
"'"|'"'|'?') __ansi+="$__next" ;;
|
|
261
|
+
*) __ansi+='\\'"$__next" ;;
|
|
262
|
+
esac
|
|
263
|
+
else __ansi+="$__char"; fi
|
|
264
|
+
elif [[ "$__join_quote" == "'" ]]; then
|
|
265
|
+
if [[ "$__char" == "'" ]]; then __join_quote=""
|
|
266
|
+
else __decoded+="$__char"; fi
|
|
267
|
+
else
|
|
268
|
+
case "$__char" in
|
|
269
|
+
'"') __join_quote="" ;;
|
|
270
|
+
'\\')
|
|
271
|
+
__next="\${__word:__k+1:1}"
|
|
272
|
+
case "$__next" in
|
|
273
|
+
'$'|'\`'|'"'|'\\') ((++__k)); __decoded+="$__next" ;;
|
|
274
|
+
*) __decoded+="$__char" ;;
|
|
275
|
+
esac ;;
|
|
276
|
+
*) __decoded+="$__char" ;;
|
|
277
|
+
esac
|
|
278
|
+
fi
|
|
279
|
+
if (( __i == __j - 1 )) &&
|
|
280
|
+
{ [[ ! \${__raw_offsets[\${#__decoded}]+x} ]] ||
|
|
281
|
+
[[ -n "$__before_quote" && -z "$__join_quote" ]]; }; then
|
|
282
|
+
__raw_offsets[\${#__decoded}]=$((__k + 1))
|
|
283
|
+
__raw_quotes[\${#__decoded}]="$__join_quote"
|
|
284
|
+
fi
|
|
285
|
+
done
|
|
286
|
+
if [[ "$__join_quote" == ansi ]]; then
|
|
287
|
+
printf -v __part '%b' "$__ansi"
|
|
288
|
+
__decoded+="$__part"
|
|
289
|
+
fi
|
|
290
|
+
# Shell syntax may precede the option prefix. Decide from decoded text,
|
|
291
|
+
# retaining the existing native-file handling for non-option words.
|
|
292
|
+
if [[ "$__decoded" == [-/+]* ]] || (( __i == __j && __decoded_option )); then
|
|
293
|
+
if (( __i == __j - 1 )); then
|
|
294
|
+
__decoded_option=1
|
|
295
|
+
__quote="$__join_quote"
|
|
296
|
+
[[ "$__quote" == ansi ]] && __quote="'"
|
|
297
|
+
__quote_offset=$__join_quote_offset
|
|
298
|
+
fi
|
|
299
|
+
__words[__i]="$__decoded"
|
|
300
|
+
fi
|
|
301
|
+
done
|
|
302
|
+
if [[ "\${__words[__j-1]}" == -*=* || "\${__words[__j-1]}" == /*:* || "\${__words[__j-1]}" != "$__raw_current" ]]; then
|
|
303
|
+
current="\${__words[__j-1]}"
|
|
304
|
+
__joined=1
|
|
305
|
+
else
|
|
306
|
+
current="$__plain_current"
|
|
307
|
+
__words[__j]=""
|
|
308
|
+
fi
|
|
309
|
+
prev=("\${__words[@]:1:__j-2}")
|
|
310
|
+
__remove="\${__words[__j]}"
|
|
311
|
+
fi
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# Joined file directives retain the exact option prefix and metadata chosen
|
|
315
|
+
# by the parser; do not infer option boundaries through speculative retries.
|
|
316
|
+
local __protocol_current="$__raw_current"
|
|
317
|
+
(( __joined )) && __protocol_current="$current"
|
|
318
|
+
local -a __lines=() __replies=()
|
|
319
|
+
local line __reply_prefix="" __file_current="$current" __literal=0
|
|
320
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
321
|
+
[[ -n "$line" ]] && __lines+=("$line")
|
|
322
|
+
done < <(${programName} ${escapedArgs} "\${prev[@]}" "$__protocol_current" 2>/dev/null)
|
|
323
|
+
for line in "\${__lines[@]}"; do
|
|
324
|
+
COMPREPLY=()
|
|
325
|
+
__reply_prefix=""
|
|
326
|
+
__file_current="$current"
|
|
327
|
+
__literal=0
|
|
328
|
+
# Protocol tags are case-sensitive even when nocasematch is enabled.
|
|
329
|
+
if [ "\${line:0:12}" = '__LITERAL__:' ]; then
|
|
330
|
+
line="\${line#__LITERAL__:}"
|
|
331
|
+
__literal=1
|
|
332
|
+
elif [ "\${line:0:16}" = '__JOINED_FILE__:' ]; then
|
|
333
|
+
line="\${line#__JOINED_FILE__:}"
|
|
334
|
+
__reply_prefix="\${line%%:*}"
|
|
335
|
+
__reply_prefix="\${__reply_prefix//%3A/:}"
|
|
336
|
+
__reply_prefix="\${__reply_prefix//%25/%}"
|
|
337
|
+
line="__FILE__:\${line#*:}"
|
|
338
|
+
__file_current="\${current:\${#__reply_prefix}}"
|
|
339
|
+
fi
|
|
340
|
+
local __raw_file_current="$__raw_current" __file_quote_offset=$__quote_offset __file_tilde_quoted=0
|
|
341
|
+
if [[ -n "$__reply_prefix" ]]; then
|
|
342
|
+
local __raw_offset="\${__raw_offsets[\${#__reply_prefix}]:-\${#__reply_prefix}}"
|
|
343
|
+
__raw_file_current="\${__raw_joined_current:__raw_offset}"
|
|
344
|
+
# Quoting may start before the option name and remain active at the
|
|
345
|
+
# delimiter. Keep that context when deciding whether tilde is literal.
|
|
346
|
+
if [[ -n "\${__raw_quotes[\${#__reply_prefix}]-}" ]]; then
|
|
347
|
+
__file_tilde_quoted=1
|
|
348
|
+
fi
|
|
349
|
+
if (( __file_quote_offset >= \${#__reply_prefix} )); then
|
|
350
|
+
__file_quote_offset=$((__file_quote_offset - \${#__reply_prefix}))
|
|
351
|
+
fi
|
|
352
|
+
fi
|
|
353
|
+
if [ "$__literal" = 0 ] && [ "\${line:0:9}" = '__FILE__:' ]; then
|
|
354
|
+
# Parse file completion directive:
|
|
355
|
+
# __FILE__:type:extensions:pattern:hidden[:insertion_prefix]
|
|
356
|
+
local insertion_prefix="" __key_current="$__file_current"
|
|
83
357
|
IFS=':' read -r _ type extensions pattern hidden insertion_prefix <<< "$line"
|
|
84
358
|
extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
|
|
85
359
|
pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
|
|
86
|
-
|
|
87
360
|
insertion_prefix="\${insertion_prefix//%3A/:}"; insertion_prefix="\${insertion_prefix//%25/%}"
|
|
88
|
-
local
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
361
|
+
local __reply_start=\${#COMPREPLY[@]}
|
|
362
|
+
|
|
363
|
+
# Inserted keys are not filesystem paths. Match only the value part,
|
|
364
|
+
# then restore the key once the file candidates have been filtered.
|
|
365
|
+
if [[ -n "$insertion_prefix" ]] &&
|
|
366
|
+
[ "\${pattern:0:\${#insertion_prefix}}" = "$insertion_prefix" ] &&
|
|
367
|
+
[ "\${__file_current:0:\${#insertion_prefix}}" = "$insertion_prefix" ]; then
|
|
368
|
+
pattern="\${pattern:\${#insertion_prefix}}"
|
|
369
|
+
__file_current="\${__file_current:\${#insertion_prefix}}"
|
|
370
|
+
if [ "\${__raw_file_current:0:\${#insertion_prefix}}" = "$insertion_prefix" ]; then
|
|
371
|
+
__raw_file_current="\${__raw_file_current:\${#insertion_prefix}}"
|
|
372
|
+
fi
|
|
94
373
|
else
|
|
95
374
|
insertion_prefix=""
|
|
96
375
|
fi
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
376
|
+
|
|
377
|
+
# path() echoes the raw shell word as its pattern. Once dequoted,
|
|
378
|
+
# use the literal current-word prefix instead of the escaped pattern.
|
|
379
|
+
if [[ "$pattern" == "$__raw_file_current" && "$__file_current" != "$__raw_file_current" ]] ||
|
|
380
|
+
{ [[ -n "$__reply_prefix" ]] && [ "$pattern" = "$__file_current" ] &&
|
|
381
|
+
{ [[ "$pattern" != *[\*\?]* ]] || [ "$__raw_file_current" != "$__file_current" ]; }; }; then
|
|
382
|
+
pattern=""
|
|
102
383
|
fi
|
|
103
384
|
|
|
104
385
|
# Save and adjust glob/shell options for safe file completion
|
|
@@ -115,12 +396,12 @@ function _${programName} () {
|
|
|
115
396
|
unset GLOBIGNORE
|
|
116
397
|
|
|
117
398
|
# Expand tilde prefix for file globbing
|
|
118
|
-
local __glob_current="$
|
|
119
|
-
if [[ "$
|
|
399
|
+
local __glob_current="$__file_current" __tilde_prefix="" __tilde_expanded=""
|
|
400
|
+
if [[ $__file_tilde_quoted == 0 && "$__raw_file_current" == "~"* && "$__file_current" =~ $__tilde_pattern ]]; then
|
|
120
401
|
__tilde_prefix="\${BASH_REMATCH[1]}"
|
|
121
402
|
eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
|
|
122
403
|
if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
|
|
123
|
-
__glob_current="\${__tilde_expanded}\${
|
|
404
|
+
__glob_current="\${__tilde_expanded}\${__file_current#\$__tilde_prefix}"
|
|
124
405
|
else
|
|
125
406
|
__tilde_prefix=""
|
|
126
407
|
fi
|
|
@@ -131,10 +412,11 @@ function _${programName} () {
|
|
|
131
412
|
# However, if the user has already typed beyond the pattern (e.g.,
|
|
132
413
|
# pattern="src/" and current="src/ma"), preserve the typed suffix
|
|
133
414
|
# for incremental filtering.
|
|
134
|
-
local __from_pattern=0
|
|
415
|
+
local __from_pattern=0 __filter_pattern=0 __pattern_prefix=""
|
|
416
|
+
local __pattern_tilde_prefix="" __pattern_tilde_expanded=""
|
|
135
417
|
if [[ -n "$pattern" ]]; then
|
|
136
418
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
137
|
-
local __norm_current="\${
|
|
419
|
+
local __norm_current="\${__file_current#./}"
|
|
138
420
|
local __norm_pattern="\${pattern#./}"
|
|
139
421
|
# For wildcard patterns, compare only the directory prefix before
|
|
140
422
|
# any wildcards so that typing src/ma narrows src/*.ts correctly
|
|
@@ -143,17 +425,34 @@ function _${programName} () {
|
|
|
143
425
|
__compare_pattern="\${__compare_pattern%%[\*\?]*}"
|
|
144
426
|
[[ "$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
|
|
145
427
|
fi
|
|
146
|
-
|
|
428
|
+
local __extends_pattern=0
|
|
429
|
+
if [[ ( -n "$__compare_pattern" || -n "$__norm_current" ) && \${#__norm_current} -ge \${#__compare_pattern} && "\${__norm_current:0:\${#__compare_pattern}}" == "$__compare_pattern" && "$__file_current" != "$pattern" ]]; then
|
|
430
|
+
__extends_pattern=1
|
|
431
|
+
fi
|
|
432
|
+
if (( __extends_pattern )) && [[ "$__norm_pattern" != *[\*\?]* ]]; then
|
|
147
433
|
# User has typed beyond or an equivalent form of the pattern
|
|
148
434
|
true
|
|
149
435
|
else
|
|
436
|
+
# Wildcards keep their file filter while the typed prefix narrows
|
|
437
|
+
# the resulting paths. Save the expanded prefix before using pattern.
|
|
438
|
+
if [[ "$__norm_pattern" == *[\*\?]* ]]; then
|
|
439
|
+
__filter_pattern=1
|
|
440
|
+
__pattern_prefix="$__glob_current"
|
|
441
|
+
# An unquoted value equal to its wildcard is a complete search
|
|
442
|
+
# pattern, not a literal filename prefix to filter against.
|
|
443
|
+
if [ "$__file_current" = "$pattern" ] && [ "$__raw_file_current" = "$__file_current" ]; then
|
|
444
|
+
__pattern_prefix=""
|
|
445
|
+
fi
|
|
446
|
+
__pattern_tilde_prefix="$__tilde_prefix"
|
|
447
|
+
__pattern_tilde_expanded="$__tilde_expanded"
|
|
448
|
+
fi
|
|
150
449
|
__from_pattern=1
|
|
151
450
|
# Reset tilde state from the current-word expansion so that a
|
|
152
451
|
# non-tilde pattern is not rewritten through stale tilde state
|
|
153
452
|
__tilde_prefix=""
|
|
154
453
|
__tilde_expanded=""
|
|
155
454
|
__glob_current="$pattern"
|
|
156
|
-
if [[ "$pattern" =~
|
|
455
|
+
if [[ "$pattern" =~ $__tilde_glob_pattern ]]; then
|
|
157
456
|
__tilde_prefix="\${BASH_REMATCH[1]}"
|
|
158
457
|
eval "__tilde_expanded=\$__tilde_prefix" 2>/dev/null || true
|
|
159
458
|
if [[ -n "$__tilde_expanded" && "$__tilde_expanded" != "$__tilde_prefix" ]]; then
|
|
@@ -175,36 +474,93 @@ function _${programName} () {
|
|
|
175
474
|
# This runs after tilde expansion so that paths like ~/.config/ are
|
|
176
475
|
# checked against the expanded path, not the literal ~ string.
|
|
177
476
|
local __inside_hidden_path=0
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
477
|
+
local __hidden_directory_pattern='/[.]([^./][^/]*|[.][^/]+)/'
|
|
478
|
+
if [[ "/\${__glob_current%/}/" =~ $__hidden_directory_pattern ||
|
|
479
|
+
"/\${__pattern_prefix%/}/" =~ $__hidden_directory_pattern ]]; then
|
|
480
|
+
__inside_hidden_path=1
|
|
481
|
+
fi
|
|
181
482
|
# Also check if the current prefix explicitly targets hidden entries
|
|
182
483
|
# (e.g., user typed "." or ".e" to complete .env)
|
|
183
484
|
local __prefix_targets_hidden=0
|
|
184
485
|
local __prefix_base="\${__glob_current##*/}"
|
|
486
|
+
if [[ -n "$__pattern_prefix" ]]; then __prefix_base="\${__pattern_prefix##*/}"; fi
|
|
185
487
|
[[ "$__prefix_base" == .* ]] && __prefix_targets_hidden=1
|
|
186
488
|
if [[ "$hidden" == "1" || "$__inside_hidden_path" == "1" || "$__prefix_targets_hidden" == "1" ]]; then shopt -s dotglob; fi
|
|
187
489
|
|
|
188
490
|
# Pre-expand file candidates. When the glob base came from the
|
|
189
491
|
# program's pattern and contains wildcard characters (* or ?),
|
|
190
|
-
#
|
|
492
|
+
# expand it as data with shell globbing. Empty IFS avoids splitting paths;
|
|
493
|
+
# NUL records preserve spaces and newlines without requiring mapfile.
|
|
494
|
+
# Shell expansion respects dotglob on older Bash versions as well.
|
|
191
495
|
# Otherwise append * to treat it as a prefix.
|
|
192
|
-
#
|
|
193
|
-
#
|
|
496
|
+
# Only * and ? are wildcard syntax; escape brackets, backslashes, and
|
|
497
|
+
# extended-glob operators so other pattern characters stay literal.
|
|
194
498
|
local -a __candidates=()
|
|
195
499
|
if [[ "$__from_pattern" == "1" && "$__glob_current" == *[\*\?]* ]]; then
|
|
196
|
-
|
|
500
|
+
local __pathname_glob="" __glob_char __glob_index
|
|
501
|
+
for ((__glob_index=0; __glob_index<\${#__glob_current}; __glob_index++)); do
|
|
502
|
+
__glob_char="\${__glob_current:__glob_index:1}"
|
|
503
|
+
case "$__glob_char" in
|
|
504
|
+
'['|']'|'\\'|'('|')'|'|'|'+'|'@'|'!')
|
|
505
|
+
__pathname_glob+="\\\\$__glob_char" ;;
|
|
506
|
+
*) __pathname_glob+="$__glob_char" ;;
|
|
507
|
+
esac
|
|
508
|
+
done
|
|
509
|
+
[[ "$type" == "directory" ]] && __pathname_glob="\${__pathname_glob%/}"
|
|
510
|
+
local __glob_dir="\${__pathname_glob%/*}"
|
|
511
|
+
[[ "$__glob_dir" == "$__pathname_glob" ]] && __glob_dir="."
|
|
512
|
+
[[ -z "$__glob_dir" ]] && __glob_dir="/"
|
|
513
|
+
local __navigation_dir="" __basename_glob="\${__pathname_glob##*/}"
|
|
514
|
+
# Once an offered directory is entered, keep the basename filter but
|
|
515
|
+
# enumerate there. Only descendants of matching pattern bases qualify.
|
|
516
|
+
if [[ "$__pattern_prefix" == */* ]]; then
|
|
517
|
+
local __typed_dir="\${__pattern_prefix%/*}" __base_dir __norm_base __norm_dir
|
|
518
|
+
# Resolve directory ancestry in subshells, preserving the typed spelling
|
|
519
|
+
# for insertion. The trailing slash also preserves newline-ending names.
|
|
520
|
+
if __norm_dir=$(CDPATH= builtin cd -P -- "$__typed_dir" >/dev/null 2>&1 && printf '%s/' "\${PWD%/}"); then
|
|
521
|
+
while IFS= read -r -d '' __base_dir; do
|
|
522
|
+
[[ -d "$__base_dir" ]] || continue
|
|
523
|
+
__norm_base=$(CDPATH= builtin cd -P -- "$__base_dir" >/dev/null 2>&1 && printf '%s/' "\${PWD%/}") || continue
|
|
524
|
+
if [ "\${__norm_dir:0:\${#__norm_base}}" = "$__norm_base" ]; then
|
|
525
|
+
__navigation_dir="$__typed_dir"
|
|
526
|
+
break
|
|
527
|
+
fi
|
|
528
|
+
done < <(IFS=; printf '%s\\0' $__glob_dir)
|
|
529
|
+
fi
|
|
530
|
+
fi
|
|
531
|
+
local __candidate
|
|
532
|
+
while IFS= read -r -d '' __candidate; do
|
|
533
|
+
if [[ -e "$__candidate" || -L "$__candidate" ]]; then
|
|
534
|
+
[[ -d "$__candidate" ]] && __candidate="\${__candidate%/}"
|
|
535
|
+
__candidates+=("$__candidate")
|
|
536
|
+
fi
|
|
537
|
+
done < <(
|
|
538
|
+
IFS=
|
|
539
|
+
if [[ -n "$__navigation_dir" ]]; then
|
|
540
|
+
printf '%s\\0' "$__navigation_dir"/$__basename_glob
|
|
541
|
+
else
|
|
542
|
+
printf '%s\\0' $__pathname_glob
|
|
543
|
+
fi
|
|
544
|
+
)
|
|
197
545
|
# For file/any modes, also include directories from the base
|
|
198
546
|
# directory for navigation even when the glob itself does not
|
|
199
547
|
# match directory names. Skip this for directory mode so that
|
|
200
548
|
# the pattern's basename filter is respected.
|
|
201
549
|
if [[ "$type" != "directory" ]]; then
|
|
202
|
-
local __glob_dir="\${__glob_current%/*}"
|
|
203
|
-
[[ "$__glob_dir" == "$__glob_current" ]] && __glob_dir="."
|
|
204
550
|
local __d
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
551
|
+
local __directory_pattern="\${__glob_dir%/}/*/"
|
|
552
|
+
while IFS= read -r -d '' __d; do
|
|
553
|
+
[[ -d "$__d" ]] || continue
|
|
554
|
+
__d="\${__d%/}"
|
|
555
|
+
__candidates+=("$__d")
|
|
556
|
+
done < <(
|
|
557
|
+
IFS=
|
|
558
|
+
if [[ -n "$__navigation_dir" ]]; then
|
|
559
|
+
printf '%s\\0' "$__navigation_dir"/*/
|
|
560
|
+
else
|
|
561
|
+
printf '%s\\0' $__directory_pattern
|
|
562
|
+
fi
|
|
563
|
+
)
|
|
208
564
|
fi
|
|
209
565
|
else
|
|
210
566
|
__candidates=("$__glob_current"*)
|
|
@@ -215,6 +571,26 @@ function _${programName} () {
|
|
|
215
571
|
fi
|
|
216
572
|
fi
|
|
217
573
|
|
|
574
|
+
if (( __filter_pattern )); then
|
|
575
|
+
local __nocasematch_was_set=0
|
|
576
|
+
shopt -q nocasematch && __nocasematch_was_set=1
|
|
577
|
+
if shopt -q nocaseglob; then shopt -s nocasematch; else shopt -u nocasematch; fi
|
|
578
|
+
local -a __filtered_candidates=()
|
|
579
|
+
for __candidate in "\${__candidates[@]}"; do
|
|
580
|
+
if [[ "$__candidate" == "$__pattern_prefix"* ||
|
|
581
|
+
"\${__candidate#./}" == "\${__pattern_prefix#./}"* ]]; then
|
|
582
|
+
if [[ "$__pattern_prefix" == ./* && "$__candidate" != ./* ]]; then
|
|
583
|
+
__candidate="./$__candidate"
|
|
584
|
+
elif [[ "$__pattern_prefix" != ./* && "$__pattern_prefix" != . ]]; then
|
|
585
|
+
__candidate="\${__candidate#./}"
|
|
586
|
+
fi
|
|
587
|
+
__filtered_candidates+=("$__candidate")
|
|
588
|
+
fi
|
|
589
|
+
done
|
|
590
|
+
if (( __nocasematch_was_set )); then shopt -s nocasematch; else shopt -u nocasematch; fi
|
|
591
|
+
__candidates=("\${__filtered_candidates[@]}")
|
|
592
|
+
fi
|
|
593
|
+
|
|
218
594
|
# Generate file completions based on type
|
|
219
595
|
case "$type" in
|
|
220
596
|
file)
|
|
@@ -222,10 +598,11 @@ function _${programName} () {
|
|
|
222
598
|
if [[ -n "$extensions" ]]; then
|
|
223
599
|
# Files with extension filtering + directories
|
|
224
600
|
local ext_pattern="\${extensions//,/|}"
|
|
601
|
+
ext_pattern='\\.('"$ext_pattern"')$'
|
|
225
602
|
for item in "\${__candidates[@]}"; do
|
|
226
603
|
if [[ -d "$item" ]]; then
|
|
227
604
|
COMPREPLY+=("$item/")
|
|
228
|
-
elif [[ -f "$item" && "$item" =~
|
|
605
|
+
elif [[ -f "$item" && "$item" =~ $ext_pattern ]]; then
|
|
229
606
|
COMPREPLY+=("$item")
|
|
230
607
|
fi
|
|
231
608
|
done
|
|
@@ -252,10 +629,11 @@ function _${programName} () {
|
|
|
252
629
|
# Files with extension filtering + directories
|
|
253
630
|
# Files with extension filtering
|
|
254
631
|
local ext_pattern="\${extensions//,/|}"
|
|
632
|
+
ext_pattern='\\.('"$ext_pattern"')$'
|
|
255
633
|
for item in "\${__candidates[@]}"; do
|
|
256
634
|
if [[ -d "$item" ]]; then
|
|
257
635
|
COMPREPLY+=("$item/")
|
|
258
|
-
elif [[ ( -e "$item" || -L "$item" ) && "$item" =~
|
|
636
|
+
elif [[ ( -e "$item" || -L "$item" ) && "$item" =~ $ext_pattern ]]; then
|
|
259
637
|
COMPREPLY+=("$item")
|
|
260
638
|
fi
|
|
261
639
|
done
|
|
@@ -274,7 +652,12 @@ function _${programName} () {
|
|
|
274
652
|
esac
|
|
275
653
|
|
|
276
654
|
# Restore tilde prefix in completion results
|
|
277
|
-
if
|
|
655
|
+
if (( __filter_pattern )); then
|
|
656
|
+
__tilde_prefix="$__pattern_tilde_prefix"
|
|
657
|
+
__tilde_expanded="$__pattern_tilde_expanded"
|
|
658
|
+
fi
|
|
659
|
+
if [[ -n "$__tilde_prefix" &&
|
|
660
|
+
( -z "$__quote" || $__file_quote_offset -gt 0 ) ]]; then
|
|
278
661
|
local __i
|
|
279
662
|
for __i in "\${!COMPREPLY[@]}"; do
|
|
280
663
|
COMPREPLY[\$__i]="\${COMPREPLY[\$__i]/#\$__tilde_expanded/\$__tilde_prefix}"
|
|
@@ -289,41 +672,155 @@ function _${programName} () {
|
|
|
289
672
|
if [[ "$__failglob_was_set" == "1" ]]; then shopt -s failglob; fi
|
|
290
673
|
if [[ "$__noglob_was_set" == "1" ]]; then set -f; fi
|
|
291
674
|
|
|
675
|
+
# Readline appends directory separators when it receives the full path.
|
|
676
|
+
# Midword quotes still need ours because replies omit the typed prefix.
|
|
677
|
+
if [[ -n "$__quote" && $__file_quote_offset -eq 0 ]]; then
|
|
678
|
+
local __reply_index
|
|
679
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
680
|
+
COMPREPLY[__reply_index]="\${COMPREPLY[__reply_index]%/}"
|
|
681
|
+
done
|
|
682
|
+
fi
|
|
683
|
+
|
|
684
|
+
# Readline replaces only the text after an open quote. Remove the
|
|
685
|
+
# already typed prefix so it is not inserted a second time.
|
|
686
|
+
# Joined replies are trimmed once against __remove after restoring option=,
|
|
687
|
+
# and keyed replies once their key has been restored.
|
|
688
|
+
if [[ -n "$__quote" && $__file_quote_offset -gt 0 && -z "$__reply_prefix" && -z "$insertion_prefix" ]]; then
|
|
689
|
+
local __quote_prefix="\${__file_current:0:__file_quote_offset}"
|
|
690
|
+
local __reply_index
|
|
691
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
692
|
+
COMPREPLY[__reply_index]="\${COMPREPLY[__reply_index]#"$__quote_prefix"}"
|
|
693
|
+
done
|
|
694
|
+
fi
|
|
695
|
+
|
|
696
|
+
# Readline expands a leading tilde in a returned filename even when the
|
|
697
|
+
# user did not type one. Keep such local paths explicitly relative.
|
|
698
|
+
if [[ ( "$__raw_file_current" != "~"* || $__file_tilde_quoted == 1 ) && -z "$__tilde_prefix" ]]; then
|
|
699
|
+
local __reply_index
|
|
700
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
701
|
+
if [[ "\${COMPREPLY[__reply_index]}" =~ $__tilde_glob_pattern ]]; then
|
|
702
|
+
COMPREPLY[__reply_index]="./\${COMPREPLY[__reply_index]}"
|
|
703
|
+
fi
|
|
704
|
+
done
|
|
705
|
+
fi
|
|
706
|
+
|
|
707
|
+
# Bash 5.3 leaves substitutions unescaped inside an open double quote.
|
|
708
|
+
# Earlier versions already escape them during filename processing.
|
|
709
|
+
if [[ "$__quote" == '"' &&
|
|
710
|
+
( \${BASH_VERSINFO[0]} -gt 5 ||
|
|
711
|
+
( \${BASH_VERSINFO[0]} -eq 5 && \${BASH_VERSINFO[1]} -ge 3 ) ) &&
|
|
712
|
+
$(type -t compopt) == builtin ]]; then
|
|
713
|
+
local __escaped_reply __escape_index __reply_index
|
|
714
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
715
|
+
__escaped_reply=""
|
|
716
|
+
for ((__escape_index=0; __escape_index<\${#COMPREPLY[__reply_index]}; __escape_index++)); do
|
|
717
|
+
__char="\${COMPREPLY[__reply_index]:__escape_index:1}"
|
|
718
|
+
__next="\${COMPREPLY[__reply_index]:__escape_index+1:1}"
|
|
719
|
+
if [[ "$__char" == '\\' &&
|
|
720
|
+
( "$__next" == '$' || "$__next" == '\`' ) ]]; then
|
|
721
|
+
__escaped_reply+="\\\\$__char"
|
|
722
|
+
elif [[ "$__char" == '$' || "$__char" == '\`' ]]; then
|
|
723
|
+
__escaped_reply+="\\\\$__char"
|
|
724
|
+
else
|
|
725
|
+
__escaped_reply+="$__char"
|
|
726
|
+
fi
|
|
727
|
+
done
|
|
728
|
+
COMPREPLY[__reply_index]="$__escaped_reply"
|
|
729
|
+
done
|
|
730
|
+
fi
|
|
731
|
+
|
|
292
732
|
# Filter out hidden files unless requested
|
|
293
733
|
if [[ "$hidden" != "1" && "$__inside_hidden_path" == "0" && "$__prefix_targets_hidden" == "0" ]]; then
|
|
294
734
|
local filtered=()
|
|
295
735
|
local __name
|
|
296
736
|
for item in "\${COMPREPLY[@]}"; do
|
|
297
737
|
__name="\${item%/}"; __name="\${__name##*/}"
|
|
298
|
-
[[ "$__name" != .* ]]
|
|
738
|
+
if [[ "$__name" != .* ]]; then
|
|
739
|
+
filtered+=("$item")
|
|
740
|
+
fi
|
|
299
741
|
done
|
|
300
742
|
COMPREPLY=("\${filtered[@]}")
|
|
301
743
|
fi
|
|
302
744
|
if [[ -n "$insertion_prefix" ]]; then
|
|
303
|
-
local
|
|
304
|
-
for
|
|
305
|
-
COMPREPLY[
|
|
745
|
+
local __reply_index
|
|
746
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
747
|
+
COMPREPLY[__reply_index]="$insertion_prefix\${COMPREPLY[__reply_index]}"
|
|
306
748
|
done
|
|
749
|
+
if [[ -n "$__quote" && $__file_quote_offset -gt 0 && -z "$__reply_prefix" ]]; then
|
|
750
|
+
local __quote_prefix="\${__key_current:0:__file_quote_offset}"
|
|
751
|
+
for ((__reply_index=__reply_start; __reply_index<\${#COMPREPLY[@]}; __reply_index++)); do
|
|
752
|
+
COMPREPLY[__reply_index]="\${COMPREPLY[__reply_index]#"$__quote_prefix"}"
|
|
753
|
+
done
|
|
754
|
+
fi
|
|
307
755
|
fi
|
|
308
|
-
if [[
|
|
309
|
-
COMPREPLY=("\${__saved_reply[@]}" "\${COMPREPLY[@]}")
|
|
310
|
-
fi
|
|
311
|
-
current="$__saved_current"
|
|
756
|
+
if [[ \${#COMPREPLY[@]} -gt $__reply_start ]]; then __file_mode=1; fi
|
|
312
757
|
else
|
|
313
758
|
# Regular literal completion
|
|
314
759
|
COMPREPLY+=("$line")
|
|
315
760
|
fi
|
|
316
|
-
|
|
761
|
+
for __word in "\${COMPREPLY[@]}"; do
|
|
762
|
+
__replies+=("$__reply_prefix$__word")
|
|
763
|
+
done
|
|
764
|
+
done
|
|
765
|
+
COMPREPLY=()
|
|
766
|
+
for __word in "\${__replies[@]}"; do
|
|
767
|
+
if [[ "\${__word:0:\${#__remove}}" == "$__remove" ]]; then
|
|
768
|
+
COMPREPLY+=("\${__word:\${#__remove}}")
|
|
769
|
+
fi
|
|
770
|
+
done
|
|
771
|
+
if (( \${#COMPREPLY[@]} > 1 )) && command -v sort >/dev/null 2>&1; then
|
|
772
|
+
# Sort shell-escaped keys, not filenames: each key occupies one line even
|
|
773
|
+
# when a candidate contains tabs or newlines. Keep the first occurrence's
|
|
774
|
+
# index, then restore the original order without evaluating escaped data.
|
|
775
|
+
# This works without associative arrays or NUL-aware sort on Bash 3.2.
|
|
776
|
+
local -a __unique_replies=()
|
|
777
|
+
local __index __key __last_key=""
|
|
778
|
+
while IFS= read -r __index; do
|
|
779
|
+
__unique_replies+=("\${COMPREPLY[__index]}")
|
|
780
|
+
done < <(
|
|
781
|
+
for ((__index=0; __index<\${#COMPREPLY[@]}; __index++)); do
|
|
782
|
+
printf '%q\\t%d\\n' "\${COMPREPLY[__index]}" "$__index"
|
|
783
|
+
done |
|
|
784
|
+
LC_ALL=C sort -t $'\\t' -k1,1 -k2,2n |
|
|
785
|
+
{
|
|
786
|
+
while IFS=$'\\t' read -r __key __index; do
|
|
787
|
+
if [ "$__key" != "$__last_key" ]; then
|
|
788
|
+
printf '%s\\n' "$__index"
|
|
789
|
+
__last_key="$__key"
|
|
790
|
+
fi
|
|
791
|
+
done
|
|
792
|
+
} | LC_ALL=C sort -n
|
|
793
|
+
)
|
|
794
|
+
COMPREPLY=("\${__unique_replies[@]}")
|
|
795
|
+
fi
|
|
796
|
+
if [[ $__file_mode == 1 && $(type -t compopt) == builtin ]]; then
|
|
797
|
+
compopt -o filenames 2>/dev/null || true
|
|
798
|
+
fi
|
|
317
799
|
}
|
|
318
800
|
|
|
319
|
-
|
|
801
|
+
# Bash 3.2 has no compopt: use filename quoting for all candidates there.
|
|
802
|
+
if [[ $(type -t compopt) == builtin ]]; then
|
|
803
|
+
complete -F _${programName} -- ${programName}
|
|
804
|
+
else
|
|
805
|
+
complete -o filenames -F _${programName} -- ${programName}
|
|
806
|
+
fi
|
|
320
807
|
`;
|
|
321
808
|
},
|
|
322
809
|
*encodeSuggestions(suggestions) {
|
|
323
810
|
let i = 0;
|
|
324
811
|
for (const suggestion of suggestions) {
|
|
325
812
|
if (i > 0) yield "\n";
|
|
326
|
-
|
|
813
|
+
const joined = getJoinedFileSuggestion(suggestion);
|
|
814
|
+
if (joined !== void 0) {
|
|
815
|
+
if (joined.literal && suggestion.kind === "literal") {
|
|
816
|
+
yield encodeBashLiteral(suggestion.text);
|
|
817
|
+
yield "\n";
|
|
818
|
+
}
|
|
819
|
+
for (const [index, { prefix, file }] of joined.files.entries()) {
|
|
820
|
+
if (index > 0) yield "\n";
|
|
821
|
+
yield `__JOINED_FILE__:${encodePattern(prefix)}:${file.type}:${encodeExtensions(file.extensions)}:${encodePattern(sanitizeForTransport(file.pattern ?? ""))}:${file.includeHidden ? "1" : "0"}${encodeFilePrefix(file)}`;
|
|
822
|
+
}
|
|
823
|
+
} else if (suggestion.kind === "literal") yield encodeBashLiteral(suggestion.text);
|
|
327
824
|
else {
|
|
328
825
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
329
826
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -343,16 +840,36 @@ const zsh = {
|
|
|
343
840
|
generateScript(programName, args = []) {
|
|
344
841
|
validateProgramName(programName);
|
|
345
842
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
|
|
843
|
+
const functionName = /[.-]/.test(programName) ? `_optique:${programName}` : `_${programName}`;
|
|
844
|
+
const fixedGlobFunction = "__optique-fixed-glob-" + Array.from(programName, (character) => character.charCodeAt(0).toString(16).padStart(2, "0")).join("");
|
|
346
845
|
return `#compdef ${programName}
|
|
347
|
-
function
|
|
348
|
-
|
|
846
|
+
function ${functionName} () {
|
|
847
|
+
# Completion fields omit surrounding quotes, unlike words[CURRENT].
|
|
848
|
+
# Single-quoted fields are already literal; other fields retain escapes.
|
|
849
|
+
local current="\$IPREFIX\$PREFIX\$SUFFIX\$ISUFFIX"
|
|
850
|
+
local __logical_prefix="\$IPREFIX\$PREFIX"
|
|
851
|
+
# Let zsh match the suffix when completing in the middle of a word.
|
|
852
|
+
if [[ -o complete_in_word ]]; then
|
|
853
|
+
current="\$__logical_prefix"
|
|
854
|
+
fi
|
|
855
|
+
if [[ "\$compstate[quote]" == '\"' ]]; then
|
|
856
|
+
# Restore the omitted quoting context before removing shell escapes.
|
|
857
|
+
# In double quotes, a backslash before an ordinary character is literal.
|
|
858
|
+
current="\$QIPREFIX\$current\${QISUFFIX:-\$compstate[quote]}"
|
|
859
|
+
__logical_prefix="\$QIPREFIX\$__logical_prefix\$compstate[quote]"
|
|
860
|
+
current="\${(Q)current}"
|
|
861
|
+
__logical_prefix="\${(Q)__logical_prefix}"
|
|
862
|
+
elif [[ "\$compstate[quote]" != "'" ]]; then
|
|
863
|
+
current="\${(Q)current}"
|
|
864
|
+
__logical_prefix="\${(Q)__logical_prefix}"
|
|
865
|
+
fi
|
|
349
866
|
local -a prev
|
|
350
867
|
# Extract previous arguments, skipping empty ones
|
|
351
868
|
prev=()
|
|
352
869
|
local i
|
|
353
870
|
for (( i=2; i < CURRENT; i++ )); do
|
|
354
871
|
if [[ -n "\$words[i]" ]]; then
|
|
355
|
-
prev+=("\$words[i]")
|
|
872
|
+
prev+=("\${(Q)words[i]}")
|
|
356
873
|
fi
|
|
357
874
|
done
|
|
358
875
|
|
|
@@ -376,7 +893,12 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
376
893
|
desc=\${line#*\$'\\0'}
|
|
377
894
|
desc=\${desc%%\$'\\0'*}
|
|
378
895
|
|
|
379
|
-
|
|
896
|
+
local __literal=0
|
|
897
|
+
if [[ "\$value" == __LITERAL__:* ]]; then
|
|
898
|
+
value="\${value#__LITERAL__:}"
|
|
899
|
+
__literal=1
|
|
900
|
+
fi
|
|
901
|
+
if [[ \$__literal == 0 && "\$value" == __FILE__:* ]]; then
|
|
380
902
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
381
903
|
local type extensions pattern hidden insertion_prefix
|
|
382
904
|
IFS=':' read -r _ type extensions pattern hidden insertion_prefix <<< "\$value"
|
|
@@ -384,10 +906,17 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
384
906
|
__decoded_pattern="\${__decoded_pattern//\\%3A/:}"; __decoded_pattern="\${__decoded_pattern//\\%25/%}"
|
|
385
907
|
insertion_prefix="\${insertion_prefix//\\%3A/:}"; insertion_prefix="\${insertion_prefix//\\%25/%}"
|
|
386
908
|
local __saved_prefix="\$PREFIX" __saved_iprefix="\$IPREFIX"
|
|
387
|
-
|
|
909
|
+
local __saved_current="\$current" __saved_logical_prefix="\$__logical_prefix"
|
|
910
|
+
if [[ -n "\$insertion_prefix" && "\$__decoded_pattern" == "\$insertion_prefix"* &&
|
|
911
|
+
"\$PREFIX" == "\$insertion_prefix"* &&
|
|
912
|
+
"\$__logical_prefix" == "\$insertion_prefix"* &&
|
|
913
|
+
"\$current" == "\$insertion_prefix"* ]]; then
|
|
388
914
|
pattern="\${__decoded_pattern#"\$insertion_prefix"}"
|
|
389
915
|
PREFIX="\${PREFIX#"\$insertion_prefix"}"
|
|
390
916
|
IPREFIX="\$IPREFIX\$insertion_prefix"
|
|
917
|
+
# Compare file patterns with the value part, not the inserted key.
|
|
918
|
+
current="\${current#"\$insertion_prefix"}"
|
|
919
|
+
__logical_prefix="\${__logical_prefix#"\$insertion_prefix"}"
|
|
391
920
|
else
|
|
392
921
|
# Preserve legacy decoding for directives without active metadata.
|
|
393
922
|
pattern="\${pattern//%3A/:}"; pattern="\${pattern//%25/%}"
|
|
@@ -399,6 +928,11 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
399
928
|
local __was_sh_glob=0
|
|
400
929
|
[[ -o sh_glob ]] && __was_sh_glob=1
|
|
401
930
|
unsetopt sh_glob
|
|
931
|
+
local __was_extended_glob=0
|
|
932
|
+
[[ -o extended_glob ]] && __was_extended_glob=1
|
|
933
|
+
local __was_sh_word_split=0
|
|
934
|
+
[[ -o sh_word_split ]] && __was_sh_word_split=1
|
|
935
|
+
unsetopt sh_word_split
|
|
402
936
|
|
|
403
937
|
# Enable glob_dots when hidden files are requested so that
|
|
404
938
|
# _files and _directories include dot-prefixed entries
|
|
@@ -410,20 +944,351 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
410
944
|
# _directories enumerate the pattern's directory instead of the
|
|
411
945
|
# current word. If the user has already typed beyond the pattern,
|
|
412
946
|
# keep PREFIX unchanged for incremental narrowing.
|
|
413
|
-
|
|
947
|
+
local __fixed_glob=0
|
|
948
|
+
local -a __fixed_matches
|
|
949
|
+
local __initial_file_pattern="" __initial_directory="" __initial_leaf=""
|
|
950
|
+
if [[ ( "\$type" == "file" || "\$type" == "any" || "\$type" == "directory" ) &&
|
|
951
|
+
-z "\$current\$PREFIX\$SUFFIX" &&
|
|
952
|
+
-z "\${compstate[pattern_match]}" &&
|
|
953
|
+
"\$pattern" != /* && "\$pattern" != "~"* ]]; then
|
|
954
|
+
if [[ "\$pattern" == */* ]]; then
|
|
955
|
+
__initial_directory="\${pattern%/*}/"
|
|
956
|
+
fi
|
|
957
|
+
__initial_leaf="\${pattern##*/}"
|
|
958
|
+
# Native -g filters are split as shell words and use extended glob
|
|
959
|
+
# syntax. Keep unsupported filename forms on the existing path.
|
|
960
|
+
if [[ "\$__initial_leaf" == *[\*\?]* &&
|
|
961
|
+
"\$__initial_directory" != *[\*\?]* &&
|
|
962
|
+
"\$__initial_directory" != *["[]()"]* &&
|
|
963
|
+
"\$__initial_leaf" != *["[]"]* &&
|
|
964
|
+
"\$__initial_leaf" != *'\\'* &&
|
|
965
|
+
"\$__initial_leaf" != *[[:space:]]* &&
|
|
966
|
+
"\$__initial_leaf" != *["#~^()|<>;&"]* &&
|
|
967
|
+
"\$__initial_leaf" != *"'"* && "\$__initial_leaf" != *'"'* ]]; then
|
|
968
|
+
__initial_file_pattern="\$__initial_leaf"
|
|
969
|
+
# Only the literal directory belongs in PREFIX. Quote it so
|
|
970
|
+
# spaces and shell metacharacters remain literal path text.
|
|
971
|
+
if [[ -n "\$__initial_directory" ]]; then
|
|
972
|
+
PREFIX="\${(q)__initial_directory}"
|
|
973
|
+
fi
|
|
974
|
+
fi
|
|
975
|
+
fi
|
|
976
|
+
local __saved_pattern_match="\$compstate[pattern_match]"
|
|
977
|
+
local __pattern_match_changed=0
|
|
978
|
+
# path() echoes current as its pattern. Keep zsh's escaped PREFIX
|
|
979
|
+
# in that case instead of reinterpreting logical text as shell syntax.
|
|
980
|
+
if [[ -n "\$pattern" && "\$pattern" != "\$current" && -z "\$__initial_file_pattern" ]]; then
|
|
414
981
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
415
|
-
local __norm_prefix="\${
|
|
982
|
+
local __norm_prefix="\${__logical_prefix#./}"
|
|
416
983
|
local __norm_pattern="\${pattern#./}"
|
|
417
984
|
local __compare_pattern="\$__norm_pattern"
|
|
418
985
|
if [[ "\$__compare_pattern" == *[\*\?]* ]]; then
|
|
419
986
|
__compare_pattern="\${__compare_pattern%%[\*\?]*}"
|
|
420
987
|
[[ "\$__compare_pattern" == */* ]] && __compare_pattern="\${__compare_pattern%/*}/" || __compare_pattern=""
|
|
421
988
|
fi
|
|
422
|
-
if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$
|
|
423
|
-
#
|
|
424
|
-
|
|
989
|
+
if [[ ( -n "\$__compare_pattern" || -n "\$__norm_prefix" ) && \${#__norm_prefix} -ge \${#__compare_pattern} && "\${__norm_prefix[1,\${#__compare_pattern}]}" == "\$__compare_pattern" && "\$__logical_prefix" != "\$pattern" ]]; then
|
|
990
|
+
# Keep the typed prefix, but retain a fixed file glob separately.
|
|
991
|
+
if [[ "\$type" != "directory" && "\$__norm_pattern" == *[\*\?]* ]]; then
|
|
992
|
+
__fixed_glob=1
|
|
993
|
+
local __fixed_was_glob_dots=0
|
|
994
|
+
[[ -o glob_dots ]] && __fixed_was_glob_dots=1
|
|
995
|
+
if [[ "\$PREFIX" == .* || "\$PREFIX" == */.* ]]; then setopt glob_dots; fi
|
|
996
|
+
# Keep qualifiers in a string so sourcing also works with SH_GLOB.
|
|
997
|
+
local __fixed_pattern="" __glob_char="" __glob_previous=""
|
|
998
|
+
local __glob_escaped=0
|
|
999
|
+
local __glob_class=0 __glob_class_start=0 __glob_class_nested=""
|
|
1000
|
+
local -i __glob_index
|
|
1001
|
+
for (( __glob_index=1; __glob_index <= \${#pattern}; __glob_index++ )); do
|
|
1002
|
+
__glob_char="\${pattern[__glob_index]}"
|
|
1003
|
+
if [[ "\$__glob_class" == "1" ]]; then
|
|
1004
|
+
# Class members are already literal. Preserve leading ]
|
|
1005
|
+
# and nested POSIX classes without adding backslashes.
|
|
1006
|
+
__fixed_pattern+="\$__glob_char"
|
|
1007
|
+
if [[ -n "\$__glob_class_nested" ]]; then
|
|
1008
|
+
if [[ "\$__glob_char" == "]" && "\$__glob_previous" == "\$__glob_class_nested" ]]; then
|
|
1009
|
+
__glob_class_nested=""
|
|
1010
|
+
fi
|
|
1011
|
+
elif [[ "\$__glob_char" == "[" && "\${pattern[__glob_index+1]}" == ":" ]]; then
|
|
1012
|
+
__glob_class_nested="\${pattern[__glob_index+1]}"
|
|
1013
|
+
__glob_class_start=0
|
|
1014
|
+
elif [[ "\$__glob_char" == "]" && "\$__glob_class_start" == "0" ]]; then
|
|
1015
|
+
__glob_class=0
|
|
1016
|
+
elif [[ "\$__glob_class_start" == "1" && ( "\$__glob_char" == "!" || "\$__glob_char" == "^" ) &&
|
|
1017
|
+
( "\${pattern[__glob_index+1]}" != "]" || "\${pattern[__glob_index+2,-1]}" == *']'* ) ]]; then
|
|
1018
|
+
__glob_class_start=2
|
|
1019
|
+
else
|
|
1020
|
+
__glob_class_start=0
|
|
1021
|
+
fi
|
|
1022
|
+
__glob_previous="\$__glob_char"
|
|
1023
|
+
continue
|
|
1024
|
+
fi
|
|
1025
|
+
if [[ "\$__glob_escaped" == "1" ]]; then
|
|
1026
|
+
__fixed_pattern+="\$__glob_char"
|
|
1027
|
+
__glob_escaped=0
|
|
1028
|
+
__glob_previous=""
|
|
1029
|
+
continue
|
|
1030
|
+
fi
|
|
1031
|
+
if [[ "\$__glob_char" == "\\\\" ]]; then
|
|
1032
|
+
__fixed_pattern+="\$__glob_char"
|
|
1033
|
+
__glob_escaped=1
|
|
1034
|
+
elif [[ "\$__glob_char" == "[" ]]; then
|
|
1035
|
+
__fixed_pattern+="\$__glob_char"
|
|
1036
|
+
__glob_class=1
|
|
1037
|
+
__glob_class_start=1
|
|
1038
|
+
elif [[ "\$__glob_char" == [#^~\\(\\)\\|\\<\\>] &&
|
|
1039
|
+
! ( "\$__glob_char" == "~" && "\$__glob_index" == "1" ) ]]; then
|
|
1040
|
+
# Keep class negation and leading home expansion; escape
|
|
1041
|
+
# extended-glob operators in literal pathname components.
|
|
1042
|
+
__fixed_pattern+="\\\\\$__glob_char"
|
|
1043
|
+
else
|
|
1044
|
+
__fixed_pattern+="\$__glob_char"
|
|
1045
|
+
fi
|
|
1046
|
+
__glob_previous="\$__glob_char"
|
|
1047
|
+
done
|
|
1048
|
+
__fixed_pattern+="(#qN)"
|
|
1049
|
+
__fixed_matches=( \${~__fixed_pattern} )
|
|
1050
|
+
__fixed_matches=( "\${(@)__fixed_matches:a}" )
|
|
1051
|
+
# Hidden parents widen fixed matches, not native child suggestions.
|
|
1052
|
+
if [[ "\$__fixed_was_glob_dots" == "0" && "\${PREFIX##*/}" != .* ]]; then unsetopt glob_dots; fi
|
|
1053
|
+
fi
|
|
425
1054
|
else
|
|
426
|
-
|
|
1055
|
+
local __native_pattern="\$pattern"
|
|
1056
|
+
# Initial globs use the conservative native filters above.
|
|
1057
|
+
# Retained input and protected markers still need pattern matching.
|
|
1058
|
+
if [[ -n "\$compstate[context]" && "\$pattern" == *[\\*\\?\\[]* &&
|
|
1059
|
+
( -n "\$current" || "\$pattern" == *'$'* || "\$pattern" == *'\`'* ) ]]; then
|
|
1060
|
+
compstate[pattern_match]='*'
|
|
1061
|
+
__pattern_match_changed=1
|
|
1062
|
+
fi
|
|
1063
|
+
# Native helpers expand substitutions, tilde names and executable
|
|
1064
|
+
# glob qualifiers. Input-derived patterns must keep these literal.
|
|
1065
|
+
# Ordinary fixed glob/tilde patterns retain their native behavior.
|
|
1066
|
+
local __tilde_name="\${pattern#\\~}"
|
|
1067
|
+
__tilde_name="\${__tilde_name%%/*}"
|
|
1068
|
+
local -a match mbegin mend
|
|
1069
|
+
local __code_qualifier=0
|
|
1070
|
+
local __fixed_qualifier_base=""
|
|
1071
|
+
local __safe_qualifiers='([-/F.@=p*rwxAIERWXsStUG^MTNDn,]|[ugdl]<->|[amc]([Mwhmsd]|)([-+]|)<->|L([kKmMgGtTpP]|)([-+]|)<->|[oO][nLlamcdN])##'
|
|
1072
|
+
local __qualifier_pattern="\$pattern" __qualifier
|
|
1073
|
+
while _have_glob_qual "\$__qualifier_pattern" complete; do
|
|
1074
|
+
__qualifier="\${match[5]#\\#q}"
|
|
1075
|
+
__qualifier_pattern="\$match[2]"
|
|
1076
|
+
# A numeric sense (+1) is not a function qualifier (+name).
|
|
1077
|
+
# Accept only bounded attribute/sort syntax in this exception.
|
|
1078
|
+
# Expand the pattern at runtime after unsetting SH_GLOB.
|
|
1079
|
+
if [[ "\$__qualifier" == \${~__safe_qualifiers} ]]; then
|
|
1080
|
+
__fixed_qualifier_base="\$__qualifier_pattern"
|
|
1081
|
+
elif [[ "\$__qualifier" == *[e+]* ]]; then
|
|
1082
|
+
__code_qualifier=1
|
|
1083
|
+
fi
|
|
1084
|
+
done
|
|
1085
|
+
# Partial overlaps with complete backend classes/groups remain
|
|
1086
|
+
# syntax. A complete input-provided expression can be literal.
|
|
1087
|
+
local -a __syntax_starts=() __syntax_ends=() __group_stack=()
|
|
1088
|
+
local -i __scan_index __class_begin=0 __member_begin=0
|
|
1089
|
+
local __scan_char __nested_class=""
|
|
1090
|
+
for (( __scan_index=1; __scan_index <= \${#pattern}; __scan_index++ )); do
|
|
1091
|
+
__scan_char="\${pattern[__scan_index]}"
|
|
1092
|
+
if [[ "\$__scan_char" == '\\' ]]; then
|
|
1093
|
+
(( __scan_index++ ))
|
|
1094
|
+
elif (( __class_begin )); then
|
|
1095
|
+
if [[ "\$__scan_char" == '[' && "\${pattern[__scan_index+1]}" == [:.=] ]]; then
|
|
1096
|
+
__nested_class="\${pattern[__scan_index+1]}"
|
|
1097
|
+
elif [[ -n "\$__nested_class" && "\$__scan_char" == "\$__nested_class" && "\${pattern[__scan_index+1]}" == ']' ]]; then
|
|
1098
|
+
(( __scan_index++ ))
|
|
1099
|
+
__nested_class=""
|
|
1100
|
+
elif [[ -z "\$__nested_class" && "\$__scan_char" == ']' ]] && (( __scan_index > __member_begin )); then
|
|
1101
|
+
__syntax_starts+=("\$__class_begin")
|
|
1102
|
+
__syntax_ends+=("\$__scan_index")
|
|
1103
|
+
__class_begin=0
|
|
1104
|
+
elif (( __scan_index == __member_begin )) &&
|
|
1105
|
+
[[ "\$__scan_char" == '!' || "\$__scan_char" == '^' ]] &&
|
|
1106
|
+
[[ "\${pattern[__scan_index+1]}" != ']' || "\${pattern[__scan_index+2,-1]}" == *']'* ]]; then
|
|
1107
|
+
(( __member_begin++ ))
|
|
1108
|
+
fi
|
|
1109
|
+
elif [[ "\$__scan_char" == '[' ]]; then
|
|
1110
|
+
__class_begin=\$__scan_index
|
|
1111
|
+
__member_begin=\$(( __scan_index + 1 ))
|
|
1112
|
+
elif [[ "\$__scan_char" == '(' ]]; then
|
|
1113
|
+
__group_stack+=("\$__scan_index")
|
|
1114
|
+
elif [[ "\$__scan_char" == ')' ]] && (( \${#__group_stack} )); then
|
|
1115
|
+
__syntax_starts+=("\${__group_stack[-1]}")
|
|
1116
|
+
__syntax_ends+=("\$__scan_index")
|
|
1117
|
+
__group_stack[-1]=()
|
|
1118
|
+
fi
|
|
1119
|
+
done
|
|
1120
|
+
# Search retained input only where it cannot split fixed syntax.
|
|
1121
|
+
local __input_part="\$current"
|
|
1122
|
+
local __backend_prefix="\$pattern" __backend_suffix=""
|
|
1123
|
+
local __search_pattern __candidate_prefix
|
|
1124
|
+
local -i __search_offset __overlap_start __overlap_end __range_index __syntax_overlap
|
|
1125
|
+
local -i __fixed_tilde_end=0
|
|
1126
|
+
local __tilde_expression='([+-]|[+-]<->|[[:alnum:]_.-]#)'
|
|
1127
|
+
if [[ "\$pattern" == '~'* &&
|
|
1128
|
+
"\$__tilde_name" == \${~__tilde_expression} ]]; then
|
|
1129
|
+
__fixed_tilde_end=\$(( \${#__tilde_name} + 1 ))
|
|
1130
|
+
fi
|
|
1131
|
+
while [[ -n "\$__input_part" ]]; do
|
|
1132
|
+
__search_pattern="\$pattern"
|
|
1133
|
+
__search_offset=0
|
|
1134
|
+
while [[ "\$__search_pattern" == *"\$__input_part"* ]]; do
|
|
1135
|
+
__candidate_prefix="\${__search_pattern%%"\$__input_part"*}"
|
|
1136
|
+
__overlap_start=\$(( __search_offset + \${#__candidate_prefix} + 1 ))
|
|
1137
|
+
__overlap_end=\$(( __overlap_start + \${#__input_part} - 1 ))
|
|
1138
|
+
__syntax_overlap=0
|
|
1139
|
+
# Leading home/directory-stack/named-directory syntax belongs
|
|
1140
|
+
# to the fixed pattern, even when the input overlaps it.
|
|
1141
|
+
if (( __fixed_tilde_end && __overlap_start <= __fixed_tilde_end )); then
|
|
1142
|
+
__syntax_overlap=1
|
|
1143
|
+
fi
|
|
1144
|
+
for (( __range_index=1; __range_index <= \${#__syntax_starts}; __range_index++ )); do
|
|
1145
|
+
if (( __overlap_start <= __syntax_ends[__range_index] &&
|
|
1146
|
+
__overlap_end >= __syntax_starts[__range_index] &&
|
|
1147
|
+
( __overlap_start > __syntax_starts[__range_index] ||
|
|
1148
|
+
__overlap_end < __syntax_ends[__range_index] ) )); then
|
|
1149
|
+
__syntax_overlap=1
|
|
1150
|
+
break
|
|
1151
|
+
fi
|
|
1152
|
+
done
|
|
1153
|
+
# Every position in a recognized fixed attribute qualifier
|
|
1154
|
+
# is backend syntax, including its closing parenthesis.
|
|
1155
|
+
if [[ "\$__fixed_qualifier_base" == *[\\*\\?\\]] ]] &&
|
|
1156
|
+
(( __overlap_end > \${#__fixed_qualifier_base} )); then
|
|
1157
|
+
__syntax_overlap=1
|
|
1158
|
+
fi
|
|
1159
|
+
if (( ! __syntax_overlap )); then
|
|
1160
|
+
if (( __overlap_start == 1 )); then
|
|
1161
|
+
__backend_prefix=""
|
|
1162
|
+
else
|
|
1163
|
+
__backend_prefix="\${pattern[1,__overlap_start-1]}"
|
|
1164
|
+
fi
|
|
1165
|
+
__backend_suffix="\${pattern[__overlap_end+1,-1]}"
|
|
1166
|
+
break 2
|
|
1167
|
+
fi
|
|
1168
|
+
__search_offset=\$__overlap_start
|
|
1169
|
+
__search_pattern="\${pattern[__overlap_start+1,-1]}"
|
|
1170
|
+
done
|
|
1171
|
+
__input_part="\${__input_part[2,-1]}"
|
|
1172
|
+
done
|
|
1173
|
+
local __quoted_input="\${(q)__input_part}"
|
|
1174
|
+
# Pattern matching removes quoting differently from normal
|
|
1175
|
+
# completion. Character classes keep these characters literal.
|
|
1176
|
+
if [[ -n "\$compstate[pattern_match]" ]]; then
|
|
1177
|
+
__quoted_input="\${(b)__input_part}"
|
|
1178
|
+
__quoted_input="\${__quoted_input//\\\$/[\\\$]}"
|
|
1179
|
+
__quoted_input="\${__quoted_input//\\\`/[\\\`]}"
|
|
1180
|
+
fi
|
|
1181
|
+
if [[ "\$pattern" == *'$'* || "\$pattern" == *'\`'* ||
|
|
1182
|
+
( "\$pattern" == '~'* && "\$__tilde_name" == *[^[:alnum:]_.+-]* ) ]] ||
|
|
1183
|
+
(( __code_qualifier )); then
|
|
1184
|
+
__native_pattern="\${(q)pattern}"
|
|
1185
|
+
local __backend_pattern="\$__backend_prefix\$__backend_suffix"
|
|
1186
|
+
local __backend_tilde="\${__backend_pattern#\\~}"
|
|
1187
|
+
__backend_tilde="\${__backend_tilde%%/*}"
|
|
1188
|
+
local __unsafe_backend_qualifier=0
|
|
1189
|
+
__qualifier_pattern="\$__backend_pattern"
|
|
1190
|
+
while _have_glob_qual "\$__qualifier_pattern" complete; do
|
|
1191
|
+
__qualifier="\${match[5]#\\#q}"
|
|
1192
|
+
__qualifier_pattern="\$match[2]"
|
|
1193
|
+
if [[ "\$__qualifier" != \${~__safe_qualifiers} ]]; then
|
|
1194
|
+
__unsafe_backend_qualifier=1
|
|
1195
|
+
fi
|
|
1196
|
+
done
|
|
1197
|
+
# Retain trusted wildcards and safe attribute qualifiers when
|
|
1198
|
+
# all executable syntax belongs to the quoted input.
|
|
1199
|
+
if [[ -n "\$__input_part" &&
|
|
1200
|
+
"\$__backend_pattern" != *'$'* &&
|
|
1201
|
+
"\$__backend_pattern" != *'\`'* &&
|
|
1202
|
+
( "\$__backend_pattern" != '~'* ||
|
|
1203
|
+
"\$__backend_tilde" != *[^[:alnum:]_.+-]* ) ]] &&
|
|
1204
|
+
(( ! __unsafe_backend_qualifier )); then
|
|
1205
|
+
__native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
|
|
1206
|
+
elif [[ "\$__backend_pattern" != *'$('* &&
|
|
1207
|
+
"\$__backend_pattern" != *'\${'* &&
|
|
1208
|
+
"\$__backend_pattern" != *'$['* &&
|
|
1209
|
+
( "\$pattern" != '~'* ||
|
|
1210
|
+
"\$__tilde_name" != *[^[:alnum:]_.+-]* ) ]] &&
|
|
1211
|
+
(( ! __unsafe_backend_qualifier )); then
|
|
1212
|
+
# Fixed patterns also need literal substitution markers,
|
|
1213
|
+
# without quoting their wildcards or safe qualifiers.
|
|
1214
|
+
local __dollar='\\\$'
|
|
1215
|
+
local __backtick='\\\`'
|
|
1216
|
+
if [[ -n "\$compstate[pattern_match]" ]]; then
|
|
1217
|
+
__dollar='[\$]'
|
|
1218
|
+
__backtick='[\`]'
|
|
1219
|
+
fi
|
|
1220
|
+
# Existing glob classes already protect their markers.
|
|
1221
|
+
local -a __backend_parts=()
|
|
1222
|
+
local __part __escaped __char __class_kind
|
|
1223
|
+
local -i __index __in_class __class_start
|
|
1224
|
+
for __part in "\$__backend_prefix" "\$__backend_suffix"; do
|
|
1225
|
+
__escaped=""
|
|
1226
|
+
__in_class=0
|
|
1227
|
+
__class_kind=""
|
|
1228
|
+
for (( __index=1; __index <= \${#__part}; __index++ )); do
|
|
1229
|
+
__char="\${__part[__index]}"
|
|
1230
|
+
if [[ "\$__char" == '\\' ]]; then
|
|
1231
|
+
__escaped+="\$__char\${__part[__index+1]}"
|
|
1232
|
+
(( __index++ ))
|
|
1233
|
+
continue
|
|
1234
|
+
elif (( __in_class )); then
|
|
1235
|
+
if [[ "\$__char" == '[' && "\${__part[__index+1]}" == [:.=] ]]; then
|
|
1236
|
+
__class_kind="\${__part[__index+1]}"
|
|
1237
|
+
elif [[ -n "\$__class_kind" && "\$__char" == "\$__class_kind" && "\${__part[__index+1]}" == ']' ]]; then
|
|
1238
|
+
__escaped+="\$__char]"
|
|
1239
|
+
(( __index++ ))
|
|
1240
|
+
__class_kind=""
|
|
1241
|
+
continue
|
|
1242
|
+
elif [[ -z "\$__class_kind" && "\$__char" == ']' ]] && (( __index > __class_start )); then
|
|
1243
|
+
__in_class=0
|
|
1244
|
+
elif (( __index == __class_start )) && [[ "\$__char" == '!' || "\$__char" == '^' ]]; then
|
|
1245
|
+
(( __class_start++ ))
|
|
1246
|
+
fi
|
|
1247
|
+
elif [[ "\$__char" == '[' ]]; then
|
|
1248
|
+
__in_class=1
|
|
1249
|
+
__class_start=$(( __index + 1 ))
|
|
1250
|
+
elif [[ "\$__char" == '$' ]]; then
|
|
1251
|
+
__char="\$__dollar"
|
|
1252
|
+
elif [[ "\$__char" == '\`' ]]; then
|
|
1253
|
+
__char="\$__backtick"
|
|
1254
|
+
fi
|
|
1255
|
+
__escaped+="\$__char"
|
|
1256
|
+
done
|
|
1257
|
+
__backend_parts+=("\$__escaped")
|
|
1258
|
+
done
|
|
1259
|
+
__backend_prefix="\${__backend_parts[1]}"
|
|
1260
|
+
__backend_suffix="\${__backend_parts[2]}"
|
|
1261
|
+
__native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
|
|
1262
|
+
fi
|
|
1263
|
+
else
|
|
1264
|
+
# Quote retained metacharacters, including segments made only
|
|
1265
|
+
# of them. Wildcard-only overlap remains backend syntax.
|
|
1266
|
+
if [[ "\$__input_part" == *[\\\\\\(\\)\\[\\#\\^\\|\\~\\<\\>\\*\\?]* &&
|
|
1267
|
+
"\$__input_part" != [\\*\\?]## ]]; then
|
|
1268
|
+
__native_pattern="\$__backend_prefix\$__quoted_input\$__backend_suffix"
|
|
1269
|
+
fi
|
|
1270
|
+
fi
|
|
1271
|
+
PREFIX="\$__native_pattern"
|
|
1272
|
+
# Match simple fixed tilde globs through native -g filters. The
|
|
1273
|
+
# pattern matcher otherwise treats the leading ~ as glob syntax,
|
|
1274
|
+
# even after _path_files has expanded it for filesystem lookup.
|
|
1275
|
+
if (( __fixed_tilde_end )) && [[ -z "\$__input_part" &&
|
|
1276
|
+
"\$__native_pattern" == "\$pattern" && "\$pattern" == */* ]]; then
|
|
1277
|
+
__initial_directory="\${pattern%/*}/"
|
|
1278
|
+
__initial_leaf="\${pattern##*/}"
|
|
1279
|
+
if [[ "\$__initial_leaf" == *[\*\?]* &&
|
|
1280
|
+
"\$__initial_directory" != *[\*\?]* &&
|
|
1281
|
+
"\$__initial_directory" != *["[]()"]* &&
|
|
1282
|
+
"\$__initial_leaf" != *["[]()#~^|<>;&"]* &&
|
|
1283
|
+
"\$__initial_leaf" != *'\\'* &&
|
|
1284
|
+
"\$__initial_leaf" != *[[:space:]]* &&
|
|
1285
|
+
"\$__initial_leaf" != *"'"* && "\$__initial_leaf" != *'"'* ]]; then
|
|
1286
|
+
__initial_file_pattern="\$__initial_leaf"
|
|
1287
|
+
PREFIX="\$__initial_directory"
|
|
1288
|
+
compstate[pattern_match]=""
|
|
1289
|
+
__pattern_match_changed=1
|
|
1290
|
+
fi
|
|
1291
|
+
fi
|
|
427
1292
|
fi
|
|
428
1293
|
fi
|
|
429
1294
|
|
|
@@ -435,15 +1300,114 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
435
1300
|
ext_pattern="*.\$extensions"
|
|
436
1301
|
fi
|
|
437
1302
|
fi
|
|
1303
|
+
# _wanted/_next_label write to the named explanation parameter.
|
|
1304
|
+
# Keep it local for every file directive, including directory/any.
|
|
1305
|
+
local -a expl=()
|
|
438
1306
|
# Use zsh's native file completion
|
|
439
1307
|
case "\$type" in
|
|
440
|
-
file)
|
|
441
|
-
if [[ -n "\$
|
|
442
|
-
#
|
|
443
|
-
#
|
|
444
|
-
#
|
|
445
|
-
|
|
446
|
-
|
|
1308
|
+
file|any)
|
|
1309
|
+
if [[ "\$type" == "file" || -n "\$__initial_file_pattern" ]]; then
|
|
1310
|
+
# Regular-file suggestions retain symlinked files; initial
|
|
1311
|
+
# any globs use a non-directory qualifier instead.
|
|
1312
|
+
local file_pattern="\${ext_pattern:-*}(#q-.)"
|
|
1313
|
+
if [[ "\$__fixed_glob" == "1" ]]; then
|
|
1314
|
+
file_pattern="\${ext_pattern:-*}(#q-.e:${fixedGlobFunction}:)"
|
|
1315
|
+
fi
|
|
1316
|
+
if [[ -n "\$__initial_file_pattern" ]]; then
|
|
1317
|
+
local __file_qualifier="-."
|
|
1318
|
+
[[ "\$type" == "any" ]] && __file_qualifier="^-/"
|
|
1319
|
+
local -a __initial_file_patterns=()
|
|
1320
|
+
if [[ -n "\$extensions" ]]; then
|
|
1321
|
+
setopt extended_glob
|
|
1322
|
+
local __initial_extension
|
|
1323
|
+
for __initial_extension in "\${(@s:,:)extensions}"; do
|
|
1324
|
+
__initial_file_patterns+=("(\${__initial_file_pattern}~^*.\${__initial_extension})(#q\${__file_qualifier})")
|
|
1325
|
+
done
|
|
1326
|
+
else
|
|
1327
|
+
__initial_file_patterns=("\${__initial_file_pattern}(#q\${__file_qualifier})")
|
|
1328
|
+
fi
|
|
1329
|
+
file_pattern="\${(j: :)__initial_file_patterns}"
|
|
1330
|
+
fi
|
|
1331
|
+
local file_pattern_prefix="\$PREFIX"
|
|
1332
|
+
local -a match mbegin mend
|
|
1333
|
+
# Native qualifier copying concatenates flags with the explicit
|
|
1334
|
+
# regular-file group. Keep backend attributes in their own group.
|
|
1335
|
+
if [[ -n "\$compstate[pattern_match]" && -z "\$SUFFIX" ]]; then
|
|
1336
|
+
local file_qualifier file_qualifiers=""
|
|
1337
|
+
while _have_glob_qual "\$file_pattern_prefix" complete; do
|
|
1338
|
+
file_qualifier="\${match[5]#\\#q}"
|
|
1339
|
+
file_pattern_prefix="\$match[2]"
|
|
1340
|
+
file_qualifiers="(#q\$file_qualifier)\$file_qualifiers"
|
|
1341
|
+
done
|
|
1342
|
+
if [[ -n "\$file_qualifiers" ]]; then
|
|
1343
|
+
file_pattern+="\$file_qualifiers"
|
|
1344
|
+
fi
|
|
1345
|
+
fi
|
|
1346
|
+
local file_subtree_pattern="**/*(#q/N)"
|
|
1347
|
+
local -a file_subtree=() file_recursive_patterns=()
|
|
1348
|
+
local -a file_tags=(files directories)
|
|
1349
|
+
local file_recursive_pattern file_directory file_prefix file_tag
|
|
1350
|
+
if [[ -n "\$__initial_file_pattern" || -z "\$ext_pattern" ]] &&
|
|
1351
|
+
zstyle -t ":completion:\${curcontext}:" list-dirs-first; then
|
|
1352
|
+
file_tags=(directories files)
|
|
1353
|
+
fi
|
|
1354
|
+
_tags "\${file_tags[@]}"
|
|
1355
|
+
while _tags; do
|
|
1356
|
+
local file_matched=0
|
|
1357
|
+
for file_tag in "\${file_tags[@]}"; do
|
|
1358
|
+
if [[ "\$file_tag" == directories ]]; then
|
|
1359
|
+
local file_tag_prefix="\$PREFIX"
|
|
1360
|
+
PREFIX="\$file_pattern_prefix"
|
|
1361
|
+
while _next_label directories expl directory; do
|
|
1362
|
+
# File qualifiers constrain final results, not the
|
|
1363
|
+
# directories needed to navigate to those results.
|
|
1364
|
+
_path_files -/ "\${expl[@]}" && file_matched=1
|
|
1365
|
+
done
|
|
1366
|
+
PREFIX="\$file_tag_prefix"
|
|
1367
|
+
else
|
|
1368
|
+
local file_tag_prefix="\$PREFIX"
|
|
1369
|
+
PREFIX="\$file_pattern_prefix"
|
|
1370
|
+
while _next_label files expl file; do
|
|
1371
|
+
local -a file_ignore=() file_ignore_args=()
|
|
1372
|
+
# Explicit globs bypass automatic fignore. Apply it only
|
|
1373
|
+
# when the tag has not supplied its own ignore patterns.
|
|
1374
|
+
if [[ -z "\$__initial_file_pattern" && -z "\$ext_pattern" && ( "\$__fixed_glob" != "1" || "\$pattern" == "*" ) && -z "\$_comp_no_ignore" && -n "\$FIGNORE" ]] && (( \${#_comp_ignore[@]} == 0 )); then
|
|
1375
|
+
file_ignore=("?*\${^fignore[@]}")
|
|
1376
|
+
file_ignore_args=(-F file_ignore)
|
|
1377
|
+
fi
|
|
1378
|
+
if _path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}"; then
|
|
1379
|
+
file_matched=1
|
|
1380
|
+
elif [[ ( -n "\$__initial_file_pattern" || -z "\$ext_pattern" ) && "\$PREFIX\$SUFFIX" != */* ]] &&
|
|
1381
|
+
zstyle -a ":completion:\${curcontext}:files" recursive-files file_recursive_patterns; then
|
|
1382
|
+
# Match _files' recursive fallback, retaining the same
|
|
1383
|
+
# file filter and tag-specific ignore options.
|
|
1384
|
+
for file_recursive_pattern in "\${file_recursive_patterns[@]}"; do
|
|
1385
|
+
if [[ "\$PWD/" = \${~file_recursive_pattern} ]]; then
|
|
1386
|
+
if (( \${#file_subtree[@]} == 0 )); then
|
|
1387
|
+
file_subtree=(\${~file_subtree_pattern})
|
|
1388
|
+
fi
|
|
1389
|
+
for file_directory in "\${file_subtree[@]}"; do
|
|
1390
|
+
file_prefix="\$PREFIX"
|
|
1391
|
+
PREFIX="\$file_directory/\$PREFIX"
|
|
1392
|
+
_path_files -g "\${file_pattern}" "\${expl[@]}" "\${file_ignore_args[@]}" && file_matched=1
|
|
1393
|
+
PREFIX="\$file_prefix"
|
|
1394
|
+
done
|
|
1395
|
+
break
|
|
1396
|
+
fi
|
|
1397
|
+
done
|
|
1398
|
+
fi
|
|
1399
|
+
done
|
|
1400
|
+
PREFIX="\$file_tag_prefix"
|
|
1401
|
+
fi
|
|
1402
|
+
done
|
|
1403
|
+
(( file_matched )) && break
|
|
1404
|
+
done
|
|
1405
|
+
elif [[ -n "\$ext_pattern" || "\$__fixed_glob" == "1" ]]; then
|
|
1406
|
+
local file_pattern="\${ext_pattern}(#q^-/)"
|
|
1407
|
+
if [[ "\$__fixed_glob" == "1" ]]; then
|
|
1408
|
+
# Put the predicate before ^, which negates later qualifiers.
|
|
1409
|
+
file_pattern="\${ext_pattern:-*}(#qe:${fixedGlobFunction}:^-/)"
|
|
1410
|
+
fi
|
|
447
1411
|
_wanted files expl file _path_files -g "\${file_pattern}"
|
|
448
1412
|
_wanted directories expl directory _path_files -/
|
|
449
1413
|
else
|
|
@@ -451,23 +1415,25 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
451
1415
|
fi
|
|
452
1416
|
;;
|
|
453
1417
|
directory)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if [[ -n "\$ext_pattern" ]]; then
|
|
458
|
-
local file_pattern="\${ext_pattern}(#q^-/)"
|
|
459
|
-
_wanted files expl file _path_files -g "\${file_pattern}"
|
|
460
|
-
_wanted directories expl directory _path_files -/
|
|
1418
|
+
if [[ -n "\$__initial_file_pattern" ]]; then
|
|
1419
|
+
# -/ would add every directory alongside the filtered glob.
|
|
1420
|
+
_wanted directories expl directory _path_files -g "\${__initial_file_pattern}(#q-/)"
|
|
461
1421
|
else
|
|
462
|
-
|
|
1422
|
+
_directories
|
|
463
1423
|
fi
|
|
464
1424
|
;;
|
|
465
1425
|
esac
|
|
466
|
-
|
|
467
1426
|
# Restore PREFIX and glob_dots to their previous state
|
|
468
1427
|
PREFIX="\$__saved_prefix"
|
|
469
1428
|
IPREFIX="\$__saved_iprefix"
|
|
1429
|
+
current="\$__saved_current"
|
|
1430
|
+
__logical_prefix="\$__saved_logical_prefix"
|
|
1431
|
+
if [[ "\$__was_extended_glob" == "1" ]]; then setopt extended_glob; else unsetopt extended_glob; fi
|
|
1432
|
+
if (( __pattern_match_changed )); then
|
|
1433
|
+
compstate[pattern_match]="\$__saved_pattern_match"
|
|
1434
|
+
fi
|
|
470
1435
|
if [[ "\$__was_sh_glob" == "1" ]]; then setopt sh_glob; else unsetopt sh_glob; fi
|
|
1436
|
+
if [[ "\$__was_sh_word_split" == "1" ]]; then setopt sh_word_split; else unsetopt sh_word_split; fi
|
|
471
1437
|
if [[ "\$__was_glob_dots" == "1" ]]; then setopt glob_dots; else unsetopt glob_dots; fi
|
|
472
1438
|
else
|
|
473
1439
|
# Regular literal completion
|
|
@@ -484,26 +1450,46 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
484
1450
|
# Prepare completion with descriptions for _describe
|
|
485
1451
|
local -a matches
|
|
486
1452
|
local -i i
|
|
1453
|
+
local __match
|
|
487
1454
|
for (( i=1; i <= \${#completions[@]}; i++ )); do
|
|
1455
|
+
# _describe uses backslashes to escape its colon separator.
|
|
1456
|
+
__match="\${completions[i]//\\\\/\\\\\\\\}"
|
|
1457
|
+
__match="\${__match//:/\\\\:}"
|
|
488
1458
|
if [[ -n "\${descriptions[i]}" ]]; then
|
|
489
|
-
matches+=("\$
|
|
1459
|
+
matches+=("\$__match:\${descriptions[i]}")
|
|
490
1460
|
else
|
|
491
|
-
matches+=("\$
|
|
1461
|
+
matches+=("\$__match")
|
|
492
1462
|
fi
|
|
493
1463
|
done
|
|
494
|
-
|
|
1464
|
+
if [[ -n "\$QISUFFIX" ]]; then
|
|
1465
|
+
# A closing quote already exists; do not append a space inside it.
|
|
1466
|
+
_describe 'commands' matches -S ''
|
|
1467
|
+
else
|
|
1468
|
+
_describe 'commands' matches
|
|
1469
|
+
fi
|
|
495
1470
|
fi
|
|
496
1471
|
}
|
|
497
1472
|
|
|
1473
|
+
function ${fixedGlobFunction} () {
|
|
1474
|
+
local __fixed_candidate="\${REPLY:a}"
|
|
1475
|
+
(( \${__fixed_matches[(Ie)\$__fixed_candidate]} ))
|
|
1476
|
+
}
|
|
1477
|
+
|
|
498
1478
|
if (( $+functions[compdef] )); then
|
|
499
|
-
compdef
|
|
1479
|
+
compdef ${functionName} ${programName}
|
|
1480
|
+
fi
|
|
1481
|
+
|
|
1482
|
+
# An autoloaded file runs as a function body. Dispatch its initial request;
|
|
1483
|
+
# sourcing the file only defines and registers the completion helper.
|
|
1484
|
+
if [[ \$ZSH_EVAL_CONTEXT == *:loadautofunc || \$ZSH_EVAL_CONTEXT == *:shfunc ]]; then
|
|
1485
|
+
${functionName} "\$@"
|
|
500
1486
|
fi
|
|
501
1487
|
`;
|
|
502
1488
|
},
|
|
503
1489
|
*encodeSuggestions(suggestions) {
|
|
504
1490
|
for (const suggestion of suggestions) if (suggestion.kind === "literal") {
|
|
505
1491
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
506
|
-
yield `${
|
|
1492
|
+
yield `${encodeLiteral(suggestion.text)}\0${description}\0`;
|
|
507
1493
|
} else {
|
|
508
1494
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
509
1495
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -522,12 +1508,35 @@ const fish = {
|
|
|
522
1508
|
generateScript(programName, args = []) {
|
|
523
1509
|
validateProgramName(programName);
|
|
524
1510
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "\\'")}'`).join(" ");
|
|
525
|
-
const functionName = `__${programName
|
|
1511
|
+
const functionName = `__${programName}_complete`;
|
|
526
1512
|
return `
|
|
527
1513
|
function ${functionName}
|
|
528
1514
|
set -l tokens (commandline -poc)
|
|
529
1515
|
set -l current (commandline -ct)
|
|
530
1516
|
|
|
1517
|
+
# commandline -poc decodes previous tokens, but commandline -ct returns
|
|
1518
|
+
# the current token as typed. Decode its quotes and escapes as well.
|
|
1519
|
+
# Keep the token as typed if decoding fails or yields a newline or tab,
|
|
1520
|
+
# which completion output cannot carry.
|
|
1521
|
+
set -l __raw_current "$current"
|
|
1522
|
+
set -l __decoded (string unescape -- "$current")
|
|
1523
|
+
or begin
|
|
1524
|
+
# string unescape rejects an unfinished trailing backslash. fish
|
|
1525
|
+
# keeps it literally inside single quotes, which also keep a " as
|
|
1526
|
+
# is, and otherwise waits for the character it escapes.
|
|
1527
|
+
set -l __prefix (string replace -r '\\\\\\\\$' '' -- "$current")
|
|
1528
|
+
set -l __probe (string unescape -- "$__prefix\\"")
|
|
1529
|
+
set -l __plain (string unescape -- "$__prefix")
|
|
1530
|
+
if test "$__probe" = "$__plain\\""
|
|
1531
|
+
set __decoded (string unescape -- "$current\\\\")
|
|
1532
|
+
else
|
|
1533
|
+
set __decoded (string unescape -- "$__prefix")
|
|
1534
|
+
end
|
|
1535
|
+
end
|
|
1536
|
+
and test (count $__decoded) -eq 1
|
|
1537
|
+
and not string match -qr '\\t' -- "$__decoded"
|
|
1538
|
+
and set current "$__decoded"
|
|
1539
|
+
|
|
531
1540
|
# Extract previous arguments (skip the command name)
|
|
532
1541
|
set -l prev
|
|
533
1542
|
set -l count (count $tokens)
|
|
@@ -539,7 +1548,12 @@ function ${functionName}
|
|
|
539
1548
|
${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
|
|
540
1549
|
# Process each line of output
|
|
541
1550
|
for line in $output
|
|
542
|
-
|
|
1551
|
+
set -l literal 0
|
|
1552
|
+
if string match -q '__LITERAL__:*' -- $line
|
|
1553
|
+
set line (string sub -s 13 -- $line)
|
|
1554
|
+
set literal 1
|
|
1555
|
+
end
|
|
1556
|
+
if test $literal -eq 0; and string match -q '__FILE__:*' -- $line
|
|
543
1557
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
544
1558
|
set -l directive (string split \\t -- $line)[1]
|
|
545
1559
|
set -l parts (string split ':' -- $directive)
|
|
@@ -552,6 +1566,7 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
552
1566
|
set insertion_prefix (string replace -a '%25' '%' -- (string replace -a '%3A' ':' -- $parts[6]))
|
|
553
1567
|
end
|
|
554
1568
|
set -l saved_current "$current"
|
|
1569
|
+
set -l saved_raw_current "$__raw_current"
|
|
555
1570
|
set -l prefix_length (string length -- "$insertion_prefix")
|
|
556
1571
|
if test $prefix_length -gt 0
|
|
557
1572
|
and test (string sub -l $prefix_length -- "$pattern") = "$insertion_prefix"
|
|
@@ -559,10 +1574,22 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
559
1574
|
set -l start (math $prefix_length + 1)
|
|
560
1575
|
set pattern (string sub -s $start -- "$pattern")
|
|
561
1576
|
set current (string sub -s $start -- "$current")
|
|
1577
|
+
# Keep the raw word aligned so an unquoted key is not mistaken
|
|
1578
|
+
# for quoting when comparing it with the decoded value.
|
|
1579
|
+
if test (string sub -l $prefix_length -- "$__raw_current") = "$insertion_prefix"
|
|
1580
|
+
set __raw_current (string sub -s $start -- "$__raw_current")
|
|
1581
|
+
end
|
|
562
1582
|
else
|
|
563
1583
|
set insertion_prefix ""
|
|
564
1584
|
end
|
|
565
1585
|
|
|
1586
|
+
# path() echoes the current word as its pattern. When that word
|
|
1587
|
+
# was quoted or escaped, treat the echo as the typed word rather
|
|
1588
|
+
# than as a pattern from the program.
|
|
1589
|
+
if test "$current" != "$__raw_current"; and test "$pattern" = "$current"
|
|
1590
|
+
set pattern ""
|
|
1591
|
+
end
|
|
1592
|
+
|
|
566
1593
|
# When a pattern is specified, use it as the glob base instead
|
|
567
1594
|
# of the current word. If the user has already typed beyond the
|
|
568
1595
|
# pattern (e.g., pattern="src/" and current="src/ma"), keep the
|
|
@@ -574,15 +1601,13 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
574
1601
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
575
1602
|
set -l __norm_current (string replace -r '^\\./' '' -- "$current")
|
|
576
1603
|
set -l __norm_pattern (string replace -r '^\\./' '' -- "$pattern")
|
|
577
|
-
# For wildcard patterns, compare only the directory prefix
|
|
1604
|
+
# For wildcard patterns, compare only the directory prefix.
|
|
1605
|
+
# Use a regex: fish wildcards have no character classes.
|
|
1606
|
+
# Only the basename can hold wildcards; a ? or * in a
|
|
1607
|
+
# directory name such as What?/ is literal.
|
|
578
1608
|
set -l __compare_pattern "$__norm_pattern"
|
|
579
|
-
if string match -
|
|
580
|
-
set __compare_pattern (string replace -r '[
|
|
581
|
-
if string match -q '*/*' -- "$__compare_pattern"
|
|
582
|
-
set __compare_pattern (string replace -r '/[^/]*$' '/' -- "$__compare_pattern")
|
|
583
|
-
else
|
|
584
|
-
set __compare_pattern ""
|
|
585
|
-
end
|
|
1609
|
+
if string match -qr '[*?][^/]*$' -- "$__compare_pattern"
|
|
1610
|
+
set __compare_pattern (string replace -r '[^/]*$' '' -- "$__compare_pattern")
|
|
586
1611
|
end
|
|
587
1612
|
set -l __cp_len (string length -- "$__compare_pattern")
|
|
588
1613
|
set -l __nc_len (string length -- "$__norm_current")
|
|
@@ -597,14 +1622,17 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
597
1622
|
end
|
|
598
1623
|
end
|
|
599
1624
|
|
|
600
|
-
# Expand tilde prefix for globbing—fish does not expand ~
|
|
601
|
-
# inside variable substitutions, so replace it with $HOME
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1625
|
+
# Expand tilde prefix for globbing — fish does not expand ~
|
|
1626
|
+
# inside variable substitutions, so replace it with $HOME. Like
|
|
1627
|
+
# fish itself, keep a quoted or escaped tilde in the word literal.
|
|
1628
|
+
if test $__from_pattern -eq 1; or string match -q '~*' -- "$__raw_current"
|
|
1629
|
+
if string match -q '~/*' -- "$glob_base"
|
|
1630
|
+
set __tilde_prefix "~"
|
|
1631
|
+
set glob_base (string replace -r '^~' "$HOME" -- "$glob_base")
|
|
1632
|
+
else if string match -q '~' -- "$glob_base"
|
|
1633
|
+
set __tilde_prefix "~"
|
|
1634
|
+
set glob_base "$HOME/"
|
|
1635
|
+
end
|
|
608
1636
|
end
|
|
609
1637
|
|
|
610
1638
|
# If the glob base is a directory without a trailing slash,
|
|
@@ -614,54 +1642,82 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
614
1642
|
end
|
|
615
1643
|
|
|
616
1644
|
# Pre-expand file candidates. When the glob base came from
|
|
617
|
-
# the program's pattern and contains * or ?, use
|
|
618
|
-
# complete glob expression. Otherwise append * as a
|
|
1645
|
+
# the program's pattern and its basename contains * or ?, use
|
|
1646
|
+
# it as the complete glob expression. Otherwise append * as a
|
|
1647
|
+
# prefix, so a literal ? in a directory name such as What?/
|
|
1648
|
+
# still lists that directory. A pattern equal to the current
|
|
1649
|
+
# word is the typed text echoed back (as path() does), so its
|
|
1650
|
+
# ? and * are literal characters, not wildcards.
|
|
619
1651
|
# Note: [ is NOT treated as a glob because it commonly
|
|
620
1652
|
# appears in literal filenames like [draft] or foo[1].txt.
|
|
621
1653
|
set -l __has_glob 0
|
|
622
1654
|
if test $__from_pattern -eq 1
|
|
623
|
-
and string match -
|
|
1655
|
+
and string match -qr '[*?][^/]*$' -- "$glob_base"
|
|
1656
|
+
and test "$pattern" != "$current"
|
|
624
1657
|
set __has_glob 1
|
|
625
1658
|
end
|
|
626
1659
|
set -l __candidates
|
|
627
1660
|
if test $__has_glob -eq 1
|
|
628
1661
|
# Safe glob expansion without eval: split the pattern into
|
|
629
|
-
# directory and filter
|
|
630
|
-
#
|
|
631
|
-
|
|
1662
|
+
# a directory prefix and a basename filter, list the
|
|
1663
|
+
# directory, then match basenames against a regex built
|
|
1664
|
+
# from the filter. The prefix keeps one empty element for
|
|
1665
|
+
# bare patterns so that $__glob_prefix* still expands.
|
|
1666
|
+
set -l __glob_prefix ""
|
|
1667
|
+
if string match -q '*/*' -- "$glob_base"
|
|
1668
|
+
set __glob_prefix (string replace -r '[^/]*$' '' -- "$glob_base")
|
|
1669
|
+
end
|
|
632
1670
|
set -l __glob_filter (string replace -r '.*/' '' -- "$glob_base")
|
|
633
|
-
|
|
634
|
-
|
|
1671
|
+
# Translate the filter into an anchored regex: * and ? are
|
|
1672
|
+
# wildcards and every other character is literal. Fish's
|
|
1673
|
+
# own wildcards cannot be used here because ? is literal
|
|
1674
|
+
# when the qmark-noglob feature is enabled (fish 4 default).
|
|
1675
|
+
set -l __glob_regex '^'
|
|
1676
|
+
for __ch in (string split '' -- "$__glob_filter")
|
|
1677
|
+
if test "$__ch" = '*'
|
|
1678
|
+
set __glob_regex "$__glob_regex.*"
|
|
1679
|
+
else if test "$__ch" = '?'
|
|
1680
|
+
set __glob_regex "$__glob_regex."
|
|
1681
|
+
else
|
|
1682
|
+
set __glob_regex "$__glob_regex"(string escape --style=regex -- "$__ch")
|
|
1683
|
+
end
|
|
635
1684
|
end
|
|
1685
|
+
set __glob_regex "$__glob_regex"'$'
|
|
636
1686
|
# Match files by the glob filter. Fish's * does not match
|
|
637
|
-
# dotfiles, so also scan .* when the filter targets them
|
|
638
|
-
|
|
1687
|
+
# dotfiles, so also scan .* when the filter targets them or
|
|
1688
|
+
# hidden files are requested.
|
|
1689
|
+
set -l __entries $__glob_prefix*
|
|
1690
|
+
if test "$hidden" = "1"; or string match -q '.*' -- "$__glob_filter"
|
|
1691
|
+
set -a __entries $__glob_prefix.*
|
|
1692
|
+
end
|
|
1693
|
+
for __item in $__entries
|
|
639
1694
|
if test -e "$__item"
|
|
640
|
-
set -l __bn (
|
|
641
|
-
if
|
|
642
|
-
|
|
1695
|
+
set -l __bn (string replace -r '.*/' '' -- "$__item")
|
|
1696
|
+
if contains -- "$__bn" . ..
|
|
1697
|
+
continue
|
|
643
1698
|
end
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if string match -q '.*' -- "$__glob_filter"
|
|
647
|
-
for __item in $__glob_dir/.*
|
|
648
|
-
if test -e "$__item"
|
|
649
|
-
set -l __bn (basename "$__item")
|
|
650
|
-
if test "$__bn" = "." -o "$__bn" = ".."
|
|
651
|
-
continue
|
|
652
|
-
end
|
|
653
|
-
if string match -q "$__glob_filter" -- "$__bn"
|
|
654
|
-
set -a __candidates "$__item"
|
|
655
|
-
end
|
|
1699
|
+
if string match -qr -- "$__glob_regex" "$__bn"
|
|
1700
|
+
set -a __candidates "$__item"
|
|
656
1701
|
end
|
|
657
1702
|
end
|
|
658
1703
|
end
|
|
659
1704
|
# For file/any modes, also include directories for navigation.
|
|
660
1705
|
# Skip for directory mode so the pattern filter is respected.
|
|
1706
|
+
# Directories the filter already matched are not repeated.
|
|
661
1707
|
if test "$type" != "directory"
|
|
662
|
-
|
|
1708
|
+
set -l __nav_dirs $__glob_prefix*/
|
|
1709
|
+
if test "$hidden" = "1"
|
|
1710
|
+
set -a __nav_dirs $__glob_prefix.*/
|
|
1711
|
+
end
|
|
1712
|
+
for __item in $__nav_dirs
|
|
1713
|
+
set __item (string replace -r '/$' '' -- "$__item")
|
|
1714
|
+
set -l __bn (string replace -r '.*/' '' -- "$__item")
|
|
1715
|
+
if contains -- "$__bn" . ..
|
|
1716
|
+
continue
|
|
1717
|
+
end
|
|
663
1718
|
if test -d "$__item"
|
|
664
|
-
|
|
1719
|
+
and not string match -qr -- "$__glob_regex" "$__bn"
|
|
1720
|
+
set -a __candidates "$__item"
|
|
665
1721
|
end
|
|
666
1722
|
end
|
|
667
1723
|
end
|
|
@@ -764,7 +1820,7 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
764
1820
|
if test "$hidden" != "1" -a (string sub -l 1 -- $__glob_basename) != "."
|
|
765
1821
|
set -l filtered
|
|
766
1822
|
for item in $items
|
|
767
|
-
set -l basename (basename $item)
|
|
1823
|
+
set -l basename (basename -- "$item")
|
|
768
1824
|
if not string match -q '.*' -- $basename
|
|
769
1825
|
set -a filtered $item
|
|
770
1826
|
end
|
|
@@ -781,11 +1837,13 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
781
1837
|
set items $restored
|
|
782
1838
|
end
|
|
783
1839
|
|
|
784
|
-
# Output file completions
|
|
1840
|
+
# Output file completions. Use printf rather than echo, which
|
|
1841
|
+
# would take filenames like -n or -e as its own options.
|
|
785
1842
|
for item in $items
|
|
786
|
-
|
|
1843
|
+
printf '%s\\n' "$insertion_prefix$item"
|
|
787
1844
|
end
|
|
788
1845
|
set current "$saved_current"
|
|
1846
|
+
set __raw_current "$saved_raw_current"
|
|
789
1847
|
else
|
|
790
1848
|
# Regular literal completion - split by tab
|
|
791
1849
|
set -l parts (string split \\t -- $line)
|
|
@@ -809,7 +1867,7 @@ complete -c ${programName} -f -a '(${functionName})'
|
|
|
809
1867
|
if (i > 0) yield "\n";
|
|
810
1868
|
if (suggestion.kind === "literal") {
|
|
811
1869
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
812
|
-
yield `${
|
|
1870
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
813
1871
|
} else {
|
|
814
1872
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
815
1873
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -822,6 +1880,16 @@ complete -c ${programName} -f -a '(${functionName})'
|
|
|
822
1880
|
}
|
|
823
1881
|
};
|
|
824
1882
|
/**
|
|
1883
|
+
* Quotes a string as a Nushell string literal. Nushell single-quoted strings
|
|
1884
|
+
* have no escape sequences, so a value containing an apostrophe is written as
|
|
1885
|
+
* a double-quoted string, in which only backslashes and double quotes need
|
|
1886
|
+
* escaping.
|
|
1887
|
+
*/
|
|
1888
|
+
function quoteNuString(value) {
|
|
1889
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
1890
|
+
return `"${value.replace(/["\\]/g, "\\$&")}"`;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
825
1893
|
* The Nushell completion generator.
|
|
826
1894
|
* @since 0.6.0
|
|
827
1895
|
*/
|
|
@@ -829,66 +1897,193 @@ const nu = {
|
|
|
829
1897
|
name: "nu",
|
|
830
1898
|
generateScript(programName, args = []) {
|
|
831
1899
|
validateProgramName(programName);
|
|
832
|
-
const escapedArgs = args.map(
|
|
1900
|
+
const escapedArgs = args.map(quoteNuString).join(" ");
|
|
833
1901
|
const safeName = programName.replace(/[^a-zA-Z0-9]+/g, "-");
|
|
834
1902
|
const functionName = `nu-complete-${safeName}`;
|
|
835
1903
|
return `
|
|
836
|
-
# Helper to split
|
|
1904
|
+
# Helper to split a command line into the arguments Nushell would pass to
|
|
1905
|
+
# an external command. Words are found the way Nushell's lexer finds them
|
|
1906
|
+
# (quotes and raw strings may contain whitespace), then each word is decoded
|
|
1907
|
+
# like an external argument. A final empty argument is added when the line
|
|
1908
|
+
# ends in unquoted whitespace, for the word about to be completed.
|
|
837
1909
|
def args-split []: string -> list<string> {
|
|
838
|
-
let STATE_NORMAL = 0
|
|
839
|
-
let STATE_IN_SINGLE_QUOTE = 1
|
|
840
|
-
let STATE_IN_DOUBLE_QUOTE = 2
|
|
841
|
-
let STATE_ESCAPE = 3
|
|
842
1910
|
let WHITESPACES = [" " "\\t" "\\n" "\\r"]
|
|
1911
|
+
let QUOTES = ["'" '"' "\`"]
|
|
843
1912
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1913
|
+
# Decodes one word. An unterminated word is the one still being typed,
|
|
1914
|
+
# so its open quote is taken as if it were closed.
|
|
1915
|
+
let decode = {|word: string, unterminated: bool|
|
|
1916
|
+
let hashes = if ($word | str starts-with "r#") {
|
|
1917
|
+
$word | str replace -r '(?s)^r(#+).*$' '$1'
|
|
1918
|
+
} else {
|
|
1919
|
+
""
|
|
1920
|
+
}
|
|
1921
|
+
if ($hashes | is-not-empty) {
|
|
1922
|
+
let prefix = "r" + $hashes + "'"
|
|
1923
|
+
let suffix = "'" + $hashes
|
|
1924
|
+
let size = ($hashes | str length)
|
|
1925
|
+
if ($word | str starts-with $prefix) and ($word | str ends-with $suffix) and (($word | str length) >= ($size * 2 + 3)) {
|
|
1926
|
+
$word | str replace $prefix "" | str replace -r ($suffix + '$') ""
|
|
1927
|
+
} else if $unterminated and ($word | str starts-with $prefix) {
|
|
1928
|
+
$word | str replace $prefix ""
|
|
1929
|
+
} else {
|
|
1930
|
+
$word
|
|
1931
|
+
}
|
|
1932
|
+
} else {
|
|
1933
|
+
mut result = ""
|
|
1934
|
+
mut quote = ""
|
|
1935
|
+
mut quoted = ""
|
|
1936
|
+
mut escaped = false
|
|
1937
|
+
for char in ($word | split chars) {
|
|
1938
|
+
if $quote == "" {
|
|
1939
|
+
if ($char in $QUOTES) {
|
|
1940
|
+
$quote = $char
|
|
1941
|
+
$quoted = ""
|
|
1942
|
+
} else {
|
|
1943
|
+
$result = $result + $char
|
|
1944
|
+
}
|
|
1945
|
+
} else if $escaped {
|
|
1946
|
+
$quoted = $quoted + $char
|
|
1947
|
+
$escaped = false
|
|
1948
|
+
} else if $char == $quote {
|
|
1949
|
+
let body = $quoted
|
|
1950
|
+
$result = $result + (if $quote == '"' {
|
|
1951
|
+
try { '"' + $body + '"' | from nuon } catch { $body }
|
|
1952
|
+
} else {
|
|
1953
|
+
$body
|
|
1954
|
+
})
|
|
1955
|
+
$quote = ""
|
|
1956
|
+
} else {
|
|
1957
|
+
$quoted = $quoted + $char
|
|
1958
|
+
$escaped = ($quote == '"' and $char == '\\')
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if $quote != "" {
|
|
1962
|
+
let body = $quoted
|
|
1963
|
+
$result = $result + (if not $unterminated {
|
|
1964
|
+
$quote + $body
|
|
1965
|
+
} else if $quote == '"' {
|
|
1966
|
+
try { '"' + $body + '"' | from nuon } catch { $body }
|
|
1967
|
+
} else {
|
|
1968
|
+
$body
|
|
1969
|
+
})
|
|
1970
|
+
}
|
|
1971
|
+
$result
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
mut words: list<string> = []
|
|
1976
|
+
mut word = ""
|
|
1977
|
+
mut in_word = false
|
|
1978
|
+
mut quote = ""
|
|
1979
|
+
mut escaped = false
|
|
1980
|
+
mut raw_hashes = ""
|
|
1981
|
+
mut raw_pending = ""
|
|
1982
|
+
mut prev = ""
|
|
1983
|
+
mut trailing_space = false
|
|
848
1984
|
|
|
849
1985
|
for char in ($in | split chars) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
if $char == "
|
|
858
|
-
$
|
|
859
|
-
} else if $char == '"
|
|
860
|
-
$
|
|
861
|
-
} else if ($char in $WHITESPACES) {
|
|
862
|
-
if ($current_token | is-not-empty) {
|
|
863
|
-
$result = $result | append $current_token
|
|
864
|
-
$current_token = ""
|
|
865
|
-
}
|
|
1986
|
+
$trailing_space = false
|
|
1987
|
+
if $quote == "#" and $char != "#" and $char != "'" {
|
|
1988
|
+
# Not a raw string after all; lex this character normally
|
|
1989
|
+
$quote = ""
|
|
1990
|
+
}
|
|
1991
|
+
if $quote == "r" {
|
|
1992
|
+
# Inside a raw string, which ends at ' followed by the same number of #
|
|
1993
|
+
if ($raw_pending | is-not-empty) and $char == "#" {
|
|
1994
|
+
$raw_pending = $raw_pending + $char
|
|
1995
|
+
} else if $char == "'" {
|
|
1996
|
+
$raw_pending = $char
|
|
866
1997
|
} else {
|
|
867
|
-
$
|
|
1998
|
+
$raw_pending = ""
|
|
1999
|
+
}
|
|
2000
|
+
if $raw_pending == ("'" + $raw_hashes) {
|
|
2001
|
+
$quote = ""
|
|
2002
|
+
$raw_pending = ""
|
|
868
2003
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
2004
|
+
$word = $word + $char
|
|
2005
|
+
} else if $quote == "#" {
|
|
2006
|
+
# Between r# and the opening ' of a raw string
|
|
2007
|
+
$word = $word + $char
|
|
2008
|
+
if $char == "#" {
|
|
2009
|
+
$raw_hashes = $raw_hashes + $char
|
|
872
2010
|
} else {
|
|
873
|
-
$
|
|
2011
|
+
$quote = "r"
|
|
874
2012
|
}
|
|
875
|
-
} else if $
|
|
876
|
-
|
|
877
|
-
|
|
2013
|
+
} else if $quote != "" {
|
|
2014
|
+
$word = $word + $char
|
|
2015
|
+
if $escaped {
|
|
2016
|
+
$escaped = false
|
|
2017
|
+
} else if $char == $quote {
|
|
2018
|
+
$quote = ""
|
|
878
2019
|
} else {
|
|
879
|
-
$
|
|
2020
|
+
$escaped = ($quote == '"' and $char == '\\')
|
|
880
2021
|
}
|
|
2022
|
+
} else if ($char in $WHITESPACES) {
|
|
2023
|
+
if $in_word {
|
|
2024
|
+
$words = $words | append (do $decode $word false)
|
|
2025
|
+
$word = ""
|
|
2026
|
+
$in_word = false
|
|
2027
|
+
}
|
|
2028
|
+
$trailing_space = true
|
|
2029
|
+
} else {
|
|
2030
|
+
if ($char in $QUOTES) {
|
|
2031
|
+
$quote = $char
|
|
2032
|
+
} else if $char == "#" and $prev == "r" {
|
|
2033
|
+
$quote = "#"
|
|
2034
|
+
$raw_hashes = "#"
|
|
2035
|
+
}
|
|
2036
|
+
$word = $word + $char
|
|
2037
|
+
$in_word = true
|
|
881
2038
|
}
|
|
2039
|
+
$prev = $char
|
|
2040
|
+
}
|
|
2041
|
+
if $in_word {
|
|
2042
|
+
$words = $words | append (do $decode $word ($quote != ""))
|
|
2043
|
+
} else if $trailing_space {
|
|
2044
|
+
$words = $words | append ""
|
|
882
2045
|
}
|
|
883
|
-
|
|
884
|
-
|
|
2046
|
+
$words
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
# Helper to tell whether Nushell would not read a value back verbatim as a
|
|
2050
|
+
# single argument. A leading ! is included because the line editor expands
|
|
2051
|
+
# history (e.g., !!) there. With --allow-expansion, a leading tilde and n-dot
|
|
2052
|
+
# path segments (e.g., ...) are allowed to expand.
|
|
2053
|
+
def needs-quote [--allow-expansion]: string -> bool {
|
|
2054
|
+
let value = $in
|
|
2055
|
+
let special = ["'" '"' '\`' "#" "(" ")" "[" "]" "{" "}" "|" ";" "&" "$" "*" "?" "<" ">"]
|
|
2056
|
+
(
|
|
2057
|
+
($value =~ '\\s')
|
|
2058
|
+
or ($special | any {|c| $value | str contains $c })
|
|
2059
|
+
or ($value =~ '^(\\+\\+|[-+*/])?=$')
|
|
2060
|
+
or ($value | str starts-with '!')
|
|
2061
|
+
or ((not $allow_expansion) and (
|
|
2062
|
+
($value | str starts-with '~')
|
|
2063
|
+
or ($value | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
|
|
2064
|
+
))
|
|
2065
|
+
)
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
# Helper to quote a value so that Nushell reads it back verbatim as a
|
|
2069
|
+
# single argument. The line editor expands history at a ! that follows
|
|
2070
|
+
# whitespace even inside quotes, so such a ! is put between two quoted parts,
|
|
2071
|
+
# right after a quote, where it is left alone.
|
|
2072
|
+
def quote-arg []: string -> string {
|
|
2073
|
+
let value = $in
|
|
2074
|
+
if ($value | is-empty) or not ($value | needs-quote) {
|
|
2075
|
+
$value
|
|
2076
|
+
} else if ($value | str contains "'") {
|
|
2077
|
+
'"' + ($value | str replace -a '\\' '\\\\' | str replace -a '"' '\\"' | str replace -ar '(\\s)!' '\${1}"!"') + '"'
|
|
2078
|
+
} else {
|
|
2079
|
+
"'" + ($value | str replace -ar '(\\s)!' "\${1}'!'") + "'"
|
|
885
2080
|
}
|
|
886
|
-
$result
|
|
887
2081
|
}
|
|
888
2082
|
|
|
889
2083
|
# Completion command that calls back to the CLI
|
|
890
2084
|
export def "${functionName}" [context: string] {
|
|
891
|
-
# Split context into
|
|
2085
|
+
# Split context into the arguments Nushell would pass, including an empty
|
|
2086
|
+
# one for a new word when the context ends in unquoted whitespace
|
|
892
2087
|
let tokens = $context | args-split
|
|
893
2088
|
|
|
894
2089
|
# Remove the command name (first token) to get arguments
|
|
@@ -898,18 +2093,11 @@ export def "${functionName}" [context: string] {
|
|
|
898
2093
|
[]
|
|
899
2094
|
}
|
|
900
2095
|
|
|
901
|
-
# If context ends with whitespace, add empty string to represent the new argument
|
|
902
|
-
let args_with_empty = if ($context | str ends-with ' ') {
|
|
903
|
-
$args | append ''
|
|
904
|
-
} else {
|
|
905
|
-
$args
|
|
906
|
-
}
|
|
907
|
-
|
|
908
2096
|
# Ensure at least one argument (empty string) to get completions not script
|
|
909
|
-
let final_args = if ($
|
|
2097
|
+
let final_args = if ($args | is-empty) {
|
|
910
2098
|
['']
|
|
911
2099
|
} else {
|
|
912
|
-
$
|
|
2100
|
+
$args
|
|
913
2101
|
}
|
|
914
2102
|
|
|
915
2103
|
let output = try {
|
|
@@ -918,8 +2106,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
918
2106
|
}
|
|
919
2107
|
|
|
920
2108
|
# Process each line of output
|
|
921
|
-
$output | lines | each {|
|
|
922
|
-
|
|
2109
|
+
$output | lines | each {|raw_line|
|
|
2110
|
+
let literal = ($raw_line | str starts-with '__LITERAL__:')
|
|
2111
|
+
let line = if $literal { $raw_line | str substring 12.. } else { $raw_line }
|
|
2112
|
+
if (not $literal) and ($line | str starts-with '__FILE__:') {
|
|
923
2113
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
924
2114
|
let directive = ($line | split row "\t" | first)
|
|
925
2115
|
let parts = ($directive | split row ':')
|
|
@@ -950,12 +2140,12 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
950
2140
|
# prefix for incremental narrowing. Normalize path separators
|
|
951
2141
|
# before comparing so that Windows backslashes match forward slashes
|
|
952
2142
|
# in the transported pattern.
|
|
2143
|
+
let is_win = (($nu.os-info.name | str downcase) == "windows")
|
|
953
2144
|
let glob_base = if ($pattern | is-not-empty) {
|
|
954
2145
|
# Normalize separators and leading ./; downcase only on Windows
|
|
955
2146
|
# where filesystems are typically case-insensitive
|
|
956
2147
|
let norm_prefix_raw = ($prefix | str replace -a '\\' '/' | str replace -r '^\\./' '')
|
|
957
2148
|
let norm_pattern_raw = ($pattern | str replace -a '\\' '/' | str replace -r '^\\./' '')
|
|
958
|
-
let is_win = (($nu.os-info.name | str downcase) == "windows")
|
|
959
2149
|
let norm_prefix = (if $is_win { $norm_prefix_raw | str downcase } else { $norm_prefix_raw })
|
|
960
2150
|
let norm_pattern = (if $is_win { $norm_pattern_raw | str downcase } else { $norm_pattern_raw })
|
|
961
2151
|
# For wildcard patterns, compare only the directory prefix
|
|
@@ -974,10 +2164,27 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
974
2164
|
$prefix
|
|
975
2165
|
}
|
|
976
2166
|
|
|
2167
|
+
# Intersect fixed glob matches with the narrowed prefix candidates.
|
|
2168
|
+
# Directory-only suggestions retain their existing navigation behavior.
|
|
2169
|
+
let narrow_fixed_glob = ($type != "directory") and ($pattern =~ '[*?]') and ($prefix != $pattern) and ($glob_base == $prefix)
|
|
2170
|
+
let fixed_matches = if $narrow_fixed_glob {
|
|
2171
|
+
try {
|
|
2172
|
+
let fixed_pattern = ($pattern | into glob)
|
|
2173
|
+
let include_hidden = $hidden or ($prefix | path basename | str starts-with '.')
|
|
2174
|
+
let matches = if $include_hidden { ls -a --directory $fixed_pattern } else { ls --directory $fixed_pattern }
|
|
2175
|
+
$matches | each {|item|
|
|
2176
|
+
let name = ($env.PWD | path join $item.name | path expand --no-symlink)
|
|
2177
|
+
if $is_win { $name | str downcase } else { $name }
|
|
2178
|
+
}
|
|
2179
|
+
} catch { [] }
|
|
2180
|
+
} else { [] }
|
|
2181
|
+
|
|
977
2182
|
# If the glob base is a directory without a trailing slash (e.g., "src"),
|
|
978
2183
|
# append one so that ls enumerates its contents instead of matching
|
|
979
|
-
# siblings like "src-old"
|
|
980
|
-
|
|
2184
|
+
# siblings like "src-old". path expand follows symlinks, so that
|
|
2185
|
+
# a symlink to a directory is treated the same way; --no-symlink first
|
|
2186
|
+
# resolves .. against the logical $env.PWD as ls does
|
|
2187
|
+
let glob_base = if ($glob_base | is-not-empty) and ($glob_base | path expand --no-symlink | path expand | path type) == "dir" and (not ($glob_base | str ends-with "/")) {
|
|
981
2188
|
$glob_base + "/"
|
|
982
2189
|
} else {
|
|
983
2190
|
$glob_base
|
|
@@ -986,56 +2193,78 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
986
2193
|
# Generate file completions based on type.
|
|
987
2194
|
# When the glob base contains wildcard characters, use it as-is;
|
|
988
2195
|
# otherwise append * to treat it as a prefix.
|
|
989
|
-
# Note: into glob is required so that ls expands wildcards from a variable
|
|
2196
|
+
# Note: into glob is required so that ls expands wildcards from a variable.
|
|
2197
|
+
# A literal prefix has its brackets wrapped as [[] and []] so that path
|
|
2198
|
+
# text like "report[1]" is not parsed as a glob character class; a
|
|
2199
|
+
# wildcard pattern is a glob as a whole, so its brackets stay classes.
|
|
990
2200
|
let has_glob = ($glob_base =~ '[*?]')
|
|
991
2201
|
let ls_pattern = if ($glob_base | is-empty) {
|
|
992
2202
|
"."
|
|
993
2203
|
} else if $has_glob {
|
|
994
2204
|
($glob_base | into glob)
|
|
995
2205
|
} else {
|
|
996
|
-
($glob_base + "*" | into glob)
|
|
2206
|
+
(($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
|
|
997
2207
|
}
|
|
998
2208
|
|
|
999
2209
|
# When using a glob pattern, also compute a directory listing pattern
|
|
1000
|
-
# so that file/any modes can include directories for navigation
|
|
2210
|
+
# so that file/any modes can include directories for navigation; its
|
|
2211
|
+
# directory part is a literal path
|
|
1001
2212
|
let glob_dir_pattern = if $has_glob {
|
|
1002
2213
|
let dir_part = ($glob_base | path dirname)
|
|
1003
2214
|
let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
|
|
1004
|
-
($dir + "/*" | into glob)
|
|
2215
|
+
(($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
|
|
1005
2216
|
} else {
|
|
1006
2217
|
null
|
|
1007
2218
|
}
|
|
1008
2219
|
|
|
1009
|
-
#
|
|
2220
|
+
# List entries matching a pattern, using ls -a to include hidden
|
|
2221
|
+
# files when requested. ls reports every symlink as type "symlink",
|
|
2222
|
+
# so classify each one by its target's type instead (path expand
|
|
2223
|
+
# follows symlinks); broken or looping symlinks stay "symlink".
|
|
2224
|
+
# Join the name to $env.PWD first so that a leading ~ in an entry
|
|
2225
|
+
# name is not expanded to a home directory, and resolve .. against
|
|
2226
|
+
# the logical $env.PWD (--no-symlink) before following the symlink
|
|
2227
|
+
let list_entries = {|pattern|
|
|
2228
|
+
(if $hidden { ls -a $pattern } else { ls $pattern }) | each {|item|
|
|
2229
|
+
if $item.type == symlink {
|
|
2230
|
+
$item | update type ($env.PWD | path join $item.name | path expand --no-symlink | path expand | path type)
|
|
2231
|
+
} else {
|
|
2232
|
+
$item
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
1010
2237
|
let items = try {
|
|
1011
2238
|
match $type {
|
|
1012
2239
|
"file" => {
|
|
1013
2240
|
if ($extensions | is-empty) {
|
|
1014
|
-
(
|
|
2241
|
+
(do $list_entries $ls_pattern) | where type == file or type == dir
|
|
1015
2242
|
} else {
|
|
1016
2243
|
let ext_list = ($extensions | split row ',' | each {|e| $e | str replace -r '^\\.' '' })
|
|
1017
|
-
let all_items = (
|
|
2244
|
+
let all_items = (do $list_entries $ls_pattern)
|
|
1018
2245
|
let dirs = $all_items | where type == dir
|
|
1019
2246
|
let files = $all_items | where type == file | where {|f|
|
|
1020
|
-
|
|
1021
|
-
|
|
2247
|
+
$ext_list | any {|ext|
|
|
2248
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
2249
|
+
}
|
|
1022
2250
|
}
|
|
1023
2251
|
$dirs | append $files
|
|
1024
2252
|
}
|
|
1025
2253
|
},
|
|
1026
2254
|
"directory" => {
|
|
1027
|
-
(
|
|
2255
|
+
(do $list_entries $ls_pattern) | where type == dir
|
|
1028
2256
|
},
|
|
1029
2257
|
"any" => {
|
|
1030
2258
|
if ($extensions | is-empty) {
|
|
1031
|
-
|
|
2259
|
+
do $list_entries $ls_pattern
|
|
1032
2260
|
} else {
|
|
1033
2261
|
let ext_list = ($extensions | split row ',' | each {|e| $e | str replace -r '^\\.' '' })
|
|
1034
|
-
let all_items = (
|
|
2262
|
+
let all_items = (do $list_entries $ls_pattern)
|
|
1035
2263
|
let dirs = $all_items | where type == dir
|
|
1036
2264
|
let files = $all_items | where type == file | where {|f|
|
|
1037
|
-
|
|
1038
|
-
|
|
2265
|
+
$ext_list | any {|ext|
|
|
2266
|
+
$f.name | path basename | str ends-with ('.' + $ext)
|
|
2267
|
+
}
|
|
1039
2268
|
}
|
|
1040
2269
|
$dirs | append $files
|
|
1041
2270
|
}
|
|
@@ -1050,7 +2279,7 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1050
2279
|
# even when the glob itself does not match directory names)
|
|
1051
2280
|
let items = if ($glob_dir_pattern != null) and ($type != "directory") {
|
|
1052
2281
|
let extra_dirs = try {
|
|
1053
|
-
(
|
|
2282
|
+
(do $list_entries $glob_dir_pattern) | where type == dir
|
|
1054
2283
|
} catch {
|
|
1055
2284
|
[]
|
|
1056
2285
|
}
|
|
@@ -1072,6 +2301,17 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1072
2301
|
}
|
|
1073
2302
|
}
|
|
1074
2303
|
|
|
2304
|
+
let filtered = if $narrow_fixed_glob {
|
|
2305
|
+
$filtered | where {|item|
|
|
2306
|
+
# Any-mode listings already include directory symlinks; keep them
|
|
2307
|
+
# navigable without changing their existing completion rendering.
|
|
2308
|
+
let navigation_dir = ($item.type == dir) or ($item.type == symlink and (try { ($item.name | path expand | path type) == "dir" } catch { false }))
|
|
2309
|
+
let name = ($env.PWD | path join $item.name | path expand --no-symlink)
|
|
2310
|
+
let normalized_name = if $is_win { $name | str downcase } else { $name }
|
|
2311
|
+
$navigation_dir or ($normalized_name in $fixed_matches)
|
|
2312
|
+
}
|
|
2313
|
+
} else { $filtered }
|
|
2314
|
+
|
|
1075
2315
|
# Extract directory prefix to preserve in completion text
|
|
1076
2316
|
let dir_prefix = if ($glob_base | is-empty) {
|
|
1077
2317
|
""
|
|
@@ -1082,19 +2322,56 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1082
2322
|
if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
|
|
1083
2323
|
}
|
|
1084
2324
|
|
|
1085
|
-
# Format file completions
|
|
2325
|
+
# Format file completions, quoting names that Nushell would not read
|
|
2326
|
+
# back as a single argument
|
|
1086
2327
|
$filtered | each {|item|
|
|
1087
|
-
let
|
|
1088
|
-
|
|
2328
|
+
let base = if $item.type == dir {
|
|
2329
|
+
($item.name | path basename) + "/"
|
|
2330
|
+
} else {
|
|
2331
|
+
$item.name | path basename
|
|
2332
|
+
}
|
|
2333
|
+
let name = $dir_prefix + $base
|
|
2334
|
+
# Nushell expands a leading tilde even in a file name, but not after ./
|
|
2335
|
+
let spelled = if ($dir_prefix | is-empty) and ($base | str starts-with '~') {
|
|
2336
|
+
'./' + $base
|
|
2337
|
+
} else {
|
|
2338
|
+
$name
|
|
2339
|
+
}
|
|
2340
|
+
# A tilde or n-dot prefix typed by the user is meant to be expanded
|
|
2341
|
+
# by Nushell, which quoting would prevent, so expand such a prefix
|
|
2342
|
+
# here when the value has to be quoted
|
|
2343
|
+
let literal_ndots = ($base | str replace -r '[/\\\\]$' '') =~ '^\\.{3,}$'
|
|
2344
|
+
let expandable = ($dir_prefix | str starts-with '~') or ($dir_prefix | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
|
|
2345
|
+
let value = if not ($literal_ndots or ($spelled | needs-quote --allow-expansion)) {
|
|
2346
|
+
$spelled
|
|
2347
|
+
} else if $expandable {
|
|
2348
|
+
let expanded_prefix = ($dir_prefix | path expand --no-symlink)
|
|
2349
|
+
let expanded_prefix = if ($expanded_prefix | str ends-with "/") { $expanded_prefix } else { $expanded_prefix + "/" }
|
|
2350
|
+
($expanded_prefix + $base) | quote-arg
|
|
2351
|
+
} else {
|
|
2352
|
+
$spelled | quote-arg
|
|
2353
|
+
}
|
|
2354
|
+
if ($insertion_prefix | is-not-empty) {
|
|
2355
|
+
# A key before the path keeps any tilde or n-dot segment from
|
|
2356
|
+
# being expanded, so quote the whole argument as written and show
|
|
2357
|
+
# the key in the menu as well.
|
|
2358
|
+
let keyed = $insertion_prefix + $name
|
|
2359
|
+
let keyed_value = ($keyed | quote-arg)
|
|
2360
|
+
if $keyed_value == $keyed {
|
|
2361
|
+
{ value: $keyed_value }
|
|
2362
|
+
} else {
|
|
2363
|
+
{ value: $keyed_value, display_override: $keyed }
|
|
2364
|
+
}
|
|
2365
|
+
} else if $value == $name {
|
|
2366
|
+
{ value: $value }
|
|
1089
2367
|
} else {
|
|
1090
|
-
$
|
|
2368
|
+
{ value: $value, display_override: $name }
|
|
1091
2369
|
}
|
|
1092
|
-
{ value: ($insertion_prefix + $name) }
|
|
1093
2370
|
}
|
|
1094
2371
|
} else {
|
|
1095
2372
|
# Regular literal completion - split by tab
|
|
1096
2373
|
let parts = ($line | split row "\t")
|
|
1097
|
-
if ($parts | length) >= 2 {
|
|
2374
|
+
let record = if ($parts | length) >= 2 {
|
|
1098
2375
|
# value\\tdescription format
|
|
1099
2376
|
{ value: ($parts | get 0), description: ($parts | get 1) }
|
|
1100
2377
|
} else if ($parts | length) == 1 and ($parts | get 0 | is-not-empty) {
|
|
@@ -1103,6 +2380,18 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1103
2380
|
} else {
|
|
1104
2381
|
null
|
|
1105
2382
|
}
|
|
2383
|
+
# Quote values that Nushell would not read back as a single argument,
|
|
2384
|
+
# while the menu keeps showing them as they are
|
|
2385
|
+
if $record == null {
|
|
2386
|
+
null
|
|
2387
|
+
} else {
|
|
2388
|
+
let value = ($record.value | quote-arg)
|
|
2389
|
+
if $value == $record.value {
|
|
2390
|
+
$record
|
|
2391
|
+
} else {
|
|
2392
|
+
$record | upsert value $value | insert display_override $record.value
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
1106
2395
|
}
|
|
1107
2396
|
} | flatten | compact
|
|
1108
2397
|
}
|
|
@@ -1140,7 +2429,7 @@ ${functionName}-external
|
|
|
1140
2429
|
if (i > 0) yield "\n";
|
|
1141
2430
|
if (suggestion.kind === "literal") {
|
|
1142
2431
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1143
|
-
yield `${
|
|
2432
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1144
2433
|
} else {
|
|
1145
2434
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1146
2435
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1165,27 +2454,48 @@ const pwsh = {
|
|
|
1165
2454
|
Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
1166
2455
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
1167
2456
|
|
|
2457
|
+
# CompletionText is shell syntax; display labels remain unquoted.
|
|
2458
|
+
\$quoteArgument = {
|
|
2459
|
+
param([string]\$value)
|
|
2460
|
+
if (\$value -cmatch '\\A[a-zA-Z0-9_./:\\\\-]+\\z' -and \$value -cnotmatch '\\A-(?!-).*[.:]' -and \$value -cnotmatch '\\A-?(?:[0-9]|\\.[0-9])') {
|
|
2461
|
+
return \$value
|
|
2462
|
+
}
|
|
2463
|
+
return "'" + [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent(\$value) + "'"
|
|
2464
|
+
}
|
|
2465
|
+
|
|
1168
2466
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1169
2467
|
\$arguments = @()
|
|
1170
2468
|
\$commandElements = \$commandAst.CommandElements
|
|
2469
|
+
\$valuePrefix = \$wordToComplete
|
|
2470
|
+
\$quoteType = \$null
|
|
1171
2471
|
|
|
1172
|
-
#
|
|
1173
|
-
#
|
|
1174
|
-
\$
|
|
1175
|
-
if (\$commandElements.Count -gt 1) {
|
|
1176
|
-
\$lastElement = \$commandElements[\$commandElements.Count - 1]
|
|
1177
|
-
\$lastText = if (\$lastElement -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
|
|
1178
|
-
\$lastElement.Value
|
|
1179
|
-
} else {
|
|
1180
|
-
\$lastElement.Extent.Text
|
|
1181
|
-
}
|
|
1182
|
-
if (\$lastText -eq \$wordToComplete) {
|
|
1183
|
-
\$maxIndex = \$commandElements.Count - 2
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
for (\$i = 1; \$i -le \$maxIndex; \$i++) {
|
|
2472
|
+
# Keep only complete arguments before the cursor. The current word
|
|
2473
|
+
# (including a token ending exactly at the cursor) is appended below.
|
|
2474
|
+
for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
|
|
1188
2475
|
\$element = \$commandElements[\$i]
|
|
2476
|
+
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
2477
|
+
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
2478
|
+
# Only the AST retains the literal value and original quote type.
|
|
2479
|
+
# PSReadLine leaves the cursor before a directory's closing quote.
|
|
2480
|
+
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
2481
|
+
(\$element.Extent.EndOffset -eq \$cursorPosition -or
|
|
2482
|
+
(\$element.Extent.EndOffset -eq (\$cursorPosition + 1) -and
|
|
2483
|
+
\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')))) {
|
|
2484
|
+
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
2485
|
+
\$valuePrefix = \$element.Value
|
|
2486
|
+
\$quoteType = \$element.StringConstantType
|
|
2487
|
+
} elseif (\$element.Extent.Text -match "^[^=]+=['\\u2018-\\u201b]") {
|
|
2488
|
+
# Joined values are BareWord constants despite their quotes.
|
|
2489
|
+
# Quote the entire replacement to keep it one argument.
|
|
2490
|
+
\$valuePrefix = \$element.Value
|
|
2491
|
+
\$quoteType = 'SingleQuoted'
|
|
2492
|
+
} elseif (\$element.Extent.Text -match '^[^=]+=["\\u201c-\\u201e]') {
|
|
2493
|
+
\$valuePrefix = \$element.Value
|
|
2494
|
+
\$quoteType = 'DoubleQuoted'
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
break
|
|
2498
|
+
}
|
|
1189
2499
|
|
|
1190
2500
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
|
|
1191
2501
|
\$arguments += \$element.Value
|
|
@@ -1194,23 +2504,41 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1194
2504
|
}
|
|
1195
2505
|
}
|
|
1196
2506
|
|
|
2507
|
+
# CompletionText replaces the whole token, including its opening quote.
|
|
2508
|
+
# Escape literals without evaluating variables or expressions.
|
|
2509
|
+
\$quoteCompletion = {
|
|
2510
|
+
param(\$text)
|
|
2511
|
+
if (\$quoteType -eq 'SingleQuoted') {
|
|
2512
|
+
"'" + (\$text -replace "['\\u2018-\\u201b]", '\$0\$0') + "'"
|
|
2513
|
+
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
2514
|
+
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
2515
|
+
} else {
|
|
2516
|
+
& \$quoteArgument \$text
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
1197
2520
|
# Build arguments array for completion command
|
|
1198
2521
|
\$completionArgs = @()
|
|
1199
2522
|
${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
1200
2523
|
` : ""} \$completionArgs += \$arguments
|
|
1201
|
-
\$completionArgs += \$wordToComplete
|
|
1202
2524
|
|
|
1203
2525
|
# Call completion command and capture output
|
|
1204
2526
|
try {
|
|
1205
|
-
|
|
2527
|
+
# Quote the prefix separately: array arguments expand a literal tilde
|
|
2528
|
+
# when passed to native programs on Unix.
|
|
2529
|
+
\$output = & ${programName} \$completionArgs "\$valuePrefix" 2>\$null
|
|
1206
2530
|
if (-not \$output) { return }
|
|
1207
2531
|
|
|
1208
2532
|
# Parse tab-separated output and create CompletionResult objects
|
|
1209
2533
|
\$output -split "\`n" | ForEach-Object {
|
|
1210
|
-
\$
|
|
2534
|
+
\$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
|
|
2535
|
+
# Quoted values preserve whitespace; only remove the CR of CRLF.
|
|
2536
|
+
# Keep the existing trimming for unquoted completions.
|
|
2537
|
+
\$line = if (\$null -ne \$quoteType) { \$_.TrimEnd([char]13) } else { \$_.Trim() }
|
|
1211
2538
|
if (-not \$line) { return }
|
|
2539
|
+
if (\$literal) { \$line = \$line.Substring(12) }
|
|
1212
2540
|
|
|
1213
|
-
if (\$
|
|
2541
|
+
if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
|
|
1214
2542
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1215
2543
|
\$directive = (\$line -split "\`t")[0]
|
|
1216
2544
|
\$parts = \$directive -split ':', 6
|
|
@@ -1219,7 +2547,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1219
2547
|
\$pattern = \$parts[3] -replace '%3A', ':' -replace '%25', '%'
|
|
1220
2548
|
\$hidden = \$parts[4] -eq '1'
|
|
1221
2549
|
\$insertionPrefix = if (\$parts.Length -gt 5) { \$parts[5] -replace '%3A', ':' -replace '%25', '%' } else { '' }
|
|
1222
|
-
\$matchingWord = \$
|
|
2550
|
+
\$matchingWord = \$valuePrefix
|
|
1223
2551
|
if (\$insertionPrefix -and \$pattern.StartsWith(\$insertionPrefix, [System.StringComparison]::Ordinal) -and \$matchingWord.StartsWith(\$insertionPrefix, [System.StringComparison]::Ordinal)) {
|
|
1224
2552
|
\$pattern = \$pattern.Substring(\$insertionPrefix.Length)
|
|
1225
2553
|
\$matchingWord = \$matchingWord.Substring(\$insertionPrefix.Length)
|
|
@@ -1244,6 +2572,19 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1244
2572
|
\$comparePattern = ''
|
|
1245
2573
|
}
|
|
1246
2574
|
}
|
|
2575
|
+
# A quoted home-pattern directory inserts an absolute path.
|
|
2576
|
+
# Compare subsequent input against the same expanded prefix.
|
|
2577
|
+
# Detect home syntax before removing ./, which can target a
|
|
2578
|
+
# literal local tilde directory rather than the home directory.
|
|
2579
|
+
\$homePattern = \$pattern -and (\$pattern -eq '~' -or \$pattern.Replace('\\', '/').StartsWith('~/'))
|
|
2580
|
+
if (\$homePattern) {
|
|
2581
|
+
# Preserve the home root when the pattern is exactly ~.
|
|
2582
|
+
\$expandedPattern = if (\$comparePattern -eq '~') { \$HOME } else { \$HOME.Replace('\\', '/').TrimEnd('/') + \$comparePattern.Substring(1) }
|
|
2583
|
+
\$expandedPattern = [IO.Path]::GetFullPath(\$expandedPattern).Replace('\\', '/')
|
|
2584
|
+
if (\$normalizedWord.StartsWith(\$expandedPattern, \$comparison)) {
|
|
2585
|
+
\$comparePattern = \$expandedPattern
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
1247
2588
|
\$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$matchingWord -ne \$pattern) {
|
|
1248
2589
|
\$matchingWord
|
|
1249
2590
|
} elseif (\$pattern) {
|
|
@@ -1257,15 +2598,35 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1257
2598
|
\$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
|
|
1258
2599
|
\$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
|
|
1259
2600
|
|
|
2601
|
+
# A quoted tilde is literal when passed to a native command.
|
|
2602
|
+
# Prevent PowerShell's file provider from expanding it to home.
|
|
2603
|
+
# Parser-provided home patterns still need home expansion.
|
|
2604
|
+
\$prefixFromInput = \$matchingWord -and \$prefix -ceq \$matchingWord
|
|
2605
|
+
\$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
|
|
2606
|
+
|
|
2607
|
+
# Detect narrowing before directory normalization changes prefix.
|
|
2608
|
+
\$narrowFixedGlob = \$type -ne 'directory' -and \$pattern -match '[\*\?]' -and \$prefix -ceq \$matchingWord -and \$pattern -cne \$matchingWord
|
|
2609
|
+
|
|
1260
2610
|
# If prefix is a directory without trailing slash, append
|
|
1261
2611
|
# one so Get-ChildItem lists its contents
|
|
1262
|
-
if (\$prefix -and (Test-Path -Path \$
|
|
2612
|
+
if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
|
|
1263
2613
|
\$prefix = \$prefix + '/'
|
|
2614
|
+
\$lookupPrefix = \$lookupPrefix + '/'
|
|
1264
2615
|
}
|
|
1265
2616
|
|
|
1266
2617
|
# Build the glob path—when the prefix contains wildcard
|
|
1267
2618
|
# characters, use it as-is; otherwise append *
|
|
1268
|
-
\$globPath = if (\$
|
|
2619
|
+
\$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
|
|
2620
|
+
|
|
2621
|
+
# Keep the original native glob as an independent file filter
|
|
2622
|
+
# when the current word narrows its candidates.
|
|
2623
|
+
\$fixedMatches = @()
|
|
2624
|
+
if (\$narrowFixedGlob) {
|
|
2625
|
+
# Include hidden parents here; candidate enumeration still
|
|
2626
|
+
# controls which hidden children can be suggested.
|
|
2627
|
+
\$fixedMatches = @(Get-Item -Force -Path \$pattern -ErrorAction SilentlyContinue |
|
|
2628
|
+
Select-Object -ExpandProperty FullName)
|
|
2629
|
+
}
|
|
1269
2630
|
|
|
1270
2631
|
# Get file system items based on type
|
|
1271
2632
|
\$items = @()
|
|
@@ -1276,7 +2637,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1276
2637
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1277
2638
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1278
2639
|
Where-Object {
|
|
1279
|
-
\$
|
|
2640
|
+
\$fileName = \$_.Name
|
|
2641
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2642
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2643
|
+
}).Count -gt 0)
|
|
1280
2644
|
}
|
|
1281
2645
|
} else {
|
|
1282
2646
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1291,7 +2655,10 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1291
2655
|
\$extList = (\$extensions -split ',') -replace '^\\.',''
|
|
1292
2656
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue |
|
|
1293
2657
|
Where-Object {
|
|
1294
|
-
\$
|
|
2658
|
+
\$fileName = \$_.Name
|
|
2659
|
+
\$_.PSIsContainer -or (@(\$extList | Where-Object {
|
|
2660
|
+
\$fileName.EndsWith('.' + \$_, [System.StringComparison]::Ordinal)
|
|
2661
|
+
}).Count -gt 0)
|
|
1295
2662
|
}
|
|
1296
2663
|
} else {
|
|
1297
2664
|
\$items = Get-ChildItem @forceParam -Path \$globPath -ErrorAction SilentlyContinue
|
|
@@ -1302,7 +2669,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1302
2669
|
# For file/any modes with glob patterns, also add directories
|
|
1303
2670
|
# from the base directory for navigation
|
|
1304
2671
|
if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
|
|
1305
|
-
\$globDir = Split-Path -Parent \$
|
|
2672
|
+
\$globDir = Split-Path -Parent \$lookupPrefix
|
|
1306
2673
|
if (-not \$globDir) { \$globDir = '.' }
|
|
1307
2674
|
\$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
|
|
1308
2675
|
if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
|
|
@@ -1313,6 +2680,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1313
2680
|
\$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
|
|
1314
2681
|
}
|
|
1315
2682
|
|
|
2683
|
+
if (\$narrowFixedGlob) {
|
|
2684
|
+
\$items = \$items | Where-Object {
|
|
2685
|
+
if (\$_.PSIsContainer) { \$true }
|
|
2686
|
+
elseif (\$IsWindows) { \$fixedMatches -contains \$_.FullName }
|
|
2687
|
+
else { \$fixedMatches -ccontains \$_.FullName }
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
1316
2691
|
# Extract directory prefix to preserve in completion text
|
|
1317
2692
|
\$dirPrefix = if (\$prefix -and (\$prefix.Contains('/') -or \$prefix.Contains('\\'))) {
|
|
1318
2693
|
\$slashIdx = [Math]::Max(\$prefix.LastIndexOf('/'), \$prefix.LastIndexOf('\\'))
|
|
@@ -1323,10 +2698,34 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1323
2698
|
\$items | ForEach-Object {
|
|
1324
2699
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1325
2700
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
2701
|
+
\$resultType = if (\$_.PSIsContainer) { 'ProviderContainer' } else { 'ParameterValue' }
|
|
2702
|
+
# PSReadLine recognizes the platform separator for containers.
|
|
2703
|
+
# Keep the display label's existing slash convention.
|
|
2704
|
+
\$directorySuffix = [IO.Path]::DirectorySeparatorChar
|
|
2705
|
+
\$pathText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)\$directorySuffix" } else { \$completionText }
|
|
2706
|
+
# Quoting disables native tilde expansion. A home pattern
|
|
2707
|
+
# supplied by the parser must insert the discovered path.
|
|
2708
|
+
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
2709
|
+
if (\$_.PSIsContainer) { \$_.FullName + \$directorySuffix } else { \$_.FullName }
|
|
2710
|
+
} else { \$pathText }
|
|
2711
|
+
# Quoting suppresses native tilde expansion. Keep safe
|
|
2712
|
+
# ~/ paths bare; otherwise use the enumerated full path.
|
|
2713
|
+
# Keyed paths keep the typed spelling, including ~/, and
|
|
2714
|
+
# always take the quoted argument form.
|
|
2715
|
+
\$insertionText = if (\$null -ne \$quoteType) {
|
|
2716
|
+
& \$quoteCompletion (\$insertionPrefix + \$replacementText)
|
|
2717
|
+
} elseif (-not \$insertionPrefix -and \$completionText -cmatch '\\A~/[a-zA-Z0-9_./:\\\\-]*\\z') {
|
|
2718
|
+
\$pathText
|
|
2719
|
+
} else {
|
|
2720
|
+
\$argumentText = if (-not \$insertionPrefix -and (\$completionText.StartsWith('~/') -or \$completionText.StartsWith('~\\'))) {
|
|
2721
|
+
if (\$_.PSIsContainer) { \$_.FullName + \$directorySuffix } else { \$_.FullName }
|
|
2722
|
+
} else { \$pathText }
|
|
2723
|
+
& \$quoteArgument (\$insertionPrefix + \$argumentText)
|
|
2724
|
+
}
|
|
1326
2725
|
[System.Management.Automation.CompletionResult]::new(
|
|
1327
|
-
|
|
2726
|
+
\$insertionText,
|
|
1328
2727
|
\$completionText,
|
|
1329
|
-
|
|
2728
|
+
\$resultType,
|
|
1330
2729
|
\$itemType
|
|
1331
2730
|
)
|
|
1332
2731
|
}
|
|
@@ -1334,11 +2733,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1334
2733
|
# Parse literal completion: text\\tlistItemText\\tdescription
|
|
1335
2734
|
\$parts = \$line -split "\`t", 3
|
|
1336
2735
|
\$completionText = \$parts[0]
|
|
2736
|
+
# Empty display text is invalid for CompletionResult. Keep
|
|
2737
|
+
# ignoring empty suggestions without dropping later records.
|
|
2738
|
+
if (-not \$completionText) { return }
|
|
1337
2739
|
\$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
|
|
1338
2740
|
\$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
|
|
1339
2741
|
|
|
1340
2742
|
[System.Management.Automation.CompletionResult]::new(
|
|
1341
|
-
\$completionText,
|
|
2743
|
+
(& \$quoteCompletion \$completionText),
|
|
1342
2744
|
\$listItemText,
|
|
1343
2745
|
'ParameterValue',
|
|
1344
2746
|
\$toolTip
|
|
@@ -1358,7 +2760,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1358
2760
|
if (suggestion.kind === "literal") {
|
|
1359
2761
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1360
2762
|
const text = sanitizeForTransport(suggestion.text);
|
|
1361
|
-
yield `${text}\t${text}\t${description}`;
|
|
2763
|
+
yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
|
|
1362
2764
|
} else {
|
|
1363
2765
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1364
2766
|
const hidden = suggestion.includeHidden ? "1" : "0";
|