The VikiD Truth

Knowledge base / Computing

BASH | Bash Cookbook |

Using Variables in Bash

Here is the truth about Bash variables: they are all strings, and everything interesting is about when those strings get split, expanded or left alone. Master expansion and the rest of the shell starts to behave.

What it is, in plain words

A variable in Bash is a name bound to a string value (arrays are the structured exception). You assign with =, no spaces:

name=vikix
count=3
os="Void Linux"

You read with $name or, safer, ${name}. Environment variables (PATH, HOME, EDITOR) are just variables marked for export to child processes.

echo "$name"
echo "${name}-box"
export EDITOR=nvim

Why it matters

Almost every Bash bug is an expansion bug: a missing quote that splits a filename with spaces, an unquoted $@ that mangles arguments, a $var that was empty and deleted a flag. Knowing the rules means writing scripts that survive real-world input.

An everyday analogy: mail merge

A variable is a blank in a form letter. Expansion is printing the letter with the blank filled in. Quoting is putting the filled-in value in an envelope so the post office (the shell) doesn’t tear it into separate words at every space.

flowchart LR
  A["\$name"] --> B[expand to value]
  B --> C{quoted?}
  C -- '"..."' --> D[one word]
  C -- unquoted --> E[split on IFS]

A hands-on example

# Basics
greeting="hello"
echo "$greeting world"

# Default values
echo "${unset_var:-fallback}"      # use fallback if unset/empty
echo "${unset_var:=fallback}"      # also assign it
echo "${greeting:+set}"            # expand if set

# Substrings and lengths
file="archive.tar.gz"
echo "${#file}"                    # length: 14
echo "${file%.gz}"                 # archive.tar
echo "${file%%.*}"                 # archive
echo "${file#*.}"                  # tar.gz
echo "${file##*.}"                 # gz

# Arrays (Bash)
hosts=(vikix pi.local "nas box")
echo "${hosts[0]}"
echo "${hosts[@]}"                 # all elements, correctly quoted
echo "${#hosts[@]}"                # count

# Looping safely
for h in "${hosts[@]}"; do
  echo "host: $h"
done

# Environment vs shell
FOO=bar            # shell only
export FOO         # also children
env | grep '^FOO='

The quoting rules that matter:

FormMeaning
"$var"expand, keep as one word
'$var'literal $var, no expand
$varexpand and split on IFS (usually space/tab/newline)
"$@"all arguments, each separately quoted – always use this
"$*"all arguments as one word
# Dangerous
rm $file                 # if file="a b.txt", removes "a" and "b.txt"
# Safe
rm -- "$file"

Special variables: $0 script name, $1…$9 arguments, $# count, $? last exit status, $$ PID, $! last background PID, $_ last argument of previous command.

Integer and readonly attributes

declare -i n=5          # integer attribute
n=n+2                   # arithmetic, still stored as string "7"
declare -r pi=3.14159   # readonly
# pi=2                  # error
declare -a nums=(1 2 3) # indexed array
declare -A map=([os]=void [wm]=stump)  # associative (Bash 4+)
echo "${map[os]}"

local inside a function makes a variable scoped to that call – use it by default so functions don’t leak names into the global namespace.

greet() {
  local name=$1
  echo "hi $name"
}

Export, env and child processes

A child process (another script, python, git) only sees variables that were exported. A common pattern for one-shot environment:

EDITOR=nvim crontab -e
env -i HOME="$HOME" PATH="/usr/bin:/bin" bash --noprofile --norc

The second starts a clean shell – useful for testing install scripts.

Common misconceptions

Connections

#!/bin/bash
echo "hello"
WORD='script'
echo "This is a $WORD"
echo "This ${WORD}ing a $WORD"
ENDING='ed'
echo "${WORD}$ENDING"
ENDING='s'
echo "${WORD}$ENDING"
ENDING='alicious'
echo "${WORD}$ENDING"

# Echo the exit status - notice 0 generally means success
echo ${?}

# User UID
echo ${UID}

# User name - notice the use of $() construct for a command substitution
# you could use `id -un` also

echo $(id -un)

Last edited · 3 revisions

Tags: #Computing