Bash scripting guide · part 4 of 32
Environment variables
How export passes variables to child processes, the one-command NAME=value prefix, loading .env files, and why a child cannot change its parent.
Every process on Unix carries an environment: a list of NAME=value strings it received
when it started. Programs read it for configuration: PATH for where to find commands,
HOME, LANG for the locale, TZ for the time zone, EDITOR, and whatever your own tools
look for, like DATABASE_URL.
The shell keeps two kinds of variables. Shell variables live only inside the shell.
Environment variables are shell variables marked for export: whenever the shell starts a
program, it copies the exported ones into that program's environment. export sets the mark.
export decides what children see
Only exported variables reach child processes
color=blue
export size=large
bash -c 'echo "child sees color=[$color] size=[$size]"'
export color
bash -c 'echo "after export: color=[$color]"'
child sees color=[] size=[large] after export: color=[blue]
The child bash got size because it was exported, but not color. Once color is
exported too, every program started afterwards receives it. export can mark an existing
variable or assign and mark in one step.
The copy happens at the moment the child starts, and it is one-way. A child gets its own
environment and can change it freely, but nothing it does travels back up to the parent. A
script cannot set a variable in the shell that ran it; only sourceing the script can,
because then no child is involved.
A child cannot change its parent's environment
export STAGE=dev
bash -c 'export STAGE=prod; echo "child set STAGE=$STAGE"'
echo "parent still has STAGE=$STAGE"
child set STAGE=prod parent still has STAGE=dev
The child's export changed only the child's copy. This is why tools that "set
environment variables for you" either print export lines for you to eval, or ask you
to source a file.
Setting a variable for one command
Writing NAME=value directly before a command puts it in that command's environment only,
without touching the shell. It is the cleanest way to adjust one run, and you can stack several
pairs.
Change the environment of a single command
TZ=Asia/Tokyo date -d '2026-03-14 12:00 UTC' '+%H:%M %Z'
date -d '2026-03-14 12:00 UTC' '+%H:%M %Z'
LC_ALL=C TZ=America/New_York date -d '2026-03-14 12:00 UTC' '+%a %H:%M %Z'
21:00 JST 12:00 UTC Sat 08:00 EDT
Only the first date ran with TZ=Asia/Tokyo; the second used the shell's own UTC.
The prefix assignments apply to the command they precede and are gone afterwards.
There is a classic trap in that form. The shell expands the command's arguments before it
applies the prefix assignment, and the assignment goes into the child's environment, not into
the current shell. So $NAME on the same line still shows the old value.
The prefix does not affect expansions on the same line
GREETING=hello echo "greeting is [$GREETING]"
GREETING=hello bash -c 'echo "greeting is [$GREETING]"'
greeting is [] greeting is [hello]
In the first line, $GREETING was expanded by the current shell, where it is not set,
before echo ran. In the second, the single quotes delay the expansion until the child
bash runs, and the child does have GREETING in its environment.
Looking at the environment
env and printenv with no arguments print the whole environment; printenv NAME prints one
value and exits 1 when it is not set. Both show exported variables only. declare -p NAME
shows a shell variable and its flags: an x among them means exported.
Inspect what is and is not exported
local_only=1
export SHARED=2
printenv SHARED
printenv local_only || echo "local_only is not in the environment"
declare -p local_only SHARED
env | grep -E '^(HOME|LANG|PATH|SHARED)='
2 local_only is not in the environment declare -- local_only="1" declare -x SHARED="2" HOME=/home/dev LANG=C.UTF-8 SHARED=2 PATH=/usr/local/bin:/usr/bin:/bin
printenv finds SHARED and fails on local_only. declare -p shows the difference as
-- versus -x. The sandbox environment is small and predictable: HOME, LANG, PATH
and a few others.
export -n NAME removes the export mark but keeps the variable; unset NAME removes both.
Exporting only works for scalar values: bash arrays cannot be put in the environment, because
the environment is a flat list of strings.
Loading a .env file
Many projects keep settings in a .env file of NAME=value lines, like the fixture's:
API_URL=http://localhost:8080
DEBUG=false
source .env would create shell variables but not export them, so programs you start would
not see them. set -a ("allexport") marks every variable assigned while it is on for export.
Export every variable from a .env file
source .env
python3 -c 'import os; print("without set -a:", os.environ.get("API_URL"))'
set -a
source .env
set +a
python3 -c 'import os; print("with set -a:", os.environ.get("API_URL"))'
without set -a: None with set -a: http://localhost:8080
After a plain source, Python sees no API_URL. Wrapped in set -a / set +a, the same
file's assignments are exported. Remember that sourcing runs the file as shell code: values
with spaces need quotes, and a .env file from an untrusted source can run commands.
PATH
PATH is a colon-separated list of directories searched in order for commands. Add to it,
never replace it: prepend when your copy should win, append when it should only fill gaps.
An empty element (a leading or trailing colon, or ::) means the current directory, which is
a security hole, so check your edits.
Prepend a directory to PATH
echo "$PATH" | tr ':' '\n'
export PATH="$HOME/bin:$PATH"
echo "now first: ${PATH%%:*}"
/usr/local/bin /usr/bin /bin now first: /home/dev/bin
tr shows one directory per line. After prepending, ~/bin is searched first.
${PATH%%:*} strips everything from the first colon, leaving the first element. In a
script, the change lasts until the script exits; for your login shell, put the export in
~/.profile or ~/.bashrc.
A clean environment
Scripts that work in your terminal and fail under cron, systemd or CI usually depend on
something in your interactive environment: a PATH entry, LANG, an exported token. Running
with env -i reproduces the bare environment those systems give you.
Test a script with an empty environment
#!/usr/bin/env bash
echo "HOME=${HOME:-<unset>} LANG=${LANG:-<unset>}"
echo "PATH=$PATH"
bash report.sh
env -i /bin/bash report.sh
HOME=/home/dev LANG=C.UTF-8 PATH=/usr/local/bin:/usr/bin:/bin HOME=<unset> LANG=<unset> PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.
With env -i the script gets no HOME or LANG, and bash falls back to its compiled-in
default PATH. That default differs from the sandbox's and even ends in ., the current
directory. Nothing is inherited, so whatever your script needs must be set explicitly.
Conventions
- Environment variable names are UPPERCASE with underscores. Keep your script's internal variables lowercase so they never collide with one.
- Export only what child programs need. Everything exported leaks into every process the script starts, including secrets.
- Do not put secrets on the command line (
TOKEN=x cmdis fine in a script, but on an interactive command line it lands in your history). Other users cannot read another user's environment through/procon Linux, but command-line arguments are visible to everyone inps.
Related
envPrint the environment, or run a command with a modified environment.printenvPrint all environment variables, or the values of the ones you name.exportMark shell variables (or bash functions) to be passed to every program the shell starts.setTurn shell options such as -e, -u, -x and pipefail on or off, and replace the positional parameters.unsetRemove shell variables, array elements or functions, so they no longer exist rather than being empty.declareCreate variables with attributes (array, associative array, integer, read-only, exported), or show how variables are defined.sourceRun the commands in a file inside the current shell, so its variables, functions and directory changes stay in effect.- VariablesAssigning and reading shell variables, the no-spaces rule, braces, unset vs empty, scope, readonly, and the special variables bash sets for you.
- Running scriptsThe four ways to start a script (./script, bash script, PATH, source), what each one changes, and the errors you get when one goes wrong.
- The shebang lineWhat the kernel does with #!, why
#!/usr/bin/env bashand#!/bin/bashboth exist, and the ways a shebang line breaks.