#autoload # Utility function for in-path completion. # Supported arguments are: `-f', `-/', `-g ', `-J ', # `-V ', `-W paths', `-X explanation', `-P prefix', `-S suffix', # `-q', `-r remove-chars', `-R remove-func', and `-F '. All but # the last have the same syntax and meaning as for `compgen' or # `compadd', respectively. The `-F ' 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' 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]" [[ -z "$testpath" && "$linepath[-1]" = / ]] && linepath="$linepath[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] ]]