Variables and Data Types
📦 Topic 2 — Variables and Data Types
Variables are how a script remembers information. In this chapter you will learn how to create and use your own variables, how bash handles different kinds of data, and how to work with the special variables that bash sets up for you automatically — including the ones that tell you about command-line arguments and the exit status of commands.
1 — Creating and Using Variables
In bash you create a variable simply by assigning a value to a name. There is no var keyword, no type declaration — just a name, an equals sign, and a value.
#!/bin/bash
# Assign values — NO spaces around the = sign
name="Philip"
city="London"
age=32
# Use a variable by prefixing its name with $
echo "Hello, my name is $name."
Hello, my name is Philip.
echo "I live in $city and I am $age years old."
I live in London and I am 32 years old.
name="Philip" assigns a variable. name = "Philip" tries to run a command called name with arguments = and "Philip" — and fails with "command not found".
Curly Braces: ${variable}
You can wrap a variable name in curly braces — ${name}. This is optional in most cases, but becomes required when the variable name is immediately followed by other characters that could be confused with part of the name.
fruit="apple"
# Without braces — bash reads this as variable 'fruitpie' (undefined)
echo "I want $fruitpie"
I want
# With braces — bash correctly reads 'fruit' and appends 'pie'
echo "I want ${fruit}pie"
I want applepie
# Also useful for clarity in complex strings
echo "${fruit}s are tasty"
apples are tasty
Variable Naming Rules
- Names may contain letters, digits, and underscores
- Names must not start with a digit
- Names are case-sensitive —
Name,name, andNAMEare three different variables - By convention, lowercase for your own variables; UPPERCASE reserved for environment variables and constants
username="philip"
file_count=10
_internal="ok"
MAX_RETRIES=3
myVar2="test"
2fast="no" # starts with digit
my-var="no" # hyphens not allowed
my var="no" # spaces not allowed
$price="no" # $ is for reading, not naming
2 — Quotes and Variable Expansion
How you quote a value determines whether bash expands variables inside it. This is one of the most important distinctions to understand in bash.
name="World"
# Double quotes — variables ARE expanded
echo "Hello, $name!"
Hello, World!
# Single quotes — variables are NOT expanded (everything is literal)
echo 'Hello, $name!'
Hello, $name!
# No quotes — variables expand BUT word splitting applies (avoid for strings)
echo Hello, $name!
Hello, World!
# Danger with no quotes: spaces in variables cause word splitting
file="my document.txt"
ls $file # treated as two arguments: 'my' and 'document.txt'
ls "$file" # correct: treated as one argument
"$var" — unless you have a specific reason not to. It prevents word-splitting and glob expansion from causing unexpected behaviour.
Escaping Special Characters
# Inside double quotes, use \ to escape $ or " characters
price=5
echo "The cost is \$$price"
The cost is $5
# To include a literal double quote inside double quotes
echo "She said \"hello\""
She said "hello"
# To include a literal single quote inside single-quoted string — you can't.
# Instead, end the string, add an escaped quote, then resume:
echo 'it'\'"'"s a trap' # messy — double quotes are usually cleaner
3 — Data Types in Bash
Bash is a weakly typed language — all variables are stored as strings internally. However, bash can treat a variable as an integer when you use arithmetic operators, and you can use the declare built-in to give variables explicit attributes.
| Type / Attribute | How to create | Behaviour |
|---|---|---|
| String (default) | name="hello" | Everything is a string unless you specify otherwise. Arithmetic on an unset variable returns 0. |
| Integer | declare -i count=5 | Bash enforces integer-only values. Assigning a non-integer sets the variable to 0. Arithmetic is performed automatically. |
| Read-only | readonly MAX=100 or declare -r MAX=100 | Value cannot be changed after declaration. Attempting to reassign produces an error. |
| Exported (env var) | export VAR="value" or declare -x VAR | Variable is passed to child processes (subshells, scripts called from this script). |
| Array | declare -a items | Indexed array. Covered in Topic 8. |
| Associative array | declare -A map | Key-value map. Also covered in Topic 8. |
# Integer variable — arithmetic assigned directly
declare -i count=10
count+=5
echo "$count"
15
# Read-only — cannot be reassigned
readonly MAX_SIZE=100
MAX_SIZE=200
bash: MAX_SIZE: readonly variable
# Check variable attributes with declare -p
declare -p count
declare -i count="15"
declare -p MAX_SIZE
declare -r MAX_SIZE="100"
4 — Environment Variables
Environment variables are variables that are passed down from a parent process to its child processes. Your shell session already has dozens of them set before you run a single script — they describe the system, your user account, and your preferences.
| Variable | Contains | Example value |
|---|---|---|
$HOME | Your home directory | /home/philip |
$USER | Your username | philip |
$PATH | Colon-separated list of directories searched for executables | /usr/local/bin:/usr/bin:/bin |
$PWD | Current working directory | /home/philip/scripts |
$OLDPWD | Previous working directory | /home/philip |
$SHELL | Path to the current shell | /bin/bash |
$HOSTNAME | Machine hostname | raspberrypi |
$LANG | Language / locale setting | en_GB.UTF-8 |
$EDITOR | Default text editor | nano |
$TERM | Terminal type | xterm-256color |
# A variable created normally is LOCAL — not visible to child processes
greeting="hello"
bash -c 'echo $greeting'
# empty — child process can't see it
# export makes it available to child processes
export greeting="hello"
bash -c 'echo $greeting'
hello
# List all current environment variables
env
# or just the ones matching a pattern
env | grep "HOME"
HOME=/home/philip
export inside a script only affect that script and its children — never the parent shell that launched the script.5 — Special Variables
Bash automatically populates a set of read-only variables that give you information about the script itself, its arguments, and the outcome of the last command. These are among the most useful variables in shell scripting.
Positional Parameters — Script Arguments
| Variable | Contains |
|---|---|
$0 | The name of the script itself (as it was called) |
$1 … $9 | The 1st through 9th arguments passed to the script |
${10} … ${N} | Arguments beyond the 9th (must use curly braces) |
$# | The total number of arguments passed |
$@ | All arguments as separate quoted strings — "$1" "$2" "$3" … |
$* | All arguments as a single string — usually less useful than $@ |
#!/bin/bash
# args_demo.sh
echo "Script name : $0"
echo "First arg : $1"
echo "Second arg : $2"
echo "All args : $@"
echo "Arg count : $#"
# Running the script:
./args_demo.sh hello world
Script name : ./args_demo.sh
First arg : hello
Second arg : world
All args : hello world
Arg count : 2
Process and Status Variables
| Variable | Contains |
|---|---|
$? | Exit status of the last command (0 = success, non-zero = failure) |
$$ | PID (Process ID) of the current script |
$! | PID of the last background command (started with &) |
$- | Current shell option flags |
# $? holds the exit status of the LAST command that ran
ls /home
echo "Exit status: $?"
Exit status: 0 # 0 means success
ls /nonexistent_directory
ls: cannot access '/nonexistent_directory': No such file or directory
echo "Exit status: $?"
Exit status: 2 # non-zero means failure
# Practical use: check if the last command succeeded
cp file.txt backup.txt
if [ "$?" -eq 0 ]; then
echo "Backup created successfully."
else
echo "Backup FAILED."
fi
$? immediately after the command — if you run any other command first (even echo), $? will reflect that command's status instead."$@" expands to "$1" "$2" "$3" (each argument properly quoted separately). "$*" expands to "$1 $2 $3" (all arguments joined into one string). Use "$@" when passing arguments to another command — it preserves arguments that contain spaces.
6 — Command Substitution
Command substitution lets you capture the output of a command and use it as a value — either assigning it to a variable or inserting it directly into a string.
# Modern syntax (recommended): $( )
today=$(date +%Y-%m-%d)
echo "Today is $today"
Today is 2026-06-09
# Can be used directly inside a string
echo "You are logged in as $(whoami) on $(hostname)"
You are logged in as philip on raspberrypi
# Can be nested
parent_dir=$(dirname $(pwd))
echo "Parent: $parent_dir"
# Old backtick syntax — still works but harder to nest and read
files=`ls -1 | wc -l`
echo "Files in directory: $files"
$( ) rather than backticks for new code. Backtick syntax is harder to read and cannot be nested without escaping.7 — Default Values and Unsetting Variables
Bash provides a compact syntax for supplying default values when a variable is not set or is empty. This is far cleaner than writing an if check every time.
# ${var:-default} — use default if var is unset or empty
colour=""
echo "Colour: ${colour:-blue}"
Colour: blue
# Note: colour is still empty after this — the default is only used in the expansion
# ${var:=default} — use default AND assign it to var if unset or empty
echo "Colour: ${colour:=blue}"
Colour: blue
echo "$colour"
blue
# Now colour has been set to "blue"
# ${var:?message} — print an error message and exit if var is unset
required=""
echo "${required:?Error: required variable is not set}"
bash: required: Error: required variable is not set
# ${var:+replacement} — use replacement only if var IS set
debug="true"
echo "${debug:+[DEBUG MODE ON]}"
[DEBUG MODE ON]
temp="temporary value"
echo "$temp"
temporary value
# Remove the variable entirely
unset temp
echo "'$temp'"
''
# $temp is now completely unset (not just empty)
# Check if a variable is set
if [ -z "${temp+x}" ]; then
echo "temp is not set"
fi
8 — Quick Reference
| Syntax | What it does |
|---|---|
name="value" | Assign a variable (no spaces around =) |
$name / ${name} | Read a variable's value |
"$name" | Read variable with word-splitting protection (always prefer this) |
'$name' | Literal string — no expansion |
export name | Make variable available to child processes |
readonly name | Prevent variable from being reassigned |
unset name | Remove a variable entirely |
$(command) | Command substitution — capture output of a command |
${var:-default} | Use default if var is empty/unset |
${var:=default} | Use default AND assign it if var is empty/unset |
${var:?msg} | Exit with error message if var is empty/unset |
$0 | Script name |
$1 … $9 | Positional arguments |
$# | Number of arguments |
$@ | All arguments (individually quoted) |
$? | Exit status of last command |
$$ | PID of current script |
✏️ Exercises
Apply what you have learned in this chapter. Try each exercise yourself before looking at the sample solution.
profile.sh that stores your name, age, and favourite programming language in variables, then prints a short bio using those variables. All three pieces of data should appear in a single echo statement.#!/bin/bash
# profile.sh
name="Philip"
age=32
language="Python"
echo "My name is $name, I am $age years old, and my favourite language is $language."
args_info.sh that prints: the script's own name, how many arguments were passed, the first and second argument (or the text "not provided" if they were not given), and all arguments on one line.$0, $#, ${1:-not provided}, ${2:-not provided}, and $@. Test it by running it with no arguments, one argument, and two arguments.#!/bin/bash
# args_info.sh
echo "Script name : $0"
echo "Arg count : $#"
echo "First arg : ${1:-not provided}"
echo "Second arg : ${2:-not provided}"
echo "All args : $@"
cmd_check.sh that runs the command ls /tmp and then ls /nonexistent, printing the exit status after each one with a human-readable label ("Success" or "Failed").$? immediately after each command. You can use if [ "$status" -eq 0 ] to test it. Store the exit status in a variable first so it doesn't get overwritten.#!/bin/bash
# cmd_check.sh
ls /tmp > /dev/null 2&1
status=$?
if [ "$status" -eq 0 ]; then
echo "ls /tmp → Success (exit $status)"
else
echo "ls /tmp → Failed (exit $status)"
fi
ls /nonexistent > /dev/null 2&1
status=$?
if [ "$status" -eq 0 ]; then
echo "ls /nonexistent → Success (exit $status)"
else
echo "ls /nonexistent → Failed (exit $status)"
fi
> /dev/null 2>&1 silences the command's output so only your custom messages appear. Redirection is covered fully in Topic 3.
snapshot.sh that captures the current date, the current user, the current directory, and the number of files in the current directory into variables using command substitution, then prints a formatted summary. Run it from a couple of different directories to verify it works correctly each time.$(date +"%Y-%m-%d %H:%M"), $(whoami), $(pwd), and $(ls -1 | wc -l) to populate your variables.#!/bin/bash
# snapshot.sh — captures a point-in-time snapshot
timestamp=$(date +"%Y-%m-%d %H:%M")
current_user=$(whoami)
current_dir=$(pwd)
file_count=$(ls -1 | wc -l)
echo "────────────────────────────"
echo " Snapshot: $timestamp"
echo " User : $current_user"
echo " Dir : $current_dir"
echo " Files : $file_count"
echo "────────────────────────────"