about summary refs log tree commit diff
path: root/Completion/Core/_path_files
blob: 535ba537cadc1387a64be2ea6ee7ab0a82bfbeee (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
#autoload

# Utility function for in-path completion.
# Supported arguments are: `-f', `-/', `-g <patterns>', `-J <group>',
# `-V <group>', `-W paths', `-X explanation', `-P prefix', `-S suffix',
# `-q', `-r remove-chars', `-R remove-func', and `-F <ignore>'. All but 
# the last have the same syntax and meaning as for `compgen' or
# `compadd', respectively. The `-F <ignore>' option may be used to give
# a list of suffixes either by giving the name of an array or
# literally by giving them in a string surrounded by
# parentheses. Files with one of the suffixes thus given are treated
# like files with one of the suffixes in the `fignore' array in normal
# completion.
#
# This function uses the helper functions `_match_test' and `_match_pattern'.

# First see if we should generate matches for the global matcher in use.

_match_test _path_files || return 1

# Yes, so...

local nm prepaths str linepath realpath donepath patstr prepath testpath rest
local tmp1 collect tmp2 suffixes i ignore matchflags opt group sopt pats gopt
local addpfx addsfx expl orig ostr nm=$compstate[nmatches] menu remsfx patlast
local origflags mflags

setopt localoptions nullglob rcexpandparam extendedglob
unsetopt markdirs globsubst shwordsplit nounset

prepaths=('')
ignore=()
group=()
sopt='-'
gopt=''
pats=()
addpfx=()
addsfx=()
remsfx=()
expl=()

# Get the options.

while getopts "P:S:qr:R:W:F:J:V:X:f/g:" opt; do
  case "$opt" in
  P)     addpfx=(-P "$OPTARG")
         ;;
  S)     addsfx=(-S "$OPTARG")
         ;;
  q)     tmp1=yes
         ;;
  [rR])  remsfx=("-$opt" "$OPTARG")
         ;;
  W)     tmp1="$OPTARG"
         if [[ "$tmp1[1]" = '(' ]]; then
           prepaths=( ${^=tmp1[2,-2]}/ )
         else
           prepaths=( ${(P)=${tmp1}} )
           (( ! $#prepaths )) && prepaths=( ${tmp1}/ )
         fi
         (( ! $#prepaths )) && prepaths=( '' )
         ;;
  F)     tmp1="$OPTARG"
         if [[ "$tmp1[1]" = '(' ]]; then
           ignore=( ${^=tmp1[2,-2]}/ )
         else
           ignore=( ${(P)${tmp1}} )
         fi
	 (( $#ignore )) && ignore=(-F "( $ignore )")
         ;;
  [JV])  group=("-$opt" "$OPTARG")
         ;;
  X)     expl=(-X "$OPTARG")
         ;;
  f)     sopt="${sopt}f"
         pats=("$pats[@]" '*')
	 ;;
  /)     sopt="${sopt}/"
         pats=("$pats[@]" '*(-/)')
	 ;;
  g)     gopt='-g'
         pats=("$pats[@]" ${=OPTARG})
	 ;;
  esac
done

[[ -n "$tmp1" && $#addsfx -ne 0 ]] && addsfx[1]=-qS

# If we were given no file selection option, we behave as if we were given
# a `-f'.

if [[ "$sopt" = - ]]; then
  if [[ -z "$gopt" ]]; then
    sopt='-f'
    pats=('*')
  else
    unset sopt
  fi
fi

# str holds the whole string from the command line with a `*' between
# the prefix and the suffix. Then we see if we will do menucompletion.

if [[ $#compstate[pattern_match] -ne 0 ]]; then
  str="${PREFIX}*${SUFFIX}"
else
  str="${PREFIX:q}*${SUFFIX:q}"
  [[ "$str" = \\\~* ]] && str="$str[2,-1]"
fi
orig="${PREFIX}${SUFFIX}"

[[ $compstate[insert] = *menu || -n "$_comp_correct" ||
   ( $#compstate[pattern_match] -ne 0 &&
     "$orig" != "${orig:q}" ) ]] && menu=yes


# We will first try normal completion called with `compgen', but only if we
# weren't given a `-F', `-r', or `-R' option.

if [[ $#ignore -eq 0 && $#remsfx -eq 0 && -z "$_comp_correct" ]]; then
  # First build an array containing the `-W' option, if there is any and we
  # want to use it. We don't want to use it if the string from the command line
  # is a absolute path or relative to the current directory.

  if [[ -z "$tmp1[1]" || "$str[1]" = [~/] || "$str" = (.|..)/* ]]; then
    tmp1=()
  else
    tmp1=(-W "( $prepaths )")
  fi

  # Now call compgen.

  nm=$compstate[nmatches]
  if [[ -z "$gopt" ]]; then
    compgen "$addpfx[@]" "$addsfx[@]" "$group[@]" "$expl[@]" "$tmp1[@]" $sopt
  else
    compgen "$addpfx[@]" "$addsfx[@]" "$group[@]" "$expl[@]" "$tmp1[@]" $sopt -g "$pats"
  fi

  # If this generated any matches, we don't want to do in-path completion.

  [[ compstate[nmatches] -eq nm ]] || return 0
fi

# No `-F' option, so we want to use `fignore'.

(( $#ignore )) || ignore=(-F fignore)

# Now let's have a closer look at the string to complete.

if [[ "$str[1]" = \~ ]]; then
  # It begins with `~', so remember anything before the first slash to be able
  # to report it to the completion code. Also get an expanded version of it
  # (in `realpath'), so that we can generate the matches. Then remove that
  # prefix from the string to complete, set `donepath' to build the correct
  # paths and make sure that the loop below is run only once with an empty
  # prefix path by setting `prepaths'.
  
  linepath="${str%%/*}/"
  eval realpath\=$linepath
  [[ "$realpath" = "$linepath" ]] && return 1
  str="${str#*/}"
  orig="${orig#*/}"
  donepath=''
  prepaths=( '' )
else
  # If the string does not start with a `~' we don't remove a prefix from the
  # string.

  linepath=''
  realpath=''

  if [[ "$str[1]" = / ]]; then
    # If it is a absolut path name, we remove the first slash and put it in
    # `donepath' meaning that we treat it as the path that was already handled.
    # Also, we don't use the paths from `-W'.

    str="$str[2,-1]"
    orig="$orig[2,-1]"
    donepath='/'
    prepaths=( '' )
  else
    # The common case, we just use the string as it is, unless it begins with
    # `./' or `../' in which case we don't use the paths from `-W'.
    
    [[ "$str" = (.|..)/* ]] && prepaths=( '' )
    donepath=''
  fi
fi

# Now build the glob pattern by calling `_match_pattern'.
patstr="$str"
matchflags=""
_match_pattern _path_files patstr matchflags
origflags="$matchflags"
[[ -n "$_comp_correct" ]] && matchflags="$matchflags(#a$_comp_correct)"

# We almost expect the pattern to have changed `..' into `*.*.', `/.' into
# `/*.', and probably to contain two or more consecutive `*'s. Since these
# have special meaning for globbing, we remove them. But before that, we
# add the pattern for matching any characters before a slash.

patstr="$patstr:gs-/-*/-:gs/*.*./../:gs-/*.-/.-:gs/**/*/:gs-.*/-./-"

# We take the last pathname component from the pattern and store it in
# `patlast', replacing `*'s in it with patterns that match any character
# but not slashes. Later we will generate matches using `patstr' with the
# patterns we were given (like `*.c') appended to it, producing all matching
# files. These filenames are then compared to `patlast' and all names not
# matching that will be removed. All this is needed to be able to correctly
# support `completeinword' as otherwise we would have something like `a*x'
# from the line (the `*' was inserted above) and appending the `-g' pattern
# `*.tex' would yield `a*x*.tex' which is not what we want.

if [[ "$patstr" = */* ]]; then
  if [[ -n "$_comp_correct" && "${#orig##*/}" -le _comp_correct ]]; then
    patlast="*/${origflags}${${patstr##*/}//\*/[^/]#}"
  else
    patlast="*/${matchflags}${${patstr##*/}//\*/[^/]#}"
  fi
  patstr="${patstr%/*}/"
else
  if [[ -n "$_comp_correct" && "$#orig" -le _comp_correct ]]; then
    patlast="${origflags}${patstr//\*/[^/]#}"
  else
    patlast="${matchflags}${patstr//\*/[^/]#}"
  fi
  patstr=""
fi

# First we skip over all pathname components in `str' which really exist in
# the file-system, so that `/usr/lib/l<TAB>' doesn't offer you `lib' and
# `lib5'. Pathname components skipped this way are taken from `orig' and added
# to `donepath'.

while [[ "$orig" = */* ]] do
  tmp1=( $realpath$donepath${orig%%/*}/${~matchflags}${~patstr#*/}$^pats )
  tmp1=("${(@M)tmp1:#$~patlast}")
  [[ $#tmp1 -gt 0 && -e "$realpath$donepath${orig%%/*}" ]] || break
  donepath="$donepath${orig%%/*}/"
  orig="${orig#*/}"
  patstr="${patstr#*/}"
done

# Finally, generate the matches. First we loop over all the paths from `-W'.
# Note that in this loop `str' is used as a modifyable version of `patstr'
# and `testpath' is a modifyable version of `donepath'.

for prepath in "$prepaths[@]"; do
  str="$patstr"
  testpath="$donepath"
  ostr="$orig"

  [[ -z "$prepath" || "$prepath[-1]" = / ]] || prepath="${prepath}/"

  # The second loop tests the components of the path in `str' to get the
  # possible matches.

  while [[ "$str" = */* ]] do
    # `rest' is the pathname after the first slash that is left. In `tmp1'
    # we get the globbing matches for the pathname component currently
    # handled.

    if [[ -n "$_comp_correct" && "${#ostr%%/*}" -le _comp_correct ]]; then
      mflags="$origflags"
    else
      mflags="$matchflags"
    fi
    rest="${str#*/}"
    tmp1="${prepath}${realpath}${testpath}${~mflags}${str%%/*}(-/)"
    tmp1=( $~tmp1 )

    if [[ $#tmp1 -eq 0 ]]; then
      # If this didn't produce any matches, we don't need to test this path
      # any further, so continue with the next `-W' path, if any.

      continue 2
    elif [[ $#tmp1 -gt 1 ]]; then
      # If it produced more than one match, we want to remove those which
      # don't have possible following pathname components matching the 
      # rest of the string we are completing. (The case with only one
      # match is handled below.)
      # In `collect' we will collect those of the produced pathnames that
      # have a matching possible path-suffix. In `suffixes' we build an
      # array containing strings build from the rest of the string to 
      # complete and the glob patterns we were given as arguments.

      collect=()
      suffixes=( $rest$^pats )
      suffixes=( "${(@)suffixes:gs.**.*.}" )

      if [[ -n "$_comp_correct" && "${#ostr#*/}" -le _comp_correct ]]; then
        mflags="$origflags"
      else
        mflags="$matchflags"
      fi

      # In the loop the prefixes from the `tmp1' array produced above and
      # the suffixes we just built are used to produce possible matches
      # via globbing.

      for i in "$tmp1[@]" ; do
        tmp2=( ${~i}/${~mflags}${~suffixes} )
        tmp2=("${(@M)tmp2:#$~patlast}")
        [[ $#tmp2 -ne 0 ]] && collect=( $collect $i )
      done

      # If this test showed that none of the matches from the glob in `tmp1'
      # has a possible sub-path matching what's on the line, we add the
      # matches found in `tmp1' and otherwise give up and continue with the
      # next `-W' path.

      if [[ $#collect -eq 0 ]]; then
        continue 2
      elif [[ $#collect -ne 1 ]]; then
        # If we have more than one possible match, this means that the
	# pathname component currently handled is ambiguous, so we give
	# it to the completion code.
	# First we build the full path prefix in `tmp1'.

        tmp1="$prepath$realpath$testpath"

	# Now produce all matching pathnames in `collect'.

        collect=( ${~collect}/${~matchflags}${~suffixes} )
	collect=("${(@M)collect:#$~patlast}")

	# And then remove the common path prefix from all these matches.

        collect=( ${collect#$tmp1} )

	# Finally, we add all these matches with the common (unexpanded)
	# pathprefix (the `-p' option), the path-prefix (the `-W' option)
	# to allow the completion code to test file type, and the path-
	# suffix (the `-s' option). We also tell the completion code that
	# these are file names and that `fignore' should be used as usual
	# (the `-f' and `-F' options).

	if [[ -n "$menu" ]]; then
          compadd -QU "$addpfx[@]" "$addsfx[@]" "$group[@]" "$expl[@]" \
                  -i "$IPREFIX" -p "$linepath${testpath:q}" \
		  -s "/${ostr#*/}" \
		  -W "$tmp1" -f "$ignore[@]" - "${(@)${(@)collect%%/*}:q}"
	else
          for i in $collect; do
            compadd -QU "$addpfx[@]" "$addsfx[@]" "$group[@]" "$expl[@]" \
	            -i "$IPREFIX" -p "$linepath${testpath:q}" -s "/${${i#*/}:q}" \
		    -M 'r:|/=*' -W "$tmp1" -f "$ignore[@]" - "${${i%%/*}:q}"
          done
	fi

	# We have just finished handling all the matches from above, so we
	# can continue with the next `-W' path.

	continue 2
      fi

      # We reach this point if only one of the path prefixes in `tmp1'
      # has a existing path-suffix matching the string from the line.
      # In this case we accept this match and continue with the next
      # path-name component.

      tmp1=( "$collect[1]" )
    elif [[ -n "$_comp_correct" && "$mflags" = "$matchflags" ]]; then

      # If we got only one match with auto-correction and if we get none
      # without correction, stop now.

      tmp2="${prepath}${realpath}${testpath}${~origflags}${str%%/*}(-/)"
      tmp2=( $~tmp2 )

      if [[ $#tmp1 -ne $#tmp2 ]]; then
        compadd -QU "$addpfx[@]" -S '' "$group[@]" "$expl[@]" \
                -i "$IPREFIX" -p "$linepath${testpath:q}" -s "/${ostr#*/}" \
		- "${${tmp1#${prepath}${realpath}${testpath}}:q}"
        continue 2
      fi
    fi
    # This is also reached if the first globbing produced only one match
    # in this case we just continue with the next pathname component, too.

    tmp1="$tmp1[1]"
    testpath="$testpath${tmp1##*/}/"
    str="$rest"
    ostr="${ostr#*/}"
  done

  # We are here if all pathname components except the last one (which is still
  # not tested) are unambiguous. So we add matches with the full path prefix, 
  # no path suffix, the `-W' we are currently handling, all the matches we
  # can produce in this directory, if any.

  if [[ -n "$_comp_correct" && "${#ostr#*/}" -le _comp_correct ]]; then
    mflags="$origflags"
  else
    mflags="$matchflags"
  fi
  tmp1="$prepath$realpath$testpath"
  suffixes=( $str$^pats )
  suffixes=( "${(@)suffixes:gs.**.*.}" )
  tmp2=( ${~tmp1}${~matchflags}${~suffixes} )
  tmp2=("${(@M)tmp2:#$~patlast}")

  if [[ $#tmp2 -eq 0 ]]; then
    # No match, insert the expanded path and add the original tail.

    [[ "$testpath[-1]" = / ]] && testpath="$testpath[1,-2]"
    [[ -n "$ostr" && -n "$linepath$testpath" ]] && ostr="/$ostr"

    # But only if something changed.
    [[ "$linepath$testpath$ostr" = "$PREFIX$SUFFIX" ]] && return 1

    compadd -QU -S '' "$group[@]" "$expl[@]" \
            -i "$IPREFIX" -f - "$linepath${testpath:q}$ostr"
  else
    compadd -QU "$addpfx[@]" "$addsfx[@]" "$remsfx[@]" "$group[@]" "$expl[@]" \
            -i "$IPREFIX" -p "$linepath${testpath:q}" -f "$ignore[@]" \
	    -W "$prepath$realpath$testpath" - "${(@)${(@)tmp2#$tmp1}:q}"
  fi
done

# This sets the return value to indicate that we added matches (or not).

[[ nm -ne compstate[nmatches] ]]