# Important/most useful bash settings (including error handling settings)
```sh
# Error handling
set -e # Exit immediately if a command exits with a non-zero status.
set -u # Treat unset variables as an error when substituting.
set -E # If set, the ERR trap is inherited by shell functions.
set -o pipefail # the return value of a pipeline is the status of the last command to exit with
# a non-zero status, or zero if no command exited with a non-zero status
# To turn off settings:
set +e # Disables -e
set +euE # Disables -e, -u, and -E
# Globbing (*), brace expansion {}, and clobbering (overwriting)
set -f # Disable file name generation (globbing).
set -B # Enable brace expansion e.g. /etc/{shadow,passwd} (enabled by default)
set -C # If set, disallow existing regular files to be overwritten by redirection of output.
# Other
set -x # Print commands and their arguments as they are executed.
# Set Options (-o)
set -o [option] # Enable the option '[option]' (see 'help set')
set +o [option] # Disable the option '[option]' (see 'help set')
set -o history # Enable bash command history (enabled by default)
set -o pipefail # See "Error Handling" above. Pipe chains fail if anything returns non-zero.
set +o pipefail # disable pipefail
```
# ZSH Notes including quirks
```sh
####
# Read user input into a variable
####
# Note: if the output variable already contains a value (e.g. user_name='John Doe'),
# it will pre-fill the user's input with that value
# (user can hit backspace to replace existing value)
#
local user_name
vared -p "Enter your name: " -c user_name
echo "Hello, ${user_name}"
####
# Splitting strings into arrays, and using those arrays
####
##
## Parsing new-line ( \n ) separated data into an array
##
# First we'll create some new-line separated data into /tmp/example.txt
{
echo -e "hello world\nlorem ipsum dolor"
echo -e "123\n456\n7891"
} >> /tmp/example.txt
# Read the newline separated data into a variable as a string
nl_data=$(cat /tmp/example.txt)
# By using (f) with the variable inside of a string, and wrapping it in an array,
# we can quickly split each line into an array element
nl_data=("${(f)nl_data}")
echo ${nl_data[1]} # output: hello world
echo ${nl_data[2]} # output: lorem ipsum dolor
# To save time validating/casting numeric variables from the array, we can use the numeric cast
# syntax directly with the array: $((array_name[index]))
num_x=$((nl_data[3])) num_y=$((nl_data[4])) num_z=$((nl_data[5]))
echo "x: $num_x y: $num_y z: $num_z"
# output: x: 123 y: 456 z: 7891
```
## Parsing data split by arbitrary characters such as commas, colons, pipes etc.
```sh
# First we create the string variable 'lorem' containing comma separated values
lorem="hello world,lorem ipsum dolor,,,example"
# Similar to how we split the newline separated data, we use the variable inside of a string, inside of an array,
# but instead of using (f) - we use (@s/SPLIT_BY/) where SPLIT_BY is the delimiter between values, which can be
# either a single character, or multiple.
lsplit=("${(@s/,/)lorem}")
echo $lsplit[1] # output: hello world
echo $lsplit[2] # output: lorem ipsum dolor
# Duplicate delimiters aren't filtered out - which may be important if you're parsing a delimited string
# where some columns may be purposely empty.
echo $lsplit[3] # output:
echo $lsplit[4] # output:
# We can see our 'example' value was placed at index 5, after the 2 blank values caused by the ,,,
echo $lsplit[5] # output: example
# You can split by multiple characters the same way:
vsplit=("${(@s/,,,/)lorem}")
# We can see that when we split by ,,, - the delimiter is treated as a whole, so the , between
# 'hello world' and 'lorem ipsum dolor' was safely ignored.
echo $vsplit[1] # output: hello world,lorem ipsum dolor
echo $vsplit[2] # output: example
```
### Correct way to check variable existence with ZSH
```sh
# To check if var is NOT set, use (( ! ${+INSTALL_PKGS} ))
if (( ${+INSTALL_PKGS} )); then
echo "INSTALL_PKGS is set from environment"
else
INSTALL_PKGS=(git curl wget)
fi
####
# ZSH dynamic variable existence check
####
is-var-set() {
(( ${(P)+${1}} ));
}
INSTALL_PKGS=''
is-var-set "INSTALL_PKGS" && echo "INSTALL_PKGS is set" || echo "INSTALL_PKGS is not set"
# output: INSTALL_PKGS is set
unset INSTALL_PKGS
is-var-set "INSTALL_PKGS" && echo "INSTALL_PKGS is set" || echo "INSTALL_PKGS is not set"
# output: INSTALL_PKGS is not set
```
### ZSH Get content of variable whose name is contained in another variable
```sh
example_var="hello world"
test_var="example_var"
echo ${(P)${test_var}}
# output: hello world
```
## ZSH Working with Arrays
```sh
my_ray=(hello world)
###
# Check if my_ray contains the element 'hello'
###
[[ ${my_ray[(ie)hello]} -le ${#my_ray} ]] && echo "true" || echo "false"
# output: true
###
# Helper function to check if an element exists in a given array name
###
# hasElement [element] [array_name]
#
# $ hasElement hello my_ray && echo "true" || echo "false"
# true
# $ hasElement orange my_ray && echo "true" || echo "false"
# false
#
# orig source: https://unix.stackexchange.com/a/411307/166253
hasElement() {
local param_el="$1" array_name="$2" _arr
# Lookup variable with name '$array_name', obtain it's contents, and re-create the array
# into the local _arr variable for sanity.
_arr=("${(P)${array_name}[@]}")
# Check if our local _arr array contains the element $param_el
[[ ${_arr[(ie)$param_el]} -le ${#_arr} ]]
}
###
# Iterating over the values of an array works the same as bash
###
for v in "${my_ray[@]}"; do
echo "value: $v"
done
###
# Iterating over the indexes of an array instead of the keys
###
for i in {1..$#my_ray}; do
echo "i: $i || value: ${my_ray[$i]}"
done
# i: 1 || value: hello
# i: 2 || value: world
###########
```
### ZSH Associative Arrays (hash maps / dictionaries)
```sh
###
# Defining an assoc array + creating/changing items
###
declare -A hello
# Older, more compatible array definition ( key value key value )
# Works with zsh 5.5+ or so
hello=( lorem ipsum hello world )
# Newer array definition ( [key]=value [key]=value )
# Works with zsh 5.7+ or so
hello=( [lorem]="ipsum" [hello]="world" )
# Standard, **most compatible** associative array item creation/modification
# Works on zsh 5.4+ or so
hello[foo]="bar"
###
# Reading assoc array items
###
echo $hello[lorem]
# output: ipsum
for key value in ${(kv)hello}; do
echo "key: $key || value: $value"
done
# key: foo || value: bar
# key: lorem || value: ipsum
# key: hello || value: world
```
# Trim characters from start / end of variable (BASH + ZSH)
## Remove character (e.g. remove slash) from end ending of variable (bash and zsh) ending slash
```sh
hello="/world/"
lorem=${hello%/}
echo $lorem # output: /world
# for stripping chars from parameter variables, i.e. $1 $2 $3
lorem=${1%/}
```
## Remove character (e.g. remove slash) from START of variable (bash and zsh) e.g. starting slash
```sh
hello="/world/"
lorem=${hello#/}
echo $lorem # output: world/
# for stripping chars from parameter variables, i.e. $1 $2 $3
lorem=${1#/}
```
## Remove character from START AND END of variable
```sh
hello="/world/"
lorem=${${hello#/}%/}
echo $lorem # output: world
```
# Bash Traps
```sh
CLEAN_FILES=()
_cleanup-files() {
for f in "${CLEAN_FILES[@]}"; do
[[ -z "$f" ]] && continue
[[ ! -f "$f" ]] && [[ ! -d "$f" ]] && continue
fname_len=$(wc -c <<< "$f")
fname_len=$(( fname_len - 1 ))
(( fname_len < 4 )) && >&2 echo -e " [!!!] Skipping file (path too short - unsafe!): '$f'" && continue
>&2 echo -en " -> Removing temporary file/folder: '$f' ..."
rm -rf "$f"
>&2 echo -en " [DONE]\n"
done
}
# Get a temporary file using mktemp while also marking it for auto-cleanup by adding to CLEANUP_FILES
mktemp-clean() {
local tmpres ret=0
tmpres=$(mktemp "$@")
ret=$?
if (( ret )); then >&2 echo -e "\n [!!!] mktemp returned non-zero code: $ret\n"; return $ret; done
CLEAN_FILES+=("$tmpres")
printf "%s" "$tmpres"
}
_handle-error() {
ret=$?
echo -e "\n [!!!] Unexpected error occurred. Last return code: $ret \n"
echo -e "\n [!!!] _handle-error arguments: $* \n"
_mktemp-clean
return $ret
}
# On exit
trap "_cleanup-files" EXIT
# On error
trap "_handle-error" ERR
# Catch USR1 KILL signal
trap "echo 'you sent USR1!'" USR1
# Block CTRL-C during dangerous operations that are unsafe to stop
trap "" SIGINT
super_critical_function_or_command
# Reset SIGINT trap back to default
trap - SIGINT
normal_function_or_command
```
# Bash / ZSH Common Stuff
## Detect shell, locate relative path to script, then use dirname/cd/pwd to find the absolute path to the folder containing this script.
```sh
! [ -z ${ZSH_VERSION+x} ] && _SDIR=${(%):-%N} || _SDIR="${BASH_SOURCE[0]}"
DIR="$( cd "$( dirname "${_SDIR}" )" && pwd )"
# bash-only version
# directory where the script is located, so we can source files regardless of where PWD is
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
```
## Runtime overridable variables
```sh
# Override where the dotfiles and zsh_files get copied to
# by using CONFIG_DIR=/where/you/want ./core.sh
: ${CONFIG_DIR="$HOME"}
# It may be safer to quote the outside of the runtime overridable variable.
: "${CONFIG_DIR="$HOME"}"
```
## String manipulation
```sh
x="hello" y="world" z="lorem"
```
#### Appending to a string
```sh
z+=" ipsum"
# also valid:
z="${z} ipsum"
```
#### Extracting characters from a string by index
```sh
echo ${x:0:2}
# out: he
echo ${x:2}
# out: llo
## NOTE: When indexing a string in reverse, you MUST put a space after the first :
## otherwise it won't work. ${x:-1} = does not work. ${x: -1} = works.
###
echo ${x: -1}
# out: o
echo ${x: -3}
# out: llo
echo ${x: -4:3}
# out: ell
```
#### Convert a string from lowercase to uppercase and vice versa.
```sh
echo $x | tr '[:lower:]' '[:upper:]'
# out: HELLO
echo "HELLO" | tr '[:upper:]' '[:lower:]'
# out: hello
```
## Read from STDIN ( standard input ) with fallback to arguments
The following solution reads from a file if the script is called with a file name as the first parameter $1 otherwise from standard input.
```sh
while read line
do
echo "$line"
done < "${1:-/dev/stdin}"
```
The substitution ${1:-...} takes $1 if defined otherwise the file name of the standard input of the own process is used.
```sh
_NL=$'\n'
my_reader() {
local xdata=""
if (( $# > 0 )); then
xdata="$1"
else
while read -r line; do
xdata+="${line}${_NL}";
done < /dev/stdin
fi
echo "Inputted data:"
echo " --- start ---"
cat <<< "$xdata"
echo " --- end ---"
}
x="hello world${_NL}this is a test"
my_reader <<< "$x"
# Inputted data:
# --- start ---
# hello world
# this is a test
#
# --- end ---
my_reader "$x"
#Inputted data:
# --- start ---
# hello world
# this is a test
# --- end ---
```
## Convert absolute path to relative path
https://unix.stackexchange.com/a/522013/166253
```sh
realpath --relative-to=/hello/world /hello/path/example
$ realpath --relative-to=/hello/world /hello/path/example
../path/example
```
# Arguments
### All arguments from $2 and above
```sh
asdf() {
echo "${@:2}"
}
# hello = $1, world = $2, test = $3
asdf hello world test
# Output:
# world test
```
## Use 'shift' to remove (pop) the first argument from the array
This affects both $1 $2 etc. as well as $# (num args) and $@ (all args in the array)
```sh
shift_tst() {
local a b
a=$1; shift
b=$1; shift
echo "a = $a ; b = $b"
}
shift_tst hello world
# Output: a = hello ; b = world
```
## Loop over arguments, and use $z as a running counter
```sh
loop_args() {
local z=1
for i in "$@"; do
echo "arg ${z}: $i"
z=$((z+1)) # Increment $z by 1
done
}
```
## Passthru arguments to another function / program
```sh
passthru() {
loop_args "${@:2}"
}
# passthru() passed everything after and including 2nd arg to loop_args
# thus 'hello' was ignored, but 'world' and 'test' were passed through to loop_args
passthru hello world test
# arg 1: world
# arg 2: test
```
# Arrays
## Handling arrays / key value pairs
```sh
# Creating an array (list)
mylist=(lorem ipsum)
# Adding items to an array
mylist+=(world)
# Get length of array
echo "${#mylist[@]}"
# Output: 3
# Looping over all items in an array
for i in "${mylist[@]}"; do
echo "item: $i"
done
# Output:
#
# item: lorem
# item: ipsum
# item: world
#
# Looping over all arguments
dump_args() {
local num=0
for i in "$@"; do
arg_num=$((arg_num+1)); echo "argument ${num}: $i";
done
}
```
## Converting command output / variables into arrays
### If it outputs multiple lines, each of which should be an element
#### For bash 4.x
```sh
mapfile -t array < <(mycommand)
```
#### For bash 3.x+
```sh
array=()
while IFS='' read -r line; do array+=("$line"); done < <(mycommand)
```
### If it outputs a line with multiple words (separated by spaces), other delimiters can be chosen with IFS, each of which should be an element:
```sh
IFS=" " read -r -a array <<< "$(mycommand)"
```
## Parse comma separated data
### backup $IFS into _IFS so we can restore it after any changes
```sh
_IFS="$IFS"
```
### Comma separated data from a file
```sh
cat CSV_file | sed -n 1'p' | tr ',' '\n' | while read word; do echo $word; done
```
### Comma separated from a variable
```sh
mydata='hello,world,test'
printf '%s\n' "$mydata" | sed -n 1'p' | tr ',' '\n' | while read word; do echo $word; done
```
### load comma separated variable into array 'myray'
```sh
IFS=',' read -ra myray <<< "$mydata"
IFS="$_IFS"
echo ${myray[@]} # outputs: hello world test
```
### alternative method (does not work on bash 4.4!)
```
read -a myray <<< $(printf '%s\n' "$mydata" | sed -n 1'p' | tr ',' '\n')
```
# Safe numerical conversions and comparisons
## ``(( x <>= y ))`` can be used to compare two values as numbers, even if they're strings.
```sh
x="1"
if (( $x > 0 )); then
echo "x is greater than 0"
fi
# For simple named variables, you don't even need the $
if (( x >= 1 )); then
echo "x is greater than or equal to 1"
fi
```
## Using $(()) we can forcefully cast a variable into a number. If it cannot be converted into a number, it will be set to integer 0
```sh
k="abcdefg"
k=$(($k))
echo "$k" # Outputs 0 because "abcdefg" cannot be converted into a number.
```
# Safe string interpolation of variables without needing spaces
```sh
a="hello "
b="world"
echo "${a}${b}"
```
# Check existence of files, folders, available commands and variables
## load file if it exists (e.g. .env files)
```sh
if [[ -f "$DIR/.env" ]]; then
source "$DIR/.env"
fi
```
## Create example folder if it doesn't exist
```sh
if [[ ! -d "$DIR/somefolder" ]]; then
mkdir -p "$DIR/somefolder"
fi
```
## If the variable INSTALL_PKGS does not exist, then set it.
```sh
if [ -z ${INSTALL_PKGS+x} ]; then
INSTALL_PKGS=(git curl wget)
fi
```
## Correct way to check variable existence with ZSH
```sh
# To check if var is NOT set, use (( ! ${+INSTALL_PKGS} ))
if (( ${+INSTALL_PKGS} )); then
echo "INSTALL_PKGS is set from environment"
else
INSTALL_PKGS=(git curl wget)
fi
```
## ZSH dynamic variable existence check
```sh
is-var-set() {
local var_name="$1"
eval "(( \${+${var_name}} ))"
}
is-var-set "INSTALL_PKGS" && echo "INSTALL_PKGS is set" || echo "INSTALL_PKGS is not set"
```
## ZSH Get content of variable whose name is contained in another variable
```sh
example_var="hello world"
test_var="example_var"
echo ${(P)${test_var}}
# output: hello world
```
## If the command 'git' isn't found, then install it
```sh
cmd='git'
if ! [ -x "$(command -v $cmd)" ]; then
echo "WARNING: Command $cmd was not found. installing now..."
apt install -y "$cmd"
fi
```
# Case Statements
```sh
while true; do
read -p "Enter your choice > " mychoice
# For ZSH you should use vared instead of read
# e.g. vared -p "Enter your choice > " -c mychoice
case "$mychoice" in
hi|HE*|he*|He*)
echo "Hello!"
break
;;
world|WORLD)
echo "Hello world!"
break
;;
*)
echo "Wrong answer... Try again."
;;
esac
done
```
# Command Grouping / Subshells
## One line actions and error handling using {} grouping (generally does not create a sub-shell)
Note that { } requires spacing, and even single commands must be terminated with ;
```sh
[[ $x -gt 1 ]] || { echo "ERROR: x is not greater than 1, cannot continue"; exit 1; }
```
## Using {} grouping to group output from several commands and pipe it
```sh
{
echo "hello"
echo "world"
} >> hello.txt
```
## Using () subshells to force commands to run in a child shell. Subshells cannot change any variables in the parent shell.
```sh
a=123
( a=456; )
echo "$a" # Outputs 123, as the above a=456 ran inside a sub-shell.
```