Last active
March 9, 2026 17:46
-
-
Save webstrand/11df8f48e7146727d977263ccd84ced9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| load-environ() { | |
| local - | |
| set -euo pipefail | |
| if [[ "${1:-}" == "--help" || "${1:-}" == "-h" || -z "${1:-}" ]]; then | |
| cat <<'EOF' | |
| load-environ — import variables from an environ(7) file | |
| Usage: | |
| load-environ FILE [OPS...] [-- CMD ARGS...] | |
| If no OPS are provided, --all is implied. | |
| OPS are processed left to right, building the set of variables to load: | |
| --all add all safe variables | |
| --none clear the set | |
| --clobber add all variables, including unsafe ones (requires --) | |
| VAR add a single variable from the file | |
| K=V add a literal variable | |
| -i, --include VAR add VAR (use to escape flag-shaped names: -i --none) | |
| -x, --exclude VAR remove VAR from the set | |
| If a CMD is provided, it is executed with the constructed | |
| environment. Otherwise the constructed environment is exported | |
| into the current shell. | |
| Examples: | |
| load-environ ./env # export everything safe | |
| load-environ ./env PATH CC CFLAGS # export only these three | |
| load-environ ./env -x SECRET # everything except SECRET | |
| load-environ ./env -- make # run make under the environ | |
| load-environ ./env --clobber -- bash # full environ, new shell | |
| EOF | |
| return 0 | |
| fi | |
| local file="$1" | |
| shift | |
| if [[ ! -r "$file" ]]; then | |
| echo "load-environ: cannot read '$file'" >&2 | |
| return 1 | |
| fi | |
| # regex for vars that would break an interactive shell | |
| local _le_blocked='^(BASH_.*|FUNCNAME|SHELLOPTS|BASHOPTS|SHLVL' | |
| _le_blocked+='|UID|EUID|PPID|GROUPS' | |
| _le_blocked+='|RANDOM|LINENO|SECONDS|PIPESTATUS|_' | |
| _le_blocked+='|HOSTNAME|HOSTTYPE|MACHTYPE|OSTYPE' | |
| _le_blocked+='|OPTIND|OPTERR|OPTARG' | |
| _le_blocked+='|COMP_.*|DIRSTACK|REPLY' | |
| _le_blocked+='|HOME|SHELL|USER|LOGNAME' | |
| _le_blocked+='|IFS|PWD|OLDPWD|CDPATH' | |
| _le_blocked+='|PS1|PS2|PS4|PROMPT_COMMAND' | |
| _le_blocked+='|HISTFILE|HISTSIZE|HISTCONTROL|HISTIGNORE' | |
| _le_blocked+='|IGNOREEOF|GLOBIGNORE' | |
| _le_blocked+='|INPUTRC|FIGNORE' | |
| _le_blocked+='|TERM|COLUMNS|LINES|COLORTERM' | |
| _le_blocked+='|TERM_PROGRAM|TERM_PROGRAM_VERSION|VTE_VERSION' | |
| _le_blocked+='|GPG_TTY' | |
| _le_blocked+='|MAIL|MAILCHECK|MAILPATH)$' | |
| # parse environ file into associative array | |
| local -A envmap=() | |
| local entry key | |
| while IFS= read -r -d '' entry; do | |
| [[ "$entry" == *=* ]] || continue | |
| key="${entry%%=*}" | |
| envmap["$key"]="$entry" | |
| done < "$file" | |
| # progressively build result map — start with all safe | |
| local -A result=() | |
| local must_exec=false | |
| local past_sep=false | |
| local default_all=true | |
| for key in "${!envmap[@]}"; do | |
| [[ "$key" =~ $_le_blocked ]] || result["$key"]="${envmap[$key]}" | |
| done | |
| while [[ $# -gt 0 ]]; do | |
| if $past_sep; then break; fi | |
| case "$1" in | |
| --) | |
| past_sep=true | |
| shift | |
| ;; | |
| --help|-h) | |
| load-environ --help | |
| return | |
| ;; | |
| --all) | |
| for key in "${!envmap[@]}"; do | |
| [[ "$key" =~ $_le_blocked ]] || result["$key"]="${envmap[$key]}" | |
| done | |
| default_all=false | |
| shift | |
| ;; | |
| --clobber) | |
| must_exec=true | |
| for key in "${!envmap[@]}"; do | |
| result["$key"]="${envmap[$key]}" | |
| done | |
| default_all=false | |
| shift | |
| ;; | |
| --none) | |
| must_exec=false | |
| result=() | |
| default_all=false | |
| shift | |
| ;; | |
| --exclude|-x) | |
| local ekey="${2:?-x requires an argument}" | |
| unset 'result[$ekey]' | |
| shift 2 | |
| ;; | |
| --include|-i) | |
| if [[ $# -lt 2 ]]; then | |
| echo "load-environ: -i requires an argument" >&2 | |
| return 1 | |
| fi | |
| shift | |
| ;& | |
| *) | |
| if $default_all; then | |
| result=() | |
| default_all=false | |
| fi | |
| if [[ "$1" == *=* ]]; then | |
| result["${1%%=*}"]="$1" | |
| elif [[ -v "envmap[$1]" ]]; then | |
| result["$1"]="${envmap[$1]}" | |
| else | |
| echo "load-environ: '$1' not found in $file" >&2 | |
| return 1 | |
| fi | |
| shift | |
| ;; | |
| esac | |
| done | |
| # build flat array | |
| local -a vars=() | |
| for entry in "${result[@]}"; do | |
| vars+=("$entry") | |
| done | |
| if $past_sep; then | |
| if [[ $# -eq 0 ]]; then | |
| echo "load-environ: expected command after --" >&2 | |
| return 1 | |
| fi | |
| env -- "${vars[@]}" "$@" | |
| elif $must_exec; then | |
| echo "load-environ: --clobber requires -- followed by a command" >&2 | |
| return 1 | |
| else | |
| for entry in "${vars[@]}"; do | |
| # shellcheck disable=SC2163 # $entry is KEY=value, not a bare name | |
| export "$entry" | |
| done | |
| fi | |
| } | |
| if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then | |
| has_sep=false | |
| for arg in "$@"; do | |
| [[ "$arg" == "--" ]] && has_sep=true && break | |
| done | |
| if ! $has_sep; then | |
| echo "load-environ: no current shell; source this file, or provide -- CMD ARGS..." >&2 | |
| exit 1 | |
| fi | |
| load-environ "$@" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment