Bash Variables, Expressions, and Statements

In the world of shell scripting, variables serve as named containers used to store strings or numeric values. They are the building blocks of any dynamic script, allowing you to manage data efficiently.

Naming and Assignment

Variable names in Bash must consist of letters, numbers, and underscores, and they cannot begin with a digit. A critical rule for beginners is the syntax of assignment: you must not include spaces around the equals sign.

name="Bash"
count=10
path="/usr/local/bin"

If you write name = "Bash", Bash will attempt to execute name as a command, leading to an error.

Referencing Variables

When accessing a variable's value, it is a best practice to wrap it in double quotes, such as "$var" or "${var}". This prevents the shell from performing unintended word splitting or globbing (pattern expansion) if the variable contains spaces or special characters.

echo "Hello, $name"
echo "The length of the name is: ${#name}"

Special Variables

Bash provides several built-in special variables that offer metadata about the current execution environment:

  • $0: The name of the script or the shell itself.
  • $1, $2, etc.: Positional parameters (arguments passed to the script).
  • $#: The total number of positional parameters.
  • $?: The exit status of the most recently executed command (0 indicates success).
  • $$: The Process ID (PID) of the current shell.
  • $@ and $*: Represent all positional parameters.

Scope and Visibility

Variables can be managed based on their intended reach:

  • Environment Variables: Use export to make a variable available to child processes.
  • Read-only Variables: Use readonly to prevent a variable from being modified or unset.
  • Local Variables: Inside functions, use the local keyword to ensure the variable exists only within that function's scope, preventing accidental modification of global variables.
export APP_ENV="production"
readonly MAX_RETRY=3

demo_function() {
  local msg="Internal message"
  echo "$msg"
}

Mastering Parameter Expansion

Parameter expansion is one of Bash's most potent features. It allows you to manipulate strings and handle default values directly within the variable reference, eliminating the need to call external utilities like sed or awk.

Default Values and Error Handling

You can define fallback behaviors if a variable is unset or empty:

  • ${var:-default}: Returns default if var is unset or null, but does not assign it.
  • ${var:=default}: Returns default and assigns it to var if it was unset or null.
  • ${var:+alt}: Returns alt only if var is set and not null.
  • ${var:?message}: Outputs an error message and exits the script if var is unset or null.
user="${USER:-guest}"
echo "Current user: $user"

# This will exit the script if the first argument is missing
file_path="${1:?Error: Please provide a filename.}"

Substrings and Length

Bash allows you to extract specific portions of a string using offsets and lengths:

text="Bash Script"
echo "${#text}"        # Returns the length: 11
echo "${text:0:4}"     # Returns "Bash" (starting at index 0, length 4)
echo "${text: -6}"     # Returns "Script" (negative offset; note the space before the minus)

Pattern Matching and Replacement

You can strip prefixes or suffixes from a string using pattern matching:

  • ${file#*/}: Removes the shortest match of the pattern from the beginning.
  • ${file##*/}: Removes the longest match from the beginning.
  • ${file%.*}: Removes the shortest match of the pattern from the end.
  • ${file%%.*}: Removes the longest match from the end.
file="/tmp/demo.tar.gz"
echo "${file#*/}"      # tmp/demo.tar.gz
echo "${file##*/}"     # demo.tar.gz
echo "${file%%.*}"     # /tmp/demo

For string replacement:

  • ${str/pattern/replacement}: Replaces the first occurrence.
  • ${str//pattern/replacement}: Replaces all occurrences.
str="foo bar foo"
echo "${str/foo/baz}"  # baz bar foo
echo "${str//foo/baz}" # baz bar baz

Case Transformation and Indirect Referencing

Bash can also handle case conversion and indirect variable access:

word="bash"
echo "${word^}"        # Bash (capitalize first letter)
echo "${word^^}"       # BASH (capitalize all)
echo "${word,}"        # bash (lowercase first letter)
echo "${word,,}"       # bash (lowercase all)

# Indirect expansion
var_name="name"
name="Alice"
echo "${!var_name}"    # Alice

Arithmetic and Command Substitution

Arithmetic Expressions

Bash supports integer arithmetic through two primary methods:

  1. Arithmetic Expansion $((expression)): Used to evaluate an expression and return the result.
  2. Arithmetic Command ((expression)): Used to perform operations, often for control flow.

In an arithmetic context, the $ prefix for variables is optional.

a=5
b=3
sum=$((a + b))
((a++))      # Increment a
((b += 2))   # Add 2 to b
echo "$sum $a $b" # Output: 8 6 5

Note that ((...)) returns an exit status: it returns 0 (true) if the expression evaluates to a non-zero value, and 1 (false) if it evaluates to zero.

Command Substitution

Command substitution allows you to capture the output of a command and assign it to a variable or use it in a command. The modern syntax $(command) is highly preferred over the legacy backticks (`command`).

today=$(date +%F)
files=$(ls | wc -l)
echo "Today is $today, and there are $files files in this directory."

Tip: If you need to preserve newlines in the output, always wrap the substitution in double quotes: content="$(cat file.txt)".

Conditional Expressions and Testing

Bash provides several ways to perform tests, but the double bracket syntax [[ ... ]] is the modern standard. It is more robust than the older [ (test) command because it supports regular expressions and does not require as much quoting to prevent errors.

String and Numeric Tests

  • Strings: Use -z for empty strings, -n for non-empty strings, and == or != for equality/inequality.
  • Numbers: Use -eq (equal), -ne (not equal), -lt (less than), -le (less or equal), -gt (greater than), and -ge (greater or equal).

File Tests

Bash can inspect the filesystem directly:

  • -e: File exists.
  • -f: Is a regular file.
  • -d: Is a directory.
  • -r, -w, -x: File is readable, writable, or executable.
if [[ -n "$name" && "$name" =~ ^[A-Za-z]+$ ]]; then
  echo "Valid name provided."
elif [[ -d "$path" ]]; then
  echo "Path is a directory."
else
  echo "Condition not met."
fi

For multi-branch logic, the case statement is more readable than multiple if-elif blocks:

case "$1" in
  start) echo "Starting service..." ;;
  stop)  echo "Stopping service..." ;;
  *)     echo "Usage: $0 {start|stop}" ;;
esac

Advanced Variable Attributes

The declare (or typeset) command allows you to define specific attributes for variables, which can enforce data types or behaviors.

  • declare -i: Treats the variable as an integer.
  • declare -a: Defines an indexed array.
  • declare -A: Defines an associative array (key-value pairs).
  • declare -r: Makes the variable read-only.
  • declare -x: Exports the variable to the environment.
  • declare -n: Creates a nameref (indirect reference).
declare -i num=10
declare -a arr=(a b c)
declare -A map
declare -r PI=3.14

Professional Best Practices

To write secure, maintainable, and professional Bash scripts, follow these guidelines:

  1. Always Quote Variables: Use "$var" to prevent unexpected behavior caused by spaces or wildcards.
  2. Use Braces for Clarity: Use ${var} when the variable is adjacent to other characters (e.g., "${prefix}_suffix") to avoid ambiguity.
  3. Enable Strict Mode: Start your scripts with set -u to treat unset variables as errors, and set -o pipefail to ensure errors in a pipeline are correctly captured.
  4. Prefer Modern Syntax: Use [[ ... ]] for tests and $(( ... )) for arithmetic.
  5. Encapsulate with local: Always declare variables inside functions as local to prevent side effects in the global scope.
  6. Avoid eval: Never use eval with untrusted input, as it opens your script to command injection vulnerabilities.