#!/bin/sh # # srcenv - A cross-shell tool for sourcing POSIX compliant .env scripts # # shellcheck disable=SC2317 version() { if [ -z "$cmd" ]; then echo 'srcenv 1.6.0' else out 'srcenv 1.6.0' fi } # region Help / Usage header() { out 'srcenv is a cross-shell tool for sourcing POSIX compliant .env scripts.'; } desc() { out " srcenv takes a snapshot of the POSIX shell environment, sources the .env scripts and prints a shell specific script exporting the environment variables that have changed since the snapshot." } help() { colors NORMAL BOLD out " For listing the command options, use '${cmd:-${arg0##*/}} ${BOLD}--help${NORMAL}'." } man() { colors NORMAL ITALIC out " For more advanced usage see the srcenv(1) manpage ${ITALIC}('man srcenv')${NORMAL} and/or https://github.com/ins0mniaque/srcenv." } usage() { colors NORMAL BOLD ITALIC UNDERLINE YELLOW if [ -z "$cmd" ]; then warn=; command -v "$jq" > /dev/null || \ warn=$(printf "%$((32 + (${#NORMAL} * 2 + ${#BOLD} * 2 + ${#YELLOW})))s" \ "${BOLD}${YELLOW}warning: ${NORMAL}${BOLD}$jq${NORMAL} not found") out " ${BOLD}${UNDERLINE}Usage${NORMAL}: ${BOLD}srcenv${NORMAL} <${ITALIC}shell${NORMAL}> [${ITALIC}options${NORMAL}] [${ITALIC}files${NORMAL}] $warn init|rc <${ITALIC}shell${NORMAL}> [--cmd ${ITALIC}name${NORMAL}] [--sh ${ITALIC}sh${NORMAL}] [-- ${ITALIC}options${NORMAL}] [-h|--help|--version]" else warn=; command -v "$jq" > /dev/null || \ warn=$(printf "%$((46 - ${#cmd} + (${#NORMAL} * 2 + ${#BOLD} * 2 + ${#YELLOW})))s" \ "${BOLD}${YELLOW}warning: ${NORMAL}${BOLD}$jq${NORMAL} not found") help=$(printf "%$((29 + ${#cmd}))s" \ "[-h|--help|--version]") out " ${BOLD}${UNDERLINE}Usage${NORMAL}: ${BOLD}$cmd${NORMAL} [${ITALIC}options${NORMAL}] [${ITALIC}files${NORMAL}] $warn $help" fi } options() { colors NORMAL BOLD ITALIC UNDERLINE if [ -z "$cmd" ]; then out " ${BOLD}${UNDERLINE}Commands${NORMAL}: ${BOLD}init ${NORMAL}Generate the integration script to be sourced with command name 'src' ${ITALIC}(change with ${BOLD}--cmd${NORMAL}${ITALIC})${NORMAL} and POSIX shell 'sh' ${ITALIC}(change with ${BOLD}--sh${NORMAL}${ITALIC})${NORMAL} ${BOLD}rc ${NORMAL}Generate the command to add to your shell's configuration file to source the integration script ${BOLD}${UNDERLINE}Shells${NORMAL}: ${BOLD}ash, dash ${NORMAL}Format the output as an Ash/Dash script ${BOLD}bash ${NORMAL}Format the output as a Bash script ${BOLD}clink ${NORMAL}Format the output as a Clink/Windows Command shell script ${BOLD}cmd, command ${NORMAL}Format the output as a Windows Command shell script ${BOLD}csh, tcsh ${NORMAL}Format the output as a Csh/Tcsh script ${BOLD}elvish ${NORMAL}Format the output as an Elvish script ${BOLD}env ${NORMAL}Format the output as a .env file ${BOLD}fish ${NORMAL}Format the output as a Fish script ${BOLD}ion ${NORMAL}Format the output as an ion script ${BOLD}json ${NORMAL}Format the output as JSON ${BOLD}ksh, pdksh, mksh ${NORMAL}Format the output as a Ksh script ${BOLD}launchd ${NORMAL}Format the output as a launchd environment script ${BOLD}murex ${NORMAL}Format the output as a Murex script ${BOLD}nu, nushell ${NORMAL}Format the output as a Nushell script ${BOLD}posix, sh ${NORMAL}Format the output as a POSIX shell script ${BOLD}pwsh, powershell ${NORMAL}Format the output as a PowerShell script ${BOLD}systemd ${NORMAL}Format the output as a systemd EnvironmentFile ${BOLD}xonsh ${NORMAL}Format the output as a Xonsh script ${BOLD}zsh ${NORMAL}Format the output as a Zsh script" fi out " ${BOLD}${UNDERLINE}Options${NORMAL}: ${BOLD}--color WHEN, --color=WHEN ${NORMAL}Specify when to use colored output: ${BOLD:-*}auto${BOLD:-*}${NORMAL}, never or always ${ITALIC}${DIM}i.e. 'auto' disables colors if the output goes to a pipe${NORMAL} ${BOLD}--export-colors ${NORMAL}Cache terminal capabilities to SRCENV_RESTORE to improve performance ${BOLD}--clear-colors ${NORMAL}Clear cached terminal capabilities from SRCENV_RESTORE ${BOLD}-x VAR, --exclude VAR, ${BOLD}-x=VAR, --exclude=VAR ${NORMAL}Exclude VAR from exported variables ${ITALIC}(can be used multiple times)${NORMAL} ${BOLD}-f FORMAT, --format FORMAT, ${BOLD}-f=FORMAT, --format=FORMAT ${NORMAL}Format the output as anything ${ITALIC}(shell or jq interpolated string)${NORMAL} ${ITALIC}${DIM}e.g. 'json' or '\(\$k)=\(.[\$k]|@sh)??\(\$k)='${NORMAL} ${BOLD}-i INPUT, --input INPUT, ${BOLD}-i=INPUT, --input=INPUT ${NORMAL}Source from string value of INPUT ${BOLD}- ${NORMAL}Source from STDIN ${BOLD}-b, --backup ${NORMAL}Backup changes to SRCENV_RESTORE for restore ${BOLD}-r, --restore ${NORMAL}Restore backed up changes from SRCENV_RESTORE ${BOLD}-c, --clear ${NORMAL}Clear backed up changes from SRCENV_RESTORE ${BOLD}-n ${NORMAL}Do not backup changes and do not restore backed up changes ${BOLD}--no-backup ${NORMAL}Do not backup changes to SRCENV_RESTORE for restore ${BOLD}--no-restore ${NORMAL}Do not restore backed up changes from SRCENV_RESTORE ${BOLD}-m, --modify ${NORMAL}Allow modifying existing environment variables ${ITALIC}(Default)${NORMAL} ${BOLD}-w, --write-protect ${NORMAL}Do not allow modifying existing environment variables ${BOLD}-j, --json ${NORMAL}Treat input as JSON${NORMAL} ${BOLD}-p, --posix ${NORMAL}Treat input as POSIX ${ITALIC}(Default)${NORMAL} ${BOLD}-e, --export ${NORMAL}Export all variables ${ITALIC}(Default for .env files)${NORMAL} ${BOLD}-l, --local ${NORMAL}Do not export all variables ${BOLD}-s, --sort ${NORMAL}Sort the environment variables alphabetically ${ITALIC}(Default)${NORMAL} ${BOLD}-u, --unsorted ${NORMAL}Keep the environment variables unsorted ${BOLD}-q, --quiet ${NORMAL}Do not display changed environment variables ${BOLD}-v, --verbose ${NORMAL}Display changed environment variables ${BOLD}-h, --help ${NORMAL}Display help and exit ${BOLD}--version ${NORMAL}Display the version number and exit ${BOLD}--debug ${NORMAL}Display jq filter without sourcing and exit" } noinput() { colors NORMAL BOLD RED out "${BOLD}${RED}error:${NORMAL} no input files or arguments" } nojq() { colors NORMAL BOLD RED GREEN YELLOW [ -n "$SRCENV_JQ" ] && \ out "${BOLD}${YELLOW}warn: ${NORMAL} ${YELLOW}jq${NORMAL} is set to ${GREEN}$SRCENV_JQ${NORMAL}; unset ${YELLOW}SRCENV_JQ${NORMAL} to revert to ${GREEN}jq${NORMAL}" out "${BOLD}${RED}error:${NORMAL} ${YELLOW}jq${NORMAL} not found; see https://jqlang.github.io/jq/download for installation options" } nosha() { colors NORMAL BOLD RED GREEN YELLOW [ -n "$SRCENV_HASH" ] && \ out "${BOLD}${YELLOW}warn: ${NORMAL} ${YELLOW}hash command${NORMAL} is set to ${GREEN}$SRCENV_HASH${NORMAL}; unset ${YELLOW}SRCENV_HASH${NORMAL} to revert to default hash command" out "${BOLD}${RED}error:${NORMAL} ${YELLOW}shasum${NORMAL} or ${YELLOW}sha256sum${NORMAL} or ${YELLOW}openssl${NORMAL} not found; install any or set SRCENV_HASH for custom hash command" } noformat() { colors NORMAL BOLD RED out "${BOLD}${RED}error:${NORMAL} no shell or format specified" } noshell() { colors NORMAL BOLD RED YELLOW case $1 in '') out "${BOLD}${RED}error:${NORMAL} no shell specified" ;; *) out "${BOLD}${RED}error:${NORMAL} ${YELLOW}$1${NORMAL} is not a supported shell" esac } nosupport() { colors NORMAL BOLD RED YELLOW out "${BOLD}${RED}error:${NORMAL} ${BOLD}$1${NORMAL} not supported for shell ${YELLOW}$shell${NORMAL}" } autoskip() { colors NORMAL BOLD DIM GREEN out "${BOLD}${DIM}srcenv:${NORMAL} ${GREEN}$dir${NORMAL} is already sourced; skipping" } automiss() { colors NORMAL BOLD DIM RED YELLOW out "${BOLD}${DIM}srcenv:${NORMAL} No entry for ${RED}$dir${NORMAL} in ${YELLOW}$auto_data${NORMAL}; updating cache" } autoprompt() { colors NORMAL BOLD DIM GREEN YELLOW [ "$dir" != "$PWD" ] && \ dirarg=" ${BOLD}--dir '$dir'${NORMAL}${GREEN}" || \ dirarg= out "${BOLD}${DIM}srcenv:${NORMAL} auto ${YELLOW}$dir${NORMAL} disabled; verify changed files and use '${GREEN}${arg0##*/}$dirarg --auto $args${NORMAL}' to enable" return 1 } autoadded() { colors NORMAL BOLD DIM GREEN YELLOW out "${BOLD}${DIM}srcenv:${NORMAL} auto ${YELLOW}$dir${NORMAL} added: ${GREEN}$args${NORMAL}" } autoaddfailed() { colors NORMAL BOLD RED YELLOW out "${BOLD}${RED}error:${NORMAL} Failed to add auto ${YELLOW}$dir${NORMAL}" } autoremoved() { colors NORMAL BOLD DIM YELLOW out "${BOLD}${DIM}srcenv:${NORMAL} auto ${YELLOW}$dir${NORMAL} removed" } autoremovefailed() { colors NORMAL BOLD RED YELLOW out "${BOLD}${RED}error:${NORMAL} Failed to remove auto ${YELLOW}$dir${NORMAL}${1+: $1}" } wronghash() { colors NORMAL BOLD DIM RED GREEN YELLOW out "${BOLD}${DIM}srcenv:${NORMAL} ${BOLD}${RED}hash mismatch:${NORMAL} ${YELLOW}${1#"$dir/"}${NORMAL}: ${RED}$3${NORMAL} != ${GREEN}$2${NORMAL}" } invalid() { colors NORMAL BOLD RED YELLOW out "${BOLD}${RED}error:${NORMAL} ${2:-invalid option} -- ${YELLOW}$1${NORMAL}" } err() { if [ "$2" = 0 ]; then out "$1"; return fi colors NORMAL BOLD RED YELLOW escape=$(printf '\033') case $1 in '') ;; $escape*) out "$1" ;; $jq:\ *\*) error=${1#"$jq": } error=${error#error*: } error=${error%%, line 1:"${LF}"*} out "${BOLD}${RED}error:${NORMAL} ${YELLOW}SRCENV_RESTORE${NORMAL} environment variable contains ${RED}invalid JSON${NORMAL}: $error" ;; $jq:\ *) error=${1#"$jq": } error=${error#error: } error=${error%%, line 1:"${LF}"*} out "${BOLD}${RED}error:${NORMAL} ${BOLD}Invalid format:${NORMAL} $error" ;; *) printf '%s\n' "$1" | while IFS= read -r line; do line=${line#"$arg0": } line=${line#line [0-9]*: } line=${line#"$arg0"\[[0-9]*\]: } out "${BOLD}${RED}error:${NORMAL} $line" done ;; esac } out() { echo >&2 "$1"; } [ -n "$MUREX_PID" ] && out() { colors NORMAL printf '%s\n' "$1" | while IFS= read -r line; do echo >&2 "${NORMAL}$line" done } [ -t 1 ] && colorless= || colorless=1 color=${SRCENV_COLOR:-auto} colors=$SRCENV_COLORS NORMAL=; BOLD=; DIM=; ITALIC=; UNDERLINE= BLACK=; RED=; GREEN=; YELLOW=; BLUE=; MAGENTA=; CYAN=; WHITE= colors() { case $color in never) return ;; always) ;; *) [ -n "$colorless" ] && [ -z "$cmd" ] && return ;; esac if [ -n "$SRCENV_COLORS" ]; then [ -z "$colors" ] && return NORMAL=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} BOLD=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} DIM=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} ITALIC=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} UNDERLINE=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} BLACK=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} RED=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} GREEN=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} YELLOW=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} BLUE=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} MAGENTA=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} CYAN=${colors%%"${DLM}"*}; colors=${colors#*"${DLM}"} WHITE=${colors%%"${DLM}"*}; colors= return fi for var in "$@"; do case $var in NORMAL) NORMAL=${NORMAL:-$( tput sgr0 2> /dev/null || printf '\033[0m')} ;; BOLD) BOLD=${BOLD:-$( tput bold 2> /dev/null || printf '\033[1m')} ;; DIM) DIM=${DIM:-$( tput dim 2> /dev/null || printf '\033[2m')} ;; ITALIC) ITALIC=${ITALIC:-$( tput sitm 2> /dev/null || printf '\033[3m')} ;; UNDERLINE) UNDERLINE=${UNDERLINE:-$(tput smul 2> /dev/null || printf '\033[4m')} ;; BLACK) BLACK=${BLACK:-$( tput setaf 0 2> /dev/null || printf '\033[30m')} ;; RED) RED=${RED:-$( tput setaf 1 2> /dev/null || printf '\033[31m')} ;; GREEN) GREEN=${GREEN:-$( tput setaf 2 2> /dev/null || printf '\033[32m')} ;; YELLOW) YELLOW=${YELLOW:-$( tput setaf 3 2> /dev/null || printf '\033[33m')} ;; BLUE) BLUE=${BLUE:-$( tput setaf 4 2> /dev/null || printf '\033[34m')} ;; MAGENTA) MAGENTA=${MAGENTA:-$( tput setaf 5 2> /dev/null || printf '\033[35m')} ;; CYAN) CYAN=${CYAN:-$( tput setaf 6 2> /dev/null || printf '\033[36m')} ;; WHITE) WHITE=${WHITE:-$( tput setaf 7 2> /dev/null || printf '\033[37m')} ;; esac done } exportcolors() { color=always colors NORMAL BOLD DIM ITALIC UNDERLINE BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE export SRCENV_COLORS="$NORMAL${DLM}$BOLD${DLM}$DIM${DLM}$ITALIC${DLM}$UNDERLINE${DLM}$BLACK${DLM}$RED${DLM}$GREEN${DLM}$YELLOW${DLM}$BLUE${DLM}$MAGENTA${DLM}$CYAN${DLM}$WHITE" } clearcolors() { unset SRCENV_COLORS } validcolor() { case $2 in auto|always|never) ;; *) colors NORMAL GREEN invalid "$1" "color must be either ${GREEN}auto${NORMAL}, ${GREEN}never${NORMAL} or ${GREEN}always${NORMAL}"; usage; help; exit 2 ;; esac } parse() { case $2 in -*|'') invalid "$1" 'option requires an argument'; usage; help; return 2 ;; *) [ -n "$3" ] && ! "$3" "$1 '$2'" "$2" && return $?; printf '%s' "$2" ;; esac } # endregion Help / Usage # region auto() locate() { if [ -n "$SRCENV_DATA_DIR" ]; then auto_data="$SRCENV_DATA_DIR/auto.json" elif [ -n "$XDG_DATA_HOME" ]; then auto_data="$XDG_DATA_HOME/srcenv/auto.json" elif [ -n "$COMSPEC" ] && \ [ -n "$LOCALAPPDATA" ]; then auto_data="$LOCALAPPDATA/srcenv/auto.json" else auto_data="$HOME/.local/share/srcenv/auto.json" fi } autoable() { case $shell in *) return 1 ;; esac } auto() { if [ "$dir" = "$SRCENV_AUTO_DIR" ]; then autoskip; exit 0 fi locate if ! jq=$(command -v "$jq"); then nojq; exit 1 fi args=; ok= if [ -f "$auto_data" ]; then # shellcheck disable=SC2016 filter='. | with_entries(select(.key | startswith($dir'${COMSPEC:+[1:]}'))) | to_entries | sort_by(.key) | first | if .value == null then "ok=0" else "dir=\(.key|@sh)\nargs=\(.value.args|@sh)\nok=1\n" + ([(.value.hashes | to_entries[] | "sha \"$dir/\(.key)\" \"\(.value)\" || ok=")] | join("\n")) end' eval "$("$jq" ${SRCENV_JQ_BINARY:+-b} --arg dir "${COMSPEC:+\\}$dir" -r "$filter" "$auto_data")" else ok=0 fi [ -z "$ok" ] && autoprompt && ok=1 && args="--auto $args" case $ok in '') set -- --restore --verbose --input "export SRCENV_AUTO_DIR=\$dir" ;; 0) set -- --restore --verbose --input 'unset SRCENV_AUTO_DIR'; automiss; cache ;; 1) # shellcheck disable=SC2086 set -- --backup --restore --verbose $args --no-auto --input "export SRCENV_AUTO_DIR=\$dir" ;; esac main --dir "$dir" "$@" } autoadd() { [ -d "${auto_data%/*}" ] || mkdir -p "${auto_data%/*}" [ -f "$auto_data" ] && source=$auto_data || source=-n # shellcheck disable=SC2016 adddir='. += { "\($dir'${COMSPEC:+[1:]}')": { "args": $args, "hashes": ($hashes | split("\n\n") | [(.[] | split("\n") | { "\(.[0])": "\(.[1])" })] | add) } }' stdout=$("$jq" ${SRCENV_JQ_BINARY:+-b} --arg dir "${COMSPEC:+\\}$dir" --arg args "$args" --arg hashes "${hashes%"${LF}${LF}"}" "$adddir" "$source") exitcode=$? if [ "$exitcode" = "0" ]; then printf '%s' "$stdout" > "$auto_data" || exitcode=74 fi if [ "$exitcode" = "0" ]; then [ -n "$verbose" ] && autoadded else autoaddfailed fi return "$exitcode" } autoremove() { if [ ! -f "$auto_data" ]; then autoremovefailed "$auto_data not found"; return 1 fi # shellcheck disable=SC2016 removedir='if .[$dir'${COMSPEC:+[1:]}'] == null then false else . | del(.[$dir'${COMSPEC:+[1:]}']) end' stdout=$("$jq" ${SRCENV_JQ_BINARY:+-b} --arg dir "${COMSPEC:+\\}$dir" -e "$removedir" "$auto_data") exitcode=$? if [ "$exitcode" = "0" ]; then printf '%s' "$stdout" > "$auto_data" || exitcode=74 fi if [ "$exitcode" = "0" ]; then [ -n "$verbose" ] && autoremoved elif [ "$exitcode" = "1" ]; then autoremovefailed "No matching entry in $auto_data" else autoremovefailed fi return "$exitcode" } cache() { if ! jq=$(command -v "$jq"); then nojq; exit 1 fi [ -f "$auto_data" ] && source=$auto_data || source=-n setup; build cache='{ SRCENV_AUTO_DIRS: (. // {} | keys | sort_by(length) | join(":")) }' cache="$prefix$cache | $keys | $format$suffix" "$jq" ${SRCENV_JQ_BINARY:+-b} -r "$cache" "$source" } sha() { if [ -z "$sha" ]; then if command -v sha256sum > /dev/null; then sha=sha256sum elif command -v shasum > /dev/null; then sha='shasum -a 256' elif command -v openssl > /dev/null; then sha='openssl dgst -sha256' else nosha; return 1 fi fi hash=$($sha "$1") hash=${hash%% *} if [ -z "$2" ]; then echo "$hash" elif [ "$hash" != "$2" ]; then wronghash "$@" "$hash"; return 1 fi } # endregion auto() # region init() / rc() initrc() { case $shell in csh|tcsh|xonsh) quote=\" ;; *) quote=\' ;; esac case $arg0 in /*) srcenv="${quote}$arg0${quote}" ;; ./*) srcenv="${quote}$PWD/${arg0#./}${quote}" ;; */*) srcenv="${quote}$PWD/$arg0${quote}" ;; *) srcenv="${quote}$(command -v "$arg0")${quote}" || \ srcenv="${quote}$PWD/$arg0${quote}" ;; esac while [ $# -gt 0 ]; do case $1 in --auto) auto=1; shift ;; --no-auto) auto=; shift ;; --no-cmd) name=; shift ;; --cmd) name=$(parse "$1" "$2" validname) || exit $?; [ $# -gt 1 ] && shift; shift ;; --cmd=*) name=$(parse "${1%%=*}" "${1#*=}" validname) || exit $?; shift ;; --sh) sh=$(parse "$1" "$2" validsh) || exit $?; [ $# -gt 1 ] && shift; shift ;; --sh=*) sh=$(parse "${1%%=*}" "${1#*=}" validsh) || exit $?; shift ;; -h|--help) header; usage; desc; options; man; exit 0 ;; --version) version; exit 0 ;; --) shift; opts="$*"; break ;; *) invalid "$1"; usage; help; exit 2 ;; esac done } # shellcheck disable=SC2016 hook() { locate; cache; echo printf '%s\n\n' '_srcenv_auto() { exit_code=$?; [ "$SRCENV_AUTO_PWD" = "$PWD" ] && return "$exit_code" export SRCENV_AUTO_PWD=$PWD dir=$SRCENV_AUTO_PWD while [ -n "$dir" ]; do case ":$SRCENV_AUTO_DIRS:" in *:$dir/*|*:$dir:) [ "$dir" = "$SRCENV_AUTO_DIR" ] && dir=; break; ;; esac case $dir in /) dir= ;; */*) dir=${dir%/*}; [ -z "$dir" ] && dir=/ ;; *) dir= ;; esac done [ -n "$dir" ] && eval "$('"$srcenv auto $shell"')" return "$exit_code" }' echo 'case ";${PROMPT_COMMAND[*]:-};" in *\;_srcenv_auto\;*) ;; *) case $(declare -p PROMPT_COMMAND 2>&1) in declare\ -a*) export PROMPT_COMMAND=(_srcenv_auto "${PROMPT_COMMAND[@]}") ;; *) export PROMPT_COMMAND="_srcenv_auto${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;; esac ;; esac' } init() { auto=; name=src; sh=; opts='--backup --restore --verbose' # TODO: Enable on bump to 1.7.0 # autoable && auto=1 case $shell in clink|cmd|command|pwsh|powershell) sh='sh' ;; esac initrc "$@" || exit $? [ -n "$auto" ] && \ case $shell in bash) hook ;; *) nosupport --auto; return 1 ;; esac [ -n "$auto" ] && \ [ -n "$name" ] && \ echo case $shell in nu|nushell) srcenv="${sh:+"$sh "}$srcenv json $opts --cmd $name" ;; *) srcenv="${sh:+"$sh "}$srcenv $shell $opts --cmd $name" ;; esac [ -n "$name" ] && \ case $shell in ash|dash|\ bash|\ ksh|pdksh|mksh|\ posix|sh|\ zsh) echo "$name() { eval \"\$($srcenv \"\$@\")\"; }" ;; clink|\ cmd|command) echo "DOSKEY $name=@echo off \$T $srcenv \$* \$G %TEMP%\srcenv.$name.cmd \$T\$T call %TEMP%\srcenv.$name.cmd \$T del %TEMP%\srcenv.$name.cmd \$T echo on" ;; csh|tcsh) if [ -e /dev/stdin ]; then echo "alias $name '$srcenv \!* | source /dev/stdin'" else echo "alias $name 'set mktemp = \"\`mktemp\`\"; $srcenv \!* > \"\$mktemp\"; source \"\$mktemp\"; rm -f \"\$mktemp\"'" fi ;; elvish) echo "var $name = {|@a| eval ($srcenv \$@a | slurp) }" ;; fish) echo "function $name; $srcenv \$argv | source; end" ;; ion) echo "fn $name args:[str]${LF} let args = \$or(@args \"--help\")${LF} eval \"\$($srcenv \$args ^> /dev/stderr)\"${LF}end" ;; murex) echo "function $name { $srcenv @PARAMS -> source }" ;; nu|nushell) echo "def --env --wrapped $name [...args] { ^$srcenv ...(\$args) | from json | default {} | load-env }" ;; pwsh|powershell) echo "function $name { \$script=(($srcenv \$args) -join \"\`n\"); if (\$script) { Invoke-Expression \$script } }" ;; xonsh) echo "aliases['$name'] = 'execx(\$($srcenv @(\$args)))'" ;; *) nosupport init; return 1 ;; esac } rc() { auto=2; name=src; sh=; opts=; noauto=; nocmd= initrc "$@" || exit $? [ -z "$auto" ] && noauto=1 [ -z "$name" ] && nocmd=1 [ "$name" = src ] && name= if [ "$auto" = 1 ] && ! autoable; then nosupport --auto; return 1 fi case $shell in launchd) srcenv="${sh:+"$sh "}'\\'$srcenv\\'' $shell $opts" ;; *) srcenv="$srcenv init $shell${noauto:+" --no-auto"}${nocmd:+" --no-cmd"}${name:+" --cmd $name"}${sh:+" --sh $sh"}${opts:+" -- $opts"}" ;; esac case $shell in ash|posix|sh|\ ion) echo "eval \"\$($srcenv)\"" ;; bash|dash|\ ksh|pdksh|mksh|\ zsh) echo "source <($srcenv)" ;; clink) printf '%s\n' '-- Add to %LOCALAPPDATA%\clink\srcenv.lua' printf '%s\n' "os.execute(io.popen('sh $srcenv'):read('*a'))" ;; cmd|command) echo 'rem Initialize' echo "@echo off & sh $srcenv > %TEMP%\srcenv.init.cmd && call %TEMP%\srcenv.init.cmd & del %TEMP%\srcenv.init.cmd & echo on" echo echo 'rem AutoRun' echo "reg add \"HKCU\\SOFTWARE\\Microsoft\\Command Processor\" /v AutoRun /t REG_SZ /d \"@echo off & sh $srcenv > %TEMP%\srcenv.init.cmd && call %TEMP%\srcenv.init.cmd & del %TEMP%\srcenv.init.cmd & echo on\"" ;; csh|tcsh) if [ -e /dev/stdin ]; then echo "$srcenv | source /dev/stdin" else echo "eval \"\`$srcenv\`\"" fi ;; elvish) echo "var ${name:-src}~ = { }; eval &on-end={|ns| set ${name:-src}~ = \$ns[${name:-src}] } ($srcenv)" ;; fish) echo "$srcenv | source" ;; launchd) [ -z "$opts" ] && colors NORMAL ITALIC YELLOW && invalid '' "missing ${YELLOW}--${NORMAL} or [${ITALIC}options${NORMAL}] [${ITALIC}files${NORMAL}] after${YELLOW}" && exit 2 plist='~'/Library/LaunchAgents/"${name:-srcenv}.plist" echo "mkdir -p ${plist%/*}" echo echo "plutil -create xml1 $plist" echo "plutil -insert Label -string ${name:-srcenv} $plist" echo "plutil -insert ProgramArguments -array $plist" echo "plutil -insert ProgramArguments.0 -string ${sh:-sh} $plist" echo "plutil -insert ProgramArguments.1 -string '-c' $plist" echo "plutil -insert ProgramArguments.2 -string 'eval \"\$($srcenv)\"' $plist" echo "plutil -insert RunAtLoad -bool true $plist" ;; murex) echo "$srcenv -> source" ;; nu|nushell) echo '# Add to env.nu' echo "^$srcenv | save -f ${name:-srcenv}.init.nu" echo echo '# Add to config.nu' echo "source ${name:-srcenv}.init.nu" ;; pwsh|powershell) echo "Invoke-Expression (sh $srcenv)" ;; xonsh) echo "execx(\$($srcenv))" ;; *) nosupport rc; return 1 ;; esac } validname() { case $2 in *[!A-Za-z0-9_]*) invalid "$1" 'name must contain only alphanumeric and underscore characters'; usage; help; exit 2 ;; [!A-Za-z]*) invalid "$1" 'name must start with a letter'; usage; help; exit 2 ;; esac } validsh() { if ! command -v "$2" >/dev/null; then invalid "$1" 'POSIX shell not found'; usage; help; exit 2 fi } # endregion init() / rc() # region setformat() # shellcheck disable=SC2016 fmt() { format='if .[$k] != null then "'$1'" else "'$2'" end'; } # shellcheck disable=SC2016 setformat() { prefix=; suffix= case $1 in ash|dash|\ bash|\ ksh|pdksh|mksh|\ posix|sh|\ zsh) fmt 'export \($k)=\(.[$k]|@sh)' \ 'unset \($k)' ;; clink|\ cmd|command) fmt 'set \($k)=\(.[$k] | gsub("\n"; "%\\n%"))' \ 'set \($k)=' prefix='@echo off\n\n(set \\n=^^^\n\n^\n\n)\n' suffix='\nset \\n=' ;; csh|tcsh) fmt 'setenv \($k) \(.[$k]|@sh|gsub("\\n"; "\\\n")|gsub("!"; "\\!"))' \ 'unsetenv \($k)' ;; elvish) fmt 'set-env \($k) \(.[$k]|@sh|gsub("'"'\\\\\\\\''"'"; "'\'\''"))' \ 'unset-env \($k)' ;; fish) fmt 'set -gx \($k) \(.[$k]|@sh)' \ 'set -e \($k)' ;; ion) fmt 'export \($k)=\(.[$k]|@sh)' \ 'export \($k)='\'\' ;; launchd) fmt 'launchctl setenv \($k) \(.[$k]|@sh)' \ 'launchctl unsetenv \($k)' ;; murex) fmt 'out \(.[$k]|@sh) -> export \($k)' \ '!export \($k)' ;; nu|nushell) fmt '$env.\($k) = \(.[$k]|@json)' \ '$env.\($k) = null' ;; pwsh|powershell) fmt '$Env:\($k) = \(.[$k]|@sh|gsub("'"'\\\\\\\\''"'"; "'\'\''"))' \ '$Env:\($k) = $null' ;; xonsh) fmt ' $\($k) = \(.[$k]|@json)' \ ' del $\($k)' prefix='with ${...}.swap(UPDATE_OS_ENVIRON=True):' suffix=' pass' ;; env|systemd) fmt '\($k)=\(.[$k]|@sh)' \ '\($k)=' ;; json) format=. ;; '') return 1 ;; *) format=$1 case $format in *\{\{*) prefix=${format%\{\{*}; format=${format#*\{\{} ;; esac case $format in *\}\}*) suffix=${format#*\}\}}; format=${format%\}\}*} ;; esac fmt "${format%\?\?*}" \ "${format#*\?\?}" ;; esac } # endregion setformat() # region run() setup() { src=src; args=$*; arg=; input=; action=; auto=; otua=; colorize=; debug= prefix=; suffix=; exclude=; format=; backup=; restore=; clear= modify=1; export=; sort=1; tounix=; verbose= setformat "$shell" } # shellcheck disable=SC2016 build() { [ -n "$exclude" ] && exclude=" | del(${exclude%, })" [ -n "$modify" ] && modify= || modify='$snapshot[$k] == null and ' veto='del(.PWD, .OLDPWD, .SHLVL, ._, ._AST_FEATURES, .UPDATE_OS_ENVIRON, .XONSH_CAPTURE_ALWAYS)' diff='reduce ($snapshot | . += env | '$veto$exclude' | keys_unsorted[]) as $k ({}; if '$modify'env[$k] != $snapshot[$k] then .[$k] = env[$k] else . end)' ffid='reduce ($snapshot | . += env | '$veto$exclude' | keys_unsorted[]) as $k ({}; if '$modify'env[$k] != $snapshot[$k] then .[$k] = $snapshot[$k] else . end)' if [ -z "$sort" ]; then keys='keys_unsorted[] as $k' sort=. else keys='keys[] as $k' sort='to_entries | sort_by(.key) | from_entries' fi [ -n "$prefix" ] && prefix='"'$prefix'\n" + ' [ -n "$suffix" ] && suffix=' + "\n'$suffix'"' if [ -n "$prefix" ] || [ -n "$suffix" ] || [ -n "$verbose" ]; then prefix="$prefix([" suffix="] | join(\"\n\"))$suffix" fi [ "$format" = . ] && keys=$sort [ -n "$backup" ] && backup=' | ('$ffid') as $ffid | if $ffid != {} then . += { SRCENV_RESTORE: ($ffid | if .SRCENV_AUTO_DIR != null then . += { SRCENV_AUTO_DIR: null } else . end | . += { SRCENV_RESTORE: null } | '$sort' | tostring) } else . end' [ -n "$restore" ] && restore='env.SRCENV_RESTORE // "{}" | fromjson | . += ' [ -n "$clear" ] && restore="$restore{ SRCENV_RESTORE: null } | . += " [ -n "$tounix" ] && tounix=' | def tounix: . | gsub("\\\\"; "/") | gsub("(?^|;|:)(?[[:alpha:]]):/"; .sep + "/" + (.drive | ascii_downcase) + "/") | gsub(";"; ":"); '${tounix# |} if [ -n "$verbose" ]; then [ "$format" = . ] && format='keys[] as $k | " \($k|tojson): \(.[$k]|tojson)"' && \ prefix='"{\n" + ([' && \ suffix='] | join(",\n")) + "\n}" | if . == "{\n\n}" then "{}" else . end' colors NORMAL BOLD ITALIC DIM RED GREEN YELLOW header="${BOLD}${DIM}srcenv:${NORMAL} " changed="$restore$diff${backup:+" | . += { SRCENV_RESTORE: \"\" }"}" added="\"${GREEN}+\\(\$k)${NORMAL}\"" modified="\"${YELLOW}~\\(\$k)${NORMAL}\"" removed="\"${RED}-\\(\$k)${NORMAL}\"" nochanges="\"${ITALIC}No changes${NORMAL}\"" verbose=' + "\n'$header'" + (['$changed' | keys[] as $k | if .[$k] then if $snapshot[$k] then '$modified' else '$added' end else '$removed' end] | join(", ") | if . == "" then '$nochanges' else . end)' fi filter=". as \$snapshot | $prefix$restore$diff$backup$tounix | $keys | $format$suffix$verbose" } DLM=$(printf '\032') LF=' ' capture() { # shellcheck disable=SC2153 if [ "$KSH_VERSION" = "Version AJM 93u+ 2012-08-01" ]; then stdout=$( (printf "${DLM}%s${DLM}" "$(trap 'printf "${DLM}%d" "$?"' EXIT; "$@" 3>&1 1>&2 2>&3)" ) 2>&1 ) exitcode=${stdout%"${DLM}"*"${DLM}"} exitcode=${exitcode#*"${DLM}"} stderr=${stdout%"${DLM}"} stderr=${stderr##*"${DLM}"} stderr=${stderr%"${LF}"} stdout=${stdout%%"${DLM}"*} stdout=${stdout%"${LF}"} else stdout=$( (printf "${DLM}%s${DLM}" "$(trap 'printf "${DLM}%d" "$?"' EXIT; "$@")" 1>&2) 2>&1 ) exitcode=${stdout%"${DLM}"} exitcode=${exitcode##*"${DLM}"} stderr=${stdout%%"${DLM}"*} stderr=${stderr%"${LF}"} stdout=${stdout#*"${DLM}"} stdout=${stdout%"${DLM}"*"${DLM}"} stdout=${stdout%"${LF}"} fi return "$exitcode" } run() { pwd=$PWD if [ "$dir" != "$pwd" ]; then cd "$dir" || exit $? fi if [ -n "$auto" ]; then hashes=; skip= for arg in "$@"; do [ -n "$skip" ] && skip= && continue case $arg in --) ;; --input) skip=1 ;; -) invalid "Automatic sourcing from STDIN is not supported" -; exit 1 ;; *) hashes="$hashes$arg${LF}$(sha "$arg")${LF}${LF}" || exit 1 ;; esac done fi if ! jq=$(command -v "$jq"); then nojq; exit 1 fi snapshot=$("$jq" -n env) || exit $? [ -n "$colorize" ] && "$colorize" while [ $# -gt 0 ]; do case $1 in --) shift ;; --input) "$src" var "$2" "$export" || exit $?; shift; shift ;; -) stdin=; while read -r line; do stdin="$stdin$line${LF}"; done stdin=${stdin%"${LF}"} "$src" var "$stdin" "$export" || exit $?; shift ;; *.env) "$src" file "$1" "${export:---export}" || exit $?; shift ;; *) "$src" file "$1" "$export" || exit $?; shift ;; esac done if [ "$dir" != "$pwd" ]; then cd "$pwd" || exit $? fi if [ -n "$auto" ]; then locate; autoadd; cache fi if [ -n "$verbose" ]; then stdout=$(printf "%s\n" "$snapshot" | "$jq" ${SRCENV_JQ_BINARY:+-b} -r "$filter") exitcode=$? out "${stdout##*"${LF}"}" printf "%s\n" "${stdout%"${LF}"*}" exit "$exitcode" else printf "%s\n" "$snapshot" | "$jq" ${SRCENV_JQ_BINARY:+-b} -r "$filter" fi } src() { exitcode=0 [ "$3" = --export ] && set -a # shellcheck disable=SC3044 [ -n "$BASH_VERSION" ] && shopt -s expand_aliases 2> /dev/null alias exit=return if [ "$1" = var ]; then if [ -n "$ZSH_VERSION" ]; then eval "srcenv_zsh_input() { $2; }; srcenv_zsh_input" 1>&2 || exitcode=$? else eval "$2" 1>&2 || exitcode=$? fi elif [ -f "$2" ]; then # shellcheck disable=SC1090 case $2 in */*) cd "${2%/*}" && \ . "./${2##*/}" 1>&2 || exitcode=$? ;; *) . "./$2" 1>&2 || exitcode=$? ;; esac cd "$dir" || exitcode=$? else out "$2: No such file or directory" exitcode=2 fi # shellcheck disable=SC3044 [ -n "$BASH_VERSION" ] && shopt -u expand_aliases 2> /dev/null unalias exit [ "$3" = --export ] && set +a return "$exitcode" } # shellcheck disable=SC2016 json() { if [ "$1" = var ]; then env=$(printf "%s\n" "$2" | "$jq" ${SRCENV_JQ_BINARY:+-b} -r 'keys[] as $k | "export \($k)=\(.[$k]|@sh)"') || return 5 elif [ -f "$2" ]; then env=$("$jq" ${SRCENV_JQ_BINARY:+-b} -r 'keys[] as $k | "export \($k)=\(.[$k]|@sh)"' "$2") || return 5 else out "$2: No such file or directory" return 2 fi eval "$env" 1>&2 || return $? } # endregion run() main() { setup "$@" while [ $# -gt 0 ] && [ "$1" != -- ]; do arg=1 case $1 in --cmd) cmd=$(parse "$1" "$2") || exit $?; [ $# -gt 1 ] && shift; arg=; input=; action=; shift ;; --cmd=*) cmd=$(parse "${1%%=*}" "${1#*=}") || exit $?; arg=; input=; action=; shift ;; --color) color=$(parse "$1" "$2" validcolor) || exit $?; [ $# -gt 1 ] && shift; shift ;; --color=*) color=$(parse "${1%%=*}" "${1#*=}" validcolor) || exit $?; shift ;; --export-colors) colorize=exportcolors; action=1; shift ;; --clear-colors) colorize=clearcolors; action=1; shift ;; -d|--dir) dir=$(parse "$1" "$2") || exit $?; [ $# -gt 1 ] && shift; shift ;; -d=*|--dir=*) dir=$(parse "${1%%=*}" "${1#*=}") || exit $?; shift ;; -x|--exclude) value=$(parse "$1" "$2" validname) || exit $?; [ $# -gt 1 ] && shift exclude="$exclude.$value, "; shift ;; -x=*|--exclude=*) value=$(parse "${1%%=*}" "${1#*=}" validname) || exit $? exclude="$exclude.$value, "; shift ;; -f|--format) value=$(parse "$1" "$2") || exit $?; [ $# -gt 1 ] && shift setformat "$value"; shift ;; -f=*|--format=*) value=$(parse "${1%%=*}" "${1#*=}") || exit $? setformat "$value"; shift ;; --tounix) value=$(parse "$1" "$2" validname) || exit $?; [ $# -gt 1 ] && shift tounix="$tounix | if .$value then .$value |= tounix end"; shift ;; --tounix=*) value=$(parse "${1%%=*}" "${1#*=}" validname) || exit $? tounix="$tounix | if .$value then .$value |= tounix end"; shift ;; -a|--auto) auto=1; input=1; shift ;; --no-auto) auto=; shift ;; -o|--off|--otua) otua=1; shift ;; -b|--backup) backup=1; clear=; input=1; shift ;; -r|--restore) restore=1; action=1; shift ;; -c|--clear) backup=; clear=1; action=1; shift ;; -n) backup=; restore=; shift ;; --no-backup) backup=; shift ;; --no-restore) restore=; shift ;; -m|--modify) modify=1; shift ;; -w|--write-protect) modify=; shift ;; -j|--json) src=json; input=1; shift ;; -p|--posix) src=src; input=1; shift ;; -e|--export) export=--export; input=1; shift ;; -l|--local) export=0; input=1; shift ;; -s|--sort) sort=1; shift ;; -u|--unsorted) sort=; shift ;; -q|--quiet) verbose=; shift ;; -v|--verbose) verbose=1; shift ;; -h|--help) header; usage; desc; options; man; exit 0 ;; --version) version; exit 0 ;; --debug) debug=1; shift ;; -i|--input) value=$(parse "$1" "$2") || exit $?; [ $# -gt 1 ] && shift set -- "$@" -- --input "$value"; shift ;; -i=*|--input=*) value=$(parse "${1%%=*}" "${1#*=}") || exit $? set -- "$@" -- --input "$value"; shift ;; -) set -- "$@" -- -; shift ;; -[!-]?*) opts=${1#??}; opt=${1%"$opts"}; shift case $opt in -[!dfix]) opts="-$opts" ;; esac set -- "$opt" "$opts" "$@" ;; -*) invalid "$1"; usage; help; exit 2 ;; *) set -- "$@" -- "$1"; shift ;; esac done if [ -n "$otua" ]; then locate; autoremove; exit $? fi if [ $# = 0 ]; then if [ -z "$arg" ]; then header; usage; desc; help; man; exit 0 elif [ -n "$input" ] || [ -z "$action" ]; then noinput; usage; help; exit 2 elif [ -n "$colorize" ]; then backup=; restore= fi fi if [ -z "$format" ]; then noformat; usage; help; exit 2 elif [ -n "$COMSPEC" ]; then case $shell in clink|cmd|command|pwsh|powershell) ;; *) tounix="$tounix | if .PATH then .PATH |= tounix end" tounix="$tounix | if .SRCENV_AUTO_DIR then .SRCENV_AUTO_DIR |= tounix end" ;; esac fi build if [ -n "$debug" ]; then command -v bat > /dev/null && filter=$(printf '%s\n' "$filter" | bat --color=always -pl jq) out "$filter"; exit 0 fi capture run "$@" [ -n "$stderr" ] && err "$stderr" "$exitcode" [ -n "$stdout" ] && printf '%s\n' "$stdout" exit "$exitcode" } arg0=$0 jq=${SRCENV_JQ:-jq} sha=$SRCENV_HASH dir=$PWD cmd= case $1 in auto|init|rc) command=$1; shift ;; *) command= ;; esac case $1 in ash|dash|\ bash|\ ksh|pdksh|mksh|\ posix|sh|\ zsh|\ clink|\ cmd|command|\ csh|tcsh|\ elvish|\ fish|\ ion|\ launchd|\ murex|\ nu|nushell|\ pwsh|powershell|\ xonsh|\ env|systemd|\ json) shell=$1; shift ;; *) shell= ;; esac if [ -n "$command" ]; then if [ -z "$shell" ]; then noshell "$1"; usage; help; exit 2 fi "$command" "$@"; exit $? fi [ ! -t 0 ] && set -- "$@" - main "$@"