3
\$\begingroup\$

Intention

I came with the idea of generic, portable, highly reliable, and further customizable function for Shell scripts, written in POSIX, for error handling.

Purpose

The function shall find out, if the terminal has color support, and act accordingly. If it has, then I highlight the error origin and the exit code with different colors. The main message shall be tabbed from the left, all for the best readability.

Example function call and error handler output (textual) + Explanation

print_usage_and_exit is a self-explanatory function, which watches over the number of given arguments, it accepts exactly one, let's give it some more:

Example function:

print_usage_and_exit()
{
    # check if exactly one argument has been passed
    [ "${#}" -eq 1 ] || print_error_and_exit 1 "print_usage_and_exit" "Exactly one argument has not been passed!\\n\\tPassed: ${*}"

    # check if the argument is a number
    is_number "${1}" || print_error_and_exit 1 "print_usage_and_exit" "The argument is not a number! Exit code expected."

    echo "Usage:   ${0} [-1]"
    echo "         -1: One-time coin collect."
    echo "Default: Repeat coin collecting until interrupted."
    exit "${1}"
}

Example function call - erroneous:

print_usage_and_exit a b c 1 2 3

Example output:

print_usage_and_exit()

        Exactly one argument has not been passed!
        Passed: a b c 1 2 3

exit code = 1

Example error handler output (visual)

Example error handler output (visual)

The actual error handler function code

print_error_and_exit()
# expected arguments:
# $1 = exit code
# $2 = error origin (usually function name)
# $3 = error message
{
    # check if exactly 3 arguments have been passed
    # if not, print out an internal error without colors
    if [ "${#}" -ne 3 ]
    then
        printf "print_error_and_exit() internal error\\n\\n\\tWrong number of arguments has been passed: %b!\\n\\tExpected the following 3:\\n\\t\\t\$1 - exit code\\n\\t\\t\$2 - error origin\\n\\t\\t\$3 - error message\\n\\nexit code = 1\\n" "${#}" 1>&2
        exit 1
    fi

    # check if the first argument is a number
    # if not, print out an internal error without colors
    if ! [ "${1}" -eq "${1}" ] 2> /dev/null
    then
        printf "print_error_and_exit() internal error\\n\\n\\tThe first argument is not a number: %b!\\n\\tExpected an exit code from the script.\\n\\nexit code = 1\\n" "${1}" 1>&2
        exit 1
    fi

    # check if we have color support
    if [ -x /usr/bin/tput ] && tput setaf 1 > /dev/null 2>&1
    then
        # colors definitions
        readonly bold=$(tput bold)
        readonly red=$(tput setaf 1)
        readonly yellow=$(tput setaf 3)
        readonly nocolor=$(tput sgr0)
        # combinations to reduce the number of printf references
        readonly bold_red="${bold}${red}"
        readonly bold_yellow="${bold}${yellow}"
        # here we do have color support, so we highlight the error origin and the exit code
        printf "%b%b()\\n\\n\\t%b%b%b\\n\\nexit code = %b%b\\n" "${bold_yellow}" "${2}" "${nocolor}" "$3" "${bold_red}" "${1}" "${nocolor}" 1>&2
        exit "$1"
    else
        # here we do not have color support
        printf "%b()\\n\\n\\t%b\\n\\nexit code = %b\\n" "${2}" "${3}" "${1}" 1>&2
        exit "$1"
    fi
}

EDIT

I realized the tput could very well be anywhere else than in /usr/bin/.

So, while keeping the original code untouched, I have changed the line:

if [ -x /usr/bin/tput ] && tput setaf 1 > /dev/null 2>&1

to a more plausible check:

if command -v tput > /dev/null 2>&1 && tput setaf 1 > /dev/null 2>&1
\$\endgroup\$
3
\$\begingroup\$

I'm a big fan of tput, which many script authors seem to overlook, for generating appropriate terminal escapes. Perhaps my enthusiasm started back in the early '90s, when using idiosyncratic (non-ANSI) terminals, but it still glows bright whenever I run a command in an Emacs buffer or output to a file.


A style point, on which you may disagree: I prefer not to enclose parameter names in braces unless it's required (for transforming the expansion, or to separate from an immediately subsequent word). So [ "$#" -ne 3 ] rather than [ "${#}" -ne 3 ], for example. The braces aren't wrong, but do feel unidiomatic there.


A simple improvement: you can save a lot of doubled-backslashes in the printf format strings by using single quotes rather than double quotes (this also protects you against accidentally expanding variables into the format string). I also find it easier to match arguments to format if I make judicious use of line continuation. Example:

    printf '%s%b()\n\n\t%s%b%s\n\nexit code = %b%s\n' \
           "$bold_yellow" "$2" \
           "$nocolor" "$3" "$bold_red" \
           "$1" "$nocolor" >&2

The definition of is_number isn't shown in your example function, but something similar is written in full here:

# check if the first argument is a number
# if not, print out an internal error without colors
if ! [ "${1}" -eq "${1}" ] 2> /dev/null
then

I think it makes sense to have is_number for that test; it would certainly reduce the need for comments:

if ! is_number "$1"
then    # print out an internal error without colors

It probably makes sense to redirect output to the error stream once at the beginning of print_error_and_exit, since we won't be generating any ordinary output from this point onwards:

print_error_and_exit()
{
    exec >&2

That saves the tedium of adding a redirect to every output command, and neatly avoids the easy mistake of missing one.


When we test that tput succeeds, perhaps we should tput sgr0 without redirecting to null, so that the output device is in a known state (thus killing two birds with one stone)?

If tput works at all, then when its argument can't be converted, its output is empty, which is fine. If we want to be really robust when tput doesn't even exist, then we could test like this:

tput sgr0 2>/dev/null || alias tput=true

Then we don't need a separate branch for the non-coloured output (we'll just output empty strings in the formatting positions). That won't quite work as I've written it, unless we specifically export the alias to sub-shells, but we can more conveniently just use a variable:

tput=tput
$tput sgr0 2>/dev/null || tput=true

We might also choose to test that the output is a tty (test -t 1, if we've done the exec I suggested, else test -t 2).


$bold_yellow saves no typing compared to $bold$yellow, and it's only used once anyway, so it can easily be eliminated. Same for $bold$red.


Modified code

Applying my suggestions, we get:

is_number()
{
    test "$1" -eq "$1" 2>/dev/null
}

print_error_and_exit()
# expected arguments:
# $1 = exit code
# $2 = error origin (usually function name)
# $3 = error message
{
    # all output to error stream
    exec >&2

    if [ "$#" -ne 3 ]
    then    # wrong argument count - internal error
        printf 'print_error_and_exit() internal error\n\n\tWrong number of arguments has been passed: %b!\n\tExpected the following 3:\n\t\t\$1 - exit code\n\t\t\$2 - error origin\n\t\t\$3 - error message\n\nexit code = 1\n' \
               "$#"
        exit 1
    fi

    if ! is_number "$1"
    then    # wrong argument type - internal error
        printf 'print_error_and_exit() internal error\n\n\tThe first argument is not a number: %b!\n\tExpected an exit code from the script.\n\nexit code = 1\n' \
               "$1"
        exit 1
    fi

    # if tput doesn't work, then ignore
    tput=tput
    test -t 1 || tput=true
    $tput sgr0 2>/dev/null || tput=true

    # colors definitions
    readonly bold=$($tput bold)
    readonly red=$($tput setaf 1)
    readonly yellow=$($tput setaf 3)
    readonly nocolor=$($tput sgr0)

    # highlight the error origin and the exit code
    printf '%s%b()\n\n\t%s%b%s\n\nexit code = %b%s\n' \
           "${bold}$yellow" "$2" \
           "$nocolor" "$3" "${bold}$red" \
           "$1" "$nocolor"

    exit "$1"
}
\$\endgroup\$
2
\$\begingroup\$

I largely re-written the error handling functions. The reasons were multiple. Mostly, I needed greater flexibility, reliability and even a bit more general-purpose use.


File func-color_support

First, I created this separate file for defining colors and determining if terminal colors are supported:

#!/bin/sh


# do not warn that variables appear unused
# this file is being sourced by other scripts
# link to wiki: https://github.com/koalaman/shellcheck/wiki/SC2034
# shellcheck disable=SC2034


### REQUIREMENTS
# none


### METHODS
## tput_colors_supported
# bool function - getter of the terminal color support
# if true, then this code defines global color constants


tput_colors_supported ()
{
    command -v tput && tput bold && [ "$(tput colors)" -ge 8 ]
} > /dev/null 2>&1


if tput_colors_supported; then

    # tput special sequences
    number_of_colors=$(tput colors)
    color_reset=$(tput sgr0)
    bold_text=$(tput bold)

    # definitions of basic colors with bold test
    color_red=${bold_text}$(tput setaf 1)
    color_green=${bold_text}$(tput setaf 2)
    color_yellow=${bold_text}$(tput setaf 3)
    color_blue=${bold_text}$(tput setaf 4)
    color_magenta=${bold_text}$(tput setaf 5)
    color_cyan=${bold_text}$(tput setaf 6)
    color_white=${bold_text}$(tput setaf 7)

else

    number_of_colors=0; color_reset=; bold_text=; color_red=; color_green=; color_yellow=; color_blue=; color_magenta=; color_cyan=; color_white=

fi
  • I would like to pinpoint determining the number of colors available:

    [ "$(tput colors)" -ge 8 ]
    
  • I was however puzzled by some older articles reporting that tput colors returns a bad number on their systems, so just be aware of it.

  • Additionally, notice that I redirected all output from the function to the black hole without using exec. I had some problems with it ever since, I recommend to avoid it, if possible.

  • Plus, note that I removed the readonly keyword, as it can cause problems when re-sourcing this file.

  • Finally, if you are curious as to why I define all of the empty variables in the else, that is because I use set -u in the main script.


The main functions

File func-print_error_and_exit_script

#!/bin/sh


### REQUIREMENTS
# func-color_support

. /path/to/func-color_support


### METHODS

## print_error
# prints custom heading and error message without exiting the script; in color, if available

## print_error_and_exit_script
# direcly uses print_error, but this one exits the script with customizable exit code (optional 3rd argument)


print_error ()
# expected arguments:
# $1 = error heading (string)
# $2 = error message (string)
{
    # check if exactly 2 non-empty arguments have been passed
    # if not, print out an input check error without colors
    if [ $# -ne 2 ] || [ -z "$1" ] || [ -z "$2" ]; then
        # $1 and $2 are to be shown literally in this message
        # link to wiki: https://github.com/koalaman/shellcheck/wiki/SC2016
        # shellcheck disable=SC2016
        printf 'print_error() input check\n\nWrong number or empty arguments have been passed to the function: %s\n\nExpected the following 2:\n$1 - error heading\n$2 - error message\n' $#
        return 1
    fi

    error_heading=$1
    error_message=$2

    printf '%b\n\n%b\n\n%b\n' \
        "Error heading: ${color_yellow}${error_heading}${color_reset}" \
        "Error message: ${error_message}" \
        "${color_red}Fatal error occurred.${color_reset}"
} >&2


print_error_and_exit_script ()
# expected arguments:
# $1 = error heading (string)
# $2 = error message (string)
# $3 = return code (number) - optional, if not given, 1 is used
{
    # check if 2 (or 3) non-empty arguments have been passed
    # if not, print out an input check error without colors
    if { [ $# -ne 2 ] && [ $# -ne 3 ]; } || [ -z "$1" ] || [ -z "$2" ]; then
        # $1, $2, $3 are to be shown literally in this message
        # link to wiki: https://github.com/koalaman/shellcheck/wiki/SC2016
        # shellcheck disable=SC2016
        printf 'print_error_and_exit_script() input check\n\nWrong number or empty arguments have been passed to the function: %s\n\nExpected the following 2 or 3:\n$1 - error heading (string)\n$2 - error message (string)\n$3 - return code (number) - optional, if not given, 1 is used\n' $#
        return 1
    fi

    if [ $# -eq 2 ] || [ -z "$3" ]; then
        return_code=1
    elif [ -n "$3" ] && ! [ "$3" -eq "$3" ] 2> /dev/null; then
        printf 'print_error_and_exit_script() input check\n\nThird argument is expected to be a return code, i.e. a number.\nBut something else was passed instead: %s\n' "$3"
        return 1
    else
        return_code=$3
    fi

    print_error "$1" "$2"

    exit "$return_code"

} >&2
  • Again, as explained above, I avoided exec, and redirected both functions' output with >&2.

  • Considering my need sometimes to only print error and not exit script, the job has been divided into two functions:

    • print_error: prints custom heading and error message without exiting the script; in color, if available

    • print_error_and_exit_script: direcly uses print_error, but this one exits the script with customizable exit code (optional 3rd argument)


Visual example

This is just so you see it in action, sudoedit_run is a function utilizing print_error.

sudoedit_run--errors

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.