Generic error handler function for POSIX shell scripts 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)



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&gt;&amp;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&gt; /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&gt;&amp;2
        exit 1
    fi

    # check if we have color support
    if [ -x /usr/bin/tput ] &amp;&amp; tput setaf 1 &gt; /dev/null 2&gt;&amp;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&gt;&amp;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&gt;&amp;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 ] &amp;&amp; tput setaf 1 &gt; /dev/null 2&gt;&amp;1


to a more plausible check:

if command -v tput &gt; /dev/null 2&gt;&amp;1 &amp;&amp; tput setaf 1 &gt; /dev/null 2&gt;&amp;1
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 [ &quot;$#&quot; -ne 3 ] rather than [ &quot;${#}&quot; -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' \
           &quot;$bold_yellow&quot; &quot;$2&quot; \
           &quot;$nocolor&quot; &quot;$3&quot; &quot;$bold_red&quot; \
           &quot;$1&quot; &quot;$nocolor&quot; &gt;&amp;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 ! [ &quot;${1}&quot; -eq &quot;${1}&quot; ] 2&gt; /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 &quot;$1&quot;
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 &gt;&amp;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&gt;/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&gt;/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 &quot;$1&quot; -eq &quot;$1&quot; 2&gt;/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 &gt;&amp;2

    if [ &quot;$#&quot; -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' \
               &quot;$#&quot;
        exit 1
    fi

    if ! is_number &quot;$1&quot;
    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' \
               &quot;$1&quot;
        exit 1
    fi

    # if tput doesn't work, then ignore
    tput=tput
    test -t 1 || tput=true
    $tput sgr0 2&gt;/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' \
           &quot;${bold}$yellow&quot; &quot;$2&quot; \
           &quot;$nocolor&quot; &quot;$3&quot; &quot;${bold}$red&quot; \
           &quot;$1&quot; &quot;$nocolor&quot;

    exit &quot;$1&quot;
}