“if grep”, the bomb

Take 2

This is version two of the post, based on the feedback in this Reddit discussion. Some things need clarification. That includes the title which should be something like

Why NGS has more ergonomic error handling than bash

Background

I was using bash and Python for “DevOps” scripting. Frustrated by both, I’ve started working on Next Generation Shell (NGS) in 2013. I was annoyed by bash’s lack of data structures and error handling on one side and Pythons clumsiness when running external processes and small scale data manipulation.

Error Handling in NGS

NGS uses exceptions, as many other languages. This allows “safely ignoring” errors in small scripts in which case they just terminate on errors, somewhat like set -e in bash turned on all the time.

External Processes Error Handling in NGS

NGS introduces a unique feature (to the best of my knowledge) for error handling. When running external processes, it looks at exit codes and throws exceptions when errors occur.

Compared to set -e behavior in bash:

  • The feature works uniformly. In bash, if, while and until are exceptions to the set -e: the script doesn’t exit if an error occurs in the condition.
  • For known programs, NGS knows what are the error exit codes. For grep for example, it’s 2 but not 1.
  • For unknown programs, non-zero is an error (conservative approach)
  • The behavior is easily overridable on per-program and on per-execution basis.

I would like to compare NGS vs bash code for (roughly) equivalent things.

Comparison 1:

# NGS
if $(grep PATTERN FILE) {
  THEN
} else {
  ELSE
}
# bash - adapted from u/rustyflavor
if grep PATTERN FILE; then
  THEN
else
  if [[ $? -gt 1 ]];then
    echo "Executing external process 'grep' failed" >/dev/stderr
    echo "Command: grep PATTERN FILE" >/dev/stderr
    # and outputs backtrace
    exit 240
  fi
  ELSE
fi

Comparison 2:

# NGS
# "ok:" - any exit code is OK, not an exception
# Additional examples of "ok:"
#   ok:[0,1,5,7]
#   ok:0..20
if $(ok: grep PATTERN FILE) {
  THEN
} else {
  ELSE
}
# bash
if grep PATTERN FILE; then
  THEN
else
  # Not found or an error
  ELSE
fi

It is more semantically correct behavior to treat errors separately. Treating boolean false and errors coming from external processes in exactly the same way (like in the last example) can in some cases lead to undesirable behavior.

Take 2 was written on 2023-11-15

Edit: there is a thought to create a schema for external programs which would include exit codes, including which exit codes are errors. It could potentially be shared between all shells and anyone else who wishes to run external programs.

Why Compare bash and NGS?

They have intersection of problems they solve, including ergonomically running external processes (scripting). Therefore for this intersection, NGS is more or less viable alternative to bash (depending on your situation). At work, we have the freedom and the situation allows to use NGS instead of bash (almost everywhere).

Take 1

Keeping “Take 1” here for historical reasons.

Error handling in bash has the reputation. I would like to highlight how we collectively became numb to the issue so that we ignore the bombs in plain sight.

By show of hands, who have seen bash script with the following piece of code?

if grep SOME_PATTERN SOME_FILE;then
  # found the thing! all good
else
  # didn't find the thing! nuke from the orbit!
fi

“if” has two branches (then and else). As many other programs, grep can return more than two different exit codes. In case of grep it’s zero for boolean true, one for boolean false, and two for syntax or other error. How two branches work with more than two different exit codes? Not very well, as you might have guessed.

In many programming languages there are obvious solutions to this problem. Not in bash. I mean I would probably have seen it by now, right? Nope, scripts are full of if grep, waiting for the day when they explode because somebody moved the file for example.

Solving N > 2 different exit codes in bash is left as an exercise to the reader. Note that the simple solution of storing exit code in a variable and checking it later with additional ifs is not only verbose but has another problem: what if you have syntax error in that if? set -e to the rescue? Looking forward for creative solutions here.

If you run $(grep ...) in NGS, you are pretty safe. Boolean exit codes (0 and 1) are interpreted as expected in the shell context while error exit code (2) coming from grep is converted to exception, aligned with the rest of the language.


Happy coding, hopefully not in bash, you know, for your mental health.

Leave a comment