diff --git a/autoenv.zsh b/autoenv.zsh index e996ca2..81a8742 100644 --- a/autoenv.zsh +++ b/autoenv.zsh @@ -18,12 +18,74 @@ export AUTOENV_ENV_FILENAME=$HOME/.env_auth : ${AUTOENV_HANDLE_LEAVE:=1} -# Internal: stack of entered (and handled) directories. +# 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. +autoenv_source_parent() { + local parent_env_file=$(_autoenv_get_file_upwards $PWD) + + if [[ -n $parent_env_file ]] \ + && _autoenv_check_authorized_env_file $parent_env_file; then + + local parent_env_dir=${parent_env_file:A:h} + _autoenv_source $parent_env_file enter $parent_env_dir + fi +} + + +# Internal: stack of entered (and handled) directories. {{{ _autoenv_stack_entered=() +typeset -A _autoenv_stack_entered_mtime +_autoenv_stack_entered_mtime=() + +# Add an entry to the stack, and remember its mtime. +_autoenv_stack_entered_add() { + local env_file=$1 + + # Remove any existing entry. + _autoenv_stack_entered[$_autoenv_stack_entered[(i)$1]]=() + + # Append it to the stack, and remember its mtime. + _autoenv_stack_entered+=($env_file) + _autoenv_stack_entered_mtime[$env_file]=$(_autoenv_get_file_mtime $env_file) +} + +_autoenv_get_file_mtime() { + if [[ -f $1 ]]; then + zstat +mtime $1 + else + echo 0 + fi +} + +# Remove an entry from the stack. +_autoenv_stack_entered_remove() { + local env_file=$1 + _autoenv_stack_entered=(${_autoenv_stack_entered#$env_file}) + _autoenv_stack_entered_mtime[$env_file]= +} + +# Is the given entry already in the stack? +_autoenv_stack_entered_contains() { + local env_file=$1 + if (( ${+_autoenv_stack_entered[(r)${env_file}]} )); then + # Entry is in stack. + if [[ $_autoenv_stack_entered_mtime[$env_file] == $(_autoenv_get_file_mtime $env_file) ]]; then + # Entry has the expected mtime. + return + fi + fi + return 1 +} +# }}} + +# Load zstat module, but only its builtin `zstat`. +zmodload -F zsh/stat b:zstat _autoenv_hash_pair() { - local env_file=$1 + local env_file=${1:A} if (( $+2 )); then env_shasum=$2 else @@ -86,24 +148,20 @@ _autoenv_check_authorized_env_file() { return 0 } -# Initialize $_autoenv_sourced_varstash, but do not overwrite an existing one -# from e.g. `exec zsh` (to reload your shell config). -: ${_autoenv_sourced_varstash:=0} - # Get directory of this file (absolute, with resolved symlinks). _autoenv_this_dir=${0:A:h} _autoenv_source() { local env_file=$1 _autoenv_event=$2 - _autoenv_envfile_dir=$3 + local _autoenv_envfile_dir=$3 + _autoenv_from_dir=$_autoenv_chpwd_prev_dir _autoenv_to_dir=$PWD # Source varstash library once. - if [[ $_autoenv_sourced_varstash == 0 ]]; then + if [[ -z "$functions[(I)autostash]" ]]; then source $_autoenv_this_dir/lib/varstash - export _autoenv_sourced_varstash=1 # NOTE: Varstash uses $PWD as default for varstash_dir, we might set it to # ${env_file:h}. fi @@ -114,55 +172,69 @@ _autoenv_source() { source $env_file builtin cd -q $new_dir - unset _autoenv_event _autoenv_from_dir + # 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_get_file_upwards() { + local look_from=${1:-$PWD} + local look_for=${2:-$AUTOENV_FILE_ENTER} + # Look for files in parent dirs, using an extended Zsh glob. + setopt localoptions extendedglob + local m + # Y1: short-circuit: first match. + # :A: absolute path, resolving symlinks. + m=($look_from/(../)##${look_for}(NY1:A)) + if (( $#m )); then + echo $m[1] + fi +} + + _autoenv_chpwd_prev_dir=$PWD _autoenv_chpwd_handler() { local env_file="$PWD/$AUTOENV_FILE_ENTER" # Handle leave event for previously sourced env files. if [[ $AUTOENV_HANDLE_LEAVE == 1 ]] && (( $#_autoenv_stack_entered )); then - for prev_dir in ${_autoenv_stack_entered}; do + local prev_file prev_dir + for prev_file in ${_autoenv_stack_entered}; do + prev_dir=${prev_file:A:h} 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 fi - # Remove this entry from the stack. - _autoenv_stack_entered=(${_autoenv_stack_entered#$prev_dir}) + _autoenv_stack_entered_remove $prev_dir fi done fi if ! [[ -f $env_file ]] && [[ $AUTOENV_LOOK_UPWARDS == 1 ]]; then - # Look for files in parent dirs, using an extended Zsh glob. - setopt localoptions extendedglob - local m - m=((../)#${AUTOENV_FILE_ENTER}(N)) - if (( $#m )); then - env_file=${${m[1]}:A} - else + env_file=$(_autoenv_get_file_upwards $PWD) + if [[ -z $env_file ]]; then _autoenv_chpwd_prev_dir=$PWD return fi 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_chpwd_prev_dir=$PWD + return + fi + if ! _autoenv_check_authorized_env_file $env_file; then _autoenv_chpwd_prev_dir=$PWD return fi - # Load the env file only once: check if $env_file's parent - # is in $_autoenv_stack_entered. - local env_file_dir=${env_file:A:h} - if (( ${+_autoenv_stack_entered[(r)${env_file_dir}]} )); then - _autoenv_chpwd_prev_dir=$PWD - return - fi - - _autoenv_stack_entered+=(${env_file_dir}) + _autoenv_stack_entered_add $env_file + # Source the enter env file. _autoenv_source $env_file enter $PWD _autoenv_chpwd_prev_dir=$PWD diff --git a/tests/_autoenv_stack.t b/tests/_autoenv_stack.t new file mode 100644 index 0000000..df1e0f9 --- /dev/null +++ b/tests/_autoenv_stack.t @@ -0,0 +1,52 @@ +Tests for internal stack handling. + + $ source $TESTDIR/setup.sh + +Non-existing entries are allowed and handled without error. + + $ _autoenv_stack_entered_add non-existing + $ echo $_autoenv_stack_entered + non-existing + +Add existing entries. + + $ mkdir -p sub/sub2 + $ touch -t 201401010101 sub/file + $ _autoenv_stack_entered_add sub + $ _autoenv_stack_entered_add sub/file + $ _autoenv_stack_entered_add sub/sub2 + $ echo $_autoenv_stack_entered + non-existing sub sub/file sub/sub2 + + $ _autoenv_stack_entered_add non-existing + $ echo $_autoenv_stack_entered + sub sub/file sub/sub2 non-existing + + $ echo ${(k)_autoenv_stack_entered} + sub sub/file sub/sub2 non-existing + + $ echo $_autoenv_stack_entered_mtime + 0 1388538060 0 0 (glob) + +Touch the file and re-add it. + + $ touch -t 201401012359 sub/file + $ _autoenv_stack_entered_add sub/file + +The mtime should have been updated. + + $ echo ${_autoenv_stack_entered_mtime[sub/file]} + 1388620740 + +It should have moved to the end of the stack. + + $ echo ${(k)_autoenv_stack_entered} + sub sub/sub2 non-existing sub/file + +Test lookup of containing elements. + + $ _autoenv_stack_entered_contains sub/file + $ _autoenv_stack_entered_contains non-existing + $ _autoenv_stack_entered_contains not-added + [1] + diff --git a/tests/_autoenv_utils.t b/tests/_autoenv_utils.t new file mode 100644 index 0000000..79c4bf1 --- /dev/null +++ b/tests/_autoenv_utils.t @@ -0,0 +1,15 @@ +Tests for internal util methods. + + $ source $TESTDIR/setup.sh + +Non-existing entries are allowed and handled without error. + + $ mkdir -p sub/sub2 + $ touch file sub/file sub/sub2/file + +Should not get the file from the current dir. + $ _autoenv_get_file_upwards . file + + $ cd sub/sub2 + $ _autoenv_get_file_upwards . file + */_autoenv_utils.t/sub/file (glob) diff --git a/tests/recurse-upwards.t b/tests/recurse-upwards.t new file mode 100644 index 0000000..508fb37 --- /dev/null +++ b/tests/recurse-upwards.t @@ -0,0 +1,150 @@ +Test recursing into parent .env files. + + $ source $TESTDIR/setup.sh + +Setup env actions / output. + + $ AUTOENV_LOOK_UPWARDS=1 + +Create env files in root dir. + + $ echo 'echo ENTERED_root: PWD:${PWD:t} from:${_autoenv_from_dir:t} to:${_autoenv_to_dir:t}' > .env + $ echo 'echo LEFT_root: PWD:${PWD:t} from:${_autoenv_from_dir:t} to:${_autoenv_to_dir:t}' > .env.leave + $ test_autoenv_auth_env_files + +Create env files in sub dir. + + $ mkdir -p sub/sub2 + $ cd sub + ENTERED_root: PWD:sub from:recurse-upwards.t to:sub + + $ echo 'echo ENTERED_sub: PWD:${PWD:t} from:${_autoenv_from_dir:t} to:${_autoenv_to_dir:t}' > .env + $ echo 'echo LEFT_sub: PWD:${PWD:t} from:${_autoenv_from_dir:t} to:${_autoenv_to_dir:t}' > .env.leave + $ test_autoenv_auth_env_files + +The actual tests. + + $ cd . + ENTERED_sub: PWD:sub from:sub to:sub + + $ cd .. + LEFT_sub: PWD:sub from:sub to:recurse-upwards.t + + $ cd sub/sub2 + ENTERED_sub: PWD:sub2 from:recurse-upwards.t to:sub2 + + $ cd .. + +Changing the .env file should re-source it. + + $ echo 'echo ENTER2' >> .env + +Set timestamp of auth file into the past, so it gets seen as new below. + + $ touch -t 201401010101 .env + + $ test_autoenv_auth_env_files + $ cd . + ENTERED_sub: PWD:sub from:sub to:sub + ENTER2 + +Add sub/sub2/.env file, with a call to autoenv_source_parent. + + $ echo -e "echo autoenv_source_parent_from_sub2:\nautoenv_source_parent\necho done_sub2\n" > sub2/.env + $ test_autoenv_add_to_env sub2/.env + $ cd sub2 + autoenv_source_parent_from_sub2: + ENTERED_sub: PWD:sub from:sub to:sub2 + ENTER2 + done_sub2 + +Move sub/.env away, now the root .env file should get sourced. + + $ mv ../.env ../.env.out + $ touch -t 201401010102 .env + $ cd . + autoenv_source_parent_from_sub2: + ENTERED_root: PWD:recurse-upwards.t from:sub2 to:sub2 + done_sub2 + $ mv ../.env.out ../.env + +Prepend call to autoenv_source_parent to sub/.env file. + + $ cd .. + $ echo -e "echo autoenv_source_parent_from_sub:\nautoenv_source_parent\n$(< .env)\necho done_sub" > .env + $ touch -t 201401010103 .env + $ test_autoenv_auth_env_files + + $ cd . + autoenv_source_parent_from_sub: + ENTERED_root: PWD:recurse-upwards.t from:sub to:sub + ENTERED_sub: PWD:sub from:sub to:sub + ENTER2 + done_sub + + +Add sub/sub2/.env file. + + $ echo -e "echo autoenv_source_parent_from_sub2:\nautoenv_source_parent\necho done_sub2\n" > sub2/.env + $ test_autoenv_add_to_env sub2/.env + $ cd sub2 + autoenv_source_parent_from_sub2: + autoenv_source_parent_from_sub: + ENTERED_root: PWD:recurse-upwards.t from:sub to:sub + ENTERED_sub: PWD:sub from:sub to:sub + ENTER2 + done_sub + done_sub2 + +Go to root. + + $ cd ../.. + LEFT_sub: PWD:sub from:sub2 to:recurse-upwards.t + ENTERED_root: PWD:recurse-upwards.t from:sub2 to:recurse-upwards.t + + +Changing the root .env should trigger re-authentication via autoenv_source_parent. + +First, let's answer "no". + + $ echo "echo NEW" > .env + $ _autoenv_read_answer() { echo 'n' } + $ cd sub + autoenv_source_parent_from_sub: + Attempting to load unauthorized env file: /tmp/cramtests-*/recurse-upwards.t/.env (glob) + + ********************************************** + + echo NEW + + ********************************************** + + Would you like to authorize it? [y/N] + ENTERED_sub: PWD:sub from:recurse-upwards.t to:sub + ENTER2 + done_sub + +Now with "yes". +This currently does not trigger re-execution of the .env file. + + $ _autoenv_read_answer() { echo 'y' } + $ cd . + +Touching the .env file will now source the parent env file. + + $ touch -t 201401010104 .env + $ cd . + autoenv_source_parent_from_sub: + Attempting to load unauthorized env file: /tmp/cramtests-*/recurse-upwards.t/.env (glob) + + ********************************************** + + echo NEW + + ********************************************** + + Would you like to authorize it? [y/N] + NEW + ENTERED_sub: PWD:sub from:sub to:sub + ENTER2 + done_sub diff --git a/tests/setup.sh b/tests/setup.sh index 9134af0..d5a4f6e 100644 --- a/tests/setup.sh +++ b/tests/setup.sh @@ -3,16 +3,19 @@ [[ $AUTOENV_ENV_FILENAME[0,4] == '/tmp' ]] || return 1 +# Reset any authentication. +echo -n > $AUTOENV_ENV_FILENAME + # Inject timeout for `read` while running tests. _AUTOENV_TEST_READ_ARGS='-t 1' +# Add file $1 (with optional hash $2) to authentication file. test_autoenv_add_to_env() { _autoenv_hash_pair $1 $2 >> $AUTOENV_ENV_FILENAME } # Add enter and leave env files to authentication file. test_autoenv_auth_env_files() { - echo -n > $AUTOENV_ENV_FILENAME test_autoenv_add_to_env $PWD/$AUTOENV_FILE_ENTER test_autoenv_add_to_env $PWD/$AUTOENV_FILE_LEAVE } diff --git a/tests/varstash.t b/tests/varstash.t index 621e93f..7458044 100644 --- a/tests/varstash.t +++ b/tests/varstash.t @@ -6,8 +6,8 @@ Setup test environment. $ mkdir sub $ cd sub - $ echo "autostash FOO=baz" > $AUTOENV_FILE_ENTER - $ echo "autounstash" > $AUTOENV_FILE_LEAVE + $ echo 'echo ENTER; autostash FOO=baz' > $AUTOENV_FILE_ENTER + $ echo 'echo LEAVE; autounstash' > $AUTOENV_FILE_LEAVE Manually create auth file @@ -20,11 +20,13 @@ Set environment variable. Activating the env stashes it and applies a new value. $ cd . + ENTER $ echo $FOO baz Leaving the directory unstashes it. $ cd .. + LEAVE $ echo $FOO bar