Guide contents

Lesson 3 of 14

Variables, Quoting, and the Environment

This lesson covers how the shell stores values, which values child programs inherit, how the shell finds commands, and how quoting controls what the shell does to a line before it runs. After it, the reader can set up variables and aliases, write commands that survive spaces in file names, and know which startup file to edit. It builds on pipes, redirection, and exit codes, which introduced command substitution and $?.

Command or syntax Purpose
name=value, "$name", unset Create, read, and remove shell variables
export, export -n, export -p Pass a variable to child processes
env, printenv Show or change the environment of one command
PATH, which, type, command -v Find which program a name runs
'...', "...", \ Control expansion and escape characters
${var:-default} and related forms Supply defaults, require values, trim strings
history, !!, !$ Repeat earlier commands
alias, unalias Give a command a shorter name
~/.bashrc, ~/.profile Run setup code when a shell starts
source, . Run a file in the current shell

Every command in this lesson ran in a fresh directory with bash 5.2 on Linux. The examples run with a clean environment, so output does not depend on the reader's account. Lines such as bash: line 6: appear because each example was fed to bash on standard input.

Example 1: shell variables

A shell variable is a named value stored inside the running shell. The lead-in case sets two variables and reads them back.

Terminal
name="Ada"
count=3
echo "$name has $count items"
echo "${name}_report"
echo "$name_report"
bash -c 'echo "child sees: [$name]"'
unset name
echo "after unset: [$name]"
Output
Ada has 3 items
Ada_report

child sees: []
after unset: []

An assignment is name=value with no spaces around the equals sign. With a space, the shell reads name as a command and =value as its argument. A dollar sign expands the variable: $name is replaced by the value before the command runs.

Variable names use letters, digits, and underscores, and cannot start with a digit. The line echo "$name_report" printed an empty line because the shell read the name name_report, which is unset. Braces mark where the name ends: ${name}_report expands name and leaves _report as text. An unset variable expands to an empty string, without an error, unless set -u is active.

A variable that is not exported belongs to this shell only. The child bash started with bash -c printed an empty value. unset name removes the variable, and the later expansion is empty again.

Example 2: export and environment variables

An environment variable is a variable that the shell copies into every program it starts. The command export marks a shell variable for copying.

Terminal
greeting="hello"
export LANG_CODE="en"
bash -c 'echo "greeting=[$greeting] LANG_CODE=[$LANG_CODE]"'
export greeting
greeting="changed"
bash -c 'echo "child gets: [$greeting]"'
bash -c 'export FROM_CHILD=1'
echo "parent sees FROM_CHILD: [$FROM_CHILD]"
export -n greeting
bash -c 'echo "after export -n: [$greeting]"'
echo "parent still has: [$greeting]"
export -p | grep -E 'LANG_CODE|greeting'
Output
greeting=[] LANG_CODE=[en]
child gets: [changed]
parent sees FROM_CHILD: []
after export -n: []
parent still has: [changed]
declare -x LANG_CODE="en"

export LANG_CODE="en" assigns and exports in one step, so the child saw it. The plain assignment greeting="hello" stayed local, and the first child printed greeting=[]. After export greeting, the flag stays on the variable. A later assignment (greeting="changed") is visible to children without a second export.

The copy goes one way. The child ran export FROM_CHILD=1, and the parent still had nothing. A child process receives a copy of the environment at the moment it starts and cannot change the parent's.

Command Effect
export name Mark an existing variable as exported
export name=value Assign and mark
export -n name Remove the export flag; the value stays in the shell
export -p List exported variables as declare -x lines

The last command filtered export -p with grep. After export -n, greeting no longer appears in the list, but echo showed the shell still holds the value.

Example 3: env and printenv

env prints the whole environment, or runs one command with a changed environment. printenv prints variables from the environment only.

Terminal
export APP_MODE=dev
printenv APP_MODE
printenv NOPE; echo "exit=$?"
env | grep '^APP_'
env APP_MODE=test bash -c 'echo "child: $APP_MODE"'
echo "parent: $APP_MODE"
env -u APP_MODE bash -c 'echo "unset in child: [${APP_MODE-unset}]"'
env -i APP_ONLY=1 /usr/bin/env
Output
dev
exit=1
APP_MODE=dev
child: test
parent: dev
unset in child: [unset]
APP_ONLY=1

printenv APP_MODE prints the value alone, with no name. printenv NOPE prints nothing and exits with status 1, which lets scripts test whether a variable is exported. It reads the environment, so it does not see shell variables that were never exported.

env | grep '^APP_' shows NAME=value lines. The anchor ^ limits the match to names that start with APP_.

env NAME=value command sets a variable for one command only. The child printed test, and the parent still held dev. This is the standard way to override a setting for one run. env -u NAME command removes a variable for one run. env -i starts from an empty environment, so env -i APP_ONLY=1 /usr/bin/env printed a single line. The full path is given because an empty environment also has no PATH.

Example 4: PATH

PATH is a colon-separated list of directories. When a command name has no slash, the shell searches those directories from left to right and runs the first executable file with that name.

Terminal
PATH=/usr/bin:/bin
echo "$PATH" | tr ':' '\n'
mkdir -p bin
printf '#!/bin/sh\necho "hello from mytool"\n' > bin/mytool
chmod +x bin/mytool
mytool; echo "exit=$?"
./bin/mytool
PATH="$PWD/bin:$PATH"
mytool
echo "$PATH" | tr ':' '\n' | sed "s|$PWD|<demo>|"
PATH=/nonexistent
ls; echo "exit=$?"
Output
/usr/bin
/bin
bash: line 6: mytool: command not found
exit=127
hello from mytool
hello from mytool
<demo>/bin
/usr/bin
/bin
bash: line 12: ls: command not found
exit=127

The first command split PATH into one directory per line with tr. The script mytool was created in ./bin and made executable with chmod +x. The first call failed with status 127, the code for "command not found", because bin was not on PATH. The call ./bin/mytool worked because a name that contains a slash skips the search.

PATH="$PWD/bin:$PATH" puts the new directory first. Putting a directory at the front means its programs win over system programs with the same name. Putting it at the end gives system programs priority. The sed stage replaced the demo directory with <demo> in the output.

The last lines set PATH=/nonexistent, and ls stopped working. Assignments to PATH in the shell last until the shell exits. Permanent changes go in a startup file (Example 13).

The current directory is not on PATH by default. Adding . to it makes a command in the working directory run by name, which can run an unintended program placed there.

Example 5: which, type, and command -v

Three commands answer "what does this name run?". They differ in how much of the shell they know about.

Terminal
shopt -s expand_aliases
PATH=/usr/bin:/bin
alias ll='ls -l'
which ls; type ls; command -v ls
type ll; command -v ll; which ll; echo "which exit=$?"
type cd; command -v cd
which cd; echo "which exit=$?"
type -a echo
type -t ls cd ll if
command -v nothing; echo "exit=$?"
Output
/usr/bin/ls
ls is /usr/bin/ls
/usr/bin/ls
ll is aliased to `ls -l'
alias ll='ls -l'
which exit=1
cd is a shell builtin
cd
which exit=1
echo is a shell builtin
echo is /usr/bin/echo
echo is /bin/echo
file
builtin
alias
keyword
exit=1
Command Knows about Output for a missing name
which Files on PATH only Prints nothing, exits 1
type Aliases, functions, builtins, keywords, files Prints an error, exits 1
command -v Same as type, prints a short form Prints nothing, exits 1

which ls, type ls, and command -v ls all pointed to /usr/bin/ls. The difference shows with ll, an alias. type and command -v reported it. which, which is a separate program, printed nothing because no file named ll exists. The same happened for cd, a builtin that has no file.

type -a echo lists every match in lookup order: the builtin first, then the files /usr/bin/echo and /bin/echo. type -t prints one word per name: file, builtin, alias, or keyword. if is a keyword, part of the shell grammar.

command -v is the form specified by POSIX and works in sh scripts. Scripts use it as a test: command -v jq >/dev/null || echo "jq is missing". The output of which varies between systems, and some versions print messages instead of staying silent.

Example 6: single and double quotes

Quotes stop the shell from splitting a line on spaces and from treating some characters specially. Single and double quotes protect different things.

Terminal
name="Ada Lovelace"
echo 'single: $name $(echo sub) \n'
echo "double: $name $(echo sub)"
echo unquoted:   $name   $(echo sub)
echo "a   b"
echo a   b
echo "line1
line2"
echo 'it'"'"'s'
Output
single: $name $(echo sub) \n
double: Ada Lovelace sub
unquoted: Ada Lovelace sub
a   b
a b
line1
line2
it's

Inside single quotes, every character is literal. The first echo printed $name, $(echo sub), and \n unchanged. Inside double quotes, $name and $(...) still expand, but spaces, *, and ; are literal.

The unquoted line shows the third behavior. echo unquoted: $name $(echo sub) collapsed the extra spaces, because the shell split the line into words and echo joined them with single spaces. The quoted "a b" kept its three spaces.

Quoted strings may span lines. The "line1 string continued onto the next line and kept the newline.

A single quote cannot appear inside single quotes. The workaround closes the quote, adds a double-quoted single quote, and reopens it: 'it'"'"'s' joins three pieces into it's.

Quote Expands $var and $(...) Keeps spaces and wildcards literal
none yes no
"..." yes yes
'...' no yes

Example 7: escaping

A backslash removes the special meaning of the single character after it. Escaping is the tool for one character, where quotes protect a whole string.

Terminal
echo "price: \$5"
echo price: \$5 \* \&
echo "quote: \" backslash: \\ unchanged: \n"
echo $'tab:\there\nnext line'
touch 'my file.txt'
ls my\ file.txt
echo one \
  two \
  three
printf '%s\n' 'C:\temp' "C:\\temp"
Output
price: $5
price: $5 * &
quote: " backslash: \ unchanged: \n
tab:	here
next line
my file.txt
one two three
C:\temp
C:\temp

\$5 printed a literal dollar sign, so the shell did not look for a variable named 5. Outside quotes, \* and \& printed the characters instead of acting as a wildcard or a background operator.

Inside double quotes, a backslash only works before $, a backtick, ", \, or a newline. In the third line, \" and \\ became " and \, while \n stayed as two characters. echo does not turn \n into a newline by default.

$'...' is a bash quoting form that understands C-style escapes: \t is a tab and \n a newline. It is not part of POSIX sh.

The file name my file.txt was created with quotes and listed with my\ file.txt, which escapes the space. A backslash at the end of a line joins it to the next line, which the one two three example shows. The last line compares single quotes, which keep C:\temp untouched, with double quotes, where \\ collapses to one backslash. In both cases the result was C:\temp, since \t is not special in double quotes.

On macOS, bash is version 3.2 by default and the default login shell is zsh. Both support $'...' and the quoting rules above.

Example 8: word splitting

After expansion, the shell splits the results of unquoted $var and $(...) into words. Characters in the variable IFS (internal field separator) mark the split points. The default IFS is space, tab, and newline.

Terminal
args() { echo "$# args"; }
files="a.txt b.txt"
args $files
args "$files"
empty=""
args $empty
args "$empty"
IFS=,
csv="x,y z,w"
args $csv
IFS=$' \t\n'
mkdir -p glob && cd glob && touch a.log b.log
pat='*.log'
echo $pat
echo "$pat"
set -f; echo $pat; set +f
Output
2 args
1 args
0 args
1 args
3 args
a.log b.log
*.log
*.log

The helper function args prints how many arguments it received. args $files received 2 because the value a.txt b.txt was split. args "$files" received 1.

An unquoted empty variable disappears: args $empty got 0 arguments, while args "$empty" got one empty argument. This difference changes commands such as [ -n $var ], which becomes [ -n ] and succeeds when the variable is empty.

Setting IFS=, changed the separator, so x,y z,w split into 3 words and the space no longer counted. The script reset IFS afterwards. In scripts, a change to IFS is usually limited to one command or function.

Splitting is followed by globbing. The unquoted $pat held *.log, and the shell replaced it with the matching names a.log b.log. In quotes, the pattern stayed literal. set -f turns globbing off for the shell and set +f turns it on. See navigating and managing files for the glob characters.

Example 9: quoting "$var" in practice

Quoting every expansion avoids word splitting and globbing. The rule is "$var" unless the splitting is wanted.

Terminal
mkdir -p d && cd d
touch "my file.txt"
f="my file.txt"
ls $f
ls "$f"
[ -e $f ]; echo "unquoted test exit=$?"
[ -e "$f" ]; echo "quoted test exit=$?"
show() { printf '[%s]\n' "$@"; }
show "$f" other
files=("my file.txt" b.txt)
show "${files[@]}"
star() { printf '[%s]\n' "$*"; }
star "$f" other
Output
ls: cannot access 'my': No such file or directory
ls: cannot access 'file.txt': No such file or directory
my file.txt
bash: line 6: [: my: binary operator expected
unquoted test exit=2
quoted test exit=0
[my file.txt]
[other]
[my file.txt]
[b.txt]
[my file.txt other]

With the name my file.txt stored in f, ls $f ran ls my file.txt and reported two missing files named my and file.txt. ls "$f" passed one argument. A test written as [ -e $f ] expanded to [ -e my file.txt ], which fails with a syntax error and status 2. Quoted, it returned 0.

Commands that take file names from variables, such as rm $f, carry the same risk. An unquoted variable that holds a b can delete two files named a and b.

The function show prints each of its arguments in brackets. "$@" expands to each argument as its own word, and "${files[@]}" does the same for an array. "$*" joins all values into one word, with the first character of IFS between them. The last output line shows this joined form: [my file.txt other] is one item.

Quoting is not needed in a few places: the right side of a plain assignment (x=$y), and inside [[ ... ]] for the left side of a comparison. Quoting anyway does no harm.

Example 10: default values and parameter expansion

The braces form ${name...} has operators that supply defaults, assign them, or fail when a value is missing. The colon versions treat an empty value the same as an unset one.

Terminal
unset v
echo "${v:-fallback}"; echo "v is [$v]"
echo "${v:=assigned}"; echo "v is [$v]"
v=""
echo "dash: [${v-default}] colon-dash: [${v:-default}]"
echo "plus: [${v:+set}]"; v=x; echo "plus: [${v:+set}]"
(unset req; echo "${req:?req is required}"); echo "exit=$?"
s="report.final.txt"
echo "${#s} ${s%.txt} ${s%%.*} ${s#*.} ${s##*.}"
echo "${s/final/draft} ${s^^}"
echo "${s:0:6}"
Output
fallback
v is []
assigned
v is [assigned]
dash: [] colon-dash: [default]
plus: []
plus: [set]
bash: line 7: req: req is required
exit=1
16 report.final report final.txt txt
report.draft.txt REPORT.FINAL.TXT
report
Form Result
${v:-word} word when v is unset or empty; v is unchanged
${v:=word} Same, and assigns word to v
${v:+word} word when v is set and non-empty, otherwise empty
${v:?message} Prints message and fails when v is unset or empty
${v-word} Without the colon: only when v is unset

The first pair shows :- returning the fallback and leaving v unset, while := also stored the value. With v="", ${v-default} printed nothing, because v is set (to empty), and ${v:-default} printed default.

${req:?req is required} aborted with an error message and exit status 1. The subshell parentheses limited the abort to that command. In a script, the abort stops the script.

Other forms trim and transform strings, using the value report.final.txt:

Form Result Meaning
${#s} 16 Length
${s%.txt} report.final Remove the shortest match at the end
${s%%.*} report Remove the longest match at the end
${s#*.} final.txt Remove the shortest match at the start
${s##*.} txt Remove the longest match at the start
${s/final/draft} report.draft.txt Replace the first match
${s^^} REPORT.FINAL.TXT Uppercase (bash 4 and later)
${s:0:6} report Substring: offset and length

A common use is a default for a script setting: port="${PORT:-8080}" uses the environment value when present.

Example 11: history and !!

The shell keeps a list of previous commands. history shows it, and history expansion with ! reuses entries.

Terminal
set -o history; set -H
history -s "echo one"
history -s "ls /usr/bin/ls"
!!
echo !$
!echo
history
fc -ln -3
Output
ls /usr/bin/ls
/usr/bin/ls
echo /usr/bin/ls
/usr/bin/ls
echo /usr/bin/ls
/usr/bin/ls
    1  echo one
    2  ls /usr/bin/ls
    3  ls /usr/bin/ls
    4  echo /usr/bin/ls
    5  echo /usr/bin/ls
    6  history
	 echo /usr/bin/ls
	 echo /usr/bin/ls
	 history

An interactive shell records commands by itself. A script does not, so the example turns history on with set -o history and set -H (history expansion) and adds two entries with history -s. In a terminal, none of this setup is needed.

Event Meaning
!! The previous command
!$ The last argument of the previous command
!* All arguments of the previous command
!echo The most recent command that starts with echo
!42 Entry number 42 in the list
!-2 The command two entries back

!! re-ran ls /usr/bin/ls. The shell prints the expanded line before it runs, which is the first line of each pair. echo !$ became echo /usr/bin/ls because /usr/bin/ls was the last argument of the previous command. !echo repeated the latest echo command.

history printed numbered entries, including the commands produced by expansion. fc -ln -3 printed the last three entries without numbers, and fc without options opens the last command in an editor.

A common use is sudo !!, which repeats the last command with elevated privileges. In a terminal, Ctrl-R searches backward through history by typing part of a command. Bash saves history to ~/.bash_history when the shell exits. The variables HISTSIZE (entries kept in memory), HISTFILESIZE (entries kept in the file), and HISTCONTROL=ignoredups (skip repeated lines) tune it. History expansion applies only in interactive shells by default, so it does not appear in normal scripts.

Example 12: alias

An alias is a text replacement for the first word of a command. It gives a long command a short name.

Terminal
bash -c 'alias hi="echo hi"; hi' 2>&1
shopt -s expand_aliases
alias ll='ls -lA'
alias gs='echo git status -s'
alias
mkdir -p d && touch d/.hidden d/file && ll d | sed 1d | awk '{print $1, $NF}'
gs
alias ls='echo aliased ls'
ls d
\ls d
command ls d
unalias ls
unalias gs
type gs 2>&1
mkcd() { mkdir -p "$1" && cd "$1"; }
mkcd made && pwd | sed "s|$OLDPWD|<demo>|"
Output
bash: line 1: hi: command not found
alias gs='echo git status -s'
alias ll='ls -lA'
-rw-r--r-- .hidden
-rw-r--r-- file
git status -s
aliased ls d
file
file
bash: line 14: type: gs: not found
<demo>/made

The first line shows that aliases do not exist in a script by default: hi was not found. Interactive shells expand aliases, and scripts do not unless shopt -s expand_aliases is set. For that reason, aliases belong in interactive setup files and scripts use functions.

alias name='text' defines one. Quote the text so the shell does not expand it when defining. alias without arguments lists all definitions, and unalias gs removed one. After removal, type gs could not find it.

ll d ran ls -lA d. The sed and awk stages kept only the permission string and the name column, which show that -A included .hidden. Naming an alias after an existing command replaces that command in the interactive shell. The example aliased ls, so ls d printed aliased ls d. A backslash (\ls d) or command ls d skips the alias and runs the real program, which printed file for both.

An alias cannot take arguments in the middle of its text. A function can. The mkcd function creates a directory and enters it, and $1 is its first argument. Functions defined in a startup file work like aliases at the prompt.

Example 13: ~/.bashrc versus ~/.profile

Startup files run setup code when a shell starts. Which files run depends on how the shell was started: a login shell (the first shell after logging in, or bash -l) and an interactive shell (one that reads commands from a terminal) read different files.

Terminal
export HOME="$PWD/home"; mkdir -p "$HOME"
echo 'echo "  .profile ran"; export EDITOR=vim' > "$HOME/.profile"
echo 'echo "  .bashrc ran"; alias ll="ls -l"' > "$HOME/.bashrc"
echo "login shell:";           bash -l -c 'echo "  EDITOR=[$EDITOR]"'
echo "interactive shell:";     bash -i -c 'echo "  EDITOR=[$EDITOR]"' 2>/dev/null
echo "plain bash -c:";         bash -c 'echo "  EDITOR=[$EDITOR]"'
echo 'echo "  .bash_profile ran"' > "$HOME/.bash_profile"
echo "login shell with .bash_profile:"; bash -l -c 'echo "  done"'
echo '[ -f ~/.bashrc ] && . ~/.bashrc' >> "$HOME/.bash_profile"
echo "login shell, .bash_profile sources .bashrc:"; bash -l -c 'echo "  done"'
echo "BASH_ENV:"; echo 'echo "  BASH_ENV file ran"' > "$HOME/envfile"; BASH_ENV="$HOME/envfile" bash -c 'echo "  done"'
Output
login shell:
  .profile ran
  EDITOR=[vim]
interactive shell:
  .bashrc ran
  EDITOR=[]
plain bash -c:
  EDITOR=[]
login shell with .bash_profile:
  .bash_profile ran
  done
login shell, .bash_profile sources .bashrc:
  .bash_profile ran
  .bashrc ran
  done
BASH_ENV:
  BASH_ENV file ran
  done

The example sets HOME to a scratch directory so that no real files are touched. A login shell read ~/.profile and set EDITOR. A non-login interactive shell read ~/.bashrc only. A plain bash -c read neither, so EDITOR was empty.

Shell type Files read by bash
Login The first that exists of ~/.bash_profile, ~/.bash_login, ~/.profile
Interactive, not login ~/.bashrc
Neither (scripts, bash -c) None, unless BASH_ENV names a file

The next lines show the "first that exists" rule. After ~/.bash_profile was created, the login shell ran it and skipped ~/.profile. Most setups therefore add this line to ~/.bash_profile or ~/.profile so login shells also get the interactive setup:

Terminal
[ -f ~/.bashrc ] && . ~/.bashrc

With that line in place, the final login run printed both .bash_profile ran and .bashrc ran. The last example shows BASH_ENV, which names a file that non-interactive bash reads at start.

A common split: environment variables (export EDITOR=vim, PATH changes) go in the login file, and aliases, prompts, and functions go in ~/.bashrc. Changes apply to new shells. To apply them to the current shell, use source (Example 14).

On macOS, Terminal opens a login shell for every new window, so ~/.bash_profile is read for bash. The default shell zsh reads ~/.zprofile for login shells and ~/.zshrc for interactive ones.

Example 14: source

source file runs the commands of a file in the current shell. The dot command, . file, is the POSIX name for the same action.

Terminal
cat > settings.sh <<'EOT'
export DB_HOST=localhost
DB_PORT=5432
greet() { echo "hi $1"; }
EOT
bash settings.sh
echo "after bash: [$DB_HOST] [$DB_PORT]"
source ./settings.sh
echo "after source: [$DB_HOST] [$DB_PORT]"
greet Ada
bash -c 'echo "child: [$DB_HOST] [$DB_PORT]"'
. ./settings.sh && echo "dot form works"
printf 'API_URL=http://127.0.0.1\nTOKEN=abc123\n' > .env
set -a; source .env; set +a
bash -c 'echo "child: $API_URL $TOKEN"'
printf 'echo "args: $# first=$1"\n' > showargs.sh
source ./showargs.sh one two
source nothere.sh; echo "exit=$?"
Output
after bash: [] []
after source: [localhost] [5432]
hi Ada
child: [localhost] []
dot form works
child: http://127.0.0.1 abc123
args: 2 first=one
bash: line 18: nothere.sh: No such file or directory
exit=1

bash settings.sh started a child shell. The child set the variables and exited, so the parent had nothing afterwards: after bash: [] []. source ./settings.sh ran the same lines in the current shell. Both variables and the greet function now existed, and greet Ada worked.

The child printed DB_HOST but not DB_PORT. Only DB_HOST had export in the file. DB_PORT is a shell variable of the current shell.

The .env pattern uses set -a, which marks every variable assigned afterwards as exported. source .env then exported API_URL and TOKEN, and set +a turned the behavior off. This loads files of KEY=value lines into the environment of later commands. The lines must be valid shell syntax, so values with spaces need quotes.

Arguments after the file name become its positional parameters: source ./showargs.sh one two printed args: 2 first=one. When the name has no slash, source searches PATH first, and then the current directory. A missing file is an error with status 1, as the last line shows. Use ./ before a name to be explicit.

Sourcing runs arbitrary code in the current shell. A sourced file can change the working directory, variables, and options, so only source files that come from a trusted place.

Interview points

  • A shell variable lives in one shell. An exported variable is copied into the environment of every child process started afterward. A child cannot change the parent's variables.
  • VAR=value command and env VAR=value command set a variable for one command only. The shell's own variable stays unchanged.
  • The shell searches PATH from left to right and runs the first match. A name containing a slash skips the search. Exit status 127 means command not found; 126 means found but not executable.
  • command -v and type see aliases, functions, and builtins. which only finds files on PATH.
  • Single quotes block all expansion. Double quotes block word splitting and globbing but still expand $, backticks, and \ before certain characters.
  • An unquoted $var is split on IFS and then globbed. "$var" passes one word. "$@" passes each argument as its own word.
  • ${v:-x} returns a default, ${v:=x} also assigns it, ${v:?msg} fails when the value is missing, and without the colon only unset counts, not empty.
  • Login shells read ~/.bash_profile, ~/.bash_login, or ~/.profile (the first that exists). Interactive non-login shells read ~/.bashrc. Scripts read neither.
  • source file runs the file in the current shell, so variables and functions persist. bash file runs it in a child, and nothing persists.
  • Aliases are not expanded in non-interactive shells by default. Functions replace aliases that need arguments.

Summary

  • name=value creates a shell variable. No spaces are allowed around =. unset removes it.
  • export copies a variable into the environment of child processes. Changes in a child never reach the parent.
  • env runs a command with a modified environment (NAME=value, -u, -i). printenv NAME prints one value and fails when it is absent.
  • PATH is a colon-separated list searched from left to right. Prepending a directory gives its programs priority.
  • type and command -v report aliases, functions, builtins, and files. which reports files on PATH only.
  • Single quotes keep everything literal. Double quotes allow $ expansion but stop splitting and globbing. A backslash escapes one character.
  • Unquoted expansions are split on IFS and expanded as globs. Quote "$var" by default.
  • ${v:-d}, ${v:=d}, ${v:+w}, and ${v:?m} handle unset and empty values. %, #, /, ^^, and ${#v} transform strings.
  • history lists earlier commands. !!, !$, and !string reuse them in interactive shells.
  • alias replaces the first word of an interactive command. Functions take arguments and work in scripts.
  • Login shells read ~/.profile family files, interactive shells read ~/.bashrc, and plain scripts read no startup files.
  • source runs a file in the current shell. set -a before it exports every assigned variable.