How to train your
Marcin Stożek "Perk"


# figure out the absolute path to the script being run a bit
# non-obvious, the ${0%/*} pulls the path out of $0, cd's into the
# specified directory, then uses $PWD to figure out where that
# directory lives - and all this in a subshell, so we don't affect
# $PWD
STEAMROOT="$(cd "${0%/*}" && echo $PWD)"rm -rf "$STEAMROOT/"*
What is Bash?
Shell
(command processor)
Command language
(scripting language subtype)
It's a DSL for job control
It's Turing complete
$ whatis bash
 $ whatis bash
 bash (1) - GNU Bourne-Again SHell $ whatis javascript
 javascript: nothing appropriate. $ whatis
 whatis what?
How does a usual Bash script look like at the beginning?

How does it look like after some time?


Let's see how to
tame the beast
Myth: variable has a type
Bash variables are untyped
Bash variables are character strings
Bash is stringly typed
* it allows for arithmetic operations if
a variable contains digits only;
also there are arrays
- $1, $2, $3, $4... - positional parameters
- $# - number of parameters
- $@ - expansion of positional parameters - "$1" "$2" ...
- $* - expansion of positional parameters by $IFS
- $0 - name of the script (or shell)
- $$ - PID of the current shell
- $! - PID of the last command
- $? - last command's exit status
- $IFS - internal field separator, used for word splitting
Dollar sign variables
Don't
Do
Rule: always use quotes
echo $1
echo one two *echo "$1"
echo "one two *"$ VAR="one two *"
$ echo "${VAR}"
one two *
$ echo ${VAR}
one two bin boot cdrom dev etc home...Why?
Don't
Do
Rule: no space around =
Why?
LANGUAGE= "w"
LANGUAGE =w
LANGUAGE = wLANGUAGE="w"$ FOO = bar
FOO: command not found
$ env | grep bar
$ FOO=bar env | grep bar
FOO=barDon't
Do
Rule: assignment quotes
OWNERSHIP=Ala has a catOWNERSHIP="Ala has a cat"Why?
$ OWNERSHIP=Ala has a cat
has: command not found
$ OWNERSHIP="Ala has a cat"
$ echo "${OWNERSHIP}"
Ala has a catDon't
Do
Rule: immutable globals
RULES
- Keep them to minimum
- USE_UPPER_CASE_FORMAT
- make them readonly
#!/bin/bash
my_var="Some value"#!/bin/bash
readonly MY_VAR="Some value"Don't
Do
Rule: name the parameters
with the help of ${1:-"default"}
function list_files() {
  find . -iname "*.$1"
}function list_files() {
  local type=${1:-"tmp"}
  find . -iname "*.${type}"
}Don't
Do
Rule: know your ARGS
#!/bin/bash
for f in "$@"; do
  echo "$1"
  shift
done#!/bin/bash
declare -ra ARGS=("$@")
main "${ARGS[@]}"Why use array?
Rule: know your ARGS
$ fun() { ARGS="$@"; for f in $ARGS; do echo "$f"; done; }
$ fun 1 "2 3"
1
2
3
$ fun() { ARGS="$@"; for f in "$ARGS"; do echo "$f"; done; }
$ fun 1 "2 3"
1 2 3
$ fun() { declare -a ARGS=("$@"); for f in "${ARGS[@]}"; do echo "$f"; done; }
$ fun 1 "2 3"
1
2 3Don't
Do
Rule: use explicit paths ./
for file in *; do
  something "$file"
donefor file in ./*; do
  something "$file"
doneDon't
Do
Rule: use $() instead of ``
Why
- Backticks `` are legacy and harder to read
- $() can be nested easily
var="`command`"var="$(command)"Don't
Do
Rule: use subshells ()
echo "$IFS"
(IFS="\n"; echo "$IFS")
echo "$IFS"var_backup="$var"
var="whatev"
do_something_with "$var"
var="$var_backup"Why?
$ VAR="one"
$ echo "${VAR}"
one
$ (VAR="two"; echo "${VAR}")
two
$ echo "${VAR}"
oneDon't
Do
Rule: split pipelines with \
command1 \
| command2 \
| command3 \
| command4 \
...command1 | command2 | command3 | command4 | command5...command1 \
&& command2 \
&& command3 \
|| command4 \
...Rule: use conditional expressions
pure bash instead of spawning external processes
[[ -z $string ]] \
  && echo 'True if the length of string is zero.'
[[ -n $string ]] \
  && echo 'True if the length of string is non-zero.'
[[ $string1 == $string2 ]] \
  && echo "True if the strings are equal"
[[ -e $file ]] && echo 'True if file exists.'
[[ -x $file ]] \
  && echo 'True if file exists and is executable.'
[[ -L $file ]] \
  && echo 'True if file exists and is a symbolic link.'Don't
Do
Rule: use [[ rather than [
[[ "$text" = 'some text' ]] && echo 'it works'
[[ $text = 'some text' ]] && echo 'this works to!'
[[ 1 > 2 ]] \
  && echo true \
  || echo false # finally some sane result[ $file = "bar" ] # wrong quotes
[ 1 > 2 ] # wrong - this is not what you think it is!Don't
Do
Rule: use functions
function delete_temp_files() {
  local path="$1"
  local current_pid="$$"
  find "${path}" -iname "*.${current_pid}.tmp" -delete
}
delete_temp_files "${path}"cd $path
find . -iname "*.$$.tmp" -deleteRule: use functions
Variables can be local
Code is easier to debug
Code is self documenting
Easier to understand code flow
Don't
Do
Rule: use local
$ echo "'$MEH'"
''
$ echo "'$BLAH'"
''
$ fun() { BLAH="nasty global"; local MEH="nice local"; }
$ fun
$ echo "'$MEH'"
''
$ echo "'$BLAH'"
'nasty global'function fun() {
  path="$1"
}function fun() {
  local path="$1"
}Why?
Rule: use main function
- Most of the variables can be local
- We can easily locate the script's entrypoint
- If script is really short - don't bother
#!/bin/bash
function main() {
  for i in "$@"; do
    echo "$i"
  done
}
main "$@" _________________________________________
/ You take the red pill — you stay in     \
| Wonderland, and I show you how deep the |
\ rabbit hole goes.                       /
 -----------------------------------------
        \   ^__^
         \  (@@)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
$ cowsay -p "blah blah blah..."Trivia: functional Bash
# Higher order functions
function stalosie() { echo "zgasla mi fajka mi zgasla"; }
function stao() { echo "lazienka jest zamknieta"; }
function cosie() {
  local co="$($1)"
  echo ">> ${co} <<"
}
$ cosie stalosie
>> zgasla mi fajka mi zgasla <<
$ cosie stao
>> lazienka jest zamknieta <<Trivia: Bash monad

| is not a pipe any more
It's an >>= operator equivalent
It's a part of a monad

Rule: use unofficial strict mode
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'- set -e - exit on all non-zero exit statuses
- set -u - do not allow usage of undefined variables
- set -o pipefail - fail pipe if any non-zero exit codes
- IFS=$'\n\t' - split words on newlines and tabs only
Rule: clean after yourself
function cleanup() {
  # don't do this
  # rm -rf --no-preserve-root /
  for file in /my/cache/*; do
    rm -v "${file}"
  done
}
trap cleanup EXIT- clean when you exit (because AWS is not cheap)
- very useful with set -e
Rule: use debug mode -x
Prints out every command before executing - expanded.
#!/bin/bash
set -x$ bash -x /path/to/script.sh#!/bin/bash
set -x
some_buggy_function
set +x#!/bin/bash -x$ cat test.sh 
#!/bin/bash
function main() {
  for i in "$@"; do
    echo "$i"
  done
}
main "$@"
$ bash -x test.sh one "2 three"
+ main one '2 three'
+ for i in "$@"
+ echo one
one
+ for i in "$@"
+ echo '2 three'
2 threeRule: log your output
readonly LOG_FILE="/tmp/$(basename "$0").log"
info()    { echo "[INFO]    $*" | tee -a "$LOG_FILE" >&2 ; }
warning() { echo "[WARNING] $*" | tee -a "$LOG_FILE" >&2 ; }
error()   { echo "[ERROR]   $*" | tee -a "$LOG_FILE" >&2 ; }
fatal()   { echo "[FATAL]   $*" | tee -a "$LOG_FILE" >&2 ; exit 1 ; }Rule: use shellcheck
Shell script static analysis tool
Gives warnings and suggestions
Checks for incorrect command use
Can make suggestions to improve style
$ cat skrypcik.sh 
#!/bin/bash
function main() {
  local args=$@
  local date=`date`
  for $i in args; do
    echo $date
  done
}
main $*$ shellcheck skrypcik.sh 
In skrypcik.sh line 4:
  local args=$@
        ^-- SC2034: args appears unused. Verify it or export it.
             ^-- SC2124: Assigning an array to a string! Assign as array, or use * instead of @ to concatenate.
In skrypcik.sh line 5:
  local date=`date`
        ^-- SC2155: Declare and assign separately to avoid masking return values.
             ^-- SC2006: Use $(..) instead of legacy `..`.
In skrypcik.sh line 7:
  for $i in args; do
  ^-- SC2034: i appears unused. Verify it or export it.
      ^-- SC1086: Don't use $ on the iterator name in for loops.
            ^-- SC2043: This loop will only ever run once for a constant value. Did you perhaps mean to loop over dir/*, $var or $(cmd)?
In skrypcik.sh line 8:
    echo $date
         ^-- SC2086: Double quote to prevent globbing and word splitting.
In skrypcik.sh line 12:
main $*
     ^-- SC2048: Use "$@" (with quotes) to prevent whitespace problems.
     ^-- SC2086: Double quote to prevent globbing and word splitting.rm -rf "${STEAMROOT:?}/"*rm -rf "$STEAMROOT/"*Don't
Do
Rule: use Shell Style Guide
https://google.github.io/styleguide/shell.xml
It's battle tested
World is a better place if we have style conventions
Promotes good practises
Rule: unit test with shUnit2
#!/bin/bash
# file: equality_test.sh
testEquality() {
  assertEquals 1 1
}
# Load shUnit2.
. ./shunit2$ ./equality_test.sh
testEquality
Ran 1 test.
OKRule: use dos2unix
The dos2unix converts line endings like a pro.
/bin/bash^M: bad interpreter: No such file or directoryIf you feel like
$ [ whereis my brain?
bash: [: missing `]'there is one more rule...
Rule: don't use Bash...
... if your script is > 100 lines long.
Use Python instead.
Some more #bash
$ echo 'Thank you!'
Marcin Stożek "Perk"
@marcinstozek / perk.pl
How to train your Bash
By Marcin Stożek
 
   
   
  