Skip to content

Commit

Permalink
Add strict_env and unstrict_env (direnv#572)
Browse files Browse the repository at this point in the history
Allows the gradual introduction of `set -euo pipefail` into envrc
processing, as discussed in direnv#566, as well as an escape hatch for
commands that must be run in the envrc context that do not meet `set
-euo pipefail` requirements.
  • Loading branch information
halostatue authored Nov 12, 2020
1 parent 08f9af4 commit f2f2b92
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 2 deletions.
72 changes: 72 additions & 0 deletions man/direnv-stdlib.1
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,78 @@ Checks that the direnv version is at least old as \fB\fCversion\_at\_least\fR\&.
be useful when sharing an \fB\fC\&.envrc\fR and to make sure that the users are up to
date.

.SS \fB\fCstrict\_env [<command> ...]\fR
.PP
Turns on shell execution strictness. This will force the .envrc
evaluation context to exit immediately if:
.IP \(bu 2
any command in a pipeline returns a non\-zero exit status that is not
otherwise handled as part of \fB\fCif\fR, \fB\fCwhile\fR, or \fB\fCuntil\fR tests,
return value negation (\fB\fC!\fR), or part of a boolean (\fB\fC\&\&\fR or \fB\fC||\fR)
chain.
.IP \(bu 2
any variable that has not explicitly been set or declared (with
either \fB\fCdeclare\fR or \fB\fClocal\fR) is referenced.

.PP
If followed by a command\-line, the strictness applies for the duration
of the command.

.PP
Example (Whole Script):

.PP
.RS

.nf
strict\_env
has curl

.fi
.RE

.PP
Example (Command):

.PP
.RS

.nf
strict\_env has curl

.fi
.RE

.SS \fB\fCunstrict\_env [<command> ...]\fR
.PP
Turns off shell execution strictness. If followed by a command\-line, the
strictness applies for the duration of the command.

.PP
Example (Whole Script):

.PP
.RS

.nf
unstrict\_env
has curl

.fi
.RE

.PP
Example (Command):

.PP
.RS

.nf
unstrict\_env has curl

.fi
.RE

.SH COPYRIGHT
.PP
MIT licence \- Copyright (C) 2019 @zimbatm and contributors
Expand Down
39 changes: 39 additions & 0 deletions man/direnv-stdlib.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,45 @@ Checks that the direnv version is at least old as `version_at_least`. This can
be useful when sharing an `.envrc` and to make sure that the users are up to
date.

### `strict_env [<command> ...]`

Turns on shell execution strictness. This will force the .envrc
evaluation context to exit immediately if:

- any command in a pipeline returns a non-zero exit status that is not
otherwise handled as part of `if`, `while`, or `until` tests,
return value negation (`!`), or part of a boolean (`&&` or `||`)
chain.
- any variable that has not explicitly been set or declared (with
either `declare` or `local`) is referenced.

If followed by a command-line, the strictness applies for the duration
of the command.

Example (Whole Script):

strict_env
has curl

Example (Command):

strict_env has curl

#### `unstrict_env [<command> ...]`

Turns off shell execution strictness. If followed by a command-line, the
strictness applies for the duration of the command.

Example (Whole Script):

unstrict_env
has curl

Example (Command):

unstrict_env has curl


COPYRIGHT
---------

Expand Down
85 changes: 84 additions & 1 deletion stdlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const StdLib = "#!/usr/bin/env bash\n" +
"shopt -s nullglob\n" +
"shopt -s extglob\n" +
"\n" +
"\n" +
"# NOTE: don't touch the RHS, it gets replaced at runtime\n" +
"direnv=\"$(command -v direnv)\"\n" +
"\n" +
Expand All @@ -30,6 +29,90 @@ const StdLib = "#!/usr/bin/env bash\n" +
"# algorithm and so it won't be re-exported.\n" +
"export DIRENV_IN_ENVRC=1\n" +
"\n" +
"__env_strictness() {\n" +
" local mode tmpfile old_shell_options\n" +
" local -i res\n" +
"\n" +
" tmpfile=$(mktemp)\n" +
" res=0\n" +
" mode=\"$1\"\n" +
" shift\n" +
"\n" +
" set +o | grep 'pipefail\\|nounset\\|errexit' > \"$tmpfile\"\n" +
" old_shell_options=$(< \"$tmpfile\")\n" +
" rm -f tmpfile\n" +
"\n" +
" case \"$mode\" in\n" +
" strict)\n" +
" set -o errexit -o nounset -o pipefail\n" +
" ;;\n" +
" unstrict)\n" +
" set +o errexit +o nounset +o pipefail\n" +
" ;;\n" +
" *)\n" +
" log_error \"Unknown strictness mode '${mode}'.\"\n" +
" exit 1\n" +
" ;;\n" +
" esac\n" +
"\n" +
" if (($#)); then\n" +
" \"${@}\"\n" +
" res=$?\n" +
" eval \"$old_shell_options\"\n" +
" fi\n" +
"\n" +
" # Force failure if the inner script has failed and the mode is strict\n" +
" if [[ $mode = strict && $res -gt 0 ]]; then\n" +
" exit 1\n" +
" fi\n" +
"\n" +
" return $res\n" +
"}\n" +
"\n" +
"# Usage: strict_env [<command> ...]\n" +
"#\n" +
"# Turns on shell execution strictness. This will force the .envrc\n" +
"# evaluation context to exit immediately if:\n" +
"#\n" +
"# - any command in a pipeline returns a non-zero exit status that is\n" +
"# not otherwise handled as part of `if`, `while`, or `until` tests,\n" +
"# return value negation (`!`), or part of a boolean (`&&` or `||`)\n" +
"# chain.\n" +
"# - any variable that has not explicitly been set or declared (with\n" +
"# either `declare` or `local`) is referenced.\n" +
"#\n" +
"# If followed by a command-line, the strictness applies for the duration\n" +
"# of the command.\n" +
"#\n" +
"# Example:\n" +
"#\n" +
"# strict_env\n" +
"# has curl\n" +
"#\n" +
"# strict_env has curl\n" +
"strict_env() {\n" +
" __env_strictness strict \"$@\"\n" +
"}\n" +
"\n" +
"# Usage: unstrict_env [<command> ...]\n" +
"#\n" +
"# Turns off shell execution strictness. If followed by a command-line, the\n" +
"# strictness applies for the duration of the command.\n" +
"#\n" +
"# Example:\n" +
"#\n" +
"# unstrict_env\n" +
"# has curl\n" +
"#\n" +
"# unstrict_env has curl\n" +
"unstrict_env() {\n" +
" if (($#)); then\n" +
" __env_strictness unstrict \"$@\"\n" +
" else\n" +
" set +o errexit +o nounset +o pipefail\n" +
" fi\n" +
"}\n" +
"\n" +
"# Usage: direnv_layout_dir\n" +
"#\n" +
"# Prints the folder path that direnv should use to store layout content.\n" +
Expand Down
85 changes: 84 additions & 1 deletion stdlib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ shopt -s gnu_errfmt
shopt -s nullglob
shopt -s extglob


# NOTE: don't touch the RHS, it gets replaced at runtime
direnv="$(command -v direnv)"

Expand All @@ -27,6 +26,90 @@ direnv_config_dir="${DIRENV_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/direnv}"
# algorithm and so it won't be re-exported.
export DIRENV_IN_ENVRC=1

__env_strictness() {
local mode tmpfile old_shell_options
local -i res

tmpfile=$(mktemp)
res=0
mode="$1"
shift

set +o | grep 'pipefail\|nounset\|errexit' > "$tmpfile"
old_shell_options=$(< "$tmpfile")
rm -f tmpfile

case "$mode" in
strict)
set -o errexit -o nounset -o pipefail
;;
unstrict)
set +o errexit +o nounset +o pipefail
;;
*)
log_error "Unknown strictness mode '${mode}'."
exit 1
;;
esac

if (($#)); then
"${@}"
res=$?
eval "$old_shell_options"
fi

# Force failure if the inner script has failed and the mode is strict
if [[ $mode = strict && $res -gt 0 ]]; then
exit 1
fi

return $res
}

# Usage: strict_env [<command> ...]
#
# Turns on shell execution strictness. This will force the .envrc
# evaluation context to exit immediately if:
#
# - any command in a pipeline returns a non-zero exit status that is
# not otherwise handled as part of `if`, `while`, or `until` tests,
# return value negation (`!`), or part of a boolean (`&&` or `||`)
# chain.
# - any variable that has not explicitly been set or declared (with
# either `declare` or `local`) is referenced.
#
# If followed by a command-line, the strictness applies for the duration
# of the command.
#
# Example:
#
# strict_env
# has curl
#
# strict_env has curl
strict_env() {
__env_strictness strict "$@"
}

# Usage: unstrict_env [<command> ...]
#
# Turns off shell execution strictness. If followed by a command-line, the
# strictness applies for the duration of the command.
#
# Example:
#
# unstrict_env
# has curl
#
# unstrict_env has curl
unstrict_env() {
if (($#)); then
__env_strictness unstrict "$@"
else
set +o errexit +o nounset +o pipefail
fi
}

# Usage: direnv_layout_dir
#
# Prints the folder path that direnv should use to store layout content.
Expand Down
3 changes: 3 additions & 0 deletions test/stdlib.bash
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,7 @@ test_name source_env_if_exists
[[ $FOO = bar ]]
)

# test strict_env and unstrict_env
./strict_env_test.bash

echo OK
67 changes: 67 additions & 0 deletions test/strict_env_test.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#! /usr/bin/env bash

# Note: this is _explicitly_ not setting `set -euo pipefail`
# because we are testing functions that configure that.

declare base expected actual
base="${TMPDIR:-/tmp}/$(basename "$0").$$"
expected="$base".expected
actual="$base".actual

declare -i success
success=0

# always execute relative to here
cd "$(dirname "$0")" || exit 1

# add the built direnv to the path
root=$(cd .. && pwd -P)
export PATH=$root:$PATH

load_stdlib() {
# shellcheck disable=SC1090
source "$root/stdlib.sh"
}

test_fail() {
echo "FAILED $*: expected is not actual"
exit 1
}

test_strictness() {
local step
step="$1"
echo "$2" > "$expected"

set -o | grep 'errexit\|nounset\|pipefail' > "$actual"
diff -u "$expected" "$actual" || test_fail "$step"
(( success += 1 ))
}

load_stdlib

test_strictness 'after source' $'errexit off
nounset off
pipefail off'

strict_env
test_strictness 'after strict_env' $'errexit on
nounset on
pipefail on'

unstrict_env echo HELLO > /dev/null
test_strictness "after unstrict_env with command" $'errexit on
nounset on
pipefail on'

strict_env echo HELLO > /dev/null
test_strictness "after strict_env with command" $'errexit on
nounset on
pipefail on'

unstrict_env
test_strictness 'after unstrict_env' $'errexit off
nounset off
pipefail off'

echo "OK ($success tests)"

0 comments on commit f2f2b92

Please sign in to comment.