summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog5
-rw-r--r--Doc/Zsh/contrib.yo187
-rw-r--r--Doc/Zsh/manual.yo1
-rw-r--r--Functions/Chpwd/zsh_directory_name_generic151
4 files changed, 342 insertions, 2 deletions
diff --git a/ChangeLog b/ChangeLog
index ef35827d2..7055836fa 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,8 @@
+2015-09-25  Peter Stephenson  <p.w.stephenson@ntlworld.com>
+
+	* 36630: Doc/Zsh/contrib.yo, Doc/Zsh/manual.yo,
+	Functions/Chpwd/zsh_directory_name_generic: new helper function.
+
 2015-09-24  Barton E. Schaefer  <schaefer@zsh.org>
 
 	* 36623: Doc/Zsh/contrib.yo: document bracketed-paste-magic and
diff --git a/Doc/Zsh/contrib.yo b/Doc/Zsh/contrib.yo
index b966e781c..330c6f588 100644
--- a/Doc/Zsh/contrib.yo
+++ b/Doc/Zsh/contrib.yo
@@ -12,6 +12,7 @@ such as shell functions, look for comments in the function source files.
 startmenu()
 menu(Utilities)
 menu(Recent Directories)
+menu(Other Directory Functions)
 menu(Version Control Information)
 menu(Prompt Themes)
 menu(ZLE Functions)
@@ -324,7 +325,7 @@ options tt(-Uz) are appropriate.
 )
 enditem()
 
-texinode(Recent Directories)(Version Control Information)(Utilities)(User Contributions)
+texinode(Recent Directories)(Other Directory Functions)(Utilities)(User Contributions)
 cindex(recent directories, maintaining list of)
 cindex(directories, maintaining list of recent)
 findex(cdr)
@@ -585,7 +586,189 @@ avoid side effects if the change to the directory is to be invisible at the
 command line.  See the contents of the function tt(chpwd_recent_dirs) for
 more details.
 
-texinode(Version Control Information)(Prompt Themes)(Recent Directories)(User Contributions)
+texinode(Other Directory Functions)(Version Control Information)(Recent Directories)(User Contributions)
+cindex(directories, named, dynamic, helper function)
+cindex(dynamic directory naming, helper function)
+cindex(named directories, dynamic, helper function)
+findex(zsh_directory_name_generic)
+sect(Abbreviated dynamic references to directories)
+
+The dynamic directory naming system is described in the subsection
+em(Dynamic named directories) of
+ifzman(the section em(Filename Expansion) in zmanref(expn))\
+ifnzman(noderef(Filename Expansion)).  In this, a reference to
+tt(~[)var(...)tt(]) is expanded by a function found by the hooks
+mechanism.
+
+The contributed function tt(zsh_directory_name_generic) provides a
+system allowing the user to refer to directories with only a limited
+amount of new code.  It supports all three of the standard interfaces
+for directory naming: converting from a name to a directory, converting
+in the reverse direction to find a short name, and completion of names.
+
+The main feature of this function is a path-like syntax,
+combining abbreviations at multiple levels separated by ":".
+As an example, ~[g:p:s] might specify:
+startitem()
+item(tt(g))(
+The top level directory for your git area.  This first component
+has to match, or the function will retrun indicating another
+directory name hook function should be tried.
+)
+item(tt(p))(
+The name of a project within your git area.
+)
+item(tt(s))(
+The source area within that project.
+)
+enditem()
+This allows you to collapse references to long hierarchies to a very
+compact form, particularly if the hierarchies are similar across different
+areas of the disk.
+
+Name components may be completed: if a description is shown at the top
+of the list of completions, it includes the path to which previous
+components expand, while the description for an individual completion
+shows the path segment it would add.  No additional configuration is
+needed for this as the completion system is aware of the dynamic
+directory name mechanism.
+
+subsect(Usage)
+
+To use the function, first define a wrapper function for your specific
+case.  We'll assume it's to be autoloaded.  This can have any name but
+we'll refer to it as zdn_mywrapper.  This wrapper function will define
+various variables and then call this function with the same arguments
+that the wrapper function gets.  This configuration is described below.
+
+Then arrange for the wrapper to be run as a zsh_directory_name hook:
+
+example(autoload -Uz add-zsh-hook zsh_diretory_name_generic zdn_mywrapper
+add-zsh-hook -U zsh_directory_name zdn_mywrapper)
+
+subsect(Configuration)
+
+The wrapper function should define a local associative array zdn_top.
+Alternatively, this can be set with a style called tt(mapping).  The
+context for the style is tt(:zdn:)var(wrapper-name) where
+var(wrapper-name) is the function calling zsh_directory_name_generic;
+for example:
+
+example(zstyle :zdn:zdn_mywrapper: mapping zdn_mywrapper_top)
+
+The keys in this associative array correspond to the first component of
+the name.  The values are matching directories.  They may have an
+optional suffix with a slash followed by a colon and the name of a
+variable in the same format to give the next component.  (The slash
+before the colon is to disambiguate the case where a colon is needed in
+the path for a drive.  There is otherwise no syntax for escaping this,
+so path components whose names start with a colon are not supported.)  A
+special component tt(:default:) specifies a variable in the form
+tt(/:)var(var) (the path section is ignored and so is usually empty)
+that will be used for the next component if no variable is given for the
+path.  Variables referred to within tt(zdn_top) have the same format as
+tt(zdn_top) itself, but contain relative paths.
+
+For example,
+
+example(local -A zdn_top=(
+  g   ~/git
+  ga  ~/alternate/git
+  gs  /scratch/$USER/git/:second2
+  :default: /:second1
+))
+
+This specifies the behaviour of a directory referred to as tt(~[g:...])
+or tt(~[ga:...]) or tt(~[gs:...]).  Later path components are optional;
+in that case tt(~[g]) expands to tt(~/git), and so on.  tt(gs) expands
+to tt(/scratch/$USER/git) and uses the associative array tt(second2) to
+match the second component; tt(g) and tt(ga) use the associative array
+tt(second1) to match the second component.
+
+When expanding a name to a directory, if the first component is not tt(g) or
+tt(ga) or tt(gs), it is not an error; the function simply returns 1 so that a
+later hook function can be tried.  However, matching the first component
+commits the function, so if a later component does not match, an error
+is printed (though this still does not stop later hooks from being
+executed).
+
+For components after the first, a relative path is expected, but note that
+multiple levels may still appear.  Here is an example of tt(second1):
+
+example(local -A second1=(
+  p   myproject
+  s   somproject
+  os  otherproject/subproject/:third
+))
+
+The path as found from tt(zdn_top) is extended with the matching
+directory, so tt(~[g:p]) becomes tt(~/git/myproject).  The slash between
+is added automatically (it's not possible to have a later component
+modify the name of a directory already matched).  Only tt(os) specifies
+a variable for a third component, and there's no tt(:default:), so it's
+an error to use a name like tt(~[g:p:x]) or tt(~[ga:s:y]) because
+there's nowhere to look up the tt(x) or tt(y).
+
+The associative arrays need to be visible within this function; the
+generic function therefore uses internal variable names beginning
+tt(_zdn_) in order to avoid clashes.  Note that the variable tt(reply)
+needs to be passed back to the shell, so should not be local in the
+calling function.
+
+The function does not test whether directories assembled by component
+actually exist; this allows the system to work across automounted
+file systems.  The error from the command trying to use a non-existent
+directory should be sufficient to indicate the problem.
+
+subsect(Complete example)
+
+Here is a full fictitious but usable autoloadable definition of the
+example function defined by the code above.  So tt(~[gs:p:s]) expands
+to tt(/scratch/$USER/git/myscratchproject/top/srcdir) (with tt($USER)
+also expanded).
+
+example(local -A zdn_top=(
+  g   ~/git
+  ga  ~/alternate/git
+  gs  /scratch/$USER/git/:second2
+  :default: /:second1
+)
+
+local -A second1=(
+  p   myproject
+  s   somproject
+  os  otherproject/subproject/:third
+)
+
+local -A second2=(
+  p   myscratchproject
+  s   somescratchproject
+)
+
+local -A third=(
+  s   top/srcdir
+  d   top/documentation
+)
+
+# autoload not needed if you did this at initialisation...
+autoload -Uz zsh_directory_name_generic
+zsh_directory_name_generic "$@)
+
+It is also possible to use global associative arrays, suitably named,
+and set the style for the context of your wrapper function to
+refer to this.  Then your set up code would contain the following:
+
+example(typeset -A zdn_mywrapper_top=(...)
+# ... and so on for other associative arrays ...
+zstyle ':zdn:zdn_mywrapper:' mapping zdn_mywrapper_top
+autoload -Uz add-zsh-hook zsh_directory_name_generic zdn_mywrapper
+add-zsh-hook -U zsh_directory_name zdn_mywrapper)
+
+and the function tt(zdn_mywrapper) would contain only the following:
+
+example(zsh_directory_name_generic "$@")
+
+texinode(Version Control Information)(Prompt Themes)(Other Directory Functions)(User Contributions)
 sect(Gathering information from version control systems)
 cindex(version control utility)
 
diff --git a/Doc/Zsh/manual.yo b/Doc/Zsh/manual.yo
index 86c72c0ff..119849e4c 100644
--- a/Doc/Zsh/manual.yo
+++ b/Doc/Zsh/manual.yo
@@ -164,6 +164,7 @@ User Contributions
 
 menu(Utilities)
 menu(Recent Directories)
+menu(Other Directory Functions)
 menu(Version Control Information)
 menu(Prompt Themes)
 menu(ZLE Functions)
diff --git a/Functions/Chpwd/zsh_directory_name_generic b/Functions/Chpwd/zsh_directory_name_generic
new file mode 100644
index 000000000..9430c95e4
--- /dev/null
+++ b/Functions/Chpwd/zsh_directory_name_generic
@@ -0,0 +1,151 @@
+## zsh_directory_name_generic
+#
+# This function is useful as a hook function for the zsh_directory_name
+# facility.
+#
+# See the zsh-contrib manual page for more.
+
+emulate -L zsh
+setopt extendedglob
+local -a match mbegin mend
+
+# The variable containing the top level mapping.
+local _zdn_topvar
+
+zmodload -i zsh/parameter
+zstyle -s ":zdn:${funcstack[2]}:" mapping _zdn_topvar || _zdn_topvar=zdn_top
+
+if (( ! ${(P)#_zdn_topvar} )); then
+  print -r -- "$0: $_zdn_topver is not set" >&2
+  return 1
+fi
+
+local _zdn_var=$_zdn_topvar
+local -A _zdn_assoc
+
+if [[ $1 = n ]]; then
+  # Turning a name into a directory.
+  local _zdn_name=$2
+  local -a _zdn_words
+  local _zdn_dir _zdn_cpt
+
+  _zdn_words=(${(s.:.)_zdn_name})
+  while (( ${#_zdn_words} )); do
+    if [[ -z ${_zdn_var} ]]; then
+      print -r -- "$0: too many components in directory name \`$_zdn_name'" >&2
+      return 1
+    fi
+
+    # Subscripting (P)_zdn_var directly seems not to work.
+    _zdn_assoc=(${(Pkv)_zdn_var})
+    _zdn_cpt=${_zdn_assoc[${_zdn_words[1]}]}
+    shift _zdn_words
+
+    if [[ -z $_zdn_cpt ]]; then
+      # If top level component, just try another expansion
+      if [[ $_zdn_var != $_zdn_top ]]; then
+	# Committed to this expansion, so report failure.
+	print -r -- "$0: no expansion for directory name \`$_zdn_name'" >&2
+      fi
+      return 1
+    fi
+    if [[ $_zdn_cpt = (#b)(*)/:([[:IDENT:]]##) ]]; then
+      _zdn_cpt=$match[1]
+      _zdn_var=$match[2]
+    else
+      # may be empty
+      _zdn_var=${${_zdn_assoc[:default:]}##*/:}
+    fi
+    _zdn_dir=${_zdn_dir:+$_zdn_dir/}$_zdn_cpt
+  done
+  if (( ${#_zdn_dir} )); then
+    typeset -ag reply
+    reply=($_zdn_dir)
+    return 0
+  fi
+elif [[ $1 = d ]]; then
+  # Turning a directory into a name.
+  local _zdn_dir=$2
+  local _zdn_rest=$_zdn_dir
+  local -a _zdn_cpts
+  local _zdn_pref _zdn_pref_raw _zdn_matched _zdn_cpt _zdn_name
+
+  while [[ -n $_zdn_var && -n $_zdn_rest ]]; do
+    _zdn_assoc=(${(Pkv)_zdn_var})
+    # Sorting in descending order will ensure prefixes
+    # come after longer strings with that perfix, so
+    # we match more specific directory names preferentially.
+    _zdn_cpts=(${(Ov)_zdn_assoc})
+    _zdn_cpt=''
+    for _zdn_pref_raw in $_zdn_cpts; do
+      _zdn_pref=${_zdn_pref_raw%/:*}
+      [[ -z $_zdn_pref ]] && continue
+      if [[ $_zdn_rest = $_zdn_pref(#b)(/|)(*) ]]; then
+	_zdn_cpt=${(k)_zdn_assoc[(r)$_zdn_pref_raw]}
+	# if we matched a /, too, add it...
+	_zdn_matched+=$_zdn_pref$match[1]
+	_zdn_rest=$match[2]
+	break
+      fi
+    done
+    if [[ -n $_zdn_cpt ]]; then
+      _zdn_name+=${_zdn_name:+${_zdh_name}:}$_zdn_cpt
+      if [[ ${_zdn_assoc[$_zdn_cpt]} = (#b)*/:([[:IDENT:]]##) ]]; then
+	_zdn_var=$match[1]
+      else
+	_zdn_var=${${_zdn_assoc[:default:]}##*/:}
+      fi
+    else
+      break
+    fi
+  done
+  if [[ -n $_zdn_name ]]; then
+    # matched something, so report that.
+    integer _zdn_len=${#_zdn_matched}
+    [[ $_zdn_matched[-1] = / ]] && (( _zdn_len-- ))
+    typeset -ag reply
+    reply=($_zdn_name $_zdn_len)
+    return 0
+  fi
+  # else let someone else have a go.
+elif [[ $1 = c ]]; then
+  # Completion
+
+  if [[ -n $SUFFIX ]]; then
+    _message "Can't complete in the middle of a dynamic directory name"
+  else
+    local -a _zdn_cpts
+    local _zdn_word _zdn_cpt _zdn_desc _zdn_sofar expl
+
+    while [[ -n ${_zdn_var} && ${PREFIX} = (#b)([^:]##):* ]]; do
+      _zdn_word=$match[1]
+      compset -P '[^:]##:'
+      _zdn_assoc=(${(Pkv)_zdn_var})
+      _zdn_cpt=${_zdn_assoc[$_zdn_word]}
+      # We only complete at the end so must match here
+      [[ -z $_zdn_cpt ]] && return 1
+      if [[ $_zdn_cpt = (#b)(*)/:([[:IDENT:]]##) ]]; then
+	_zdn_cpt=$match[1]
+	_zdn_var=$match[2]
+      else
+	_zdn_var=${${_zdn_assoc[:default:]}##*/:}
+      fi
+      _zdn_sofar+=${_zdn_sofar:+${_zdn_sofar}/}$_zdn_cpt
+    done
+    if [[ -n $_zdn_var ]]; then
+      _zdn_assoc=(${(Pkv)_zdn_var})
+      local -a _zdn_cpts
+      for _zdn_cpt _zdn_desc in ${(kv)_zdn_assoc}; do
+	[[ $_zdn_cpt = :* ]] && continue
+	_zdn_cpts+=(${_zdn_cpt}:${_zdn_desc%/:[[:IDENT:]]##})
+      done
+      _describe -t dirnames "directory name under ${_zdn_sofar%%/}" \
+	_zdn_cpts -S: -r ':]'
+      return
+    fi
+  fi
+fi
+
+# Failed
+return 1
+## end