Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
Dynamic directory name function
- X-seq: zsh-users 20625
- From: Peter Stephenson <p.w.stephenson@xxxxxxxxxxxx>
- To: Zsh Users <zsh-users@xxxxxxx>
- Subject: Dynamic directory name function
- Date: Tue, 22 Sep 2015 20:42:51 +0100
- List-help: <mailto:zsh-users-help@zsh.org>
- List-id: Zsh Users List <zsh-users.zsh.org>
- List-post: <mailto:zsh-users@zsh.org>
- Mailing-list: contact zsh-users-help@xxxxxxx; run by ezmlm
For some time now, I've had a hook function for the zsh_directory_name
mechanism that allows me to refer to directory hierarchies in a brief
fashion. For example ~[g:p:s] may mean the directory is in my main git
client, in a certain project, and in the source directory of that
project. So if I have multiple git areas and projects with similar
structure I can cover a lot of ground very easily.
For almost as long I've been meaning to turn this into a generic
function that you can configure just with a few variables. I've finally
done that, and this has allowed me to add the completion support that
was missing before.
The documentation should give enough indication of whether this is
useful or not.
I expect I'll add some version of this to the distribution at some
point.
pws
## zsh_directory_name_generic
#
# This function is useful as a hook function for the zsh_directory_name
# facility. It provides dynamic directory naming in both directions,
# i.e. from name to directory for use in ~[...] and from directory to
# name for use in prompts etc., as well as completion.
#
# 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:
# 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.
# p - The name of a project within your git area.
# s - The source area within that project.
# 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.
#
# To use it,
# - 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_wrapper. 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.
# - Arrange for the wrapper to be run as a zsh_directory_name hook:
# autoload -Uz add-zsh-hook zsh_diretory_name_generic zdn_wrapper
# add-zsh-hook -U zsh_directory_name zdn_wrapper
#
# Configuration:
#
# The wrapper function should define a local associative array zdn_top.
# The keys in this 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 :default: specifies a variable in the form /: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 zdn_top have the same format as zdn_top.
#
# For 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 ~[g:...] or
# ~[ga:...] or ~[gs:...]. Later path components are optional; in that
# case ~[g] expands to ~/git, and so on. gs expands to /scratch/$USER/git
# and uses the associative array second2 to match the second component;
# g and ga use the associative array second1 to match the second component.
#
# When expanding a name to a directory, if the first component is not g
# or ga or 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 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 second1:
#
# local -A second1=(
# p myproject
# s somproject
# os otherproject/subproject/:third
# )
#
# The path as found from zdn_top is extended with the matching directory,
# so ~[g:p] becomes ~/git/myproject. The "/" between is added automatically
# (it's not possible to have a later component modify the name of a directory
# already matched). Only "os" specifies a variable for a third component,
# and there's no :default:, so it's an error to use a name like ~[g:p:x]
# or ~[ga:s:y] because there's nowhere to look up the x or y.
#
# The associative arrays need to be visible within this function; the
# function therefore uses internal variable names beginning _zdn_ in
# order to avoid clashes. Note that the variable "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.
#
# Here is a full fictitious but usable autoloadable definition of the
# example function defined by the code above. So ~[gs:p:s] expands
# to /scratch/$USER/git/myscratchproject/top/srcdir.
#
# 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 -Uz zsh_directory_name_generic # paranoia in case we forgot
# zsh_directory_name_generic "$@"
emulate -L zsh
setopt extendedglob
local -a match mbegin mend
# The variable containing the top level mapping.
local _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
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_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 in $_zdn_cpts; do
_zdn_pref=${_zdn_pref%/:*}
[[ -z $_zdn_pref ]] && continue
if [[ $_zdn_rest = $_zdn_pref(#b)(/|)(*) ]]; then
_zdn_cpt=${(k)_zdn_assoc[(r)$_zdn_pref]}
# 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_pt]} = (#b)*/:(*) ]]; 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-- ))
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
Messages sorted by:
Reverse Date,
Date,
Thread,
Author