#autoload # NOTE! This file not only gets used by zsh, but by # .shared_env, and so has to work when source'd from # any sh-compatible shell. Well, any sensible sh. # Example usage: # # run_hooks .shared_env.d # # would be the last thing executed by ${ZDOTDIR:-$HOME}/.shared_env, # and would check for files under .shared_env.d in the $ZDOTDIRPATH # search path, which would be this (in order): # # * $ZDOTDIR/.shared_env.d/* # * $HOME/.shared_env.d/* # # i.e. from least specific to most specific. # # Ditto for .zshrc, .bashrc etc. # # Additionally, if a directory entry has one of the following formats, # it is treated specially: # # * person-$individual_uuid # # Run directory contents if $individual_uuid equals # $ZDOTUSER. This should be unique to a person (*not* a uid); # e-mail addresses are recommended, similar in idea to GNU arch's # global archive namespace. # # * host-$hostname # # Run directory contents if $hostname equals the current hostname. # If there is ambiguity with regards to FQDN or non-FQDN, the code # gives the benefit of the doubt in favour of a match. # # * uid-$username # # Run directory contents if $username equals the current # (non-numeric) uid. An additional special case: if $username # equals 'OWNER', the hook is only run if the current user owns # the hook file. This allows a uid-specific hook to be installed # in multiple home directories independently of its filename, # effectively marking it as "for private use only". #!! FIXME: in rare cases, compound filtering (boolean composition of #!! the above "atoms") is required. Case study: running #!! .xsession-progs.d/person-me/ from a demo account to get a nice X11 #!! environment, e.g. 'keyboard' and 'mouse' are person-specific, #!! 'wallpaper' is both person- and uid-specific, and 'RSI' is #!! person-, uid-, and host-specific. (Conceivably there could even #!! be something person- and host-specific, e.g. if I wanted to run a #!! utility to set sound card output volumes in a particular way on a #!! particular laptop.) So the current approach is too limited. #!! #!! Further consideration - is anything other than AND boolean ever #!! required? For example, filters implemented as arbitrary shell #!! code, with the "atoms" simply being special cases? I would say #!! yes, e.g. hardware-specific hooks, or hooks which need to be #!! controlled manually such as composition of yum/smart repo/channel #!! directories via a hostgroups-like approach (this refers to something #!! I implemented in my last job). #!! #!! Immutable requirement: must be able to list which hooks would be #!! run without running them, rather than embedding boolean tests as #!! code within the hook itself. The latter is also prohibitive #!! because not all hooks will be written in shell script (mutt config #!! is one of many examples). #!! #!! From the axiom that the filters have to be described out-of-band, #!! they can only be described #!! #!! a) in the filename, #!! #!! b) in the pathname (status quo), #!! #!! c) stored separately as meta-data somewhere, with the hooks all #!! in one directory, or #!! #!! d) as meta-data embedded within the hook via comments, #!! retrievable in a well-defined, consistent manner (like emacs #!! local variables). #!! #!! With c), a single flat file (e.g. '.guards') in the hook directory #!! isn't going to work, because it doesn't support the $foo.d #!! methodology. Therefore it would have to be a .guards.d #!! subdirectory, or a $foo.guard for each hook, either of which is a #!! bit of a PITA. #!! #!! Pros/cons :- #!! #!! 1. a) and d) are easiest when creating new hooks ... unless the #!! right tools are built, in which case they probably require #!! equal effort. But editing will still be slightly more awkward #!! with c) than with the others, since it will involve an extra #!! emacs buffer if changing both the hook and its filter :-) #!! #!! 2. Only a) and b) offer easy viewing via standard fileutils, grep #!! etc. but a list_hooks | xargs ... approach should deal with #!! this, as long as list_hooks can be parametrized to fake a #!! different environment (i.e. "list hooks which would have been #!! run under $other_uid on $other_host"), and this should be easy #!! to achieve. #!! #!! 3. With c) and d), the file namespace is too flat and allows for #!! collisions between the different $foo.d sources. And if you #!! expand the namespace to avoid these collisions, you're back #!! with a) or b), which defeats the point. #!! #!! 4. Only c) and d) allow a nice implementation of composite #!! boolean filters. If it was only chained AND ops, a) and b) #!! could do it but would result in ugly long filenames/pathnames #!! respectively. #!! #!! 5. Only c) and d) allow arbitrarily complex filter logic. #!! #!! 6. a) and b) are virtually guaranteed to perform better, but #!! premature optimization... #!! #!! 7. Avoiding subdirectories makes hook ordering easier to #!! implement (we don't actually care, since implementation is a #!! one-off cost which has already been done!), and a bit more #!! visible (but again we don't really care given a good #!! list_hooks). #!! #!! Points for each scheme: #!! #!! a b c d #!! 1. 1 1 0 1 #!! 2. 1 1 0 0 #!! 3. 4 4 0 0 #!! 4. 0 0 2 2 #!! 5. 0 0 5 5 #!! 6. 1 1 0 0 #!! 7. 0 0 0 0 #!! total: 7 7 7 8 #!! #!! Conclusion: in light of conflicting design goals, no single #!! solution is ideal. Therefore a hybrid which allows multiple #!! approaches is required. #!! #!! Sanity check: is the current 90% solution worth sticking with? #!! And use different approach or hack for corner cases. Can we #!! really justify substantial increases in complexity? # Requirements and rationale for search algorithm: # # * Must allow me to share config with friends. Accomplished by setting # ZDOTDIR to point to my home directory when running as another uid. # (We use the parameter name 'ZDOTDIR' because zsh supports it natively # when looking for core start-up files, defaulting to $HOME if it's not # set. But we extend the usage of the ZDOTDIR concept via this file # to include our own non-core start-up files such as .shared_env.) # # * When others use my config, their environment must not be polluted # with stuff specific to me. Accomplished by keeping all personal # stuff in the .${dotfile}.d/person-$ZDOTUSER namespace, where # $ZDOTUSER is something globally unique like the individual's # email address. (Although in my case, 'aspiers' is almost # certainly "unique enough" for practical purposes, if that # expression's not too much of an oxymoron.) # # * Must allow me to switch uid and still use config. Accomplished by # setting ZDOTDIR=~aspiers and ZDOTUSER to my chosen individual UUID # value. # # * Must allow per-uid config which would potentially span machines if home # directories are shared (e.g. on NFS). It makes obvious sense to put any # per-uid config in the uid's home directory. This is what the # ".${dotfile}.d/uid-OWNER" syntax is for. # # * Inheritance should be used wherever possible, i.e. ensuring that more # settings for specific contexts can override less specific contexts by # being loaded later. [Eh? You don't need inheritance for that ...] # # Other considerations: # # * Current search order doesn't allow specific contexts to override # decisions made in less specific contexts without coupling more specific # contexts to knowledge of the consequences of those decisions (i.e. # you have to know what effects to manually reverse). # # * Finer granularity (more, smaller files) allows easier overriding # in specific contexts which solves above issue and makes other # things better e.g. setting users/hosts to complete. This could be # prohibitively slow over NFS, but we'll cross that bridge when we come # to it, e.g. via some kind of prior compilation into one big .zwc ? # # * Would be nice to provide user with list of things which can be overridden. # This can be achieved simply by grepping for run_hooks invocations. # # * Need to support find_hooks for emacs and other non-shell-based # environments which invoke the hooks in a different way. # # * ZDOTDIR=~aspiers as root, where ~aspiers is shared via NFS, # reduces root privilege a lot since trust level for NFS write # access to ~aspiers is only by anyone who can get a port < 1024. # Default to 1, and treat empty as 0. This ensures we have an integer. : ${DEBUG_LOCAL_HOOKS=1} : ${DEBUG_LOCAL_HOOKS:=0} process_hook () { hook_path="$1" if ! [ -r "$hook_path" ]; then if [ "$DEBUG_LOCAL_HOOKS" -ge 2 ]; then echo "Couldn't read $hook_path; skipping." >&2 fi return 0 fi if [ -z "$hooks_found" ]; then hooks_found="$hook_path" else hooks_found="$hooks_found $hook_path" fi case "$action" in source) [ "$DEBUG_LOCAL_HOOKS" -gt 0 ] && sh_load_status "$hook_path" source "$hook_path" ;; find) ;; *) echo "Unrecognised run_hooks action $action; aborting." >&2 return 1 esac return 0 } should_ignore () { dirent="$1" ignore= case "$dirent" in *.dis) ignore=y ;; *.disable) ignore=y ;; *.disabled) ignore=y ;; *~) ignore=y ;; *.d) ignore=y ;; *.orig) ignore=y ;; *.zwc) ignore=y ;; *.zwc.*) ignore=y ;; \#*\#) ignore=y ;; CVS) ignore=y ;; */CVS) ignore=y ;; esac [ -n "$ignore" ] } iterate_hooks () { if [ $# != 2 ]; then echo "BUG: iterate_hooks called with wrong # of args: $*" >&2 return 1 fi action="$1" hook="$2" # origdir="`pwd`" hooks_found= if [ -z "$ZDOTDIRPATH" ]; then echo "\$ZDOTDIRPATH not defined; don't know where to search for hooks." >&2 return 1 fi source "$ZDOTDIR/.zsh/functions/enable_nullglob" [ -n "$ZSH_VERSION" ] && setopt local_options sh_word_split for dir in $ZDOTDIRPATH; do if ! [ -d "$dir" ]; then if [ -n "$shell_interactive" ]; then echo "BUG: $dir in \$ZDOTDIRPATH did not exist" >&2 fi continue fi hook_d="$dir/$hook" # Detect obsolete files from run_local_hooks for hook_path in ${hook_d%%.d}.*; do [ "$hook_path" = "$hook_d" ] && continue if should_ignore "$hook_path"; then if [ "$DEBUG_LOCAL_HOOKS" -ge 5 ]; then echo "# Ignoring obsolete hook $hook_path" fi continue elif [ "$DEBUG_LOCAL_HOOKS" -ge 2 ]; then echo "WARNING: found obsolete hook $hook_path" >&2 fi done [ -d "$hook_d" ] || break # if ! cd "$hook_d"; then # echo "BUG? Couldn't cd to $hook_d" >&2 # return 1 # fi hostnick=$(cat $ZDOTDIR/.localhost-nickname) : ${hostnick:=unknown} for hook_path in $hook_d/*; do dirent="${hook_path##*/}" if [ "$DEBUG_LOCAL_HOOKS" -ge 3 ]; then echo "# Considering possible hook $hook_path" fi if should_ignore "$dirent"; then if [ "$DEBUG_LOCAL_HOOKS" -ge 5 ]; then echo "# Ignoring possible hook $dirent" fi continue fi # Apply filter for special cases special= case "$dirent" in person-*) person="${dirent#person-}" [ "$ZDOTUSER" = "$person" ] || continue special=y ;; host-*) host="${dirent#host-}" # Be lenient with non-FQDN ambiguity if [ "${HOSTNAME%%.*}" != "${host}" ] && \ [ "${HOSTNAME}" != "${host%%.*}" ] && \ [ "${hostnick}" != "${host%%.*}" ] then [ "$DEBUG_LOCAL_HOOKS" -ge 4 ] && \ echo "# $hook_path hostname $host does not match \$HOSTNAME $HOSTNAME" continue fi special=y ;; uid-*) uid="${dirent#uid-}" if [ "$uid" = 'OWNER' ]; then uid=$( stat -c '%U' "$hook_path" ) # portable? # uid=$( command ls -l "$hook_path" | awk '{print $3}' ) # sucks fi if [ "$USERNAME" != "$uid" ]; then [ "$DEBUG_LOCAL_HOOKS" -ge 4 ] && \ echo "# $hook_path uid $uid does not match \$USERNAME $USERNAME" continue fi special=y ;; esac if [ -n "$special" ]; then [ "$DEBUG_LOCAL_HOOKS" -ge 4 ] && \ echo "# $hook_path is a special case hook" if [ -d "$hook_path" ]; then for subhook in $hook_path/*; do if should_ignore "$subhook"; then if [ "$DEBUG_LOCAL_HOOKS" -ge 5 ]; then echo "# Ignoring possible subhook $subhook" fi continue fi process_hook "$subhook" done else echo "WARNING: $hook_path should be a directory not a file" >&2 process_hook "$hook_path" fi else if ! [ -d "$hook_path" ]; then process_hook "$hook_path" fi fi done done restore_nullglob } run_hooks () { if [ $# != 1 ]; then echo "Usage: run_hooks " >&2 return 1 fi if [ "$1" = 'DEFUN' ]; then # We only want to define the functions above, not do anything. # This is for the benefit of find_hooks. return 0 fi iterate_hooks source "$@" } # Ensure we have sh_load_status. It's missing when an X session starts up. if type sh_load_status >/dev/null 2>&1; then #if [ -z "$shared_env_loaded" ]; then : else # Catch 22, this would go into infinite recursion: # . ${zdotdir:-$HOME}/.shared_env sh_load_status () { # This should only ever log to .xsession-my-errors anyway echo "$0: $*" } fi run_hooks "$@"