Variables & Parameters

Assigning Variables

A variable is a name bound to a string. Bash has no types: everything is text until arithmetic expansion reinterprets it as a number.

Assignment Syntax

greeting="hello"          # assign (no $ on the left side, ever)
count=7                   # digits are still a string until you do arithmetic
greeting="hi $USER"       # expand on the right side

No Spaces Around =

Spaces around = turn the assignment into a command named after the left word.

name = "x"     # ERROR: bash runs a command "name" with args "=" "x"
name="x"       # correct

readonly and declare

readonly MAX=100     # cannot be reassigned afterwards
declare -i n=0       # integer attribute: n=3+4 stores 7, not "3+4"
declare -r GREET="hi"
declare -l lower="ABC"   # -l forces lowercase: "abc"
declare -p MAX           # print the variable and its attributes
unset greeting           # remove the binding

Scope

Shell Variables vs Environment

A shell variable lives only in the current shell. An environment variable is copied into every child process. Without export, a child program simply cannot see your variable.

LOCAL_VAR="x"          # shell variable only
export APP_ENV="prod"  # child processes inherit this
env | grep APP_ENV     # shows APP_ENV because it was exported

export

export PATH="$HOME/bin:$PATH"   # common pattern
export EDITOR=vim
declare -x LOG_LEVEL=info       # -x is the export attribute

local (inside functions)

Inside a function, local limits the variable to that function call. Without it you would overwrite a caller's variable.

greet() {
  local name="$1"        # scoped to this function
  echo "hello $name"
}
greet bob
echo "$name"             # empty: 'name' never leaked out

Special Parameters

Positional Parameters

FormMeaning
$0Name of the script or shell
$1 … $9First … ninth argument
${10}Arguments past nine need braces
$#Number of arguments
"$@"All arguments, each still quoted separately
"$*"All arguments as one word, joined by the first IFS char
printf 'arg%d=%s\n' 0 "$0"
for a in "$@"; do echo "arg: $a"; done    # correct: preserves spaces
for a in $*;   do echo "arg: $a"; done    # wrong: splits on spaces

$? — Last Exit Status

false; echo $?        # 1
true;  echo $?        # 0

$$, $! and $_

echo "$$"     # PID of the current shell
sleep 5 &
echo "$!"     # PID of the most recent background job

${BASH_SOURCE[0]}

Inside a sourced file, $0 is the parent script. ${BASH_SOURCE[0]} gives the file's own path, which is what a library needs to find its siblings.



Parameter Expansion Forms

Defaults and Errors

echo "${1:-default}"     # use default if unset OR empty
echo "${1-default}"      # use default only if unset
: "${CONFIG:?config path is required}"   # abort with a message if unset

Substring and Length

s="hello world"
echo "${#s}"             # 11   (length)
echo "${s:6}"            # world
echo "${s:0:5}"          # hello

Trim and Replace

path="/a/b/c.txt"
echo "${path##*/}"       # c.txt        (remove longest prefix up to last /)
echo "${path%/*}"        # /a/b         (remove shortest suffix from last /)
echo "${path%.txt}"      # /a/b/c       (remove a suffix)
file="a.b.c"
echo "${file//./-}"      # a-b-c        (replace all)
echo "${file%%.*}"       # a            (strip first extension onward)
Prefix/suffix trimming is the fastest way to get filenames and directories without spawning basename or dirname. ##/%% are greedy; #/% are minimal.

Naming Conventions

  • Names may contain letters, digits and underscores, and cannot start with a digit.
  • Use UPPER_CASE for exported environment variables and lower_case for script-local variables.
  • Avoid overriding important variables such as PATH, IFS and HOME.

Pitfalls

Unset vs Empty

${x:-d} treats unset and empty alike. ${x-d} only substitutes when unset. Choose deliberately.

Case Sensitivity

Path, path and PATH are three different variables. Bash never guesses which you meant.