Every useful script needs a way to remember things. Perhaps you want to store a filename, remember which server you are connecting to, count how many files you have processed, or keep a configuration value that will be used by several commands.
In programming, we use variables for this purpose. Bash is no exception.
Variables are one of the first things you encounter when learning shell scripting, but there is more to them than simply assigning a value and printing it. Bash provides a powerful collection of features for working with variables: default values, indirect expansion, string manipulation, substring extraction, pattern matching, and much more.
The good news is that you don't need to learn everything at once. Let's start with the basics and gradually explore the features that make Bash variables so useful.
1. Creating a Variable
Unlike some programming languages, Bash does not require you to declare an ordinary variable before using it. You can simply assign it a value.
name="Pavel"
age=49
language="Bash"That's it. The variables now contain the values you assigned to them.
There is no need to specify a data type, reserve memory manually, or write a declaration statement. Bash creates or updates the variable when it encounters an assignment.
Naming variables
A variable name can contain:
- Uppercase letters (
A–Z). - Lowercase letters (
a–z). - Digits (
0–9). - Underscores (
_).
A name must begin with a letter or an underscore, not a digit.
These are valid variable names:
name="Pavel"
user_name="Pavel"
USER_NAME="Pavel"
server2="web02"
_private_value="secret"These are not valid ordinary variable names:
2server="web02"
user-name="Pavel"The first begins with a digit, while the second contains a hyphen, which Bash interprets as a separate shell character rather than part of an ordinary variable name.
Assigning multiple variables
You can assign several variables on one line:
name="Pavel" age=49 language="Bash"This is convenient for short assignments, although separate lines can be easier to read when a script grows.
Remember: there must be no spaces around the = sign.
Correct:
name="Pavel"Incorrect:
name = "Pavel"Bash interprets the second example as a command named name, followed by arguments. It does not assign a value to the variable.
Are variables always strings?
For ordinary variables, Bash does not require you to declare a type. A variable can contain text, a number represented as text, a filename, or almost any other value that can be represented in a shell variable.
count=10
message="Hello, world!"Although count looks like a number, it is not automatically a strongly typed integer as it might be in C or Java. Bash supports integer arithmetic, and you can use declare -i to give a variable an integer attribute, but ordinary variables are fundamentally different from strongly typed variables in general-purpose programming languages.
We'll see why this matters when we get to arithmetic and string manipulation.
2. Reading a Variable's Value
To retrieve a variable's value, prefix its name with $.
name="Pavel"
echo "$name"Output:
PavelYou can also write ${name}:
echo "${name}"For a simple variable, both forms work. The braces become particularly useful when the variable name touches other text.
Consider this example:
language="Bash"
echo "$languageScript"Bash interprets languageScript as one variable name. It does not automatically understand that you intended to append Script to the value of language.
Instead, write:
echo "${language}Script"Output:
BashScriptThe braces clearly identify where the variable name ends and the surrounding text begins.
This is why ${variable} is often a good habit in scripts, even when the shorter $variable form would work.
Why quoting matters
You will frequently see variables written as "$variable" rather than $variable.
The quotes prevent Bash from splitting the expanded value into multiple words and performing pathname expansion on it.
For example:
filename="monthly report.log"
touch "$filename"The command creates one file named monthly report.log.
Without the quotes:
touch $filenameBash may interpret the value as two separate filenames: monthly and report.log.
There are contexts in which unquoted expansion is intentional, but when passing a variable as a command argument, quoting it is usually the safest default.
3. Unset Variables, Empty Variables, and Default Values
One of Bash's most useful features is the ability to provide a fallback value when a variable has not been set.
Imagine a script that needs to know which environment to use. You might want it to default to development unless the user specifies something else.
Bash provides parameter expansion for exactly this situation.
Using a default value: ${variable:-default}
echo "${environment:-development}"If environment is unset or empty, Bash expands the expression to:
developmentIf the variable contains a value, Bash uses that value instead:
environment="production"
echo "${environment:-development}"Output:
productionNotice that this expression does not assign development to the variable. It only provides a temporary fallback for this particular expansion.
unset environment
echo "${environment:-development}"
echo "${environment:-development}"Both commands print development, but environment remains unset.
What is the difference between - and :-?
This distinction is important:
| Expression | Uses the fallback when |
|---|---|
${variable-default} | The variable is unset |
${variable:-default} | The variable is unset or empty |
For example:
environment=""
echo "${environment-development}"
echo "${environment:-development}"The first expansion produces an empty string because environment is set, even though its value is empty. The second produces development.
This distinction is useful when an empty value has a meaningful purpose in your script.
4. Assigning a Default Value
Sometimes you don't just want to use a fallback. You want to save it in the variable so that subsequent commands can use it.
Bash provides ${variable:=default} for this purpose.
unset environment
echo "${environment:=development}"
echo "$environment"Both commands print:
developmentThe first expansion assigns development to environment because the variable is unset. The second reads the value that was assigned.
As with the previous example, the colon matters:
| Expression | Assigns the default when |
|---|---|
${variable=default} | The variable is unset |
${variable:=default} | The variable is unset or empty |
For example:
environment=""
echo "${environment:=development}"This prints development and updates environment to that value.
By contrast, ${environment=development} would leave the empty value unchanged because the variable is already set.
This feature is particularly useful when initializing configuration variables:
: "${LOG_LEVEL:=info}"
: "${RETRY_COUNT:=3}"
echo "Log level: $LOG_LEVEL"
echo "Retries: $RETRY_COUNT"The : command is Bash's null command. It does nothing and returns success, but it allows the assignments performed during parameter expansion to happen without printing their values.
5. Requiring a Variable to Be Set
Sometimes a default value is not appropriate. Perhaps a script needs an API endpoint, a backup destination, or a filename supplied by the user.
In that case, you may want to stop immediately if the variable is missing.
Use ${variable:?message}:
: "${BACKUP_DIR:?Please set BACKUP_DIR}"
echo "Backup directory: $BACKUP_DIR"If BACKUP_DIR is unset or empty, Bash prints an error message and, in a non-interactive shell, exits with a non-zero status.
The colon again makes a difference:
${variable?message}reports an error if the variable is unset.${variable:?message}reports an error if it is unset or empty.
This is a useful way to validate required configuration values near the beginning of a script, rather than allowing a missing value to cause confusing failures much later.
6. Providing a Value Only When a Variable Is Already Set
The opposite of a default-value expansion is an expression that produces a value only when the variable is already set.
Bash provides ${variable:+value}:
environment="production"
echo "${environment:+The environment is configured}"Output:
The environment is configuredIf environment is unset or empty, the expression produces an empty string.
As before, the colon matters:
${variable+value}producesvalueif the variable is set, even if it is empty.${variable:+value}producesvalueonly if the variable is set and non-empty.
These expressions are handy when constructing optional text or arguments without writing a full conditional statement.
7. Finding the Length of a Variable
Bash can calculate the length of a string using ${#variable}.
message="Hello, world!"
echo "${#message}"Output:
13The result is the number of characters in the string, measured according to Bash's current locale and character-handling behavior.
This can be useful for validation:
username="pavel"
if (( ${#username} < 3 )); then
echo "Username is too short."
else
echo "Username length is acceptable."
fiThe same syntax works with other strings, too:
filename="backup.tar.gz"
echo "Filename length: ${#filename}"What about ${#@} and ${#*}?
These special forms relate to the positional parameters passed to a script or function.
For example, if you run:
./script.sh alpha beta gammaThen $1, $2, and $3 refer to the three arguments.
The expressions ${#@} and ${#*} give the number of positional parameters. In modern Bash scripts, $# is the conventional and clearer way to get that count:
echo "Number of arguments: $#"8. Removing Text from the Beginning or End of a String
Bash includes powerful parameter expansions for removing parts of a string that match a shell pattern. These operations do not require external tools such as sed or cut.
The general idea is simple: you provide a pattern, and Bash removes the matching portion from the beginning or end of the value.
Removing a prefix with #
The expression ${variable#pattern} removes the shortest matching prefix.
filename="backup.tar.gz"
echo "${filename#*.}"Output:
tar.gzThe pattern *. matches any sequence of characters ending with a dot. Bash removes the shortest matching prefix, leaving tar.gz.
To remove the longest matching prefix, use two hash characters:
echo "${filename##*.}"Output:
gzBecause *. can match up to the final dot, the remaining text is gz.
This is a convenient way to extract a file extension when the extension is defined as the text after the final dot.
Removing a suffix with %
The % operator works from the opposite end of the string.
${variable%pattern} removes the shortest matching suffix:
filename="backup.tar.gz"
echo "${filename%.*}"Output:
backup.tarThe pattern .* matches a dot followed by any characters. Bash removes the shortest matching suffix, which is .gz.
Use %% to remove the longest matching suffix:
echo "${filename%%.*}"Output:
backupThis time, Bash removes everything from the first dot onward.
Here's a quick summary:
| Expression | What it removes |
|---|---|
${var#pattern} | Shortest matching prefix |
${var##pattern} | Longest matching prefix |
${var%pattern} | Shortest matching suffix |
${var%%pattern} | Longest matching suffix |
These patterns use shell wildcard syntax, not regular expressions. For example, * matches any sequence of characters, and ? matches one character.
9. Changing Letter Case
Bash can convert characters to uppercase or lowercase directly in a parameter expansion.
These operators are available in Bash 4 and later.
Converting to uppercase
The ${variable^pattern} form converts the first character matching the pattern at the beginning of the value to uppercase.
word="suserlongexample"
echo "${word^[r-t]}"Output:
SuserlongexampleThe pattern [r-t] matches one character in the range r through t. Since the string begins with s, that character is converted to uppercase.
To convert every matching character, use ^^:
echo "${word^^[r-t]}"Output:
SuSeRlongexampleWithout an explicit pattern, ^ and ^^ operate on lowercase characters.
name="pavel dolinin"
echo "${name^}"
echo "${name^^}"Output:
Pavel dolinin
PAVEL DOLININConverting to lowercase
The comma operators perform the reverse operation.
name="PAVEL DOLININ"
echo "${name,}"
echo "${name,,}"Output:
pAVEL DOLININ
pavel dolininA single comma converts the first matching character at the beginning; two commas convert all matching uppercase characters.
These operations are useful for normalizing values, preparing labels, and handling case-insensitive input without invoking external commands.
10. Extracting a Substring
Sometimes you need only part of a variable's value. Bash supports substring extraction with ${variable:position:length}.
The position is zero-based, meaning that the first character is at position 0.
text="Hello, world!"
echo "${text:0:5}"
echo "${text:7:5}"Output:
Hello
worldThe first expression starts at position 0 and extracts five characters. The second starts at position 7 and extracts another five.
If you omit the length, Bash returns everything from the specified position to the end:
echo "${text:7}"Output:
world!You can also use a negative offset to count backward from the end of the string:
filename="backup.tar.gz"
echo "${filename: -2}"Output:
gzNotice the space before -2. It helps Bash distinguish a negative offset from the :- default-value operator.
Substring extraction is handy when you know the structure of a value and need to retrieve a particular portion of it. For more complicated text-processing tasks, however, a dedicated parser or tool may be easier to maintain.
11. Replacing Text Inside a Variable
Bash can replace matching patterns without creating a new process.
Suppose you have a filename containing spaces and want to replace them with underscores:
filename="monthly report final.log"
echo "${filename/ /_}"Output:
monthly_report final.logThe single-slash form replaces only the first match.
To replace every matching occurrence, use a double slash:
echo "${filename// /_}"Output:
monthly_report_final.logYou can also remove the matching text by omitting the replacement:
echo "${filename// /}"Output:
monthlyreportfinal.logThe pattern can be more than a literal space:
text="one-two-three"
echo "${text//-/_}"Output:
one_two_threeReplacing a prefix or suffix
Sometimes the match should occur only at the beginning or end of a value.
Use /# to match at the beginning:
url="https://example.com"
echo "${url/#https/http}"Output:
http://example.comUse /% to match at the end:
filename="report.txt"
echo "${filename/%.txt/.log}"Output:
report.logThe replacement operators are:
| Expression | Behavior |
|---|---|
${var/pattern/replacement} | Replace the first match |
${var//pattern/replacement} | Replace all matches |
${var/pattern} | Remove the first match |
${var/#pattern/replacement} | Replace a matching prefix |
${var/%pattern/replacement} | Replace a matching suffix |
As with prefix and suffix removal, these patterns use shell wildcard syntax rather than regular expressions.
One important detail: parameter expansion produces a new value for the expression. It does not change the original variable automatically.
text="one-two"
echo "${text//-/_}"
echo "$text"The first command prints one_two; the second still prints one-two.
To save the result, use an assignment:
text="${text//-/_}"12. Indirect Variable Expansion
This is one of the more unusual features in Bash, but it can be extremely useful.
Ordinarily, a variable contains a value:
greeting="Hello, world!"
echo "$greeting"Now imagine that you have another variable whose value is the name of the variable you want to access.
greeting="Hello, world!"
variable_name="greeting"
echo "$variable_name"Output:
greetingBash prints the contents of variable_name, which is the text greeting. It does not automatically follow that name to the other variable.
To retrieve the value of the variable named by variable_name, use indirect expansion:
echo "${!variable_name}"Output:
Hello, world!The exclamation mark tells Bash to use the value of variable_name as another variable's name and expand that variable.
This is sometimes called indirect variable expansion.
It can be useful when variable names follow a predictable pattern, such as configuration values for different environments:
URL_development="http://localhost:8080"
URL_production="https://example.com"
environment="production"
variable_name="URL_${environment}"
echo "${!variable_name}"Output:
https://example.comThis works, but indirect expansion can make scripts harder to understand if used excessively. For collections of related values, associative arrays are often a clearer alternative.
13. Finding Variable Names with a Prefix
Bash also lets you retrieve the names of variables that begin with a particular prefix.
The syntax is ${!prefix*} or ${!prefix@}.
For example:
APP_NAME="My Application"
APP_ENV="production"
APP_PORT="8080"
DB_HOST="localhost"
echo "${!APP_*}"This expands to the names of variables beginning with APP_, separated according to Bash's normal expansion rules.
The distinction between * and @ becomes more significant when these expressions are quoted and used with multiple matching names. For everyday scripts, though, the main idea is that you can discover variable names by prefix.
This feature can help with debugging or inspecting groups of related configuration variables. Be aware that the order of variable names is not guaranteed.
14. Environment Variables and Exporting Values
So far, we have been working with shell variables. There is another important concept: environment variables.
A variable created in Bash is normally available to the current shell, but it is not automatically passed to programs that the shell starts.
For example:
APP_ENV="production"
bash -c 'echo "${APP_ENV-unset}"'The child Bash process normally prints unset, because APP_ENV was not exported to its environment.
To make a variable available to child processes, use export:
export APP_ENV="production"
bash -c 'echo "$APP_ENV"'Output:
productionYou can also assign the value first and export it separately:
APP_ENV="production"
export APP_ENVThis distinction is particularly important when writing scripts that invoke other programs. A variable existing in your shell does not automatically mean that an external program can see it.
A common convention is to use uppercase names for environment variables and other externally meaningful configuration values:
export API_URL="https://example.com"
export LOG_LEVEL="info"Uppercase names are a convention, not a requirement. Bash itself has many special variables, so using lowercase names for your script's private variables can also help avoid accidental collisions.
15. Arrays: Storing Multiple Values
A regular Bash variable stores one value. If you need to keep a collection of related values, arrays are often a better solution than creating many similarly named variables.
An indexed array can be created like this:
servers=("web01" "web02" "database01")You can access individual elements by index:
echo "${servers[0]}"
echo "${servers[1]}"Output:
web01
web02Array indexes begin at zero.
To retrieve every element as a separate item, use "${servers[@]}":
for server in "${servers[@]}"
do
echo "Checking $server"
doneBash also supports associative arrays, which use string keys rather than numeric indexes:
declare -A ports=(
[http]=80
[https]=443
[ssh]=22
)
echo "${ports[https]}"Output:
443Associative arrays are useful when you want to look up a value by name, such as a service's port number or an environment's URL.
If you find yourself creating variables such as server1, server2, and server3, consider whether an array would make the code easier to manage.
16. A Few More Useful Variable Operations
Bash provides additional commands and expansions that are worth knowing.
Removing a variable
Use unset to remove a variable:
temporary_value="something"
unset temporary_valueAfterward, the variable is unset. Reading it without another assignment produces an empty expansion in ordinary cases, unless your script uses stricter handling such as set -u.
Making a variable read-only
Use declare -r or readonly to prevent subsequent assignments:
declare -r APP_NAME="My Application"Trying to change the variable afterward produces an error.
Read-only variables can help document configuration values that should not change during the script's execution.
Declaring an integer variable
Use declare -i to give a variable an integer attribute:
declare -i count=5
count=count+3
echo "$count"Output:
8Bash evaluates assignments to integer-attributed variables arithmetically. This does not turn Bash into a strongly typed language, but it can be convenient for counters and simple calculations.
For more information about declare, see the dedicated article in this Bash series.
Conclusion
Variables are much more than named containers for values. Bash's parameter expansion features let you supply defaults, validate required configuration, measure strings, remove prefixes and suffixes, change letter case, extract substrings, replace text, and even access variables indirectly.
The most useful forms to remember are:
| Syntax | Purpose |
|---|---|
$var or ${var} | Read a variable |
${var:-default} | Use a fallback if unset or empty |
${var:=default} | Assign a fallback if unset or empty |
${var:?message} | Report an error if unset or empty |
${var:+value} | Produce a value if set and non-empty |
${#var} | Get the string length |
${var#pattern} / ${var##pattern} | Remove a prefix |
${var%pattern} / ${var%%pattern} | Remove a suffix |
${var:position:length} | Extract a substring |
${var/pattern/replacement} | Replace a matching pattern |
${var//pattern/replacement} | Replace all matching patterns |
${var^^} / ${var,,} | Convert to uppercase or lowercase |
${!name} | Expand the variable whose name is stored in name |
You don't need to memorize the entire list today. Start with assignments, quoting, and ${var:-default}. As your scripts grow, learn the string-manipulation operators that solve the problems you encounter.
Bash provides plenty of tools for working with text, and variables are where that journey begins.