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
asked Oct 3, 2018 at 6:32
\$\endgroup\$

2 Answers 2

4
\$\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ドル"
}
answered Oct 3, 2018 at 11:19
\$\endgroup\$
0
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:\n1ドル - error heading\n2ドル - 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:\n1ドル - error heading (string)\n2ドル - error message (string)\n3ドル - 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

answered Sep 29, 2019 at 8:04
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.