How to Use Configuration Files and Defaults in Shell Scripts
Load a shell script configuration file with safe parsing, layered defaults and overrides from the environment and flags, without running untrusted code.

A script with a hard-coded server name and retention count works for its author and nobody else. Moving those values into a file with defaults makes it reusable, but the way the file is read decides whether the script is safe to deploy. The examples use Bash 5.2 and extend the idea in reading a config file in Bash.
Defaults, then file, then override
BACKUP_DIR=/var/backups
KEEP=7
for f in /etc/mybackup.conf "${XDG_CONFIG_HOME:-$HOME/.config}/mybackup.conf"; do
[[ -r $f ]] && source "$f"
done
BACKUP_DIR=${MYBACKUP_DIR:-$BACKUP_DIR}
KEEP=${MYBACKUP_KEEP:-$KEEP}The defaults sit at the top where a reader can see them. Later sources win, so a user file overrides the system file, and environment variables override both. Command-line flags are parsed after this block and win last. The same precedence is used by most Unix tools, which keeps the behaviour unsurprising.
The risk of sourcing
source executes every line, so a config file containing rm -rf "$HOME" or $(curl ...) runs with the script's privileges. That is acceptable for /etc/mybackup.conf owned by root with mode 644, and not acceptable for a file in a user-writable directory read by a root script. Check ownership first if in doubt, or parse instead of sourcing.
Parse without executing
load_config() {
local file=$1 key value
while IFS='=' read -r key value; do
[[ $key =~ ^[[:space:]]*(#|$) ]] && continue
value=${value%\"}; value=${value#\"}
case $key in
BACKUP_DIR) BACKUP_DIR=$value ;;
KEEP) KEEP=$value ;;
*) echo "unknown key: $key" >&2; return 1 ;;
esac
done < "$file"
}Only listed keys are accepted, values are never evaluated, and typos are reported instead of silently ignored. The cost is that each setting needs a line in the case. Validate the values after loading, for example that KEEP is a number, using the checks in checking if a value is a number.
Document the search order
Print it in --help, and add a --config FILE flag that skips the search. The XDG Base Directory specification at freedesktop.org defines XDG_CONFIG_HOME and its fallback to ~/.config, which is the convention to follow for per-user files.
Choosing a file format
A configuration file is an interface, and its format sets how easily people can edit it correctly. The simplest option for a shell script is KEY=value lines, which suit flat settings, are the format that systemd environment files and Docker env files also use, and need only a read loop to parse. The limit shows when a setting is a list or a group: the file starts to need conventions such as comma-separated values or numbered keys, and each convention is another rule to document and test. INI files add sections, which keeps related settings together, but Bash has no parser for them, so the script would need a function or a tool such as crudini. For nested data, YAML or TOML with a command-line reader such as yq or tomlq is clearer than any hand-rolled scheme, with the cost of an extra dependency.
Whatever the format, support comments and blank lines, because people explain their settings, and tolerate whitespace around the equals sign if you can. State whether quotes are part of the value. The most common source of bugs in config handling is not the logic but a disagreement between the author and the user about such details.
Validating what was read
Loading a value is half the work. After the file is read, check each setting against what the script can use: a directory should exist and be writable, a number should be a number in range, a hostname should resolve or at least match a pattern, and a choice should be one of the allowed words. Report every problem in one pass with the file and key, so a user can fix the file once instead of running the script six times. Fail before doing any work: a backup script that discovers a bad retention count after it has already copied data gives a worse experience than one that refuses to start.
Defaults deserve the same care. A default that silently points to a location on the author's machine is a trap, so choose defaults that are safe on any machine and fail loudly when a required value is missing. For settings that must differ by environment, such as a production host name, have no default at all.
Permissions and ownership of the file
If the script runs as root, the config file becomes an input from a less privileged place. A world-writable file or one in a directory writable by others allows any user to change what the script does, and with source that is code execution as the script's user. Check the owner and mode before reading, as shown for secrets elsewhere: the file should belong to root or the service account and not be writable by group or others. Resolve the real path first with realpath, so a symlink cannot point at a file that someone else controls.
Reloading and long-running scripts
A script that runs for a long time, such as a loop that polls a queue, may need to re-read its configuration without restarting. The conventional trigger is the HUP signal: trap load_config HUP re-runs the loader, and the administrator sends kill -HUP to apply a change. Because a trap runs between commands, a reload that fails halfway can leave a mixture of old and new values; parse into temporary variables, validate, then assign them all together, and keep the previous settings if validation fails. Log each reload and its result, so that whether a change applied has a visible answer in the log.
Configuration in version control
Keep example files in the repository with names like mybackup.conf.example, showing every key with a comment and a safe value, and keep the real files out. The example doubles as documentation and as a test fixture: a CI job can run the script's validator against it and fail if a new setting was added to the code but not to the example. When a setting is renamed or removed, handle the old name for at least one release by printing a clear warning that tells the user what to change, since silent ignoring turns an upgrade into a mystery.
Where this does not apply
Values that contain secrets should not live in a world-readable file, and a config file is the wrong home for them; see reading secrets in a shell script. Structured settings with lists or nesting are better in JSON or YAML read with jq or yq, as in parsing JSON in Bash. Under POSIX sh, replace [[ ]] with case and [ ] tests. The Bash builtins reference documents read and source in full.
A final practical check is to print the effective configuration in a --show-config flag, with secrets masked. Support questions about "why did it use that directory" then take one command to answer, and the order of defaults, files and overrides is visible instead of inferred from the source. This is a cheap feature that pays off the first time the script runs somewhere you cannot easily log in.


