I'm Australian
give me some bash boilerplate that:
- checks bashes version and refuses to run if its older than current
- sets up all safe and desirable shopts: globbing and lastpipe, not nullglob or whatever
- Sets up boot, machine, and invocation id from systemd's id stuff
- sets up variables for all the XDG dirs
- sets up an array called "TRAP_EXIT" that will run on the exit trap
Here’s a Bash script boilerplate that:
- Verifies that the Bash version is at least a specific minimum version.
- Enables strict/safe shell options (e.g.,
set -euo pipefail
and usefulshopt
flags). - Pulls boot, machine, and invocation IDs from
systemd
. - Sets up XDG directory variables with sensible fallbacks.
- Defines a
TRAP_EXIT
array to register exit trap commands cleanly.
#!/usr/bin/env bash
# === Minimum Bash Version Check ===
REQUIRED_BASH_MAJOR=5
REQUIRED_BASH_MINOR=0
if ((BASH_VERSINFO[0] < REQUIRED_BASH_MAJOR)) ||
((BASH_VERSINFO[0] == REQUIRED_BASH_MAJOR && BASH_VERSINFO[1] < REQUIRED_BASH_MINOR)); then
echo "Error: Bash $REQUIRED_BASH_MAJOR.$REQUIRED_BASH_MINOR or newer is required." >&2
exit 1
fi
# === Safe Shell Options ===
set -euo pipefail
IFS=$'\n\t'
# Recommended shell options
shopt -s globstar # Allow ** to match nested directories
shopt -s extglob # Enable extended globbing
shopt -s lastpipe # Allow lastpipe to work in non-interactive shells
shopt -u nullglob # Don't expand globs to null string if nothing matches
shopt -u failglob # Don't fail if globs don't match
# === Systemd IDs ===
BOOT_ID=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || echo "")
MACHINE_ID=$(cat /etc/machine-id 2>/dev/null || echo "")
INVOCATION_ID=$(journalctl --flush && grep -F 'invocation_id=' /proc/self/environ | tr '\0' '\n' | cut -d= -f2 || echo "")
# === XDG Directory Setup ===
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
# === Exit Trap Array Setup ===
TRAP_EXIT=()
# Function to run all TRAP_EXIT entries
__run_exit_trap() {
local cmd
for cmd in "${TRAP_EXIT[@]}"; do
eval "$cmd"
done
}
trap __run_exit_trap EXIT
# === Example Usage ===
# TRAP_EXIT+=('echo "Cleaning up..."')
# TRAP_EXIT+=('rm -f /tmp/some-temp-file')
# === Your Script Logic Here ===
- You can change the required Bash version as needed.
journalctl
flush is used before looking forinvocation_id
—not all environments have this, so you may wish to wrap it in more checks.- The
TRAP_EXIT
array allows multiple exit-time actions without overriding the trap logic.
Let me know if you'd like this in a function template or split into reusable sourceable components.
Why did you journal flush
Great question — and you're right to question that line.
The call to journalctl --flush
in the context of retrieving the invocation ID is not necessary, and arguably wrong in a script like this. Here's why:
journalctl --flush
manually flushes the journal from memory to disk.- It's not related to retrieving environment variables or the invocation ID of the running process.
- It's potentially expensive, as it forces a sync of volatile logs to persistent storage.
The invocation ID can be found in /proc/self/environ
, if systemd
has set it:
INVOCATION_ID=$(tr '\0' '\n' < /proc/self/environ | grep ^INVOCATION_ID= | cut -d= -f2)
Alternatively, you can use systemd-id128
(if available):
INVOCATION_ID=$(systemd-id128 show --value --invocation)
This is cleaner and intended for exactly this purpose.
Here’s the corrected part of the script:
# === Systemd IDs ===
BOOT_ID=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || echo "")
MACHINE_ID=$(cat /etc/machine-id 2>/dev/null || echo "")
INVOCATION_ID=$(tr '\0' '\n' < /proc/self/environ | grep ^INVOCATION_ID= | cut -d= -f2)
Or, if you want to fall back to systemd-id128
:
INVOCATION_ID=$(tr '\0' '\n' < /proc/self/environ | grep ^INVOCATION_ID= | cut -d= -f2 || systemd-id128 show --value --invocation 2>/dev/null || echo "")
Let me know if you'd like the full revised boilerplate.
-
shopt
is missing noclobber, patsub_replacement: Doesn't take safe inititive. -
IFS
: For some reason takes hides-genuine-issues-from-the-developer inititive just because its popular. -
THAT IS NOT THE WAY TO DO IDs!!!! There are so many things wrong, but... Without explicitly knowing the theory behind systemd style IDs, one would 100% get tripped up by this. Observe the correct form:
# Randomly generated APP_ID=aa226d5c5e3b4e47a7cd84dfd20ac667 # Hashed together for privacy of boot-id # to avoid denial of service / impersonation. BOOT_ID=$(systemd-id128 boot-id --app-specific="$APP_ID")
-
The default runtime directory is wrong by XDG specs, and its also missing
XDG_DATA_DIRS
,XDG_CONFIG_DIRS
-
Bonus points for avoiding a common denial of service with default variable setting methods.
-
Why the function for trap exit, just put the code in the trap exit, why the
local
??