Freebie

Variables

Variables

User-defined variables

Define a variable with = and no spaces on either side. Reference it with $:

days=10
guest="Alice"
logfile="/var/log/app.log"

echo "$guest checked in $days days ago"
echo "Watching $logfile"

Environment variables

Run export to make a variable available to child processes. Scripts launched from your terminal can then read it:

export BACKUP_DIR="/mnt/backups"
./run_backup.sh    # script reads $BACKUP_DIR

Run set to display all global variables. Run printenv to display only exported environment variables.

Unset a variable

unset removes a variable from the current shell environment. After unsetting, any reference to the variable returns an empty string — unless you have set -u enabled, in which case bash treats it as an error:

tmpdir="/tmp/build"
echo "$tmpdir"    # /tmp/build

unset tmpdir
echo "$tmpdir"    # (empty)

Use unset to release a sensitive value after you no longer need it, or to ensure a script does not inherit a variable that a parent process may have set:

unset DB_PASSWORD          # clear credential after use
unset -f my_function       # remove a function definition

Pass -v to remove a variable explicitly (the default), or -f to remove a function.

Local variables in functions

Variables defined inside a function are global by default. Declare them with local to restrict their scope:

function report {
    local count=0
    count=$(grep -c "ERROR" "$1")
    echo "Found $count errors in $1"
}

Parameter expansion

Parameter expansion is the mechanism bash uses to read, modify, and substitute variable values. The ${} syntax tells bash to expand the parameter named inside the braces.

Expansion syntax

You can print a variable name with either ${VAR} or $VAR. Prefer the ${VAR} syntax because it makes code less prone to interpretation.

Braces are optional for simple variable references, but required when the variable name is immediately followed by characters that would otherwise be parsed as part of the name:

NAME="world"
echo "$NAME"          # world
echo "${NAME}wide"    # worldwide  — braces delimit the variable name
echo "$NAMEwide"      # empty — bash looks for NAMEwide, which is unset

Default and assignment values

Use these forms to handle unset or empty variables without if blocks:

SyntaxBehavior
${VAR:-default}Returns default if VAR is unset or empty
${VAR:=default}Sets VAR to default and returns it if unset or empty
${VAR:?message}Exits with message as the error if VAR is unset or empty
${VAR:+value}Returns value if VAR is set and non-empty; otherwise returns empty
LOGDIR=${LOG_DIR:-/var/log}                     # fall back to /var/log
: ${TMPDIR:=/tmp}                               # set TMPDIR if not already set
echo "${CONFIG:?Config file is required}"       # exit with error if CONFIG is empty
EXTRA=${DEBUG:+--verbose}                       # add --verbose only if DEBUG is set

String length

${#VAR} returns the number of characters in the value:

FILE="report.pdf"
echo ${#FILE}    # 10

Substrings

${VAR:offset:length} extracts a substring starting at offset (zero-indexed). Omit length to extract to the end of the string:

DATE="2024-03-15"
echo ${DATE:0:4}    # 2024
echo ${DATE:5:2}    # 03
echo ${DATE:8}      # 15

Pattern removal

Use these to strip prefixes or suffixes without calling sed or awk:

SyntaxBehavior
${VAR#pattern}Removes the shortest match of pattern from the left
${VAR##pattern}Removes the longest match of pattern from the left
${VAR%pattern}Removes the shortest match of pattern from the right
${VAR%%pattern}Removes the longest match of pattern from the right
FILE="/var/log/app.log"
echo ${FILE##*/}      # app.log   — strip everything up to the last /
echo ${FILE%/*}       # /var/log  — strip everything after the last /
echo ${FILE%.log}     # /var/log/app  — strip the .log suffix

Substitution

${VAR/old/new} replaces the first match of old with new. Use // to replace all matches:

PATH_STR="/usr/local/bin:/usr/bin:/bin"
echo ${PATH_STR/bin/BIN}     # /usr/local/BIN:/usr/bin:/bin  — first match only
echo ${PATH_STR//bin/BIN}    # /usr/local/BIN:/usr/BIN:/BIN  — all matches

Case conversion (Bash 4+)

SyntaxEffect
${VAR^}Uppercase the first character
${VAR^^}Uppercase all characters
${VAR,}Lowercase the first character
${VAR,,}Lowercase all characters
NAME="alice"
echo ${NAME^}     # Alice
echo ${NAME^^}    # ALICE

Text manipulation

Three built-in expansion features let you manipulate string values without calling external tools:

Globbing lets you match multiple filenames with wildcard patterns in your script.

Parameter expansion lets you extract substrings, apply default values, and transform variable content. For example, ${VAR#pattern} removes the shortest match of pattern from the left, and ${VAR%pattern} removes from the right.

String slicing lets you remove or replace substrings:

STRING="user|admin|root"
FIRST=${STRING%%|*}        # removes everything to the right of the first |: "user"
REST=${STRING#*|}          # removes the first field and |: "admin|root"
CLEAN=${STRING//|/,}       # replaces all | with ,: "user,admin,root"

Case conversion

Convert a string between upper and lowercase with tr:

upper=$(echo "$var" | tr '[a-z]' '[A-Z]')
lower=$(echo "$var" | tr '[A-Z]' '[a-z]')

The typeset command enforces case at assignment time. The value is always stored in the specified case regardless of what you assign:

typeset -u HOSTNAME_UPPER
HOSTNAME_UPPER="web-01"
echo $HOSTNAME_UPPER    # WEB-01

typeset -l env_name
env_name="PRODUCTION"
echo $env_name          # production

Arrays

Bash arrays store multiple values indexed by number:

SERVERS=("web-01" "web-02" "db-01" "cache-01")

echo ${SERVERS[0]}          # web-01 (first element)
echo ${#SERVERS[@]}         # 4 (number of elements)
echo ${SERVERS[@]}          # web-01 web-02 db-01 cache-01 (all elements, separate words)
echo ${SERVERS[*]}          # web-01 web-02 db-01 cache-01 (all elements, single string)

for server in "${SERVERS[@]}"; do
    ssh "$server" uptime
done

Delete array elements

Use unset to remove a single element by index or to delete the entire array:

SERVERS=("web-01" "web-02" "db-01" "cache-01")

unset SERVERS[1]            # remove web-02
echo ${SERVERS[@]}          # web-01 db-01 cache-01

unset SERVERS               # delete the entire array
echo ${SERVERS[@]}          # (empty)

Removing an element leaves a gap in the index — the remaining elements do not shift. If your code iterates by index, account for the missing slot. Iterating with "${SERVERS[@]}" skips empty slots automatically.

To remove an element and repack the array into contiguous indices, reassign it:

SERVERS=("${SERVERS[@]}")

Reassign element values

Assign a new value to an element by referencing its index directly:

SERVERS=("web-01" "web-02" "db-01" "cache-01")

SERVERS[1]="web-99"
echo ${SERVERS[@]}    # web-01 web-99 db-01 cache-01

Assign to an index beyond the current length to append an element:

SERVERS[4]="backup-01"
echo ${SERVERS[@]}    # web-01 web-99 db-01 cache-01 backup-01

To append without tracking the length, use +=:

SERVERS+=("monitor-01")
echo ${SERVERS[@]}    # web-01 web-99 db-01 cache-01 backup-01 monitor-01

Associative arrays (Bash 4.0+)

Associative arrays store values indexed by string keys:

declare -A PORTS
PORTS["http"]=80
PORTS["https"]=443
PORTS["ssh"]=22

echo ${PORTS["https"]}          # 443

for service in "${!PORTS[@]}"; do   # ${!array[@]} gets the keys
    echo "$service: ${PORTS[$service]}"
done

Reading a file into an array

Read a file line by line into an array with readarray. The -t flag strips the trailing newline from each element:

readarray -t LINES < /etc/hosts

for line in "${LINES[@]}"; do
    echo "$line"
done

Math

Integer arithmetic

The let command evaluates an arithmetic expression:

let SUM=$1+$2
let RESULT=COUNT*2

Double parentheses (( )) provide arithmetic evaluation without needing $ to reference variables:

(( result = COUNT * i / maxcount ))
echo $result

Arithmetic expansion $(( )) evaluates an expression and substitutes the result:

echo $(( $1 + $2 ))
echo $(( 2 ** 10 ))    # 1024

Declare a variable as an integer with declare -i to prevent non-integer assignments:

declare -i total
total=0
total+=5

Declare local integers inside functions:

function calculate {
    local -i result
    result=$(( $1 * $2 ))
    echo $result
}

Arithmetic with expr

expr evaluates an expression and prints the result to stdout. It predates $(( )) and let, but you still encounter it in older scripts and portable POSIX sh code. Capture its output with command substitution:

result=$(expr 5 + 3)
echo $result    # 8

expr supports the following operators:

OperatorOperation
+Addition
-Subtraction
\*Multiplication (must escape *)
/Integer division
%Modulo

Every token — numbers, operators, and variables — must be a separate argument separated by spaces. Operators that are also shell metacharacters (*, (, )) must be escaped or quoted:

expr 10 + 2          # 12
expr 10 - 2          # 8
expr 10 \* 2         # 20
expr 10 / 3          # 3 (truncates)
expr 10 % 3          # 1

count=7
expr $count + 1      # 8

expr also returns an exit status: 0 if the result is non-zero, 1 if the result is zero or the expression is invalid. This makes it useful in older scripts as a counter in a while loop:

i=1
while [ $i -le 5 ]; do
    echo "iteration $i"
    i=$(expr $i + 1)
done

Prefer $(( )) in new scripts

$(( )) is faster than expr (no subprocess), handles operator precedence, and does not require escaping *.

Floating-point arithmetic with bc

Bash integer arithmetic truncates decimals. For floating-point calculations, pipe to bc:

result=$(echo "scale=4; 3.14159 * 2.5 * 2.5" | bc)
echo "Area: $result"

The scale variable sets the number of decimal places. Run bc interactively:

bc -q        # quiet mode, no copyright notice
3.44 / 5
0
scale=4
3.44 / 5
.6880
quit

A real-world example: calculate the percentage of disk space used:

USED=$(df / | awk 'NR==2 {print $3}')
TOTAL=$(df / | awk 'NR==2 {print $2}')
PCT=$(echo "scale=1; $USED / $TOTAL * 100" | bc)
echo "Disk usage: ${PCT}%"