Bash Workspace¶

Intro¶

List all the shells that are currently installed on our machine by reading the /etc/shell file:

In [3]:
cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/usr/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/bin/dash
/usr/bin/dash

A Jupyter kernel for bash¶

  • github repo
cat dog.png | display
echo "<b>Dog</b>, not a cat." | displayHTML
echo "alert('It is known khaleesi\!');" | displayJS
In [3]:
ls
README.md		python-workspace.ipynb	run-docker-notebook-d.sh
bash-workspace.ipynb	run-docker-bash.sh	run-docker.sh
golang-workspace.ipynb	run-docker-d.sh

Special Characters and Operatons¶

In [25]:
specialChars() {
  echo ~+ # same as "echo $PWD"
  cd ~
  echo ~- # same as "echo $OLDPWD"
  echo $* # all positional arguments passed to script, function, etc
  echo $@ # same as above

  :  # means no operation (no-op)

  # ternary operator in Bash!
  variable=$(( 1>2 ? 1:2 ))
  echo $variable
  # 2

  # get last positional parameter
  last_arg=${!#}
  echo $last_arg
  # ./script.sh 1 2 3 4
  # 4

  # if `username` not set, set default value to output of `whoami` command
  echo ${username:-$(whoami)}
  # output of `whoami` command

  # if variable is set, use alternative value
  path=/some/path
  path=${path:+/some/alternative/path}
  echo $path
  # /some/alternative/path
}
specialChars
/root
/root


2
/usr/bin/bash
root
/some/alternative/path

base 64 / hex string¶

In [30]:
# generating all the characters for a valid base 64 / hex string
baseAndHex() {
  b64_chars=( {a..z} {A..Z} {0..9} + / = )
  echo $b64_chars
  echo "${#b64_chars[@]}"
  # a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 + / =

  hex_chars=( {0..9} {a..f} )
  echo $hex_chars
  echo "${#hex_chars[@]}"
  # 0 1 2 3 4 5 6 7 8 9 a b c d e f
}
baseAndHex
a
65
0
16

checking if parameter is set, else printing error message and exit¶

In [35]:
checkParameter() {
  local args=("$@")
  echo "${args[0]}"
  echo "${args[1]}"
  echo "${args[2]}"
  : ${1?"usage: `basename $0` <username>"}
  : ${2?"usage: `basename $0` <username>"}
}
echo -e "\n..."
checkParameter
echo -e "\n..."
checkParameter "a"
echo -e "\n..."
checkParameter "a" "b"
...



bash: 1: usage: bash <username>

...
a


bash: 2: usage: bash <username>

...
a
b

anonymous functions¶

In [46]:
# send output of code block to file
{
  echo 'listing HOME folder'
  ls -l ~
  echo 'pinging Amazon.com'
} > results.txt
cat results.txt

echo -e "\n..."
# invoke anonymous function after conditional check
# the "-z" option in bash scripting is a test operator that checks if a string is null.
[ ! -z $HOME ] && {
  ls -l ~
  echo 'do something else'
}
listing HOME folder
total 4
drwxr-xr-x. 4 root root 28 May  8 02:46 go
-rw-r--r--. 1 root root 20 May 12 21:04 results.txt
pinging Amazon.com

...
total 4
drwxr-xr-x. 4 root root  28 May  8 02:46 go
-rw-r--r--. 1 root root 142 May 12 21:04 results.txt
do something else

string manipulation¶

In [56]:
strManipulation() {
  str='cool is Bash - Bash is so cool cool!'

  # substitute first occurence of word "cool" with "awesome"
  echo ${str/cool/awesome}

  # substitute all occurances of word "cool with "awesome"
  echo ${str//cool/awesome}

  # strip string until first occurence of "cool" from front of string
  echo ${str#*cool}

  # strip string until last occurance of "cool" from front of string
  echo ${str##*cool}

  # strip string until first occurence of "cool" from back of string
  echo ${str%cool*}

  # strip string until last occurence of "cool" from back of string
  echo ${str%%cool*}

  # if variable prefix (beginning of string) matches "cool"
  # replace "cool" with tricky
  echo ${str/#cool/tricky}

  # if variable suffix (end of string) matches "cool!"
  # replace "cool!" with tricky!
  echo ${str/%cool\\!/trick!}
}
strManipulation
bash: !/trick!}: event not found
awesome is Bash - Bash is so cool cool!
awesome is Bash - Bash is so awesome awesome!
is Bash - Bash is so cool cool!
!
cool is Bash - Bash is so cool

tricky is Bash - Bash is so cool cool!

loop using shift¶

In [57]:
# Until loop using shift to parse args passed to script or function
loopShift() {
  until [ -z $1 ]
  do
    echo "arg: $1"
    shift # moves all positional parameters to the left
  done
}
loopShift 'a' 'b' 'c' 'd' 'e' 'f'
arg: a
arg: b
arg: c
arg: d
arg: e
arg: f

parse space-separated values¶

In [59]:
# use "set" to parse space-separated values as positional arguments
parse() {
  for arg in "Will 31" "Jack 40" "Elizabeth 29"; do
    # NOTE: '--' tells Bash that values starting "-" should be treated as
    # positional arguments and not options.
    set -- $arg
    name=$1
    age=$2
    echo "$name is $age years old"
  done
  # Will is 31 years old
  # Jack is 40 years old
  # Elizabeth is 29 years old
}
parse 'a' 'b' 'c' 'd' 'e' 'f'
Will is 31 years old
Jack is 40 years old
Elizabeth is 29 years old

Handle Errors¶

In [60]:
handleError() {
  if cat file; then
    echo "Exit code 0. No problems."
  else
    echo "Exit code $?"
    echo "Handling error.."
  fi
}
handleError
cat: file: No such file or directory
Exit code 1
Handling error..

Timeout¶

In [67]:
date
timeout 2s sleep 5
date
timeout 2s echo Done
date
timeout 3s bash -c 'echo DONE'
Sun May 12 21:24:38 UTC 2024
Sun May 12 21:24:40 UTC 2024
Done
Sun May 12 21:24:40 UTC 2024
DONE

Handle Arguments And Options¶

Sometimes, we want to add arguments to provide to our script variables to make it more generic.

  • $1 is the first argument
  • $# variable returns the number of arguments provided to the command line
  • ${#args[@]} returns the number of args array elements

The shift command is another handy tool for managing arguments in bash scripts. It ‘shifts’ all arguments to the left. For instance, if you use shift, $2 becomes $1, $3 becomes $2, and so on. The original first argument is discarded.

One argument¶

In [30]:
# ONE ARGUMENT
oneArgument () {
  echo $1
}
echo -e "\nONE ARGUMENT"
oneArgument hello
ONE ARGUMENT
hello

Multiple Arguments¶

In [31]:
# MULTIPLE ARGUMENTS
multipleArguments () {
  local args=("$@")
  if [ ${#args[@]} -ne 2 ]; then
    echo "Error: Missing or too many arguments."
    exit 1
  fi
  local helloMessage=(${args[0]})
  local name=(${args[1]})
  echo $helloMessage $name
}
echo -e "\nMULTIPLE ARGUMENTS"
multipleArguments hi John
MULTIPLE ARGUMENTS
hi John

Simple Option¶

In [32]:
# SIMPLE OPTION
simpleOption () {
  local args
  local short_presentation=false
  while [ $# -gt 0 ]; do
    case "$1" in
      -s|--short-presentation)
        short_presentation=true
        shift
        ;;
      -*|--*)
        echo "Error: Invalid option: $1"
        ;;
      *)
        args+=("$1")
        shift
        ;;
    esac
  done
  if [ ${#args[@]} -ne 2 ]; then
    echo "Error: Missing or too many arguments."
    exit 1
  fi
  local helloMessage=(${args[0]})
  local name=(${args[1]})
  if [ "$short_presentation" = true ]; then
    echo $helloMessage
  else
    echo $helloMessage $name
  fi
}
echo -e "\nSIMPLE OPTION"
simpleOption hello Anna
simpleOption -s hello Anna
SIMPLE OPTION
hello Anna
hello

Value Option¶

In [33]:
# VALUE OPTION
valueOption () {
  local args
  local short_presentation=false
  local allowed_colors="green yellow red"
  check_option() {
    for option in $2; do
      if [ "$option" = "$1" ]; then
        return 0
      fi
    done
    echo "Error: Invalid option value : $1"
    exit 1
  }
  echo_color () {
    case $1 in
    green)
      echo "\033[0;32m${@:2}\033[0m"
      ;;
    yellow)
      echo "\033[0;33m${@:2}\033[0m"
      ;;
    red)
      echo "\033[0;31m${@:2}\033[0m"
      ;;
    *)
      echo "${@:2}"
      ;;
    esac
  }
  while [ $# -gt 0 ]; do
    case "$1" in
      -s|--short-presentation)
        short_presentation=true
        shift
        ;;
      -c=*|--color=*)
        check_option "${1#*=}" "$allowed_colors"
        color="${1#*=}"
        shift
        ;;
      -*|--*)
        echo "Error: Invalid option: $1"
        exit 1
        ;;
      *)
        args+=("$1")
        shift
        ;;
    esac
  done
  if [ ${#args[@]} -ne 2 ]; then
    echo "Error: Missing or too many arguments."
  fi
  local helloMessage=(${args[0]})
  local name=(${args[1]})
  if [ "$short_presentation" = true ]; then
    echo_color $color $helloMessage
  else
    echo_color $color $helloMessage $name
  fi
}

echo -e "\nVALUE OPTION"
valueOption "hi" "Jhon" --color=red
valueOption "what's up" "John" --color=green
valueOption hello Anna --color=yellow
VALUE OPTION
\033[0;31mhi Jhon\033[0m
\033[0;32mwhat's John\033[0m
\033[0;33mhello Anna\033[0m

Usage¶

In [2]:
usage() {
  echo    
  echo "Usage: $0 [...options] <hello_message> <name>"
  echo "Options:"
  echo "  -h, --help                 Show this help message."
  echo "  -c, --color COLOR          Set the color for the message (green, yellow or red)."
  echo "  -s, --short-presentation   Display presentation message without name."
  echo
  echo "Example: $0 -c green 'Hello' 'John'"
  echo
}
usage
Usage: /usr/bin/bash [...options] <hello_message> <name>
Options:
  -h, --help                 Show this help message.
  -c, --color COLOR          Set the color for the message (green, yellow or red).
  -s, --short-presentation   Display presentation message without name.

Example: /usr/bin/bash -c green 'Hello' 'John'

In [14]:
# SIMPLE USAGE
simpleUsage () {
  local args
  local short_presentation=false
  local allowed_colors="green yellow red"
  usage() {
    echo
    echo "Usage: $0 [-s|--short-presentation] [-c|--color=<green|yellow|red>] [-h|--help] <hello_message> <name>"
    echo
  }
  check_option() {
    for option in $2; do
      if [ "$option" = "$1" ]; then
        return 0
      fi
    done
    echo "Error: Invalid option value : $1"
    usage
    exit 1
  }
  echo_color () {
    case $1 in
    green)
      echo "\033[0;32m${@:2}\033[0m"
      ;;
    yellow)
      echo "\033[0;33m${@:2}\033[0m"
      ;;
    red)
      echo "\033[0;31m${@:2}\033[0m"
      ;;
    *)
      echo "${@:2}"
      ;;
    esac
  }
  while [ $# -gt 0 ]; do
    case "$1" in
      -h|--help)
        usage
        exit 1
        ;;
      -s|--short-presentation)
        short_presentation=true
        shift
        ;;
      -c=*|--color=*)
        check_option "${1#*=}" "$allowed_colors"
        color="${1#*=}"
        shift
        ;;
      -*|--*)
        echo "Error: Invalid option: $1"
        usage
        exit 1
        ;;
      *)
        args+=("$1")
        shift
        ;;
    esac
  done
  if [ ${#args[@]} -ne 2 ]; then
    echo "Error: Missing or too many arguments."
    usage
    exit 1
  fi
  local helloMessage=(${args[0]})
  local name=(${args[1]})
  if [ "$short_presentation" = true ]; then
    echo_color $color $helloMessage
  else
    echo_color $color $helloMessage $name
  fi
}

echo -e "\nSIMPLE USAGE"
# simpleUsage -h
# simpleUsage "what's up" "John" "how's it going"
# simpleUsage hello Anna --color=purple
simpleUsage hello Anna -d
SIMPLE USAGE
Error: Invalid option: -d

Usage: /usr/bin/bash [-s|--short-presentation] [-c|--color=<green|yellow|red>] [-h|--help] <hello_message> <name>

exit
Restarting Bash

String Manipulation¶

substring extraction and replacement¶

In [6]:
# substring extraction by providing character position and length
stringExtration() {
  str="2023-10-12"

  echo "${str:5:2}" # 10
  echo "${str::4}"  # 2023
  echo "${str:5}"   # 10-12
}
stringExtration

echo -e "\n..."
# substring from the right side, as folloes
stringExtration() {
  str="backup.sql"
  echo "original${str:(-4)}"  # original.sql
}
stringExtration

echo -e "\n..."
# inbuilt syntax for subtring replacements
stringExtration() {
  str="obin-linux_x64-bin"
  echo "${str/x64/armhf}"  # obin-linux_armhf-bin
  echo "${str/bin/dist}"   # odist-linux_x64-bin
  echo "${str//bin/dist}"  # odist-linux_x64-dist
}
stringExtration

echo -e "\n..."
# replace string prefixes and suffixes
# replacing a file extension with another extension is a good example
stringExtration() {
  str="db_config_backup.zip"
  echo "${str/%.zip/.conf}"   # db_config_backup.conf
  echo "${str/#db/settings}"  # settings_config_backup.zip

  # in the above substring replacement,
  # it used the exact substring segment for matching
  # to use a part of the substring, us the "*" wildcard character
  echo "${str/%.*/.bak}"      # db_config_backup.bak
  echo "${str/#*_/new}"       # newbackup.zip
}
stringExtration
10
2023
10-12

...
original.sql

...
obin-linux_armhf-bin
odist-linux_x64-bin
odist-linux_x64-dist

...
db_config_backup.conf
settings_config_backup.zip
db_config_backup.bak
newbackup.zip

regex matches, extractions, and replacements¶

Use inbuilt Bash regex features to handle text processing faster than external binaries

In [11]:
# performs a regex match with an if-condition
# and the "=~" operator
regexSample() {
  str="db_backup_2003.zip"
  if [[ $str =~ 200[0-5]+ ]]; then
    echo "regex_matched"
  fi
}
regexSample

echo -e "\n..."
# replace the if-statement with an iline conditional
regexSample() {
  [[ $str =~ 200[0-5]+ ]] && echo "regex_matched"
}
regexSample

echo -e "\n..."
# once the Bash interpreter performs a regex match,
# it typically stores all matches in the "BASH_REMATCH" shell variable.
# this variable is a read-only array, and
# it stores the entire matched data in the first index.
# IF using sub-patterns, Bash incrementally keeps those matches in other indexes.
regexSample() {
  str="db_backup_2003.zip"
  if [[ $str =~ (200[0-5])(.*$) ]]; then
    echo "${BASH_REMATCH[0]}"  # 2003.zip
    echo "${BASH_REMATCH[1]}"  # 2003
    echo "${BASH_REMATCH[2]}"  # .zip
  fi
}
regexSample

echo -e "\n..."
# it's possible to use regex definitions inside parameter expansions
regexSample() {
  str="db_backup_2003.zip"
  re="200[0-3].zip"
  echo "${str/$re/new}.bak"  # db_backup_new.bak
}
regexSample
regex_matched

...
regex_matched

...
2003.zip
2003
.zip

...
db_backup_new.bak

substring removal techniques¶

In [16]:
# use the substring replacement syntax
# but omit the replacement string parameter for string removals
removal_technique() {
  str="ver5.02-2224.e2"
  ver="${str#ver}"
  echo $ver              # 5.02-2224.e2
  maj="${ver/.*}"
  echo $maj              # 5

  # in the above, it used the exact substring
  # and a wildcard for substring removal.
  # check below how to extract a clean version number without excessive characters
  str="ver5.02-2224_release"
  ver="${str//[a-z_]}"
  echo $ver              # 5.02-2224
}
removal_technique
5.02-2224.e2
5
5.02-2224

...

case conversions and case-based variables¶

In [19]:
caseString() {
  str="Hello Bash!"
  lower="${str,,}"
  upper="${str^^}"
  echo $lower         # hello bash!
  echo $upper         # HELLO BASH!

  # uppercase or lowercase only the first character of a particular string
  ver1="V2.0-release"
  ver2="v4.0-release"
  echo "${ver1,}"     # v2.0-release
  echo "${ver2^}"     # V4.0-release

  # if need to make a specific variable strictly uppercase or lowercase,
  # add case attributes to a particular variable with the inbuilt "declare" command
  declare -l ver1
  declare -u ver2
  ver1="V4.02.2"
  ver2="v2.22.1"
  echo $ver1          # v4.02.2
  echo $ver2          # V2.22.1
  # the above "ver1" and "ver2" variables receive a case attribute during the declaration,
  # so whenever you assign a value for a specific variable,
  # Bash converts the text case based on variable attributes.
}
caseString
hello bash!
HELLO BASH!
v2.0-release
V4.0-release
v4.02.2
V2.22.1

splitting strings (string-to-array conversion)¶

In [22]:
# using "IFS" and "read" is one of the simplest and error-free ways to split a string
splitting() {
  str="C,C++,JavaScript,Python,Bash"
  IFS="," read -ra arr <<< "$str"
  echo "${#arr[@]}"  # 5
  echo "${arr[0]}"   # C
  echo "${arr[4]}"   # Bash
  # the above uses "," as the split delimiter
  # and uses the "read" inbuilt command to create an array based on "IFS"

  # BUT it breaks when including "*"
  # (expands to current directory's content)
  # as an element and space as the delimiter
  # WARNING: This code has several hidden issues.
  str="C,Bash,*"
  arr=(${str//,/ })
  echo "..."
  echo "${#arr[@]}"   # 
}
splitting
5
C
Bash
...
7