Useful Bash Snippets

I use the command line a lot for different tasks and have over the years collected some gems which I hope may be useful to others as well.

Using the env command to run bash avoids hardcoding the bash application path in the script.

#!/usr/bin/env bash
set -o errexit -o pipefail -o nounset # -o xtrace

The errexit option tells bash to exit the script immediately on any error, and pipefail makes it exit on first error in a pipe chain. The nounset forces all variables to be declared before first use and finally xtrace will make bash print the script row before executing it.

This uses the built-in networking capabilities to detect an open port on a remote host. Once the port is available, the script exits the loop.

until $(bash -c 'cat < /dev/null > /dev/tcp/psql.example.org/5432' &> /dev/null); do sleep 1; done

This can be useful in situations when you need to wait for a service to be available, such as a web- or database server.

Create a temporary directory which is deleted on script exit.

WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT SIGQUIT SIGINT SIGSTOP SIGTERM ERR

The following lines will figure out the name and path of the current bash script.

SCRIPT="$(readlink -f ${BASH_SOURCE[0]})"
SCRIPTPATH="$(dirname ${SCRIPT})"

Use the expression -d to check if a directory exist in Bash.
Note: Take care that if the target is a symbolic link to a directory, the expression below will also be true.

WORK=$HOME
if [ -d "$WORK" ]; then
 echo "Directory exist: $WORK"
fi

And just to demonstrate the opposite; check if a directory does not exist.

WORK=/foo/bar/baz
if [ ! -d "$WORK" ]; then
 echo "Directory does not exist: $WORK"
fi

The quotation characters around the environment variables ensure that the full name of the target is used, even if it contains spaces.

To extract the file name in a path, use basename command, and Bash shell parameter expansion to extract the extension, name without extension and base name of multiple extensions exist.

EXAMPLE=/home/myuser/doc/file.tar.gz
FULLPATH=$(dirname -- "${EXAMPLE}")  # /home/myuser/doc
FULLNAME=$(basename -- "${EXAMPLE}") # file.tar.gz
EXT="${FULLNAME##*.}"                # gz
NAME="${FULLNAME%.*}"                # file.tar
BASE="${FULLNAME%%.*}"               # file

The date command have an option where you can add or subtract a period of time to the current date.

$ date -R -d "+10 days"
Sun, 08 Sep 2019 21:24:01 +0700

You can also specify a time to calculate from.

$ date -R -d "Wed, 21 Aug 2019 21:25:31 +0700+8 months"
Tue, 21 Apr 2020 21:25:31 +0700

It is also possible to combine the time periods.

$ date -R -d "Thu, 08 Aug 2019 21:26:35 +0700+8 months+3 days-2 hours"
Sat, 11 Apr 2020 19:26:35 +0700

Another useful function is time zone conversion, the example below converts from Eastern Standard Time (EST) to Central European Time (CET).

$ TZ=CET date -R --date='TZ="EST" 2019-08-14 08:00'
Wed, 14 Aug 2019 15:00:00 +0200 

To calculcate the number of days until a specific time, the dates must be converted to seconds then subtracted and divided.

$ echo $((($(date +%s --date "2019-12-31 23:59:59")-$(date +%s))/(3600*24))) days left

The snippet below demonstrates how to process switches and arguments; an option that requires an argument (i), a boolean option (h) and finally the remaining argument to the command.

The usage() statement is a function called to present the command usage syntax.

usage() { echo "Usage: $0 [-i <input>] <destination>" 1>&2; exit 1; }
while getopts ":i:h" o; do
 case "${o}" in
  i)
   INPUT=${OPTARG}
   ;;
  h)
   usage
   ;;
  *)
   usage
   ;;
 esac
done
shift $((OPTIND-1))
if [ $# -gt 0 ]; then
 DESTINATION="$1"
else
 echo "Error: Destination must be specified" 1>&2; exit 1
fi

Removing duplicate lines in a file is easy, while preserving the order.

cat -n unsorted.txt | sort -k2 -k1n | uniq -f1 | sort -nk1,1 | cut -f2-

The above chain of commands prepends the output with a line number, sorts and filters on the text, sorts again on the numeric prefix and finally removes the prefix.

Using readarray combined with find you can create an array with names of directory contents without worrying about spaces or special characters in the name.

readarray -d '' FILES < <(find "$DIR" -maxdepth 1 -print0)
for FILE in "${FILES[@]}"; do
 echo $FILE
done