1#autoload
2
3# This completion function completes all paths given to it, and also tries to
4# offer completions which point to the same file as one of the paths given
5# (relative path when an absolute path is given, and vice versa; when ..'s are
6# present in the word to be completed, and some paths got from symlinks.
7
8# Usage: _canonical_paths [-A var] [-N] [-MJV12nfX] tag desc paths...
9
10# -A, if specified, takes the paths from the array variable specified. Paths can
11# also be specified on the command line as shown above. -N, if specified,
12# prevents canonicalizing the paths given before using them for completion, in
13# case they are already so. `tag' and `desc' arguments are well, obvious :) In
14# addition, the options -M, -J, -V, -1, -2, -n, -F, -X are passed to compadd.
15
16_canonical_paths_pwd() {
17  # Get the canonical directory name by changing to it.
18  integer chaselinks
19  [[ -o chaselinks ]] && (( chaselinks = 1 ))
20  setopt localoptions nopushdignoredups chaselinks
21  if builtin pushd -q -- $1 2>/dev/null; then
22    REPLY=$PWD
23    (( chaselinks )) || unsetopt chaselinks
24    builtin popd -q
25  else
26    REPLY=$1
27  fi
28}
29
30_canonical_paths_get_canonical_path() {
31  typeset newfile nondir
32  typeset -A seen
33
34  REPLY=$1
35  # Canonicalise the directory path.  We may not be able to
36  # do this if we can't read all components.
37  if [[ -d $REPLY ]]; then
38    _canonical_paths_pwd $REPLY
39  else
40    # Resolve any trailing symbolic links, guarding against loops.
41    while [[ -z ${seen[$REPLY]} ]]; do
42      seen[$REPLY]=1
43      newfile=()
44      zstat -A newfile +link $REPLY 2>/dev/null
45      if [[ -n $newfile[1] ]]; then
46	REPLY=$newfile[1]
47      else
48	break
49      fi
50    done
51    if [[ $REPLY = */*[^/] && $REPLY != /[^/]# ]]; then
52      # Don't try this if there's a trailing slash or we're in
53      # the root directory.
54      nondir=${REPLY##*/#}
55      _canonical_paths_pwd ${REPLY%/#*}
56      REPLY+="/$nondir"
57    fi
58  fi
59}
60
61_canonical_paths_add_paths () {
62  local origpref=$1 expref rltrim curpref canpref subdir
63  [[ $2 != add ]] && matches=()
64  expref=${~origpref}
65  [[ $origpref == (|*/). ]] && rltrim=.
66  curpref=${${expref%$rltrim}:-./}
67  if zstat $curpref >&/dev/null; then
68    _canonical_paths_get_canonical_path $curpref
69    canpref=$REPLY
70  else
71    canpref=$curpref
72  fi
73  [[ $curpref == */ && $canpref == *[^/] ]] && canpref+=/
74  canpref+=$rltrim
75  [[ $expref == *[^/] && $canpref == */ ]] && origpref+=/
76  matches+=(${${(M)files:#$canpref*}/$canpref/$origpref})
77  for subdir in $expref?*(@); do
78    _canonical_paths_add_paths ${subdir/$expref/$origpref} add
79  done
80}
81
82_canonical_paths() {
83  local __index
84  typeset -a __gopts __opts
85
86  zparseopts -D -a __gopts M: J: V: 1 2 n F: X: A:=__opts N=__opts
87
88  : ${1:=canonical-paths} ${2:=path}
89
90  __index=$__opts[(I)-A]
91  (( $__index )) && set -- $@ ${(P)__opts[__index+1]}
92
93  local expl ret=1 tag=$1 desc=$2
94
95  shift 2
96
97  if ! zmodload -F zsh/stat b:zstat 2>/dev/null; then
98    _wanted "$tag" expl "$desc" compadd $__gopts $@ && ret=0
99    return ret
100  fi
101
102  typeset REPLY
103  typeset -a matches files
104
105  if (( $__opts[(I)-N] )); then
106    files=($@)
107  else
108    for __index in $@; do
109      _canonical_paths_get_canonical_path $__index
110      files+=($REPLY)
111    done
112  fi
113
114  local base=$PREFIX
115  typeset -i blimit
116
117  _canonical_paths_add_paths $base
118
119  if [[ -z $base ]]; then
120    _canonical_paths_add_paths / add
121  elif [[ $base == ..(/.(|.))#(|/) ]]; then
122
123    # This style controls how many parent directory links (..) to chase searching
124    # for possible completions. The default is 8. Note that this chasing is
125    # triggered only when the user enters atleast a .. and the path completed
126    # contains only . or .. components. A value of 0 turns off .. link chasing
127    # altogether.
128
129    zstyle -s ":completion:${curcontext}:$tag" \
130      canonical-paths-back-limit blimit || blimit=8
131
132    if [[ $base != */ ]]; then
133      [[ $base != *.. ]] && base+=.
134      base+=/
135    fi
136    until [[ $base.. -ef $base || blimit -le 0 ]]; do
137      base+=../
138      _canonical_paths_add_paths $base add
139      blimit+=-1
140    done
141  fi
142
143  _wanted "$tag" expl "$desc" compadd $__gopts -Q -U -a matches && ret=0
144
145  return ret
146}
147
148_canonical_paths "$@"
149