]> Sergey Matveev's repositories - zsh-autoenv.git/blobdiff - autoenv.zsh
_autoenv_get_file_upwards: do not dereference symlinks (#73)
[zsh-autoenv.git] / autoenv.zsh
index 3ec01e910407b4d8c3afaf5043409ea23606e6b8..2deac546596dda7728b1934d1602bcf39ffbbd5d 100644 (file)
@@ -1,17 +1,35 @@
 # Initially based on
 # https://github.com/joshuaclayton/dotfiles/blob/master/zsh_profile.d/autoenv.zsh
 
-export AUTOENV_ENV_FILENAME=$HOME/.env_auth
-[ -e $AUTOENV_ENV_FILENAME ] || touch $AUTOENV_ENV_FILENAME
+# File to store confirmed authentication into.
+# This handles the deprecated, old location(s).
+if [[ -z $AUTOENV_AUTH_FILE ]]; then
+  if [[ -n $AUTOENV_ENV_FILENAME ]]; then
+    echo "zsh-autoenv: using deprecated setting for AUTOENV_AUTH_FILE from AUTOENV_ENV_FILENAME." >&2
+    echo "Please set AUTOENV_AUTH_FILE instead." >&2
+    AUTOENV_AUTH_FILE=$AUTOENV_ENV_FILENAME
+  else
+    if [[ -n $XDG_DATA_HOME ]]; then
+      AUTOENV_AUTH_FILE=$XDG_DATA_HOME/autoenv_auth
+    else
+      AUTOENV_AUTH_FILE=~/.local/share/autoenv_auth
+    fi
+    if [[ -f ~/.env_auth ]]; then
+      echo "zsh-autoenv: using deprecated location for AUTOENV_AUTH_FILE." >&2
+      echo "Please move it: mv ~/.env_auth ${(D)AUTOENV_AUTH_FILE}" >&2
+      AUTOENV_AUTH_FILE=~/.env_auth
+    fi
+  fi
+fi
 
-# Name of file to look for when entering directories.
-: ${AUTOENV_FILE_ENTER:=.env}
+# Name of the file to look for when entering directories.
+: ${AUTOENV_FILE_ENTER:=.autoenv.zsh}
 
-# Name of file to look for when leaving directories.
+# Name of the file to look for when leaving directories.
 # Requires AUTOENV_HANDLE_LEAVE=1.
-: ${AUTOENV_FILE_LEAVE:=.env.leave}
+: ${AUTOENV_FILE_LEAVE:=.autoenv_leave.zsh}
 
-# Look for .env in parent dirs?
+# Look for zsh-autoenv "enter" files in parent dirs?
 : ${AUTOENV_LOOK_UPWARDS:=1}
 
 # Handle leave events when changing away from a subtree, where an "enter"
@@ -24,12 +42,15 @@ export AUTOENV_ENV_FILENAME=$HOME/.env_auth
 # (Temporarily) disable zsh-autoenv. This gets looked at in the chpwd handler.
 : ${AUTOENV_DISABLED:=0}
 
-# Public helper functions, which can be used from your .env files:
-#
-# Source the next .env file from parent directories.
-# This is useful if you want to use a base .env file for a directory subtree.
+# Public helper functions, which can be used from your .autoenv.zsh files:
+
+# Source the next .autoenv.zsh file from parent directories.
+# This is useful if you want to use a base .autoenv.zsh file for a directory
+# subtree.
 autoenv_source_parent() {
-  local parent_env_file=$(_autoenv_get_file_upwards ${autoenv_env_file:h})
+  local look_until=${1:-/}
+  local parent_env_file=$(_autoenv_get_file_upwards \
+    ${autoenv_env_file:h} ${AUTOENV_FILE_ENTER} $look_until)
 
   if [[ -n $parent_env_file ]] \
     && _autoenv_check_authorized_env_file $parent_env_file; then
@@ -39,12 +60,10 @@ autoenv_source_parent() {
 }
 
 # Internal functions. {{{
-# Internal: stack of entered (and handled) directories. {{{
+# Internal: stack of loaded env files (i.e. entered directories). {{{
 typeset -g -a _autoenv_stack_entered
-_autoenv_stack_entered=()
 # -g: make it global, this is required when used with antigen.
 typeset -g -A _autoenv_stack_entered_mtime
-_autoenv_stack_entered_mtime=()
 
 # Add an entry to the stack, and remember its mtime.
 _autoenv_stack_entered_add() {
@@ -53,14 +72,12 @@ _autoenv_stack_entered_add() {
   # Remove any existing entry.
   _autoenv_stack_entered_remove $env_file
 
-  _autoenv_debug "[stack] adding: $env_file" 2
-
   # Append it to the stack, and remember its mtime.
+  _autoenv_debug "[stack] adding: $env_file" 2
   _autoenv_stack_entered+=($env_file)
   _autoenv_stack_entered_mtime[$env_file]=$(_autoenv_get_file_mtime $env_file)
 }
 
-
 # zstat_mime helper, conditionally defined.
 # Load zstat module, but only its builtin `zstat`.
 if ! zmodload -F zsh/stat b:zstat 2>/dev/null; then
@@ -68,8 +85,14 @@ if ! zmodload -F zsh/stat b:zstat 2>/dev/null; then
   # terse output instead of format, which is not supported by busybox.
   # Assume '+mtime' as $1.
   _autoenv_get_file_mtime() {
-    setopt localoptions pipefail
-    stat -t $1 2>/dev/null | cut -f13 -d ' ' || echo 0
+    # setopt localoptions pipefail
+    local stat
+    stat=$(stat -t $1 2>/dev/null)
+    if [[ -n $stat ]]; then
+      echo ${${(s: :)stat}[13]}
+    else
+      echo 0
+    fi
   }
 else
   _autoenv_get_file_mtime() {
@@ -77,7 +100,6 @@ else
   }
 fi
 
-
 # Remove an entry from the stack.
 _autoenv_stack_entered_remove() {
   local env_file=$1
@@ -115,11 +137,11 @@ _autoenv_stack_entered_contains() {
 
 # Internal function for debug output. {{{
 _autoenv_debug() {
-  local msg=$1
   local level=${2:-1}
-  if [[ $AUTOENV_DEBUG -lt $level ]]; then
+  if (( AUTOENV_DEBUG < level )); then
     return
   fi
+  local msg="$1"  # Might trigger a bug in Zsh 5.0.5 with shwordsplit.
   # Load zsh color support.
   if [[ -z $color ]]; then
     autoload colors
@@ -143,47 +165,84 @@ _autoenv_debug() {
 # }}}
 
 
-# Generate hash pair for a given file ($1).
-# A fixed hash value can be given as 2nd arg, but is used with tests only.
+# Generate hash pair for a given file ($1) and version ($2).
+# A fixed hash value can be given as 3rd arg, but is used with tests only.
+# The format is ":$file:$hash:$version".
 _autoenv_hash_pair() {
   local env_file=${1:A}
-  local env_shasum=${2:-}
-  if [[ -z $env_shasum ]]; then
+  local cksum_version=${2:-2}
+  local env_cksum=${3:-}
+  if [[ -z $env_cksum ]]; then
     if ! [[ -e $env_file ]]; then
       echo "Missing file argument for _autoenv_hash_pair!" >&2
       return 1
     fi
-    env_shasum=$(shasum $env_file | cut -d' ' -f1)
+    if [ $cksum_version = 2 ]; then
+      # Get the output from `cksum` and join the first two words with a dot.
+      env_cksum=${(j:.:)${:-$(cksum "$env_file")}[1,2]}
+    elif [ $cksum_version = 1 ]; then
+      env_cksum=$(shasum $env_file | cut -d' ' -f1)
+    else
+      echo "Invalid version argument (${cksum_version}) for _autoenv_hash_pair!" >&2
+      return 1
+    fi
   fi
-  echo ":${env_file}:${env_shasum}:1"
+  echo ":${env_file}:${env_cksum}:${cksum_version}"
+}
+
+
+# Checks for the existence of a hash signature in the auth file
+_autoenv_authorized_pair() {
+  local pair=$1
+  test -f $AUTOENV_AUTH_FILE \
+    && \grep -qF $pair $AUTOENV_AUTH_FILE
 }
 
+
 _autoenv_authorized_env_file() {
   local env_file=$1
-  local pair=$(_autoenv_hash_pair $env_file)
-  test -f $AUTOENV_ENV_FILENAME \
-    && \grep -qF $pair $AUTOENV_ENV_FILENAME
+  local pair
+  pair=$(_autoenv_hash_pair $env_file)
+  _autoenv_debug "v2 pair: ${pair}"
+  if ! _autoenv_authorized_pair $pair; then
+    # Fallback for v1 (SHA-1) pairs
+    pair=$(_autoenv_hash_pair $env_file 1)
+    _autoenv_debug "v1 pair: ${pair}"
+    if _autoenv_authorized_pair $pair; then
+      # Upgrade v1 entries to v2
+      _autoenv_authorize $env_file
+    else
+      return 1
+    fi
+  fi
 }
 
 _autoenv_authorize() {
   local env_file=${1:A}
   _autoenv_deauthorize $env_file
-  _autoenv_hash_pair $env_file >>| $AUTOENV_ENV_FILENAME
+  [[ -d ${AUTOENV_AUTH_FILE:h} ]] || mkdir -p ${AUTOENV_AUTH_FILE:h}
+  _autoenv_hash_pair $env_file >>| $AUTOENV_AUTH_FILE
 }
 
 # Deauthorize a given filename, by removing it from the auth file.
-# This uses `test -s` to only handle non-empty files, and a subshell to
-# allow for writing to the same file again.
+# This uses `test -s` to only handle non-empty files.
 _autoenv_deauthorize() {
   local env_file=${1:A}
-  if [[ -s $AUTOENV_ENV_FILENAME ]]; then
-    echo "$(\grep -vF :${env_file}: $AUTOENV_ENV_FILENAME)" >| $AUTOENV_ENV_FILENAME
+  if [[ -s $AUTOENV_AUTH_FILE ]]; then
+    \grep -vF :${env_file}: $AUTOENV_AUTH_FILE >| $AUTOENV_AUTH_FILE.tmp
+    \mv $AUTOENV_AUTH_FILE.tmp $AUTOENV_AUTH_FILE
   fi
 }
 
 # This function can be mocked in tests
 _autoenv_ask_for_yes() {
   local answer
+
+  # Handle/catch Ctrl-C and return, instead of letting it potentially abort the
+  # shell setup process.
+  setopt localtraps
+  trap 'return 1' INT
+
   read answer
   if [[ $answer == "yes" ]]; then
     return 0
@@ -198,16 +257,18 @@ _autoenv_check_authorized_env_file() {
     return 1
   fi
   if ! _autoenv_authorized_env_file $1; then
-    echo "Attempting to load unauthorized env file!"
-    command ls -l $1
-    echo ""
-    echo "**********************************************"
-    echo ""
-    cat $1
-    echo ""
-    echo "**********************************************"
-    echo ""
-    echo -n "Would you like to authorize it? (type 'yes') "
+    echo "Attempting to load unauthorized env file!" >&2
+    command ls -l $1 >&2
+    echo >&2
+    echo "**********************************************" >&2
+    echo >&2
+    command cat $1 >&2
+    echo >&2
+    echo "**********************************************" >&2
+    echo >&2
+    echo -n "Would you like to authorize it? (type 'yes') " >&2
+    # echo "Would you like to authorize it?"
+    # echo "('yes' to allow, 'no' to not being asked again; otherwise ignore it for the shell) "
 
     if ! _autoenv_ask_for_yes; then
       return 1
@@ -218,70 +279,111 @@ _autoenv_check_authorized_env_file() {
   return 0
 }
 
-# Get directory of this file (absolute, with resolved symlinks).
-_autoenv_source_dir=${0:A:h}
-
 _autoenv_source() {
-  local env_file=$1
-  autoenv_event=$2
-  local _autoenv_envfile_dir=${3:-${1:A:h}}
-
-  autoenv_from_dir=$_autoenv_chpwd_prev_dir
-  autoenv_to_dir=$PWD
-  autoenv_env_file=$env_file
+  # Public API for the .autoenv.zsh script.
+  local autoenv_env_file=$1
+  local autoenv_event=$2
+  local autoenv_from_dir=$OLDPWD
+  local autoenv_to_dir=$PWD
 
   # Source varstash library once.
-  if [[ -z "$functions[(I)autostash]" ]]; then
-    source $_autoenv_source_dir/lib/varstash
+  # XXX: pollutes environment with e.g. `stash`, and `autostash` will cause
+  # an overwritten `stash` function to be called!
+  if ! (( $+functions[autostash] )); then
+    if \grep -qE '\b(autostash|autounstash|stash|unstash)\b' $autoenv_env_file; then
+      source ${${funcsourcetrace[1]%:*}:h}/lib/varstash
+    fi
     # NOTE: Varstash uses $PWD as default for varstash_dir, we might set it to
-    # ${env_file:h}.
+    # ${autoenv_env_file:h}.
   fi
 
-  # Change to directory of env file, source it and cd back.
-  local new_dir=$PWD
-  _autoenv_debug "== SOURCE: ${bold_color:-}$env_file${reset_color:-}\n      PWD: $PWD"
-  (( _autoenv_debug_indent++ ))
-  source $env_file
-  (( _autoenv_debug_indent-- ))
+  # Source the env file.
+  _autoenv_debug "== SOURCE: ${bold_color:-}$autoenv_env_file${reset_color:-}\n      PWD: $PWD"
+  (( ++_autoenv_debug_indent ))
+
+  local restore_xtrace
+  if [[ $AUTOENV_DEBUG -gt 2 && ! -o xtrace ]]; then
+    restore_xtrace=1
+    setopt localoptions xtrace
+  fi
+  source $autoenv_env_file
+  if (( restore_xtrace )); then
+    setopt noxtrace
+  fi
+  (( --_autoenv_debug_indent ))
   _autoenv_debug "== END SOURCE =="
 
   if [[ $autoenv_event == enter ]]; then
-    _autoenv_stack_entered_add $env_file
+    _autoenv_stack_entered_add $autoenv_env_file
   fi
-
-  # Unset vars set for enter/leave scripts.
-  # This should not get done for recursion (via autoenv_source_parent),
-  # and can be useful to have in general after autoenv was used.
-  # unset autoenv_event autoenv_from_dir autoenv_to_dir autoenv_env_file
 }
 
 _autoenv_get_file_upwards() {
   local look_from=${1:-$PWD}
   local look_for=${2:-$AUTOENV_FILE_ENTER}
+  local look_until=${${3:-/}:A}
 
   # Manually look in parent dirs. An extended Zsh glob should use Y1 for
   # performance reasons, which is only available in zsh-5.0.5-146-g9381bb6.
   local last
   local parent_dir="$look_from/.."
+  local abs_parent_dir
   while true; do
-    parent_dir=${parent_dir:A}
-    if [[ $parent_dir == $last ]]; then
+    abs_parent_dir=${parent_dir:A}
+    if [[ $abs_parent_dir == $last ]]; then
       break
     fi
-    parent_file="${parent_dir}/${look_for}"
+    local parent_file="${parent_dir}/${look_for}"
 
     if [[ -f $parent_file ]]; then
-      echo $parent_file
+      if [[ ${parent_file[1,2]} == './' ]]; then
+        echo ${parent_file#./}
+      else
+        echo ${parent_file:a}
+      fi
       break
     fi
 
+    if [[ $abs_parent_dir == $look_until ]]; then
+      break
+    fi
     last=$parent_dir
     parent_dir="${parent_dir}/.."
   done
 }
 
+autoenv-edit() {
+  local env_file
+  local -a files
+  local -A check
+  check[enter]=$AUTOENV_FILE_ENTER
+  if [[ "$AUTOENV_FILE_ENTER" != "$AUTOENV_FILE_LEAVE" ]]; then
+    check[leave]=$AUTOENV_FILE_LEAVE
+  fi
+  local f t
+  for t f in ${(kv)check}; do
+    env_file="$f"
+    if ! [[ -f $env_file ]]; then
+      env_file=$(_autoenv_get_file_upwards . $f)
+      if [[ -z $env_file ]]; then
+        echo "No $f file found ($t)." >&2
+        continue
+      fi
+      if ! [[ $AUTOENV_LOOK_UPWARDS == 1 ]]; then
+        echo "Note: found $env_file, but AUTOENV_LOOK_UPWARDS is disabled."
+      fi
+    fi
+    files+=($env_file)
+  done
+  if [[ -z "$files" ]]; then
+    return 1
+  fi
+  echo "Editing $files.."
+  local editor
+  editor="${${AUTOENV_EDITOR:-$EDITOR}:-vim}"
+  eval $editor "$files"
+}
 
-_autoenv_chpwd_prev_dir=$PWD
 _autoenv_chpwd_handler() {
   _autoenv_debug "Calling chpwd handler: PWD=$PWD"
 
@@ -291,7 +393,7 @@ _autoenv_chpwd_handler() {
   fi
 
   local env_file="$PWD/$AUTOENV_FILE_ENTER"
-  _autoenv_debug "env_file: $env_file"
+  _autoenv_debug "Looking for env_file: $env_file"
 
   # Handle leave event for previously sourced env files.
   if [[ $AUTOENV_HANDLE_LEAVE == 1 ]] && (( $#_autoenv_stack_entered )); then
@@ -301,11 +403,13 @@ _autoenv_chpwd_handler() {
       if ! [[ ${PWD}/ == ${prev_dir}/* ]]; then
         local env_file_leave=$prev_dir/$AUTOENV_FILE_LEAVE
         if _autoenv_check_authorized_env_file $env_file_leave; then
-          _autoenv_source $env_file_leave leave $prev_dir
+          varstash_dir=$prev_dir _autoenv_source $env_file_leave leave $prev_dir
         fi
 
         # Unstash any autostashed stuff.
-        varstash_dir=$prev_dir autounstash
+        if (( $+functions[autostash] )); then
+          varstash_dir=$prev_dir autounstash
+        fi
 
         _autoenv_stack_entered_remove $prev_file
       fi
@@ -315,21 +419,20 @@ _autoenv_chpwd_handler() {
   if ! [[ -f $env_file ]] && [[ $AUTOENV_LOOK_UPWARDS == 1 ]]; then
     env_file=$(_autoenv_get_file_upwards $PWD)
     if [[ -z $env_file ]]; then
-      _autoenv_chpwd_prev_dir=$PWD
+      _autoenv_debug "No env_file found when looking upwards"
       return
     fi
+    _autoenv_debug "Found env_file: $env_file"
   fi
 
   # Load the env file only once: check if $env_file is in the stack of entered
   # directories.
   if _autoenv_stack_entered_contains $env_file; then
     _autoenv_debug "Already in stack: $env_file"
-    _autoenv_chpwd_prev_dir=$PWD
     return
   fi
 
   if ! _autoenv_check_authorized_env_file $env_file; then
-    _autoenv_chpwd_prev_dir=$PWD
     return
   fi
 
@@ -337,9 +440,7 @@ _autoenv_chpwd_handler() {
   _autoenv_debug "Sourcing from chpwd handler: $env_file"
   _autoenv_source $env_file enter
 
-  _autoenv_chpwd_prev_dir=$PWD
-
-  (( _autoenv_debug_indent++ ))
+  (( ++_autoenv_debug_indent ))
 }
 # }}}