Why this lesson
Scripts spend a lot of their time on small text jobs: "use this setting, or a default if it is missing", "the file name without the folder", "the name without .tar.gz". You could call a program for each (basename, dirname), but bash can do them itself, instantly, inside the ${ } of a variable. And when one variable must hold a list - three host names, say - you need an array.
What you need to know already: variables and expansion (6.1); quoting and word splitting (6.6).
The long form of $var
$var and ${var} mean the same thing. The braces let you add an operator after the name - that is parameter expansion ("parameter" is bash's word for a variable). Everything in this lesson is one of those operators.
Defaults and requirements
${var:-default} use "default" if var is unset OR empty. var is not changed.
${var:=default} ...and also store "default" in var.
${var:?message} STOP the script with "message" if var is unset or empty.
${var:+alt} use "alt" only if var IS set and not empty. (The reverse.)
${var:?} is the one to reach for at the top of a script:
: "${DEPLOY_ENV:?DEPLOY_ENV must be set}"
: is a command that does nothing (a no-op). It is there only so bash evaluates the expansion. If DEPLOY_ENV is missing, the script stops right here with your message, instead of failing confusingly ten lines later.
DEPLOY_ENV in capitals is an environment variable - a variable handed to a program by whoever started it (you met them in 2.21). DEPLOY_ENV=prod ./x.sh sets one for that single command only.
Drop the colon (${var-default}) and the test becomes "unset" only, so an empty value is accepted. Usually you want the colon.
Trimming
These cut a pattern (a glob, 6.6) off the front or the end of the value:
p=/var/log/nginx/access.log
${p##*/} access.log ## = longest match from the FRONT
${p#*/} var/log/nginx/access.log # = shortest
${p%/*} /var/log/nginx % = shortest match from the END
${p%%.*} /var/log/nginx/access %% = longest
f=archive.tar.gz
${f%.gz} archive.tar
${f%%.*} archive
Memory aid: # is left of $ on the keyboard (so it cuts from the left), % is right. Doubled means greedy: cut the longest possible match.
basename PATH and dirname PATH are programs that print the file part and the folder part of a path. The expansions do the same without forking (starting a new process) - which matters inside a loop over ten thousand files.
Substitution and length
${p/nginx/apache} replace the FIRST match
${p//o/0} replace ALL
${#p} length in characters
${var^^} ${var,,} upper / lower case
Arrays
An array is one variable holding a numbered list of values. Each value is an element, and its number is its index, counting from 0:
hosts=(web1 web2 "db 1")
echo "${hosts[0]}" web1
echo "${#hosts[@]}" 3 <- count
echo "${hosts[@]}" all elements
for h in "${hosts[@]}"; do ... <- ALWAYS quoted, ALWAYS [@]
hosts+=(web3) append
unset 'hosts[1]' remove (leaves a gap in the indices)
"${arr[@]}" with the quotes and the @. ${arr[*]} joins everything into one string; unquoted ${arr[@]} word-splits each element again. Only the quoted @ form gives you exactly the elements you put in.
An associative array uses names instead of numbers as its indices - a lookup table. The names are called keys. It must be declared first with declare -A (declare is the builtin that sets a variable's type):
declare -A ports=([http]=80 [https]=443)
echo "${ports[https]}" # 443
for k in "${!ports[@]}"; do echo "$k -> ${ports[$k]}"; done
${!arr[@]} gives the keys (or the indices of a normal array).
declare -i n=0 makes n an integer variable, so n+=1 adds 1 instead of gluing the text "1" on the end. That is what makes the demo counter script from 2.5 print iteration 2 rather than iteration 11.
What you can now do
- Give a setting a default (
:-) or make it required (:?). - Cut paths and extensions with
#,##,%,%%, without calling a program. - Store a list in an array and loop over it with
"${arr[@]}".