After Chapter 2 equipped us with basic commands for navigating and managing files via the terminal, it is time to delve into the foundation that makes a Bash script truly worthy of being called a "program": variables. This chapter covers how to declare and read variables, the fundamental differences between shell variables and environment variables, the two basic data types recognized by Bash (strings and numbers), up to quoting rules that often become hidden bug sources for beginners. Mastering this topic is an absolute prerequisite before moving on to Chapter 4 on arrays, since arrays are essentially variables that store multiple values at once.
3.1 Declaring and Using Variables
A variable is a name used to store a value so it can be reused without having to retype the value repeatedly. The concept is similar to noting down a table number on a slip of paper instead of memorizing the complete order details every time it is needed: once recorded under a clear name, we simply refer to that note wherever necessary.
3.1.1 Naming Rules and Assignment
Variable names in Bash can only consist of letters, numbers, and underscores (_), and must not start with a digit. Bash is also case-sensitive, meaning name and Name are treated as two distinct variables.
The rule most frequently violated by beginners involves spacing: variable assignment (=) must not contain spaces on either side of the equals sign.
name="Budi"
age=25By convention (though not a strict rule enforced by Bash), regular variable names are written in lowercase, whereas environment variables and constants are typically written in uppercase, such as PATH or HOME which will be discussed in sub-chapter 3.2. This convention helps us quickly distinguish standard variables from those belonging to the system environment.
3.1.2 Reading Values with the $ Sign and ${ }
To read the value of a variable, prepend a dollar sign ($) to its name.
name="Budi"
echo $nameThe syntax ${name} using curly braces produces the exact same effect, but it is required when a variable name is immediately adjacent to other characters without a separator, preventing Bash from misinterpreting the boundary of the variable name.
file="report"
echo "${file}_final.txt"Without curly braces, echo "$file_final.txt" is interpreted as attempting to call an undeclared variable named file_final, resulting in empty output rather than report_final.txt as intended.
3.1.3 readonly and unset: Locking and Deleting Variables
If a variable must be guaranteed not to change its value throughout script execution, such as when storing configuration constants, mark it with readonly.
readonly MAX_RETRY=5Once declared as readonly, any attempt to modify or delete the variable will fail with an error message. Conversely, to remove a standard variable that is no longer needed, use unset.
name="Budi"
unset name
echo $nameThe final line above will display empty output, because the variable name no longer exists after being processed by unset.
3.1.4 Assignment Verification and Troubleshooting
The fastest way to verify the contents of a variable is using echo, or via the declare -p command which also displays additional variable attributes (such as whether it is an integer type or read-only, discussed in sub-chapter 3.3).
declare -p nameThe most common error encountered by beginners is bash: name: command not found when writing name = "Budi" with spaces around the equals sign. Bash parses name as a command to be executed rather than a variable assignment, precisely because the no-space rule from sub-chapter 3.1.1 was broken.
In practice, another habit to guard against is referencing undeclared variables. By default, Bash emits no warnings and simply treats them as empty strings, allowing typos in variable names to pass undetected until they cause confusing bugs. The set -u option, which causes Bash to exit with an error upon encountering an undefined variable, will be discussed in detail in Chapter 25 on debugging and error handling.
3.2 Environment Variables vs Shell Variables
Not all variables in Bash share the same scope. Some exist only within the shell session where they were declared, while others are inherited by every new program executed from that shell. Understanding this distinction is crucial because many script bugs stem from assuming a variable is automatically available in another process when it is not.
3.2.1 Shell Variables: Local to the Current Shell
A shell variable is a standard variable like those covered in sub-chapter 3.1, recognized only by the shell (or script) in which it was declared. When that shell executes another program, such as calling another Bash script or an external command, the new program cannot access shell variables from its parent process.
message="Hello from parent shell"
bash -c 'echo $message'The command above will output a blank line because bash -c launches a completely new Bash instance as a separate child process, which does not inherit the message shell variable from its parent. This should be distinguished from a subshell created using parentheses like (echo $message): a subshell is created via a fork of the running shell process, thereby retaining all parent shell variables, not just those that have been exported. This difference is important to remember as subshells will be referenced again when discussing pipelines and command grouping in Chapter 7 and Chapter 12.
3.2.2 export: Converting to Environment Variables
To ensure a variable is passed down to child processes, mark it using the export builtin. A variable that has been exported is known as an environment variable.
export message="Hello from parent shell"
bash -c 'echo $message'This time, the bash -c command successfully prints the content of the message variable because environment variables are copied into the environment block of every child process spawned from that shell. The export builtin can also be applied to pre-existing variables without retyping their values.
message="Hello again"
export messageIt should be noted that inheritance only flows one way: from parent process to child process. Modifying an environment variable inside a child process will never affect the calling parent shell, as each process maintains its own independent copy of the environment.
3.2.3 Crucial Environment Variables: PATH, HOME, and Others
Many environment variables are automatically available upon login, provided by the system or the shell itself. The following table summarizes several of the most commonly used environment variables.
| Variable | Function |
|---|---|
PATH | Colon-separated list of directories searched by Bash when executing a command without an explicit path |
HOME | Path to the home directory of the currently logged-in user, also referenced by the ~ symbol |
USER | Username of the currently logged-in user |
SHELL | Path to the default login shell of the user, which may differ from the currently active shell |
PWD | Current working directory, automatically updated by the cd builtin |
OLDPWD | Previous working directory, utilized by cd - as covered in Chapter 2 |
LANG | System locale and language settings |
EDITOR | Default text editor invoked by other applications, such as crontab -e |
Understanding PATH is essential for System Administrators. Every time a command name is typed without a path (for instance, ls instead of /usr/bin/ls), Bash searches for an executable named ls within each directory listed in PATH, checking sequentially from left to right, and stops at the first match found.
echo $PATHAppending a new directory to PATH, such as a personal bin folder containing custom scripts, is a common task for both Users and System Administrators.
export PATH="$PATH:$HOME/bin"In production environments, the order of entries matters: placing the new directory at the end ($PATH:new-directory) ensures system default binaries take priority when naming collisions occur, whereas placing it at the front (new-directory:$PATH) gives custom scripts priority. Sysadmins must exercise caution with this ordering, as incorrect arrangement of PATH can cause scripts to unintentionally execute the wrong binary.
3.2.4 Inspecting the Full Environment with env and printenv
To view all active environment variables simultaneously, execute env or printenv without arguments.
envThe printenv command can also display the value of a single specific variable, similar to echo $VARIABLE, with one major key difference: printenv only recognizes exported environment variables, whereas echo $VARIABLE can display standard unexported shell variables as well.
printenv HOME3.2.5 Troubleshooting: Misconfiguring PATH
A frequent mistake made by beginners, and occasionally careless Sysadmins, is completely overwriting PATH instead of appending the new directory to its existing value.
PATH=/opt/custom-app/binThe assignment above clears the prior contents of PATH and replaces them with just a single directory, causing nearly all commands, including basic utilities like ls, to fail immediately.
ls
bash: line 1: ls: command not foundFortunately, this error only affects the current active shell session and is not permanent, unless the erroneous assignment line was saved into a configuration file such as .bashrc. The fastest recovery method in an active session is invoking utilities using absolute paths, which function normally regardless of PATH settings.
/bin/echo "Still functional via absolute path"
export PATH=/usr/bin:/binIf the correct default PATH value for the system is unknown, opening a new terminal window is the safest fix, as new sessions reload PATH cleanly from default login configurations. The main takeaway: always append new directory paths using the pattern export PATH="$PATH:new-directory" as outlined in sub-chapter 3.2.3, and never overwrite PATH directly.
3.3 Basic Data Types: Strings and Numbers
Unlike programming languages like Python or Java that require explicit data type declarations, Bash employs a significantly more permissive approach to data typing.
3.3.1 Bash Without Strict Data Types
By default, every variable in Bash is stored and treated as a string, including those containing numeric digits. Bash interprets a string as a number only within specific contexts, such as inside arithmetic expansion which is discussed in sub-chapter 3.3.3.
age="25"
echo $((age + 5))The code above evaluates to 30 even though age is technically stored as a string, because the $(( )) construct forces Bash to evaluate the expression arithmetically. This weakly-typed behavior is convenient for concise scripts, but it can also serve as a source of subtle bugs, such as when a variable intended to hold a number contains an empty string or invalid text, remaining undetected until executed in production.
3.3.2 Working with Strings
Because all variables are fundamentally strings, string concatenation in Bash does not require special operators like + found in other languages; placing them side-by-side is sufficient.
first_name="Budi"
last_name="Santoso"
full_name="$first_name $last_name"
echo $full_nameBash also provides various string manipulation capabilities using built-in parameter expansion syntax, such as retrieving string length or extracting substrings. Detailed string manipulation topics, including pattern matching with regular expressions, will be covered in Chapter 9.
echo ${#full_name}3.3.3 Arithmetic Expansion for Numerical Operations
For arithmetic operations, Bash provides arithmetic expansion using the $(( expression )) syntax. Inside these double parentheses, Bash evaluates mathematical expressions and returns the result.
result=$((10 + 5))
echo $resultSupported arithmetic operators include addition (+), subtraction (-), multiplication (*), division (/), remainder/modulo (%), and exponentiation (**).
echo $((7 / 2))
echo $((7 % 2))
echo $((2 ** 3))An essential limitation to keep in mind: Bash natively supports only integers in arithmetic operations; it lacks native support for floating-point calculations. Note that 7 / 2 in the example above yields 3 instead of 3.5, as integer division always truncates toward zero. For floating-point arithmetic needs, external utilities like bc or awk are required, which will be highlighted further during the awk discussion in Chapter 11.
echo "7 / 2" | bc -lIn addition to $(( )), the (( expression )) syntax without a leading dollar sign can be used as a standalone statement. Rather than returning a value, it evaluates conditions using the exit status. This form is often used inside conditional control structures like if, which will be discussed in Chapter 6.
number=10
if (( number > 5 )); then
echo "Greater than 5"
fi3.3.4 declare -i: Declaring Integer Variables
The declare builtin used with the -i flag assigns an integer attribute to a variable permanently. Once this attribute is applied, any assignment to the variable is automatically evaluated as an arithmetic expression without needing explicit $(( )) wrapping.
declare -i score
score="5 + 3"
echo $scoreThe snippet above yields 8 immediately, rather than outputting the raw text 5 + 3, because the integer attribute on score causes Bash to evaluate new assignments arithmetically.
3.3.5 Troubleshooting: Non-Numeric Values in Arithmetic Expansion
Because arithmetic expansion expects variable contents to be valid numbers or mathematical expressions, passing a standard text string into it triggers a runtime syntax error rather than silently evaluating to zero.
age="twenty five"
echo $((age + 5))bash: twenty five: syntax error in expression (error token is "five")This class of error most commonly arises when values used in arithmetic calculations originate from user input via read or script arguments without prior validation. In practice, a safe habit to cultivate early is validating that a variable contains numeric digits before processing it in arithmetic operations, especially if the data comes from external sources. Input validation techniques will be thoroughly covered in Chapter 23.
3.4 Quoting: Single Quote vs Double Quote vs Backtick/$()
Quoting is a topic frequently overlooked by beginners, yet choosing the wrong quote type remains one of the most widespread causes of bugs in Bash scripts, even among experienced System Administrators.
3.4.1 Word Splitting Issues Without Quoting
When an unquoted variable is expanded, Bash automatically performs two post-processing steps: word splitting (dividing the variable contents into distinct arguments based on whitespace) and pathname expansion or globbing (interpreting characters like * as wildcards). Both processes can lead to unexpected behavior.
file="monthly report.txt"
ls $fileBecause $file is unquoted, Bash splits its value into two separate arguments, monthly and report.txt, causing ls to search for two non-existent files instead of a single file named monthly report.txt.
3.4.2 Single Quote: Completely Literal
Single quotes ('...') instruct Bash to preserve the literal value of every character within the quotes, suppressing all expansions including dollar signs and backslashes.
name="Budi"
echo 'Hello, $name'The command above prints Hello, $name verbatim rather than Hello, Budi, because single quotes prevent variable expansion entirely. Due to this strict literal behavior, single quotes cannot contain single quote characters directly inside them; special escaping syntax is required to include a literal single quote.
echo 'This is a single quote: '\''example'\'''3.4.3 Double Quote: Preserves Expansion
Double quotes ("...") prevent word splitting and globbing, while still permitting variable expansion, command substitution, and arithmetic expansion to occur normally.
name="Budi"
echo "Hello, $name"This time the result is Hello, Budi, because double quotes allow the dollar sign to expand the variable. Returning to the word splitting example in sub-chapter 3.4.1, wrapping the variable reference in double quotes resolves the issue.
file="monthly report.txt"
ls "$file"For this reason, a recommended best practice is to enclose variable references in double quotes by default, unless word splitting or globbing is explicitly intended.
3.4.4 Command Substitution: $() vs Backticks
Command substitution is the mechanism of capturing the standard output of a command and using it as a value, either for variable assignment or direct insertion into another command. Two syntaxes exist: the modern $(command) form, and the legacy backtick `command` form.
today_date=$(date +%Y-%m-%d)
echo "Today: $today_date"today_date=`date +%Y-%m-%d`
echo "Today: $today_date"Both snippets produce identical output, but the $() syntax is strongly preferred for modern scripts for two practical reasons: it offers cleaner readability with clear start and end boundaries, and it supports straightforward nesting of command substitutions.
echo "Current directory contents: $(ls $(pwd))"Nesting backticks for similar use cases is significantly more complex, as inner backticks must be escaped using backslashes to avoid prematurely closing the outer backtick block, an issue that does not exist when using $().
3.4.5 Case Study: Risks of Unquoted Variables
Failing to quote variables is not merely an inconvenience regarding word splitting; it can also introduce real security vulnerabilities. Consider a script building commands dynamically from unvalidated input, such as a user-supplied filename.
file_name="document; rm -rf /tmp/cache"
eval "cat $file_name"Without proper quoting and when combined with eval, an attacker can insert shell control operators like semicolons (;) into the input to execute arbitrary commands beyond what the script intended, a vulnerability known as command injection. In-depth strategies for preventing command injection and implementing robust input validation will be discussed in Chapter 23. The fundamental guideline established here in Chapter 3: always quote variables with double quotes when passing them as command arguments, and avoid using eval on strings containing untrusted external inputs.

