Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: Dynamic directory name function



On Wed, 23 Sep 2015 09:48:21 +0100
Peter Stephenson <p.stephenson@xxxxxxxxxxx> wrote:
> On Tue, 22 Sep 2015 21:07:56 -0700
> Bart Schaefer <schaefer@xxxxxxxxxxxxxxxx> 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:<wrapper-name>:' where <wrapper-name> 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



Messages sorted by: Reverse Date, Date, Thread, Author