Bash — Variables and Parameter Expansion

Learn how Bash variables work, from basic assignments and default values to advanced parameter expansion. Discover how to manipulate strings, extract substrings, replace text, change letter case, and use variables more effectively in your scripts.

Bash — Variables and Parameter Expansion

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:

Pavel

You 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:

BashScript

The 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 $filename

Bash 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:

development

If the variable contains a value, Bash uses that value instead:

environment="production"

echo "${environment:-development}"

Output:

production

Notice 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:

ExpressionUses 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:

development

The 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:

ExpressionAssigns 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 configured

If environment is unset or empty, the expression produces an empty string.

As before, the colon matters:

  • ${variable+value} produces value if the variable is set, even if it is empty.
  • ${variable:+value} produces value only 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:

13

The 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."
fi

The 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 gamma

Then $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.gz

The 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:

gz

Because *. 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.tar

The 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:

backup

This time, Bash removes everything from the first dot onward.

Here's a quick summary:

ExpressionWhat 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:

Suserlongexample

The 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:

SuSeRlongexample

Without an explicit pattern, ^ and ^^ operate on lowercase characters.

name="pavel dolinin"

echo "${name^}"
echo "${name^^}"

Output:

Pavel dolinin
PAVEL DOLININ

Converting to lowercase

The comma operators perform the reverse operation.

name="PAVEL DOLININ"

echo "${name,}"
echo "${name,,}"

Output:

pAVEL DOLININ
pavel dolinin

A 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
world

The 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:

gz

Notice 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.log

The single-slash form replaces only the first match.

To replace every matching occurrence, use a double slash:

echo "${filename// /_}"

Output:

monthly_report_final.log

You can also remove the matching text by omitting the replacement:

echo "${filename// /}"

Output:

monthlyreportfinal.log

The pattern can be more than a literal space:

text="one-two-three"

echo "${text//-/_}"

Output:

one_two_three

Replacing 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.com

Use /% to match at the end:

filename="report.txt"

echo "${filename/%.txt/.log}"

Output:

report.log

The replacement operators are:

ExpressionBehavior
${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:

greeting

Bash 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.com

This 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:

production

You can also assign the value first and export it separately:

APP_ENV="production"
export APP_ENV

This 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
web02

Array indexes begin at zero.

To retrieve every element as a separate item, use "${servers[@]}":

for server in "${servers[@]}"
do
    echo "Checking $server"
done

Bash 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:

443

Associative 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_value

Afterward, 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:

8

Bash 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:

SyntaxPurpose
$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.

Read next

Bash — The for Loop

Learn how Bash’s for loop automates repetitive tasks, from counting through numbers to processing files, arrays, and command-line arguments. Explore both loop styles, practical examples, and common pitfalls.

Bash — The declare Built-in

Learn how Bash’s declare built-in lets you control variable attributes, create arrays, define read-only or integer variables, export values, and inspect how Bash treats your variables.