From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: (qmail 18389 invoked by alias); 23 Sep 2015 10:05:39 -0000 Mailing-List: contact zsh-users-help@zsh.org; run by ezmlm Precedence: bulk X-No-Archive: yes List-Id: Zsh Users List List-Post: List-Help: X-Seq: 20630 Received: (qmail 19125 invoked from network); 23 Sep 2015 10:05:36 -0000 X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on f.primenet.com.au X-Spam-Level: X-Spam-Status: No, score=-1.9 required=5.0 tests=BAYES_00 autolearn=ham autolearn_force=no version=3.4.0 X-AuditID: cbfec7f5-f794b6d000001495-07-56027712f011 Date: Wed, 23 Sep 2015 10:55:27 +0100 From: Peter Stephenson To: Zsh Users Subject: Re: Dynamic directory name function Message-id: <20150923105527.415e83b8@pwslap01u.europe.root.pri> In-reply-to: <20150923094821.5c5d0b80@pwslap01u.europe.root.pri> References: <20150922204251.05f3d291@ntlworld.com> <150922210756.ZM30253@torch.brasslantern.com> <20150923094821.5c5d0b80@pwslap01u.europe.root.pri> Organization: Samsung Cambridge Solution Centre X-Mailer: Claws Mail 3.7.9 (GTK+ 2.22.0; i386-redhat-linux-gnu) MIME-version: 1.0 Content-type: text/plain; charset=US-ASCII Content-transfer-encoding: 7bit X-Brightmail-Tracker: H4sIAAAAAAAAA+NgFjrCLMWRmVeSWpSXmKPExsVy+t/xq7pC5UxhBssvCVvsOLmS0YHRY9XB D0wBjFFcNimpOZllqUX6dglcGSsnrGEtuBdV8aU9vYHxjWsXIyeHhICJxIOXy1ggbDGJC/fW s3UxcnEICSxllHjxoo8dwpnGJLF1628oZxujRMfcDiaQFhYBVYmDX/Yxg9hsAoYSUzfNZgSx RQQUJc78+gZWIyygI9Gx8zkriM0rYC/R2rYCrJ5TwEGi4chtZoihMxglnt3+C9bAL6AvcfXv JyaIm+wlZl45wwjRLCjxY/I9sFuZBbQkNm9rYoWw5SU2r3kLNlRIQF3ixt3d7BMYhWYhaZmF pGUWkpYFjMyrGEVTS5MLipPSc430ihNzi0vz0vWS83M3MULC9usOxqXHrA4xCnAwKvHwWnxn DBNiTSwrrsw9xCjBwawkwvs0gClMiDclsbIqtSg/vqg0J7X4EKM0B4uSOO/MXe9DhATSE0tS s1NTC1KLYLJMHJxSDYx+T1SsjAqmcUv9/p2ytJLVlnnGDrbvc1gju86dlLo9odjSz+JF2XJ1 t4iirbtmVC7tecQeGBgcpbbhb5XXJs5TqjkVFqU/lBLNr/+fteD83/c88zPu9G/g0Ig7d/aD 7vbtWtndv+rfG/Q0vp+4PExvlW/Yg0U7cnf8FmfdtO7Mg+lZkxfu6nFWYinOSDTUYi4qTgQA Pt1+4FcCAAA= On Wed, 23 Sep 2015 09:48:21 +0100 Peter Stephenson wrote: > On Tue, 22 Sep 2015 21:07:56 -0700 > Bart Schaefer wrote: > > On Sep 22, 8:42pm, Peter Stephenson wrote: > > } Subject: Dynamic directory name function > > } > > } 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. > > > > I have one immediate suggestion: Use zstyle instead of variables. > > I did think about that. But having written the wrapper function, with > the variables the way they are, everything immediately fell out neatly > and clearly with associative arrays having a natural meaning. zstyle > would just obscure the way the hierarchy was linked for no real gain --- > quite possibly to the point where I couldn't remember how to do it even > myself and wouldn't actually use it, in which case the whole thing would > be a waste of time. However, it *is* very easy to set a zstyle for the top-level variable. That's already good enough for you to keep all the definitions outside the wrapper, if you want. (I don't --- I like having them in one place.) zmodload -i zsh/parameter zstyle -s ":zdn:${funcstack[2]}:" mapping _zdn_topvar || _zdn_topvar=zdn_top I've found a couple of bugs. Probably a good idea if I stick this in the distribution sooner rather than later to propagate fixes easily. For record, the latest is below. 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. # Alternatively, this can be set with a style. The context for the # style is ':zdn::' where is the function # calling zsh_directory_name_generic. # # 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 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 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-- )) 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