16 comments

[ 2.8 ms ] story [ 47.9 ms ] thread
I never thought of the idea of printing out a stack trace. A logging function is an example of such a good idea that is so obvious that I didn't think of it :-)

I use -e sometimes but I really dislike scripts that rely on it for all error handling instead of handling errors and logging them.

https://www.shellcheck.net/

^^ this tool has proven very useful for avoiding some of the most silly mistakes and making my scripts better. If you're maintaining scripts with other people then it is a great way of getting people to fix things without directly criticising them.

You can use bash-modules if you like stack traces in bash.
I've been writing scripts in Bourne shell since the 1980s and in Bash since whenever it came along, and I feel like the most important thing I've learned about it is: don't. Sure, it can be done, and even done well, but why? There are better languages.

Every time I write a shell script that grows to more than about 20 lines I curse myself for not having written it in Python. The longer I have waited before throwing it away and redoing it, the more I curse.

This article says nothing to change my mind. I could build logging and stack traces in Bash. I admire the author's ingenuity. But again, why?

I find that `bash -x` actially gives such a good trace that I rarely need anything else. Coupled with the ancient wisdom "bash for quick and dirty, once it gets too fancy switch to python", I use bash a lot and find it manages really well without external tools. Shoutouts to shellcheck though, for catching a lot of edge cases.
It is always helpful in recording if any bash behavior changes when it is not running in POSIX mode.

This is most common in Debian and Ubuntu, where ash is /bin/sh, and /bin/bash does not run in POSIX mode by default.

Some behavior of legacy bash of the '80s, prior to POSIX.2, can be surprising.

https://w3.pppl.gov/info/bash/Bash_POSIX_Mode.html

I had zero idea bash exposed the stack this way.. i'm utterly stumped.
Nice article. Do you have any shell debugging tip not listed in the article?
> I tend to skip the -u flag as bash scripts often interact with global variables that are set outside my scripts.

What? If globals are set outside the scripts, -u still works. If the author means they may or may not be defined outside the script, the ${VAR:-} construct allows it to expand to nothing if unset (just throw VAR=${VAR:-} at the top if you don't want to edit the body)

Also, I do not like the function return based on error code:

    function ... {
      ...
      (( check_level >= current_level ))
    }
Unless I'm reading this wrong, this is a bad idea if using set -e. This is a function and it should instead:

    return $(( check_level < current_level ))
> I tend to skip the -u flag as bash scripts often interact with global variables that are set outside my scripts.

That's throwing the baby out with the bathwater. Instead, default the optional global variables with something like:

    "${GLOBAL_VAR:-}"
That will satisfy the optionality of the variable whilst keeping the check for the cases you actually want them.
(comment deleted)
You can also skip the subshell invocation of date by using %(fmt)T from bash's printf:

  %(fmt)T -output the date-time string resulting from using FMT as a format string for strftime(3)
The man page provides a bit more detail:

  %(datefmt)T causes printf to output the date-time string resulting from using datefmt as a format string for strftime(3).  The corresponding argument  is  an  integer
  representing  the  number of seconds since the epoch.  Two special argument values may be used: -1 represents the current time, and -2 represents the time
  the shell was invoked.  If no argument is specified, conversion behaves as if -1 had been given.  This is an exception to the usual printf behavior.
With that,

    timestamp=$(date +'%y.%m.%d %H:%M:%S')
becomes

    printf -v timestamp '%(%y.%m.%d %H:%M:%S)T' -1
Curious that after 30 years the idea of a CPAN-like bash repository hasnt taken root. How many personal reimplementations of logging in bash are there by now?

I suppose what is really tripping people up is that bash can show up on all kinds of runtimes, some of which have the external tools one might need (jq, logger, etc) and some of which don't. So then you go searching for a minimum standard that can be expected to be present. Maybe POSIX or gnu coreutils. Reminds me of the shell horrors of the late 1990s where every script had to figure out if sh was really ksh and what variant of UNIX it was running on, and therefore what commands and options were available. I swear this was one of the great things about Perl when it came along, it just worked.

In 2025, I kind of see the attraction of single binaries like Go does. Ship the binary and be done. It is very un-UNIX I suppose (not so much golfing as having the beer cart drive you to the hole) but then again its not 1985 any more.

Why not leverage the bash 'caller' builtin? It's meant for printing stack traces, e.g.

    #!/bin/bash
    
    die() {
      local frame=0
      while caller $frame; do
        ((++frame));
      done
      echo "$*"
      exit 1
    }
    
    f1() { die "*** an error occured ***"; }
    f2() { f1; }
    f3() { f2; }
    
    f3


 Output
    
    
    12 f1 ./callertest.sh
    13 f2 ./callertest.sh
    14 f3 ./callertest.sh
    16 main ./callertest.sh
    *** an error occured ***
Via: https://bash-hackers.gabe565.com/commands/builtin/caller/