They key is in using hash or type to see if a command is available. For example, the output of

hash nano

will change based on whether nano is installed or not. If nano is installed, you’ll get nothing. If nano is not installed, you’ll get an error.

So, we check for an error to see if something is not installed.

command -v foo >/dev/null 2>&1 || { echo >&2 "I require foo but it's not installed.  Aborting."; exit 1; }

OR

type foo >/dev/null 2>&1 || { echo >&2 "I require foo but it's not installed.  Aborting."; exit 1; }

OR

hash foo 2>/dev/null || { echo >&2 "I require foo but it's not installed.  Aborting."; exit 1; }

Where bash is your shell/hashbang, consistently use hash (for commands) or type (to consider built-ins & keywords).

When writing a POSIX script, use command -v.

We can also use this in a function, like so:

gnudate() {
    if hash gdate 2>/dev/null; then
        gdate "$@"
    else
        date "$@"
    fi
}