Bash Scripting
Useful functions
Here are some common functions and commands I use in my scripts.
| Function | Code | |
|---|---|---|
| Use this to ensure the script runs relative to its location.
 This is useful if your script uses relative paths.  | 
#!/bin/bash
cd "$(dirname "$0")"
 | |
| Find the maximum of two numbers. You can implement min by flipping the operands. | max (){
        echo $(($1>$2 ? $1 : $2))
}
 | |
Similar to the die command in PHP which prints out a message before stopping execution.
Note that this will not work inside a subshell as it will just quit the subshell rather than the script. To get around this, you may need to kill the parent PID instead.  | 
function die {
	echo $@
	exit
}
 | |
| Indents text piped to the function. | # Supports up to 10 tabs
function Indent (){
	Tabs="\t\t\t\t\t\t\t\t\t\t"
	sed "s/^/${Tabs:0:$((2*$1))}/g"
}
echo "test" | Indent 1
	test
echo "test" | Indent 2
		test
 | |
| Extracting the file name and file extension from a path | Path="/etc/resolv.conf"
# Get path parts
Directory=$(dirname $Path)  # /etc
Filename=$(basename $Path)  # resolv.conf
# Get filename parts
Name="${Filename%.*}"  # resolv
Ext="${Filename##*.}"  # conf
 | |
| Skip the first N lines from an output | ## -n +N outputs starting on line N.
## Eg. To skip the first 2 lines, we start on line 3:
# tail -n+3
 | |
| Get the current Unix timestamp | $ date +"%s"
1596137957
 | |
| Unix timestamp to human readable time.
 You can use either   | 
$ echo 1355514777 | awk '{ print strftime("%c", $1) }'
Fri 14 Dec 2012 12:52:57 PM MST
$ date +"%c" -d @15996137957
Thu 30 Jul 2020 01:39:17 PM MDT
 | |
| Get a file's last modified time in Unix timestamp | stat -c %Y filename
 | |
| Convert a multi-line list into a comma delimited string | cat list | paste -s -d, -
 | |
| Get all IPv4 addresses on this host | ip a | grep -ohE 'inet [0-9.]{7,15}'
 | |
| Get the URL from <a href="..."> | echo '<a href="asdf">' | grep -Eo 'href="[^\"]+"' | sed 's/href="\(.*\)"/\1/'
 | |
| Replace substring in Bash | 
  | 
Useful scripting patterns
Get a specific column
You can use either awk with delimiters specified with -F.
$ echo one two three four five | awk '{print $3}'
three
$ echo one,two,three,four,five | awk -F, '{print $3}'
three
Or cut by a specific delimiter and select a specific column. This method cannot handle columns separated by more than one space. If you have neat columns, you'll need to pipe through tr -s ' ' first which collapses (or squeezes) the given repeating characters down.
$ echo one two three four five | cut -d ' ' -f3
three
$ echo "one    two    three    four     five" | tr -s ' ' | cut -d ' ' -f3
three
$ echo one,two,three,four,five | cut -d ',' -f3
three
Or use the bash built-in function read which can take in a list of variables. Variable assignment by piping to read works in zsh but will only work if used within a subshell in bash. Keep this behavior in mind if you choose to use this method of separating out words in a string. For example:
## Basic variable assignment with read
$ echo one two three four five | while read _ second _; do echo $second ; done
two
## zsh variable assignment works without a subshell/loop
% echo one two three four five | read _ second _
% echo $second 
two
The last variable contains all remaining columns. Unwanted columns can be named repeatedly with _ (like in golang) and ignored.
## Remember, to get only the 3rd column, we need to read the 4th column out.
% echo one two three four five | read _ _ third; echo $third
three four five
% echo one two three four five | read _ _ third _ ; echo $third
three
## Delimiters specified with IFS
% echo one,two,three,four,five | IFS=',' read _ _ third _ ; echo $third
three
In terms of speed, be aware that using awk or tr+cut are actual commands that spawns a sub-process whereas read is a bash built-in function. As a result, it's always faster to use read to parse out data, especially if in a loop.  If you are looping and using awk or cut, perhaps consider replacing it with the bash read equivalent. For example:
## Super slow! Invokes awk 2 times per line
$ w | tail -n +3 | while read i ; do
  Username=$(echo "$i" | awk '{print $1}')
  Source=$(echo "$i" | awk '{print $3}')
  echo $Username $Source
done
## Instead do something like this!
$ w | tail -n +3 | while read Username _ Source _ ; do
  echo $Username $Source
done
Preserve header while sorting without using a temporary file
You can sort an output on a specific column while preserving the header and without needing a temporary file by using a shim function that reads and prints the first line before passing the rest of the output down the pipe. Define this function:
# print the header (the first line of input)
# and then run the specified command on the body (the rest of the input)
# use it in a pipeline, e.g. ps | body grep somepattern
body() {
    IFS= read -r header
    printf '%s\n' "$header"
    "$@"
}
Then use this shim body function with the desired sort function. For example:
$ top -b -n 1 | head -n 15 | tail -n +7 \
  | body sort -k 5 -rn
Credit to https://unix.stackexchange.com/questions/11856/sort-but-keep-header-line-at-the-top for this neat trick.
Testing things in Bash
test is a bash built-in function to add conditionals in your script. [ is an alias of the test command and are analogous.
Use the test operators listed below with test or a single bracket test condition like the following example.
$ test -e /etc/hosts && echo "File exists"
File exists
## Note that the white space around [ and ] and also the operator are required.
$ [ -e /etc/hosts ] && echo "File exists"
File exists
$ if [ -e /etc/hosts ] ; then echo "File exists" ; fi
File exists
| Operator | Description | |
|---|---|---|
-e
 | 
file exists | |
-a
 | 
file exists (deprecated)
 This is identical in effect to   | |
-f
 | 
file is a regular file (not a directory or device file) | |
-s
 | 
file is not zero size | |
-d
 | 
file is a directory | |
-b
 | 
file is a block device. Eg. /dev/sda.
 | |
-c
 | 
file is a character device. Eg. /dev/tty0.
 | |
-p
 | 
file is a pipe. Eg. /dev/fd/0.
 | |
-h
 | 
file is a symbolic link | |
-L
 | 
file is a symbolic link | |
-S
 | 
file is a socket | |
-t
 | 
file (descriptor) is associated with a terminal device
 This test option may be used to check whether the stdin [ -t 0 ] or stdout [ -t 1 ] in a given script is a terminal.  | |
-r
 | 
file has read permission (for the user running the test) | |
-w
 | 
file has write permission (for the user running the test) | |
-x
 | 
file has execute permission (for the user running the test) | |
-g
 | 
file or directory has set-group-id (sgid) flag set
 If a directory has the sgid flag set, then a file created within that directory belongs to the group that owns the directory.  | |
-u
 | 
file has set-user-id (suid) flag set | |
-k
 | 
file or directory has sticky bit set
  | |
-O
 | 
you are owner of file | |
-G
 | 
group-id of file same as yours | |
-N
 | 
file modified since it was last read | |
f1 -nt f2
 | 
file f1 is newer than f2 | |
f1 -ot f2
 | 
file f1 is older than f2 | |
f1 -ef f2
 | 
files f1 and f2 are hard links to the same file | |
!
 | 
"not" -- reverses the sense of the tests above (returns true if condition absent). | 
Integers
Integer tests, with test or inside single brackets.
Eg.
$ test 2 -eq 2 && echo equal
| Operator | Description | 
|---|---|
-eq
 | 
is equal to | 
-ne
 | 
is not equal to | 
-gt
 | 
is greater than | 
-ge
 | 
is greater than or equal to | 
-lt
 | 
is less than | 
-le
 | 
is less than or equal to | 
Integer tests can also be done inside double parentheses. Eg.
$ ((2==2)) && echo equal
| Operator | Description | 
|---|---|
==
 | 
is equal | 
<
 | 
is less than | 
<=
 | 
is less than or equal to | 
>
 | 
is greater than | 
>=
 | 
is greater than or equal to | 
Strings
| Operator | Description | 
|---|---|
-z
 | 
string is null, that is, has zero length | 
-n
 | 
string is not null. | 
=
 | 
is equal to
## Does not work in zsh. Works only in sh and bash.
$ if [ "$a" = "$b" ] ; then
  echo "Equal"
fi
 | 
==
 | 
is equal to
## Does not work in zsh. Works only in sh and bash.
$ if [ "$a" == "$b" ] ; then
  echo "Equal"
fi
This operator inside a double bracket   | 
!=
 | 
is not equal to
$ if [ "$a" != "$b" ] ; then
  echo "Not equal"
fi
This operator inside a double bracket   | 
=~
 | 
Match using regex
$ if [ zone36-ta =~ .*ta$ ] ; then
  echo "matches"
fi
$ if [ zone36-ta =~ *ta ] ; then
  echo "matches"
fi
 | 
Command Substitution
Command substitution allows output from a command to be substituted in place of the command itself in a script. Commands can be enclosed within backticks `command` or the POSIX compatible form $(command).
## Eg.
echo "Today is " $(date)
echo "Today is " `date`
Do not confuse $(command) with $((expression)) which is used for arithmetic expansion.
Nested Substitution
If you are trying to do something similar to:
Something=`basename `ls /home/*.txt``
You will get an error since the expansion is ambiguous. Instead, use the  $(command) substitution instead:
Something=$(basename $(ls /home/*.txt))
Arithmetic Operations
Do integer arithmetic operations on variables within double brackets. For example:
Value=1
ValuePlusOne=$((Value + 1))
ValueTimes12=$((Value * 12))
The operators that are supported are:
+ - / * %
Bitwise operators also work:
^ (XOR) | (OR) & (AND) << (Shift left) >> (Shift right)
Control Blocks
Switch
read -p "What do you want to do?: " Option
case "$Option" in
	"K")
		install_new_kernel
		die "Complete!"
		;;
	"H")
		usage
		;;
	*)
		die "Stopping installation."
		;;
esac
See Also
- https://www.tldp.org/LDP/abs/html/comparison-ops.html
 - http://sipb.mit.edu/doc/safe-shell/
 - http://mywiki.wooledge.org/BashFAQ
 - https://github.com/dylanaraps/pure-sh-bible