# 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. ```