Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Bash variables and parameters

By updated

Improve this page

Bash variables have no types. A variable is a string of characters, and whether it is read as a number, an array or a path depends on the context where you use it. Understanding this simple but powerful model lets you use parameter expansion, integer arithmetic and arrays to write concise and robust scripts.

What you will learn

  • Declare variables and read the special variables $?, $#, $@, $0, $$
  • Compute with $(( )) without calling bc or expr
  • Apply default values with ${var:-value} for configurable scripts
  • Extract and transform strings without awk or sed
  • Create indexed and associative arrays to group data
  • Control the scope of a variable with export and local

When is it useful?

Variables and their expansions show up in almost every intermediate-level script:

  • read the configuration from environment variables with a default value when they are not set (${TIMEOUT:-30})
  • extract the file name or the extension of a path without calling basename
  • count the arguments passed to a script and print a usage message when $# -lt 2
  • store the list of servers to process in an array and loop over it
  • check the exit code of the last command with $?

The RHCSA objective "Create simple shell scripts" (EX200, RHEL 10) names two points this page covers directly: process the script's arguments ($1, $2) and use the output of commands ($( )). Arithmetic and arrays are not listed: they serve your scripts, not the exam.

Simple variables

A variable is declared without spaces around the =. The name starts with a letter or an underscore, and the value goes in quotes as soon as it contains a space:

Terminal window
name="Alice"
age=30
path="/var/log/nginx"
Terminal window
echo "Name: $name, Age: $age"
# Name: Alice, Age: 30

Double quotes let variables expand. Single quotes turn expansion off, and the text stays literal:

Terminal window
echo "$name" # Alice
echo '$name' # $name (literal)

Command substitution

$() runs a command and captures its output, which you can store in a variable:

Terminal window
iso_date=$(date '+%Y-%m-%d')
current_user=$(whoami)
nb_files=$(ls /etc | wc -l)
echo "Date: $iso_date, User: $current_user, Files in /etc: $nb_files"
# Date: 2026-04-10, User: bob, Files in /etc: 222

Special variables

These special parameters are set automatically by Bash, and none of them can be assigned with =: 0=x or 1=x answer command not found. Only the positional arguments can be replaced, all at once, with set -- value1 value2. Read the table line by line, with the example of a script called with two arguments.

VariableMeaningExample
$0Script namedemo-variables.sh
$1 … $9Positional arguments$1 = first argument
$#Number of arguments2 when two args are passed
$@All the arguments (list)"one" "two"
$*All the arguments (one string)"one two"
$?Exit code of the last program0 = success
$$PID of the current process12345
$!PID of the last background process12346
Terminal window
# Called as: ./script.sh one two
echo "Script : $0" # Script : ./script.sh
echo "First : $1" # First : one
echo "Nb args: $#" # Nb args: 2
echo "All : $@" # All : one two
ls /nonexistent 2>/dev/null
echo "Exit code: $?" # Exit code: 2

$@ vs $*

The difference only shows up inside double quotes. set -- replaces the positional arguments of the current shell, which simulates a script called with "with space" and arg2:

Terminal window
set -- "with space" "arg2"
for a in "$@"; do echo "→ $a"; done
# → with space
# → arg2
for a in "$*"; do echo "→ $a"; done
# → with space arg2 (merged into a single argument)

Always use "$@" to pass or walk the arguments: each argument stays a distinct word, internal spaces included. "$*" merges all the arguments into a single string, separated by a space, and the loop runs only once.

Integer arithmetic

$(( expression )) evaluates an arithmetic expression on integers, without calling expr or any external process:

Terminal window
a=10; b=3
echo "$((a + b))" # 13
echo "$((a - b))" # 7
echo "$((a * b))" # 30
echo "$((a / b))" # 3 (integer division)
echo "$((a % b))" # 1 (modulo)
echo "$((a ** b))" # 1000 (power)

To increment a variable, prefer an explicit assignment: its exit code is always 0, which makes it compatible with set -e, the mode that stops the script at the first failing command:

Terminal window
counter=0
counter=$((counter + 1))
counter=$((counter + 5))
echo "$counter" # 6

Bash only handles integers. For a decimal result, you hand over to bc or awk. bc is not always installed: it is missing from the debian:13 and almalinux:10 images measured, whereas awk is present everywhere.

Terminal window
echo "scale=2; 10 / 3" | bc # 3.33, when bc is installed
awk 'BEGIN { printf "%.2f\n", 10 / 3 }' # 3.33, with awk

Parameter expansion

Parameter expansion ${ } offers much more than plain substitution: default value, path slicing, case change, replacement, all without starting a process.

Default values

Terminal window
# Use the value when set, otherwise the default
TIMEOUT=${TIMEOUT:-30}
PREFIX=${PREFIX:-"/usr/local"}
echo "Timeout: $TIMEOUT" # Timeout: 30 (when TIMEOUT is not set)
echo "Prefix : $PREFIX" # Prefix : /usr/local

The four forms differ by a single character. The table below is the one to keep at hand: :- and := provide a fallback, :+ an alternative, :? a guard that stops the script.

SyntaxBehaviour
${var:-default}Returns default when var is empty or unset
${var:=default}Assigns and returns default when var is empty or unset
${var:+value}Returns value when var is set and not empty
${var:?message}Error with message when var is empty or unset
# Practical example: make a script configurable through the environment
#!/bin/bash
set -euo pipefail
DEST=${DEST:-"/tmp/backup"}
KEEP=${KEEP:-7}
LOG=${LOG:-"/var/log/backup.log"}
echo "Backup to $DEST, keep $KEEP days, log $LOG"

Called without variables, the script uses the defaults. Called with DEST=/mnt/nas ./script.sh, it uses /mnt/nas: the variable is set in the environment of that single command, without changing anything in your session.

String manipulation

Without calling sed or awk, Bash can transform strings directly. The results below were checked on Bash 5.2, with an nginx log path:

Terminal window
path="/var/log/nginx/access.log"
echo "${#path}" # 25 (length)
echo "${path^^}" # /VAR/LOG/NGINX/ACCESS.LOG (upper case)
echo "${path,,}" # /var/log/nginx/access.log (lower case)
echo "${path##*/}" # access.log (everything after the last /)
echo "${path%/*}" # /var/log/nginx (everything before the last /)
echo "${path##*.}" # log (extension)
echo "${path%.log}.bak" # /var/log/nginx/access.bak (replaces the extension)
echo "${path/nginx/apache}" # /var/log/apache/access.log (first occurrence)

The same syntax, summed up. Remember the rule of the doubled sign: ## and %% remove the longest match, # and % the shortest; # works from the start, % from the end.

SyntaxResult
${#var}Length of the string
${var^^}All upper case
${var,,}All lower case
${var##pattern}Removes the longest matching prefix
${var%%pattern}Removes the longest matching suffix
${var#pattern}Removes the shortest prefix
${var%pattern}Removes the shortest suffix
${var/pattern/repl}Replaces the first occurrence
${var//pattern/repl}Replaces every occurrence

Indexed arrays

An indexed array stores several values reachable by an integer index, which starts at 0:

Terminal window
servers=("web01" "web02" "db01" "cache01")
echo "${servers[0]}" # web01
echo "${servers[-1]}" # cache01 (last item)
echo "${#servers[@]}" # 4 (number of items)
echo "${servers[@]}" # web01 web02 db01 cache01
echo "${servers[@]:1:2}" # web02 db01 (slice: 2 items from index 1)

To walk all the items, loop over "${servers[@]}", quotes included, so that each value stays whole:

Terminal window
for s in "${servers[@]}"; do
echo " → $s"
done
# → web01
# → web02
# → db01
# → cache01

To add an item at the end of the array, use += with parentheses:

Terminal window
servers+=("monitor01")
echo "${servers[@]}" # web01 web02 db01 cache01 monitor01

Associative arrays

Associative arrays (declare -A) use text keys, the equivalent of a dictionary. The iteration order of the keys is not the insertion order: Bash 5.2 returns ssh https mysql http here, and nothing guarantees that order from one version to the next.

Terminal window
declare -A ports
ports["ssh"]=22
ports["http"]=80
ports["https"]=443
ports["mysql"]=3306
echo "${ports[ssh]}" # 22
echo "${!ports[@]}" # All keys: ssh https mysql http
echo "${ports[@]}" # All values: 22 443 3306 80

To walk keys and values together, loop over the keys and read each value with ${ports[$service]}:

Terminal window
for service in "${!ports[@]}"; do
echo " $service → ${ports[$service]}"
done
# ssh → 22
# https → 443
# mysql → 3306
# http → 80

Export and scope

export, make a variable visible to child processes

export puts a variable into the environment passed to every child process: the programs your script starts can then read it.

Terminal window
LANG=en_US.UTF-8
export LANG
# Same thing in one line:
export LANG=en_US.UTF-8

A non-exported variable is not passed to subprocesses, including a bash -c started from the script:

Terminal window
MY_VAR="hello"
bash -c 'echo $MY_VAR' # (empty: not passed on)
export MY_VAR
bash -c 'echo $MY_VAR' # hello

local, limit the scope to a function

Without local, any assignment inside a function changes the global variable, with side effects that are hard to follow:

Terminal window
GLOBAL="initial"
my_function() {
local LOCAL="I am local"
GLOBAL="changed in the function"
echo "In the function: $LOCAL / $GLOBAL"
}
my_function
echo "After: $GLOBAL" # changed in the function
# echo $LOCAL # bash: LOCAL: unbound variable (with set -u)

Rule: in every function, declare the internal variables with local to avoid side effects.

Troubleshooting

Most variable errors can be recognised by their message, captured here on Bash 5.2 (Debian 13 and AlmaLinux 10). Two causes come back all the time: a space around the = and a script launched with sh instead of Bash.

SymptomLikely causeSolution
name: command not found and empty variableSpace around the =var=value without spaces
: unbound variable under set -uUninitialised variableInitialise it or use ${var:-} as an empty default
An argument containing a space arrives split in two$@ used without quotesAlways write "$@"
(( expr )) stops the script under set -eArithmetic result = 0Use var=$((var + 1))
declare: not found (dash) or declare: -A: invalid option (Bash 3)Script launched with sh, or Bash older than 4.0Run with bash or ./script.sh, check bash --version

Key points

  • $? is the exit code of the last program, to read immediately: the next command overwrites it.
  • "$@" passes the arguments correctly, spaces preserved; "$*" merges them.
  • ${var:-default} makes scripts configurable without an if.
  • ${path##*/} extracts the file name; ${path%/*} extracts the directory, without calling basename/dirname.
  • declare -A creates an associative array, available only with Bash 4+.
  • Always declare the internal variables of a function with local.

Knowledge check

Check that the essentials of this guide stuck. The questions cover only what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn