Dissecting the concept of variables in BASH: naming and declaration rules, accessing with $VAR and ${VAR}, the difference between local variables and environment variables (export, readonly, unset), a list of important built-in variables like $PATH, $HOME, and $USER, and the practice of building scripts that read the environment correctly.

In episode 2 we wrote our first script and understood the shebang, chmod +x, and the three execution modes — including the proof that an export inside a script run as a program doesn't persist in the main shell — so in this episode we enter BASH's real "language": variables & environment variables.
Variables are the basic cells of every script. Without variables, your scripts are just a list of static commands that must be manually edited every time a value changes. With variables, you can write one script that applies to many servers, many users, many scenarios — just change the value, not the code. That's what separates a "one-off" script from one that's genuinely useful.
Episode 3 is the foundation for every following episode: control flow, loops, functions, arrays, and parameter expansion all stand on top of understanding variables. We'll dissect declaration rules, the difference between local variables vs environment variables (export, readonly, unset), the important built-in variables present on every system, and close with a practical script that reads the environment plus a list of the most common mistakes.
Unlike modern programming languages, in BASH variables don't need to be declared with a type — everything is treated as a string (numbers can still be computed; we'll cover that in the arithmetic episode). Just write the variable name, an equals sign, and its value:
APP_NAME="deploy-tool"
VERSION="2.4"
COUNTER=0Note the three rules you must memorize:
No spaces around =. APP_NAME = "deploy" will be interpreted as a command named APP_NAME with arguments — and produce command not found. This is the number one beginner mistake.
Variable names are case-sensitive. App and app are two different variables. Community convention: uppercase for global/constant variables, lowercase for local variables.
Only letters, numbers, and underscores — and it can't start with a number. 1var is illegal; var1 and my_var are legal.
echo "$APP_NAME"
echo "Versi: $VERSION"
echo "Saya memakai $APP_NAME versi $VERSION"$VAR vs ${VAR}There are two ways to access a variable:
| Syntax | Example | When to use |
|---|---|---|
$VAR | echo $PATH | Shorthand, convenient |
${VAR} | echo ${PATH} | Delimits the variable name, required for parameter expansion |
The difference is easiest to see when a variable sits next to other text:
version="2"
echo "versi $version000" # ⚠️ BASH reads the variable name $version000
echo "versi ${version}000" # ✅ clear: variable $version then the text 000versi
versi 2000On the first line, BASH treats $version000 as a single variable name (which is undefined) — the result is empty. The braces ${version} tell BASH exactly where the variable name ends. The habit of using ${VAR} for everything inside text will save you from subtle bugs like this one.
Tip
Inside scripts, get used to always using ${VAR} when a variable sits next to other characters, and "${VAR}" when the value can contain spaces or special characters (full details in the quoting episode). The braces aren't just style — they prevent the hard-to-trace "variable name merged with text" bug.
This is the core concept that determines how values move between processes. There are two "spaces" for variables:
An analogy: a local variable is a note in your private notebook; an environment variable is an announcement on the office bulletin board — everyone on that floor (the child processes) can read it.
LOCAL_VAR="hanya di sini"
export GLOBAL_VAR="terlihat semua orang"
bash -c 'echo "GLOBAL: $GLOBAL_VAR"'
bash -c 'echo "LOCAL : $LOCAL_VAR"'GLOBAL: terlihat semua orang
LOCAL : The bash -c process is a child shell. It sees GLOBAL_VAR (which was exported) but is blind to LOCAL_VAR. In episode 2 we proved that an export inside a script run as a program doesn't persist — it's the same explanation: export only sends a value to child processes, never returns it to the parent.
export, readonly, unsetThree variable control commands you must master:
| Command | Function | Example |
|---|---|---|
export NAME=value | Pass the variable to child processes | export DB_HOST="10.0.0.5" |
readonly NAME | Lock a variable so it can't be changed | readonly APP_NAME="prod" |
unset NAME | Delete a variable | unset DB_HOST |
export CONFIG_FILE="/etc/myapp.conf" # inherited by all child processes
readonly APP_ENV="production" # locked forever
unset CONFIG_FILE # removed from memory
echo "ENV: $APP_ENV"
APP_ENV="staging" # ⚠️ error: readonly variableENV: production
bash: APP_ENV: readonly variablereadonly is useful for constants that must not change in the middle of a script — protecting against bugs when an important value is overwritten by another part of the code. unset cleans up variables that are no longer needed.
To see all active environment variables, use:
printenv
printenv HOME USER
env | grep -i path/home/arman
armanBASH and the operating system provide pre-filled variables — you just read them. These are the "context" the system passes to every process, and they're an invaluable source of information for scripts:
| Variable | Content | Example value |
|---|---|---|
$PATH | List of command search directories | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin |
$HOME | User's home directory | /home/arman |
$USER | Current username | arman |
$SHELL | User's login shell | /usr/bin/bash |
$PWD | Current working directory | /home/arman/lab |
$OLDPWD | Previous working directory | /var/log |
$HOSTNAME | Host name | arman-vm |
$BASH_VERSION | BASH version | 5.2.21(1)-release |
$LANG | Locale & encoding | en_US.UTF-8 |
$RANDOM | Random number 0–32767 | 17213 |
$? | Exit code of the last command | 0 (success) |
echo "Saya: $USER, di host: $HOSTNAME"
echo "Direktori: $PWD (sebelumnya: $OLDPWD)"
echo "Home: $HOME"
echo "Angka acak: $RANDOM"A real use of $RANDOM is creating a unique temporary file name: ${RANDOM}-log. $? is the key to the error handling we'll build in the strict mode episode. $PATH is the most commonly modified variable — adding a tool directory:
export PATH="$PATH:$HOME/bin"Warning
Be careful when modifying $PATH — never wipe its contents. PATH="/usr/bin" will cripple most commands because the shell can no longer find ls, cp, or bash in other directories. Always add to the existing variable ($PATH:$HOME/bin), never overwrite it outright.
Now let's build a script that genuinely uses the concepts above. Goal: a simple deploy script that behaves differently depending on the environment where it runs.
#!/bin/bash
export REPORT_DIR="$HOME/reports"
mkdir -p "$REPORT_DIR"
echo "=== Laporan Environment ==="
echo "Deploy env : ${DEPLOY_ENV:-development}"
echo "User : $USER"
echo "Host : $HOSTNAME"
echo "Path config : ${APP_CONFIG:-/etc/default/app.conf}"
echo "Port app : ${APP_PORT:-8080}"
echo "Hasil disimpan di $REPORT_DIR"DEPLOY_ENV=production APP_PORT=9000 ./env-report.sh
echo "---"
./env-report.shNote the ${VAR:-default} pattern — a parameter expansion meaning "use $VAR if it's defined, otherwise use the default". With this single pattern, the same script behaves differently per environment without changing code: locally the default value is used, in production the override applies. This is the essence of a "production-grade" script — controlled by the environment, not edited per server.
Scripts like this are a real pattern in the DevOps world: a CI/CD pipeline sends environment variables (DEPLOY_ENV=staging), and the script responds without needing to know where the value came from.
Important
Never put secrets (passwords, tokens, API keys) directly in a script. Read them from environment variables — e.g. ${DB_PASSWORD:?DB_PASSWORD belum di-set}. The :? operator forces the script to stop with an error message if the variable is missing, preventing you from running a script with lost configuration. Real secrets are managed by tools like Vault (covered in the Learn Secret Management series).
Spaces around =. VAR = "x" isn't a variable declaration but a VAR command with arguments — error command not found. Write it without spaces: VAR="x".
Using an undefined variable. BASH doesn't error when an empty variable is used — it silently produces an empty string. As a result, rm -rf $DIR/ with an empty DIR becomes rm -rf / — a disaster. Protection: always give a default value, or use ${VAR:?} to force an error.
Expecting variable changes to persist. Like episode 2: a script run as a program runs in a subshell — its export doesn't change the parent shell. Want the changes to persist? source.
Variable name collisions. USER, HOME, and PATH are already used by the system. Overwriting PATH is the classic mistake that cripples commands. Use specific names with a unique prefix (e.g. MYAPP_*) to avoid collisions.
Ignoring quotes. echo $VAR with a value containing spaces will split into multiple arguments. From now on, get used to "$VAR" — the deep reason we dissect in episode 4.
| Mistake | Symptom | Solution |
|---|---|---|
VAR = value | command not found: VAR | VAR=value (no spaces) |
| Undefined variable | Empty string, script acts weird | Give a default ${VAR:-default} or force an error ${VAR:?} |
export in a regular script | Variable "disappears" after the script ends | Use source if you want it to persist |
Overwriting $PATH | All commands command not found | Always PATH="$PATH:..." |
Note
The right debugging culture: before blaming a variable, look at its value. Run echo "nilai: ${VAR}", or add set -u at the start of a script (making BASH error when an empty variable is used) and set -x (printing every executed line). We'll dive into both in the error handling & debugging episode — but knowing them now will save you hours of debugging.
In episode 3 you understood variables as the core of the BASH language: declaration rules without spaces around =, case-sensitive names, $VAR vs ${VAR} access (and why braces prevent bugs), the difference between local variables and environment variables inherited via export, the readonly and unset controls, built-in variables like $PATH, $HOME, $USER, $SHELL, $PWD, and $OLDPWD, and the ${VAR:-default} pattern that makes scripts responsive to the environment without changing code.
The core takeaways:
VAR="value"; names are case-sensitive, alphanumeric + underscore only.$VAR for brevity, ${VAR} to delimit the name — get used to ${VAR} inside text.export passes values to child processes; variables without export live only in that shell.$PATH, $HOME, $USER, $SHELL, $PWD, $OLDPWD are context that's always available.$PATH; never put secrets in a script — read them from the environment.Variables are BASH's "nouns". In episode 4 we'll cover the "punctuation" that determines how those words are spoken: quoting, escaping & word splitting — when to use single vs double quotes, how the backslash rescues special characters, the IFS mechanism that splits strings, and why "$@" can save you from the disaster of files named with spaces. The foundation you built in this episode will be tested right there. See you then!