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.
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]"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.
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'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.
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/envdev
exit=1
APP_MODE=dev
child: test
parent: dev
unset in child: [unset]
APP_ONLY=1printenv 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.
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=$?"/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=127The 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.
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=$?"/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.
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'single: $name $(echo sub) \n
double: Ada Lovelace sub
unquoted: Ada Lovelace sub
a b
a b
line1
line2
it'sInside 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.
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"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.
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 +f2 args
1 args
0 args
1 args
3 args
a.log b.log
*.log
*.logThe 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.
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" otherls: 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.
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}"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.
set -o history; set -H
history -s "echo one"
history -s "ls /usr/bin/ls"
!!
echo !$
!echo
history
fc -ln -3ls /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
historyAn 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.
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>|"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>/madeThe 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.
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"'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
doneThe 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:
[ -f ~/.bashrc ] && . ~/.bashrcWith 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.
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=$?"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=1bash 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 commandandenv VAR=value commandset a variable for one command only. The shell's own variable stays unchanged.- The shell searches
PATHfrom 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 -vandtypesee aliases, functions, and builtins.whichonly finds files onPATH.- Single quotes block all expansion. Double quotes block word splitting and globbing but still expand
$, backticks, and\before certain characters. - An unquoted
$varis split onIFSand 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 fileruns the file in the current shell, so variables and functions persist.bash fileruns 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=valuecreates a shell variable. No spaces are allowed around=.unsetremoves it.exportcopies a variable into the environment of child processes. Changes in a child never reach the parent.envruns a command with a modified environment (NAME=value,-u,-i).printenv NAMEprints one value and fails when it is absent.PATHis a colon-separated list searched from left to right. Prepending a directory gives its programs priority.typeandcommand -vreport aliases, functions, builtins, and files.whichreports files onPATHonly.- Single quotes keep everything literal. Double quotes allow
$expansion but stop splitting and globbing. A backslash escapes one character. - Unquoted expansions are split on
IFSand 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.historylists earlier commands.!!,!$, and!stringreuse them in interactive shells.aliasreplaces the first word of an interactive command. Functions take arguments and work in scripts.- Login shells read
~/.profilefamily files, interactive shells read~/.bashrc, and plain scripts read no startup files. sourceruns a file in the current shell.set -abefore it exports every assigned variable.
