BreakingExpress

Repair bugs in Bash scripts by printing a stack hint

No one needs to put in writing unhealthy code, however inevitably bugs will probably be created. Most fashionable languages like Java, JavaScript, Python, and so forth., robotically print a stack hint once they encounter an unhandled exception, however not shell scripts. It would make it a lot simpler to search out and repair bugs in shell scripts for those who may print a stack hint, and, with slightly work, you’ll be able to.

Shell scripts can span a number of recordsdata, and well-written code is additional damaged down into capabilities. Tracing points when one thing goes fallacious in a shell script might be troublesome when these scripts get giant sufficient. A stack hint that walks the code backward from the error to the start can present you the place your code failed and offer you a greater understanding of why so you’ll be able to repair it correctly.

To implement the stack hint, I exploit the trap within the following method initially of my script:

set -E

entice 'ERRO_LINENO=$LINENO' ERR
entice '_failure' EXIT

This instance accomplishes just a few issues, however I’ll tackle the second, entice ‘ERRO_LINENO=$LINENO’ ERR, first. This line ensures the script traps all instructions that exit with a non-zero exit code (i.e., an error), and saves the road variety of the command within the file the place the error was signaled. This will not be captured on exit.

The first line above (set -E) ensures that the error entice is inherited all through the script. Without this, everytime you drop into an if or till block, for instance, you’d lose monitor of the proper line quantity.

The second entice captures the exit sign from the script and sends it to the _failure operate, which I’ll outline in a second. But why on exit and never error for those who’re making an attempt to debug the script? In bash scripts, command failures are sometimes utilized in management logic or might be ignored outright as unimportant by design. For instance, say initially of your script, you are seeking to see if a selected program is already put in earlier than asking the person whether or not they’d such as you to put in it for them:

if [[ ! -z $(command -v some_command) ]]
then
   # CAPTURE LOCATION OF some_command
   SOME_COMMAND_EXEC=$(which some_command)
else
   # echo $? would give us a non-zero worth right here; i.e. an error code
   # IGNORE ERR: ASK USER IF THEY WANT TO INSTALL some_command
fi

If you had been to cease processing on each error and some_command will not be put in, this might prematurely finish the script, which is clearly not what you wish to do right here, so typically, you solely wish to log an error and stack hint when the script has exited unintentionally due to an error.

To drive your script to exit at any time when there’s an surprising error, use the set -e choice:

set -e
# SCRIPT WILL EXIT IF ANY COMMAND RETURNS A NON-ZERO CODE
# WHILE set -e IS IN FORCE
set +e
# COMMANDS WITH ERRORS WILL NOT CAUSE THE SCRIPT TO EXIT HERE

The subsequent query is, what are some examples the place you’ll most likely like your script to exit and spotlight a failure? Common examples embody the next:

  1. An unreachable distant system
  2. Authentication to a distant system fail
  3. Syntax errors in config or script recordsdata being sourced
  4. Docker picture builds
  5. Compiler errors

Combing by way of many pages of logs after a script completes in search of any attainable errors which may be arduous to identify might be extraordinarily irritating. It’s much more irritating once you uncover one thing is fallacious long gone the time you ran the script and now need to comb by way of a number of units of logs to search out what might need gone fallacious and the place. Worst is when the error has been round for some time, and also you solely uncover it on the worst attainable time. In any case, pinpointing the issue as shortly as attainable and fixing it’s at all times the precedence.

Look on the pattern stack hint code (out there for download here):

# Sample code for producing a stack hint on catastrophic failure

set -E

entice 'ERRO_LINENO=$LINENO' ERR
entice '_failure' EXIT

_failure() {
  ERR_CODE=$? # seize final command exit code
  set +xv # turns off debug logging, simply in case
  if [[  $- =~ e && ${ERR_CODE} != 0 ]]
  then
      # solely log stack hint if requested (set -e)
      # and final command failed
      echo
      echo "========= CATASTROPHIC COMMAND FAIL ========="
      echo
      echo "SCRIPT EXITED ON ERROR CODE: ${ERR_CODE}"
      echo
      LEN=${#BASH_LINENO[@]}
      for (( INDEX=0; INDEX<$LEN-1; INDEX++ ))
      do
          echo '---'
          echo "FILE: $(basename ${BASH_SOURCE[${INDEX}+1]})"
          echo "  FUNCTION: ${FUNCNAME[${INDEX}+1]}"
          if [[ ${INDEX} > 0 ]]
          then
           # instructions in stack hint
              echo "  COMMAND: ${FUNCNAME[${INDEX}]}"
              echo "  LINE: ${BASH_LINENO[${INDEX}]}"
          else
              # command that failed
              echo "  COMMAND: ${BASH_COMMAND}"
              echo "  LINE: ${ERRO_LINENO}"
          fi
      executed
      echo
      echo "======= END CATASTROPHIC COMMAND FAIL ======="
      echo
  fi
}

# set working listing to this listing for length of this take a look at
cd "$(dirname ${0})"

echo 'Beginning stacktrace take a look at'

set -e
supply ./testfile1.sh
supply ./testfile2.sh
set +e

_file1_function1

In the stacktrace.sh above, the very first thing the _failure operate does is seize the exit code of the final command utilizing the built-in shell worth $?. It then checks whether or not the exit was surprising by checking the output of $-, a built-in shell worth that holds the present bash shell settings, to see if set -e is in drive. If the script exited on an error and the error was surprising, the stack hint is output to the console.

The following built-in shell values are used to construct the stack hint:

  1. BASH_SOURCE: Array of filenames the place every command was referred to as again to the principle script.
  2. FUNCNAME: Array of line numbers matching every file in BASH_SOURCE.
  3. BASH_LINENO: Array of line numbers per file matching BASH_SOURCE.
  4. BASH_COMMAND: Last command executed with flags and arguments.

If the script exits with an error in an surprising method, it loops over the above variables and outputs each so as so a stack hint might be constructed. The line variety of the failed command will not be held within the above arrays, however that is why you captured the road quantity every time a command failed with the primary entice assertion above.

Putting all of it collectively

Create the next two recordsdata to help the take a look at, so you’ll be able to see how the data is gathered throughout a number of recordsdata. First, testfile1.sh:

_file1_function1() {
   echo
   echo "executing in _file1_function1"
   echo

   _file2_function1
}

# adsfadfaf

_file1_function2() {
   echo
   echo "executing in _file1_function2"
   echo
 
   set -e
   curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

   # operate by no means referred to as
   _file2_does_not_exist
}

And subsequent, testfile2.sh:

_file2_function1() {
   echo
   echo "executing in _file2_function1"
   echo

   curl this_curl_will_simply_fail

   _file1_function2
}

NOTE: If you create these recordsdata your self, be certain to make the stacktrace.sh file executable.

Executing stacktrace.sh will output the next:

~/shell-stack-trace-example$./stracktrace.sh
Beginning stacktrace take a look at

executing in _file1_function1

executing in _file2_function1
curl: (6) Could not resolve host: this_curl_will_simply_fail

executing in _file1_function2
curl: (6) Could not resolve host: this_curl_will_fail_and_CAUSE_A_STACK_TRACE

========= CATASTROPHIC COMMAND FAIL =========

SCRIPT EXITED ON ERROR CODE: 6

---
FILE: testfile1.sh
  FUNCTION: _file1_function2
  COMMAND: curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE
  LINE: 15
---
FILE: testfile2.sh
  FUNCTION: _file2_function1
  COMMAND: _file1_function2
  LINE: 7
---
FILE: testfile1.sh
  FUNCTION: _file1_function1
  COMMAND: _file2_function1
  LINE: 5
---
FILE: stracktrace.sh
  FUNCTION: major
  COMMAND: _file1_function1
  LINE: 53

======= END CATASTROPHIC COMMAND FAIL =======

For further credit score, strive uncommenting the road in testfile1.sh and executing stacktrace.sh once more:

# adsfadfaf

Then re-comment the road, and as a substitute remark out the next line in testfile1.sh that induced a stack hint and run stacktrace.sh one final time:

curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

This train ought to offer you an thought of the output and when it happens if in case you have typos in your scripts.

Exit mobile version