Why environment variables matter in Bash
In the previous article we split logic into functions so scripts are easier to reuse. Once a script starts running across multiple environments — local, staging, production, CI/CD runners — the next question is: where does configuration come from, and how should it be passed in?
Variables in Bash store temporary values inside a script. Environment variables pass configuration to child processes such as docker, kubectl, aws, curl, or the application launched by the script. If variables are managed poorly, a script can deploy to the wrong environment, leak secrets, or fail because required config is missing.
Local variables and assignment syntax
Bash variable assignment has no spaces around =:
| |
To read a variable, use $VAR or ${VAR}. In real scripts, ${VAR} is often clearer when concatenating strings:
| |
A very common mistake is adding spaces:
| |
When a value contains spaces or special characters, always quote the variable:
| |
Unquoted variables can split paths into multiple arguments and cause hard-to-debug operational failures.
export: when a variable becomes an environment variable
A normal Bash variable only exists in the current shell. Child processes do not see it automatically:
| |
To make a child process read it, use export:
| |
In DevOps, export is commonly used before calling a CLI or starting an app:
| |
You can also pass a variable to a single command only:
| |
This is cleaner and safer when the variable only needs to exist for one command.
Inspect, set, and remove variables with env, set, unset
A few useful commands when debugging a script environment:
| |
env is useful for checking what child processes will see. set is more detailed, but its output is long and may contain sensitive data, so avoid pasting the full output into tickets or logs.
Example for validating required variables:
| |
${!name} is indirect expansion: if name="APP_ENV", Bash reads the value of the APP_ENV variable.
Loading configuration from a .env file
A .env file keeps configuration separate from code:
| |
The simplest way to load it:
| |
set -a automatically exports variables assigned after it, so child processes can read them.
However, source .env executes the file content as Bash code. Only use it with files you control. Never source files uploaded by users or fetched from untrusted sources.
A safer pattern for deployment scripts:
| |
: "${VAR:?message}" makes the script stop immediately if the variable is unset or empty. This fail-fast pattern is very useful before running a deployment.
Reading CLI arguments with getopts
Not every config value belongs in .env. Values that change per run, such as target environment or dry-run mode, should be passed as flags:
| |
Run it:
| |
getopts is a good fit for short options such as -e production and -n. If you need long options such as --environment, you can parse them manually with case, but keeping the format simple makes scripts easier to maintain.
$IFS: controlling how Bash splits strings
IFS means Internal Field Separator — the characters Bash uses for word splitting in some expansions and in the read command. By default, it contains space, tab, and newline.
When reading a file line by line, use this pattern to preserve leading/trailing spaces and avoid special backslash handling:
| |
When splitting a simple CSV string:
| |
Avoid changing IFS globally unless you really need to. If you must change it, keep the change limited to one line or a small scope so it does not break another part of the script.
Common special variables
Bash has many special variables that are useful in operational scripts:
| Variable | Meaning |
|---|---|
$0 | Name of the running script |
$1, $2 | Positional arguments |
$@ | All arguments; usually use "$@" |
$# | Number of arguments |
$? | Exit code of the last command |
$$ | PID of the current shell |
$! | PID of the most recent background process |
${BASH_SOURCE[0]} | Path to the current script file in Bash |
Example using $! and wait to track a background job:
| |
Example for finding the directory that contains the script, regardless of where you run it from:
| |
This pattern is useful when a script needs to load a config file or library located next to it.
DevOps example: deploy script with .env, flags, and validation
The example below simulates a small deployment script. It accepts the environment through -e, supports dry-run with -n, loads .env.<environment>, validates required variables, and then runs the deploy command.
| |
A .env.staging file can look like this:
| |
Key points in this example:
- Do not hardcode host names or image tags in the script.
- Validate the allowed environment before sourcing a file.
- Use
set -ato export config for child processes if needed. - Use dry-run mode to inspect the command before executing it.
- Quote variables when building paths and arguments.
Common mistakes
- Spaces around
=in assignment:NAME = valueis invalid in Bash. - Not quoting variables: Paths with spaces or glob characters such as
*can make scripts behave incorrectly. - Exporting too much: Only export variables child processes need to read.
- Sourcing untrusted
.envfiles: Withsource,.envis Bash code and can execute commands. - Logging secrets: Avoid
set -xaround token/password handling. - Using
$1directly withset -u: Use${1:-}or validate first. - Changing
IFSglobally: It can break loops or argument parsing later in the script.
Implementation notes
- When applying this to your own project, classify configuration:
- Values fixed per environment →
.env.staging,.env.production, or a separate config file. - Values that change per run → CLI flags through
getopts. - Secrets → environment variables from a CI/CD secret store or vault; do not commit them to git.
- Values fixed per environment →
- Best practices:
- Enable
set -uto catch undeclared variables, but use${VAR:-}for optional variables. - Validate required variables early with
: "${VAR:?message}". - Use
printenv VARto debug one variable instead of printing the whole environment. - Prefix app-specific variables, for example
BLOG_APP_ENV, to avoid name collisions. - Do not commit
.envfiles containing secrets; commit only.env.example.
- Enable
- Troubleshooting:
- Child process cannot see a variable? → Check whether you used
export. - Script works locally but fails in cron/CI? → Check
PATH, working directory, and runner-provided environment variables. .envdoes not load correctly? → Check that the file uses valid Bash syntax and has no spaces around=.
- Child process cannot see a variable? → Check whether you used
🎯 Conclusion
Good variable management makes Bash scripts more stable across environments. Keep code and config separate, validate required variables before real operations, quote variables when using them in commands, and export only what child processes need.
In the next article, we will cover cron jobs with Bash: schedule syntax, crontab, @daily, @reboot, cron logs, PATH issues, and automated backup/healthcheck examples. 🚀
References
- GNU Bash Manual — Shell Parameters — Official documentation for positional parameters and special parameters.
- GNU Bash Manual — Bourne Shell Builtins — Reference for
export,set,unset,getopts, andsource. - GNU Bash Manual — Word Splitting — Explains the role of
IFSin word splitting. - The Open Group — Shell Command Language — POSIX shell reference for variables, quoting, and expansion.
