Files
hassio-addons/.templates/00-global_var.sh
Claude f00c8b9210 fix(global_var): non-destructive .env writes; correct shell quoting
Three minimum-scope robustness fixes in .templates/00-global_var.sh:

1. shell_quote no longer doubles backslashes inside single quotes
   (e.g. 'C:\Users\test' was producing 'C:\\Users\\test' in injected
   scripts) and now uses the canonical '\'' escape for single quotes
   instead of the buggy '"'"' + trailing-space-trim form, which was
   mangling values containing apostrophes (e.g. "it's" -> "it' s").

2. /.env and /etc/environment are now patched in place: existing
   content from the upstream image and the system (PATH, LANG, etc.)
   is preserved and only the marked ADDON ENV block is replaced.
   Previously the whole files were overwritten, destroying upstream
   defaults.

3. Writing to /var/run/s6/container_environment is now best-effort
   (|| true) so a read-only mount on a single entry cannot abort the
   rest of the env injection under set -e.
2026-05-18 13:32:17 +00:00

268 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
################################################################################
# Guard: only run inside Supervisor-managed add-ons
################################################################################
if ! bashio::supervisor.ping 2>/dev/null; then
echo "..."
exit 0
fi
echo ""
bashio::log.notice "Converting addon options to environment variables"
bashio::log.notice "Supports custom env_vars"
bashio::log.notice "https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2"
echo ""
################################################################################
# Inputs
################################################################################
JSONSOURCE="/data/options.json"
ENV_FILE="/.env"
ETC_ENV_FILE="/etc/environment"
[[ -f "$JSONSOURCE" ]] || bashio::exit.nok "Missing $JSONSOURCE"
command -v jq >/dev/null || bashio::exit.nok "jq is required"
mkdir -p /etc
touch "$ETC_ENV_FILE"
################################################################################
# Temp helper
################################################################################
mktemp_safe() {
local tmpdir="${TMPDIR:-/tmp}"
mkdir -p "$tmpdir"
mktemp "$tmpdir/tmp.XXXXXXXXXX"
}
################################################################################
# Secrets support
################################################################################
SECRETSOURCE=""
[[ -f /homeassistant/secrets.yaml ]] && SECRETSOURCE="/homeassistant/secrets.yaml"
[[ -z "$SECRETSOURCE" && -f /config/secrets.yaml ]] && SECRETSOURCE="/config/secrets.yaml"
resolve_secret() {
local v="$1" name line
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || { printf '%s' "$v"; return; }
name="${BASH_REMATCH[1]}"
[[ -n "$SECRETSOURCE" ]] || bashio::exit.nok "Secrets not mounted"
# Exact key match at start of line; ignore comments
line="$(
awk -v k="$name" '
/^[[:space:]]*#/ {next}
$0 ~ "^[[:space:]]*" k ":[[:space:]]*" {
sub("^[[:space:]]*" k ":[[:space:]]*", "", $0)
print
exit
}
' "$SECRETSOURCE"
)"
[[ -n "$line" ]] || bashio::exit.nok "Secret $name not found"
printf '%s' "$line"
}
################################################################################
# Quoting
################################################################################
dotenv_quote() {
# For /.env and /etc/environment: double quotes + minimal escaping
local v="$1"
v="${v//\\/\\\\}"
v="${v//\"/\\\"}"
v="${v//$'\n'/\\n}"
v="${v//$'\r'/\\r}"
printf '"%s"' "$v"
}
shell_quote() {
# Single-quote for safe injection in shell code.
# Inside single quotes only the single quote needs escaping ('->'\'').
# Do NOT escape backslashes: they are literal inside single quotes.
local s="${1//\'/\'\\\'\'}"
printf "'%s'" "$s"
}
################################################################################
# S6 + script injection block
################################################################################
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
BLOCK_END="# --- END ADDON ENV (generated) ---"
EXPORT_BLOCK="$(mktemp_safe)"
KV_FILE="$(mktemp_safe)"
trap 'rm -f "$EXPORT_BLOCK" "$KV_FILE"' EXIT
{
echo "$BLOCK_BEGIN"
echo "# Generated from $JSONSOURCE"
echo "$BLOCK_END"
} > "$EXPORT_BLOCK"
append_export() {
local k="$1" v="$2" q
q="$(shell_quote "$v")"
awk -v k="$k" -v q="$q" -v e="$BLOCK_END" '
$0==e { print "export " k "=" q }
{ print }
' "$EXPORT_BLOCK" > "$EXPORT_BLOCK.tmp"
mv "$EXPORT_BLOCK.tmp" "$EXPORT_BLOCK"
}
inject_block() {
local f="$1" tmp
tmp="$(mktemp_safe)"
awk -v b="$BLOCK_BEGIN" -v e="$BLOCK_END" -v bf="$EXPORT_BLOCK" '
function emit(){ while((getline l<bf)>0) print l; close(bf) }
BEGIN{ skip=0; injected=0 }
{
if($0==b){ skip=1; if(!injected){ emit(); injected=1 } next }
if($0==e){ skip=0; next }
if(skip) next
if(NR==1 && $0~/^#!/){
print
if(!injected){ emit(); injected=1 }
next
}
print
}
END{ if(!injected) emit() }
' "$f" > "$tmp"
cat "$tmp" > "$f"
rm -f "$tmp"
}
################################################################################
# Export handler
################################################################################
export_var() {
local k="$1" v="$2"
# Valid env var identifier only
[[ "$k" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || {
bashio::log.warning "Skipping invalid env var name: $k"
return 0
}
v="$(resolve_secret "$v")"
if [[ "${k,,}" =~ pass|token|secret|apikey|api_key|private|pwd|key ]]; then
bashio::log.blue "$k=******"
else
bashio::log.blue "$k=$v"
fi
# Runtime environment (no eval, safe for special chars)
export "$k=$v"
# S6 environment (preferred for services); best-effort, tolerate RO mounts
if [[ -d /var/run/s6/container_environment ]]; then
printf '%s' "$v" > "/var/run/s6/container_environment/$k" || true
fi
# Queue for .env and /etc/environment (written once, idempotent)
echo "$k=$(dotenv_quote "$v")" >> "$KV_FILE"
# Add to injected export block for scripts
append_export "$k" "$v"
}
################################################################################
# JSON parsing (jq bug fixed: use "? //", not "?//")
# Order: 1) top-level addon options first, 2) env_vars second (can override)
################################################################################
while IFS= read -r -d $'\0' k && IFS= read -r v; do
export_var "$k" "$v"
done < <(
jq -r '
def emit(k; v): "\((k|tostring))\u0000\((v|tostring))";
. as $root
| (
# 1) top-level scalar options excluding env_vars (processed first)
$root
| to_entries[]
| select(.key != "env_vars")
| select((.value|type) != "object" and (.value|type) != "array" and (.value|type) != "null")
| emit(.key; .value)
),
(
# 2) env_vars[] (processed second, overrides addon options if same key)
($root.env_vars? // [])[] as $e
| if ($e|type) == "object" then
if ($e|has("name") and has("value")) then
emit($e.name; ($e.value // ""))
else
$e|to_entries[]|emit(.key; (.value // ""))
end
else
# string "KEY=VALUE" form (value may contain '=')
($e|tostring) as $s
| if ($s|test("^[^=]+=")) then
($s|capture("^(?<k>[^=]+)=(?<v>.*)$")) as $m
| emit($m.k; $m.v)
else empty end
end
)
' "$JSONSOURCE"
)
################################################################################
# Write .env and /etc/environment (idempotent: replace only our block,
# preserving any pre-existing content from the upstream image / system).
# Notes:
# - /.env is commonly used by apps expecting dotenv format
# - /etc/environment is read by PAM/system tooling; KEY="value" is acceptable
################################################################################
update_env_file() {
local f="$1" tmp
tmp="$(mktemp_safe)"
if [[ -f "$f" ]]; then
awk -v b="$BLOCK_BEGIN" -v e="$BLOCK_END" '
$0==b { skip=1; next }
$0==e { skip=0; next }
!skip
' "$f" > "$tmp"
fi
{
echo "$BLOCK_BEGIN"
cat "$KV_FILE"
echo "$BLOCK_END"
} >> "$tmp"
mv "$tmp" "$f"
}
update_env_file "$ENV_FILE"
update_env_file "$ETC_ENV_FILE"
################################################################################
# Inject into scripts and shells (best-effort)
################################################################################
for f in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run /etc/cont-init.d/*.sh /entrypoint.sh /etc/bash.bashrc "${GLOBAL_VAR_FILES:-}"; do
[[ -f "$f" ]] && inject_block "$f"
done
if [[ -n "${HOME:-}" ]]; then
mkdir -p "$HOME"
touch "$HOME/.bashrc"
inject_block "$HOME/.bashrc"
fi
################################################################################
# Timezone (best-effort)
################################################################################
set +e
if [[ -n "$TZ" && -f "/usr/share/zoneinfo/$TZ" ]]; then
ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime
echo "$TZ" > /etc/timezone
fi