Refactor global variable script for better handling

Refactor global variable script to improve readability and maintainability. Added support for handling secrets and environment variables more effectively.
This commit is contained in:
Alexandre
2025-11-25 15:57:30 +01:00
committed by GitHub
parent 1a38ebc48e
commit 4944b74d5e

View File

@@ -1,163 +1,290 @@
#!/usr/bin/with-contenv bashio #!/usr/bin/with-contenv bashio
# shellcheck shell=bash # shellcheck shell=bash
set -e set -e
if ! bashio::supervisor.ping 2> /dev/null; then if ! bashio::supervisor.ping 2>/dev/null; then
echo "..." echo "..."
exit 0 exit 0
fi fi
###################################
# Export all addon options as env #
###################################
echo "" echo ""
bashio::log.notice "This script converts all addon options to environment variables. Custom variables can be set using env_vars." bashio::log.notice "This script converts all addon options to environment variables. Custom variables can be set using env_vars."
bashio::log.notice "Additional informations : https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2" bashio::log.notice "Additional informations : https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2"
echo "" echo ""
# For all keys in options.json
JSONSOURCE="/data/options.json" JSONSOURCE="/data/options.json"
# Define secrets location # Define secrets location (optional)
if [ -f /homeassistant/secrets.yaml ]; then SECRETSOURCE=""
if [[ -f /homeassistant/secrets.yaml ]]; then
SECRETSOURCE="/homeassistant/secrets.yaml" SECRETSOURCE="/homeassistant/secrets.yaml"
elif [ -f /config/secrets.yaml ]; then elif [[ -f /config/secrets.yaml ]]; then
SECRETSOURCE="/config/secrets.yaml" SECRETSOURCE="/config/secrets.yaml"
else
SECRETSOURCE="false"
fi fi
# Export keys as env variables # Injection block markers (single block, idempotent)
# echo "All addon options were exported as variables" BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
mapfile -t arr < <(jq -r 'keys[]' "${JSONSOURCE}") BLOCK_END="# --- END ADDON ENV (generated) ---"
# Escape special characters using printf and enclose in double quotes EXPORT_BLOCK_FILE="$(mktemp)"
sanitize_variable() { trap 'rm -f "$EXPORT_BLOCK_FILE"' EXIT
local raw="$1"
local escaped {
if [[ "$raw" == \[* ]]; then echo "${BLOCK_BEGIN}"
echo "One of your options is an array, skipping" echo "# Do not edit: generated from ${JSONSOURCE}"
return echo "${BLOCK_END}"
} > "${EXPORT_BLOCK_FILE}"
# Protected variables that should not be overwritten
declare -A PROTECTED_VARS=(
["PATH"]=1
["HOME"]=1
["PWD"]=1
["SHLVL"]=1
["_"]=1
["S6_BEHAVIOR_IF_STAGE2_FAILS"]=1
)
is_valid_env_name() {
[[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]
}
# Quote for shell *code* (for injection into scripts). Keep punctuation intact.
# - single-line => single quotes
# - multi-line => $'...\n...' (one physical line; safe for injection)
shell_quote_for_code() {
local s="$1"
if [[ "$s" == *$'\n'* || "$s" == *$'\r'* || "$s" == *$'\t'* ]]; then
s="${s//\\/\\\\}"
s="${s//\'/\\\'}"
s="${s//$'\n'/\\n}"
s="${s//$'\r'/\\r}"
s="${s//$'\t'/\\t}"
printf "\$'%s'" "$s"
return 0
fi fi
printf -v escaped '%q' "$raw"
# Do not espace spaces # single-quote with embedded '"'"' for literal '
escaped="${escaped//\\ / }" s="${s//\'/\'\"\'\"\' }"
if [[ "$raw" == "$escaped" ]]; then s="${s% }"
printf '%s' "$raw" printf "'%s'" "$s"
else }
printf '%s' "$escaped"
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"
}
resolve_secret_if_needed() {
local v="$1"
local name line
if [[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)[[:space:]]*$ ]]; then
name="${BASH_REMATCH[1]}"
name="${name#\"}"; name="${name%\"}"
name="${name#\'}"; name="${name%\'}"
if [[ -z "${SECRETSOURCE}" ]]; then
bashio::log.warning "Homeassistant config not mounted, secrets are not supported"
printf '%s' "$v"
return 0
fi
# 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"
)"
[[ -z "$line" ]] && bashio::exit.nok "Secret '${name}' not found in ${SECRETSOURCE}"
printf '%s' "$line"
return 0
fi fi
printf '%s' "$v"
}
append_export_line_for_injection() {
local key="$1"
local value="$2"
local quoted
quoted="$(shell_quote_for_code "$value")"
awk -v k="$key" -v q="$quoted" -v end="$BLOCK_END" '
$0 == end { print "export " k "=" q }
{ print }
' "${EXPORT_BLOCK_FILE}" > "${EXPORT_BLOCK_FILE}.tmp"
mv -f "${EXPORT_BLOCK_FILE}.tmp" "${EXPORT_BLOCK_FILE}"
}
is_shell_run_script() {
local f="$1"
local h
h="$(head -n 1 "$f" 2>/dev/null || true)"
[[ "$h" =~ ^#! ]] || return 1
[[ "$h" =~ (sh|bash|with-contenv) ]] && return 0
return 1
}
inject_block_into_file() {
local file="$1"
local tmp
tmp="$(mktemp)"
awk -v bfile="${EXPORT_BLOCK_FILE}" -v begin="${BLOCK_BEGIN}" -v end="${BLOCK_END}" '
function print_block() {
while ((getline l < bfile) > 0) print l
close(bfile)
}
BEGIN { inblock=0; printed=0 }
{
if ($0 == begin) {
inblock=1
if (!printed) { print_block(); printed=1 }
next
}
if ($0 == end) { inblock=0; next }
if (inblock) next
if (NR == 1) {
if ($0 ~ /^#!/) {
print $0
if (!printed) { print_block(); printed=1 }
next
} else {
if (!printed) { print_block(); printed=1 }
print $0
next
}
}
print $0
}
END {
if (!printed) print_block()
}
' "$file" > "$tmp"
cat "$tmp" > "$file"
rm -f "$tmp"
}
update_scripts_with_block() {
local f
local -A seen=()
shopt -s nullglob
# Added /etc/s6-overlay/s6-rc.d/*/run for newer S6 implementation (optional)
for f in /etc/services.d/*/run /etc/services.d/*/*run* /etc/cont-init.d/*.sh /etc/s6-overlay/s6-rc.d/*/run; do
[[ -f "$f" ]] || continue
[[ -n "${seen[$f]:-}" ]] && continue
seen["$f"]=1
if ! is_shell_run_script "$f"; then
bashio::log.debug "Skipping non-shell script: $f"
continue
fi
inject_block_into_file "$f"
done
shopt -u nullglob
} }
export_option() { export_option() {
local key="$1" local key="$1"
local value="$2" local value="$2"
local line secret secretnum valuepy
value=$(sanitize_variable "$value") if [[ -n "${PROTECTED_VARS[$key]:-}" ]]; then
bashio::log.warning "Skipping protected environment variable: ${key}"
if [[ -z "$value" ]]; then return 0
line="${key}=''"
else
line="${key}='${value//\'/\'\\\'\'}'"
fi fi
if [[ "${line}" == *"!secret "* ]]; then if ! is_valid_env_name "$key"; then
echo "secret detected" bashio::log.warning "Skipping invalid env var name: ${key}"
secret=${line#*secret } return 0
secret="${secret%[\"\']}"
if [[ "$SECRETSOURCE" == "false" ]]; then
bashio::log.warning "Homeassistant config not mounted, secrets are not supported"
return
fi
secretnum=$(sed -n "/$secret:/=" "$SECRETSOURCE")
[[ "$secretnum" == *' '* ]] && bashio::exit.nok "There are multiple matches for your password name. Please check your secrets.yaml file"
secret=$(sed -n "/$secret:/p" "$SECRETSOURCE")
secret=${secret#*: }
line="${line%%=*}='$secret'"
value="$secret"
fi fi
if bashio::config.false "verbose" || [[ "${key,,}" == *"pass"* ]]; then value="$(resolve_secret_if_needed "$value")"
if bashio::config.false "verbose" || [[ "${key,,}" =~ (pass|secret|token|apikey|api_key|private|pwd) ]]; then
bashio::log.blue "${key}=******" bashio::log.blue "${key}=******"
else else
bashio::log.blue "$line" bashio::log.blue "${key}=${value}"
fi fi
export "$line" export "${key}=${value}"
if command -v "python3" &> /dev/null; then if [[ -d /var/run/s6/container_environment ]]; then
[ ! -f /env.py ] && echo "import os" > /env.py printf '%s' "${value}" > "/var/run/s6/container_environment/${key}"
valuepy="${value//\\/\\\\}"
valuepy="${valuepy//[\"\']/}"
echo "os.environ['${key}'] = '$valuepy'" >> /env.py
python3 /env.py
fi fi
echo "$line" >> /.env || true echo "${key}=$(dotenv_quote "$value")" >> "/.env" 2>/dev/null || true
mkdir -p /etc mkdir -p /etc
echo "$line" >> /etc/environment echo "${key}=$(dotenv_quote "$value")" >> /etc/environment 2>/dev/null || true
if cat /etc/services.d/*/*run* &> /dev/null; then sed -i "1a export $line" /etc/services.d/*/*run* 2> /dev/null; fi
if cat /etc/cont-init.d/*.sh &> /dev/null; then sed -i "1a export $line" /etc/cont-init.d/*.sh 2> /dev/null; fi append_export_line_for_injection "$key" "$value"
if [ -d /var/run/s6/container_environment ]; then printf "%s" "${value}" > /var/run/s6/container_environment/"${key}"; fi
echo "export ${key}='${value}'" >> ~/.bashrc
} }
mapfile -t arr < <(jq -r 'keys[]' "${JSONSOURCE}")
for KEYS in "${arr[@]}"; do for KEYS in "${arr[@]}"; do
# export key jtype="$(jq -r --arg k "$KEYS" '.[$k] | type' "$JSONSOURCE")"
VALUE=$(jq -r --raw-output ".\"$KEYS\"" "$JSONSOURCE")
# Check if the value is an array if [[ "$jtype" == "array" ]]; then
if [[ "$VALUE" == \[* ]]; then
if [[ "$KEYS" == "env_vars" ]]; then if [[ "$KEYS" == "env_vars" ]]; then
mapfile -t env_entries < <(jq -c ".\"$KEYS\"[]" "$JSONSOURCE") mapfile -t env_entries < <(jq -c '.env_vars[]?' "$JSONSOURCE")
if [[ "${#env_entries[@]}" -eq 0 ]]; then
continue
fi
env_processed=false
for entry in "${env_entries[@]}"; do for entry in "${env_entries[@]}"; do
if [[ "$entry" == \{* ]]; then if [[ "$entry" == \{* ]]; then
env_name=$(jq -r 'if has("name") and has("value") then .name else empty end' <<< "$entry") env_name="$(jq -r 'if has("name") and has("value") then .name else empty end' <<<"$entry")"
if [[ -n "$env_name" ]]; then if [[ -n "$env_name" ]]; then
env_value=$(jq -r '.value // empty' <<< "$entry") env_value="$(jq -r '.value // "" | tostring' <<<"$entry")"
export_option "$env_name" "$env_value" export_option "$env_name" "$env_value"
env_processed=true else
continue mapfile -t env_keys < <(jq -r 'keys[]' <<<"$entry")
for env_key in "${env_keys[@]}"; do
env_value="$(jq -r --arg k "$env_key" '.[$k] // "" | tostring' <<<"$entry")"
export_option "$env_key" "$env_value"
done
fi fi
else
# Preserve multiline values: iterate keys and extract raw values without @tsv env_pair="$(jq -r '.' <<<"$entry")"
mapfile -t env_keys < <(jq -r 'keys[]' <<< "$entry")
for env_key in "${env_keys[@]}"; do
# Use --arg to select the key; // empty to avoid "null"
env_value=$(jq -r --arg k "$env_key" '.[$k] // empty' <<< "$entry")
export_option "$env_key" "$env_value"
env_processed=true
done
elif [[ "${entry:0:1}" == '"' ]]; then
env_pair=$(jq -r '.' <<< "$entry")
if [[ "$env_pair" == *=* ]]; then if [[ "$env_pair" == *=* ]]; then
env_key=${env_pair%%=*} export_option "${env_pair%%=*}" "${env_pair#*=}"
env_value=${env_pair#*=}
export_option "$env_key" "$env_value"
env_processed=true
else else
bashio::log.warning "env_vars entry '$env_pair' is not in KEY=VALUE format, skipping" bashio::log.warning "env_vars entry '$env_pair' is not in KEY=VALUE format, skipping"
fi fi
else
bashio::log.warning "env_vars entry format not supported, skipping"
fi fi
done done
if [[ "$env_processed" == false ]]; then
bashio::log.warning "env_vars option format not supported, skipping"
fi
else else
bashio::log.warning "One of your option is an array, skipping" bashio::log.warning "Option '${KEYS}' is an array, skipping"
fi fi
elif [[ "$jtype" == "object" ]]; then
bashio::log.warning "Option '${KEYS}' is an object, skipping"
elif [[ "$jtype" == "null" ]]; then
continue
else else
VALUE="$(jq -r --arg k "$KEYS" '.[$k] // "" | tostring' "$JSONSOURCE")"
export_option "$KEYS" "$VALUE" export_option "$KEYS" "$VALUE"
fi fi
done done
update_scripts_with_block
################ ################
# Set timezone # # Set timezone #
################ ################