# gstack-gbrain-lib.sh — shared helpers for setup-gbrain bin scripts. # # This file is NOT executable; source it: # # . "$(dirname "$0")/gstack-gbrain-lib.sh" # # Provides: # read_secret_to_env [--echo-redacted ] # — Read a secret from stdin into the named env var without echoing # to the terminal. On SIGINT/SIGTERM/EXIT, restores terminal echo so # future keystrokes are visible. Optionally emits a redacted preview # of what was read so the user can visually confirm they pasted the # right thing. # # stdin handling: when stdin is a TTY, stty -echo suppresses echo # while the user types. When stdin is piped (automated tests), the # stty calls are skipped — piping into `read` is already invisible. # # Var name must match [A-Z_][A-Z0-9_]* to prevent injection via # `read -r "$varname"` expansion. Invalid names abort. # # Exported after read so sub-processes inherit the secret. Caller # is responsible for `unset ` when done. # # Load-bearing for D3-eng (shared secret helper across PAT + URL paste), # D10 (env-var handoff, never argv), D11 (PAT scope disclosure + SIGINT # restore), D16 (pooler URL paste hygiene with redacted preview). # _gstack_gbrain_validate_varname — returns 0 if usable, 2 otherwise. # `local LC_ALL=C` is load-bearing twice over: # 1. In many macOS shells the default locale (e.g. en_US.UTF-8) makes `case` # glob brackets like `[A-Z]` match lowercase letters too. Without the # LC_ALL=C pin, names like `lower-case` pass validation and then trip # `printf -v "$varname"` and `export "$varname"` with "not a valid # identifier" errors the caller can't easily distinguish from other # failures. # 2. `local` is required because this file is documented as a sourced helper # (see header), so a bare `LC_ALL=C` would mutate the caller's locale for # the rest of the process — silently affecting downstream `sort`, `tr`, # and any locale-aware glob in the same shell. # Together they give ASCII-only bracket semantics on both macOS and Linux # (matching the documented `[A-Z_][A-Z0-9_]*` contract) without leaking. _gstack_gbrain_validate_varname() { local name="$1" local LC_ALL=C case "$name" in [A-Z_][A-Z0-9_]*) return 0 ;; *) return 2 ;; esac } read_secret_to_env() { local varname="" prompt="" redact_expr="" # Parse leading positional args (varname, prompt), then optional flags. if [ $# -lt 2 ]; then echo "read_secret_to_env: usage: read_secret_to_env [--echo-redacted ]" >&2 return 2 fi varname="$1"; shift prompt="$1"; shift while [ $# -gt 0 ]; do case "$1" in --echo-redacted) redact_expr="$2"; shift 2 ;; *) echo "read_secret_to_env: unknown flag: $1" >&2; return 2 ;; esac done if ! _gstack_gbrain_validate_varname "$varname"; then echo "read_secret_to_env: invalid var name '$varname' (must match [A-Z_][A-Z0-9_]*)" >&2 return 2 fi # stty manipulation only makes sense when stdin is a terminal. In CI / # test / piped contexts we skip it — piped input doesn't echo anyway. local is_tty=false if [ -t 0 ]; then is_tty=true; fi if $is_tty; then # Save current stty state; restore on any exit path. local saved_stty saved_stty=$(stty -g 2>/dev/null || echo "") # shellcheck disable=SC2064 trap "stty '$saved_stty' 2>/dev/null; printf '\n' >&2" INT TERM EXIT stty -echo 2>/dev/null || true fi # Prompt on stderr so the caller can capture stdout cleanly. printf '%s' "$prompt" >&2 # Read one line from stdin. `read -r` returns nonzero on EOF-without- # newline but still populates `value` with whatever it saw — we want that # content, so don't clear on failure. local value="" IFS= read -r value || true if $is_tty; then stty "$saved_stty" 2>/dev/null || true trap - INT TERM EXIT printf '\n' >&2 fi # Assign + export to the named variable. printf -v "$varname" '%s' "$value" # shellcheck disable=SC2163 export "$varname" # Optional redacted preview after successful read. if [ -n "$redact_expr" ] && [ -n "$value" ]; then local preview preview=$(printf '%s' "$value" | sed "$redact_expr" 2>/dev/null || true) if [ -n "$preview" ]; then printf 'Got: %s\n' "$preview" >&2 fi fi }