#!/usr/bin/env bash
false
echo 'Still alive?' # Still alive?; exit rc 0Urs Roesch
As part of my work I encounter many shell scripts which are poorly written but are essential for the infrastructure to function properly.
Most of the time the stake holders aren’t sufficiently trained to use a higher
level language such as python, ruby or go.
By introducing modern bash features, the scripts can be maintained by
all stake holders without creating high hurdles for entry.
If you often encounter shell or scripts written in bash and want
to learn a few tricks to make your and your co-worker’s life easier
this presentation is for you.
| A basic understanding of programming in general and shell scripting in particular is assumed. |
errexit option#!/usr/bin/env bash
false
echo 'Still alive?' # Still alive?; exit rc 0errexit option#!/usr/bin/env bash
set -o errexit
false # exit rc 1
echo 'Still alive?'To prevent exit on error, append || : after the command.
#!/usr/bin/env bash
set -o errexit
false || :
echo 'Still alive?' # Still alive?; exit rc 0nounset option#!/usr/bin/env bash
rm -rf /home/jdoe/${temp_dir} # rm -rf /home/jdoe/nounset option#!/usr/bin/env bash
set -o nounset
rm -rf /home/jdoe/${temp_dir} # bash: temp_dir: unbound variable; exit rc 1pipefail option#!/usr/bin/env bash
find /fake-dir | awk '{print $1}' # exit rc 0pipefail option#!/usr/bin/env bash
set -o pipefail
find /fake-dir | awk '{print $1}' # exit rc 1Always use the option triplet errexit, nounset and pipefail in your
scripts!
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
[...]To disable the option for a certain block or command use +o instead of -o.
[...]
# switch off exit on error
set +o errexit
false
# switch it back on
set -o errexit
[...]Ensure the variable only holds integers.
declare -i int=1
int=string # bash: string: unbound variable
int=2 # declare -i int="2"Fun fact, declaring a variable as an integer allows for simple integer math.
declare -i int
int=1+2 # declare -i int="3" # addition
int=2-1 # declare -i int="1" # subtraction
int=8/2 # declare -i int="4" # division
int=9/2 # declare -i int="4" # division rounded
int=9%2 # declare -i int="1" # modulo
int=3*2 # declare -i int="6" # multiplicationCaution! The zero padded numbers 08 and 09 are interpreted as octal
and throw an error. |
declare -i int
int=01 # declare -i int="1"
# ...
int=07 # declare -i int="7"
int=08 # bash: 08: value too great for base (error token is "08")
int=09 # bash: 09: value too great for base (error token is "09")Define a variable as a constant.
declare -r readonly=set-in-stone
readonly=change # bash: readonly: readonly variableThe content of the variable is converted to lowercase.
declare -l lower=LOWER
echo ${lower} # lower| Requires bash >= 4.0 |
The content of the variable is converted to uppercase.
declare -u upper=upper
echo ${upper} # UPPERThe content of the variable is an array.
declare -a array=( 1 2 3 )
typeset -p array # declare -a array=([0]="1" [1]="2" [2]="3" )
array=foo
typeset -p array # declare -a array=([0]="foo" [1]="2" [2]="3")The content of the variable is an associative array.
declare -A hash=( [a]=foo [b]=bar )
typeset -p hash # declare -A hash=([b]="bar" [a]="foo" )
hash=c
typeset -p hash # declare -A hash=([0]="c" [b]="bar" [a]="foo" )
hash+=([c]=foo)
typeset -p hash # declare -A hash=([c]="foo" [b]="bar" [a]="foo" )| Requires bash >= 4.0 |
There are few other switches to declare and the certainly can be combined.
To create a readonly array use declare -ra for instance.
Using declare helps narrowing the scope of the values a variable can hold.
This makes the script more predictable!
counter=0
while true; do
counter=`expr ${counter} + 1` # declare -- counter="1"
# [...]
done+= operatordeclare -i counter=0
while true; do
counter+=1 # declare -i counter="1"
# [...]
done| Without declaring the variable as an integer the behavior is not as expected. |
counter=0
while true; do
counter+=1 # declare -- counter="01"
# [...]
donestring="foo"
string="${string}bar" # declare -- string="foobar"+= operatordeclare -- string=foo
string+=bar # declare -- string="foobar"list="foo"
list="${list} bar" # declare -- list="foo bar"+= operatordeclare -a list=( foo )
list+=( bar ) # declare -a list=(foo bar)The += operator helps with the assignment of existing and brings
a bit more comfort to the previously wordy and often times ugly process
of appending or incrementing numbers.
A -= assignment operator does not exist! |
There are two basic modes, the first is to convert the case of the first letter. The second is to convert the whole string.
declare -- to_lower="ALL CAPS"
echo ${to_lower,} # aLL CAPS
echo ${to_lower,,} # all caps
declare -- to_upper="all lower"
echo ${to_upper^} # All lower
echo ${to_upper^^} # ALL LOWER| Requires bash >= 4.0 |
There is also the option of only matching a pattern to adjust the case.
declare -- fruits="banana apple pear"
echo ${fruits^a} # banana apple pear
echo ${fruits^[bp]} # Banana apple pear
echo ${fruits^^a} # bAnAnA Apple peAr
echo ${fruits^^[ae]} # bAnAnA ApplE pEAr| Requires bash >= 4.0 |
if [ `echo ${string1} | tr "[A-Z]" "[a-z]"` = "${string2}" ]; then
# [...]
fiif [[ ${string1,,} == ${string2} ]]; then
# [...]
fiThis one really gets me! I almost never see ${parameter/pattern/}
substitutions in the wild. And is has been around for ages.
declare -- string="lagoon racoon"
echo ${string/oo/u} # lagun racoon
echo ${string//oo/u} # lagun racunstring="lagoon racoon"
echo ${string} | sed 's/oo/u/g' # lagun racundeclare -- string="lagoon racoon"
echo ${string//oo/u} # lagun racunParameter expansion in shell scripts can be a bit cryptic at first but it is definitely a lot faster then to run a sub shell for every little change made to a small string.
There are many more expansions available but the examples here are bash specific and won’t work in the predecessor shells such as Bourne or Korn shell.
Combining bash arrays with the upper case parameter expansion is mighty powerful. Let’s see how it’s done.
declare -a fruit=( banana apple pear )
echo ${fruit[@]^} # Banana Apple Pear
echo ${fruit[@]^^} # BANANA APPLE PEAR
echo ${fruit[@]^a} # banana Apple pear
echo ${fruit[@]^[bp]} # Banana apple Pear
echo ${fruit[@]^^a} # bAnAnA Apple peAr
echo ${fruit[@]^^[ae]} # bAnAnA ApplE pEAr| Requires bash >= 4.0 |
Applying substitution to each element of an array can also be done. Here a few examples.
declare -a array=( lagoon racoon )
echo ${array[@]/o/u} # laguon racuon
echo ${array[@]//o/u} # laguun racuundeclare -a sshopts=( BatchMode=yes User=foobar )
echo ${sshopts[@]/#/-o } # -o BatchMode=yes -o User=foobardeclare -a fruits=( banana apple pear )
echo ${fruits[@]/%/,} # banana, apple, pear,declare -a fruits=( bananas apples pears )
echo ${fruits[@]/%s/} # banana apple pearParameter expansion combined with bash arrays allows to make volatile changes
to a list of values at the time of echoing. No sed, tr and awk constructs
and excessive looping are required.
Double square brackets are more forgiving when it comes to dealing with empty strings in a comparison.
foo=
[ ${foo} = foo ] # bash: [: =: unary operator expectedEffectively the above conditional statement is expanded as
[ = foo ] because the variable foo is empty.
There are two common ways to prevent the error:
foo=
[ "${foo}" = "foo" ] # exit rc 1
[ x${foo} = xfoo ] # exit rc 1Korn shell introduced the more robusts [[ which does not
suffer the same limitations.
declare -- foo=
[[ ${foo} == foo ]] # exit rc 1Bash has also a regex operator =~ for matching strings.
This often overlooked feature can save you lots of typing.
string=foobar
if echo ${string} | grep -q '[Bb]ar'; then
# [ ... ]
fideclare -- string=foobar
if [[ ${string} =~ [Bb]ar ]]; then
# [ ... ]
fiThese are but a few small examples creating "perfect conditions"
for your bash scripts. Although they are not POSIX compatible
the fact that ksh, zsh and bash among other implement them
makes them virtually portable.
With the exception of a few reserved characters such as $, |,
( and { among others pretty much everything else works.
function @() { echo '@'; }
@ # @
function /() { echo '/'; }
/ # /
function -() { echo '-'; }
- # -
function :() { echo ':'; }
: # :
function ü() { echo 'ü'; }
ü # ü
function 照() { echo '照'; }
照 # 照This opens possibilities of creating prefixes or fake namespaces for functions. Pretty useful for sourced files and libraries.
function funky() {
# [ ... ]
}
function test::funky() {
# [ ... ]
}
funky # execute funky
test::funky # test funky functionIt it is even possible to place functions within functions. Unfortunately there they are still accessible globally.
function parent() {
function @child() {
# [ ... ]
}
@child
# [ ... ]
@child
}Surprisingly bash functions can be creatively named and have fewer restrictions than most other scripting languages. Caution is advised when using special characters tho. I don’t think many bash scripters use them.
The bash printf can be used instead of a loop for printing as it
prints each additional argument not unlike a loop.
declare -a fruit=( banana apple pear )
for f in ${fruit[@]}; do echo ${f}; done # banana\napple\npeardeclare -a fruit=( banana apple pear )
printf "%s\n" "${fruit[@]}" # banana\napple\npearDon’t manually write out lines use printf!
printf "%0.1s" -{1..16} # ----------------One can even emulate the sprintf function by using the -v
switch.
printf -v line "%0.1s" -{1..16}
echo ${line} # ----------------| Requires bash >= 3.1 |
With the %q placeholder printf provides a shell escaping routine.
printf "%q" 'cat foo | grep "foo bar"'
# cat\ foo\ \|\ grep\ \"foo\ bar\"Use printf instead of the date command to convert
an Unix epoch timestamp to a human readable format.
printf "%(%Y-%m-%d)T\n" 1515151515
2018-01-05| Requires bash >= 4.2 |
IMHO, one of most underused builtin in bash scripts is printf. It is very versatile and can help reduce code and complexity if used appropriately.
A few examples on how to reduce clutter with examples taken from the Real World ™.
info="Joe:Doe:Sysadmin"
fname=`echo ${info} | awk -F : '{ print $1 }'` # Joe
lname=`echo ${info} | awk -F : '{ print $2 }'` # Doe
role=`echo ${info} | awk -F : '{ print $3 }'` # Sysadmindeclare -- info="Joe:Doe:Sysadmin"
IFS=: read fname lname role <<< "${info}"The same but read everything into an array.
declare -- info="Joe:Doe:Sysadmin"
IFS=: read -a person <<< "${info}"
typeset -p person # declare -a person=([0]="Joe" [1]="Doe" [2]="Sysadmin")if [ ${host} = jp-prod ]; then
prod_host ${host}
elif [ ${host} = ch-prod ]; then
prod_host ${host}
elif [ ${host} = jp-uat ]; then
uat_host ${host}
# [ ... ]
else
dev_host ${host}
ficase ${host} in
*-prod) prod_host ${host};;
*-uat) uat_host ${host};;
*) dev_host ${host};;
esacExecute a locally defined function on a remote machine without first copying the code.
function bootstrap() { config_host; hostname; }
ssh ch-dev.acme.com "$(declare -f bootstrap); bootstrap"No Sysadmin was hurt during the making of this presentation! |