From 649d06a8cdd9948e3d49fa717f9088d8a87a6262 Mon Sep 17 00:00:00 2001 From: Peter Stephenson Date: Fri, 25 Sep 2015 21:30:34 +0100 Subject: 36630: new function zsh_directory_name_generic --- ChangeLog | 5 + Doc/Zsh/contrib.yo | 187 ++++++++++++++++++++++++++++- Doc/Zsh/manual.yo | 1 + Functions/Chpwd/zsh_directory_name_generic | 151 +++++++++++++++++++++++ 4 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 Functions/Chpwd/zsh_directory_name_generic diff --git a/ChangeLog b/ChangeLog index ef35827d2..7055836fa 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2015-09-25 Peter Stephenson + + * 36630: Doc/Zsh/contrib.yo, Doc/Zsh/manual.yo, + Functions/Chpwd/zsh_directory_name_generic: new helper function. + 2015-09-24 Barton E. Schaefer * 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 -- cgit 1.4.1