Home Bash scripting How To Debug Bash Scripts In Linux And Unix

How To Debug Bash Scripts In Linux And Unix

Debugging Bash Scripts

By Karthick
Published: Updated: 3.3K views

Debugging helps you to fix the errors in your program. In this article, we will discuss various methods to debug bash scripts in Linux and Unix operating systems.

Introduction

In my initial days of programming, I have spent hours trying to find the error in my code, and in the end, it might be something simple. You might have faced the same situation too.

Knowing how to use the proper debugging technique would help you solve the errors quickly. Unlike other languages like Python, Java, etc. there is no debugger tool for bash where you can set breakpoints, step over code, etc.

There are some built-in features that help to debug the bash shell scripts. We are going to see in detail those features in the upcoming sections.

Three ways to use Debugging options

When you want to enable debugging options in your scripts, you can do it in three ways.

1. Enable the debugging options from the terminal shell when calling the script.

$ bash [ debugging flags ] scriptname

2. Enable the debugging options by passing the debugging flags to the shebang line in the script.

#!/bin/bash [ debugging flags ]

3. Enable the debugging options by using the set command from the script.

set -o nounset
set -u

What is Set command used for?

The set command is a shell built-in command which can be used to control the bash parameters and alter bash behavior in certain ways.

Normally you will not run set commands from the terminal to alter your shell behavior. It will be widely used within shell scripts either for debugging or to enable bash strict mode.

$ type -a set
set is a shell builtin

You can access the help section of the set command to know what flags it supports and what each flag does.

$ set --help

Debug part of the script or full script

Before learning about the debugging options, you have to understand that you can either debug the entire script or just a certain part of the code. You have to use the set command to enable and disable the debugging options.

  • set -<debugging-flag> will enable the debug mode.
  • set +<debugging-flag> will disable the debug mode.

Take a look at the below code. set -x will enable the xtrace mode for the script and set +x will disable the xtrace mode. Anything that comes between set -x and set +x will be running in xtrace debugging mode.

You will learn about xtrace mode in the upcoming section. So for any debugging flag, the only thing you have to remember is, set - will enable the mode and set + will disable the mode.

#!/bin/bash

set -x
read -p "Pass Dir name : " D_OBJECT
read -p "Pass File name : " F_OBJECT
set +x

touch ${D_OBJECT}/${F_OBJECT}

Fail if no variable is defined

When working with variables in bash, the downside is if we try to use an undefined variable, the script will not fail with some error message like "Variable not defined". Instead it will print an empty string.

Take a look at the below code where I am getting input from the user and storing it in the variable $OBJECT. I tried to run the test operator (-f and -d) on the $OBJECT1 variable which is not defined.

#!/bin/bash

read -p "Please provide the object name :  " OBJECT
if [[ -f $OBJECT1 ]]
then
    echo "$OBJECT is a file"
elif [[ -d $OBJECT1 ]]
then
    echo "$OBJECT is a directory"
fi

When I run this code, it should have thrown me an error but it did not and even the script exited with return code zero.

Script Completed Successfully
Script Completed Successfully

To override this behavior, use the -u flag which will throw an error when an undefined variable is used.

I will run the same code again with the wrong variable name but this time it will throw an "Unbound variable" error.

Unbound Variable
Unbound Variable

You can also set the -u option using the set command or pass it as an argument to the shebang.

set -u
set -o nounset

(or)

#! /bin/bash -u

Xtrace mode to the rescue

This is the mode I use widely when I debug bash scripts for logical errors. Xtrace mode will display code line by line but with parameters expanded.

In the previous section when I ran the code without the -u flag, it got completed successfully but I was expecting the output in the terminal. Now I can run the same script in xtrace mode and see exactly where the issue is happening in the script.

Take a look at the following sample code.

#!/bin/bash

read -p "Please provide the object name :  " OBJECT
if [[ -f $OBJECT1 ]]
then
    echo "$OBJECT is a file"
elif [[ -d $OBJECT1 ]]
then
    echo "$OBJECT is a directory"
fi

When I run the above code, it returns me no output.

Logical Error
Logical Error

To debug this issue, I can run the script in xtrace mode by passing the -x flag.

In the below output, you can see the variables are expanded and printed out. This tells me that there are empty strings assigned to the conditional statements -f and -d. This way I can logically check and fix the errors.

Xtrace Mode
Xtrace Mode

The plus sign you see in the output can be changed by setting the PS4 variable in the script. By default, PS4 is set to (+).

$ echo $PS4
+
$ PS4=" ==> " bash -x debugging.sh
Set or change PS4 variable
Set or change PS4 variable

You can also set the Xtrace mode using the set command or pass it as an argument to the shebang.

set -x
set -o xtrace

(or)

#! /bin/bash -x

Similarly, when debugging you can redirect the Xtrace debug logs to a file instead of printing them to the terminal.

Take a look at the below code. I am assigning a file descriptor 6 to the .log file and BASH_XTRACEFD="6" will redirect the xtrace debug logs to file descriptor 6.

#!/bin/bash


exec 6> redirected_debug.log 
PS4=' ==> ' 
BASH_XTRACEFD="6" 
read -p "Please provide the object name :  " OBJECT
if [[ -f $OBJECT1 ]]
then
    echo "$OBJECT is a file"
elif [[ -d $OBJECT1 ]]
then
    echo "$OBJECT is a directory"
fi

When I run this code instead of printing the xtrace output in the terminal, it will be redirected to the .log file.

$ cat redirected_debug.log 
==> read -p 'Please provide the object name :  ' OBJECT
==> [[ -f '' ]]
==> [[ -d '' ]]

PIPE exit status

The default behavior when using a pipe is it will take the exit code of the last run command in the pipe. Even if the previous commands in the pipe failed, it will run the rest of the pipe.

Take a look at the below example. I tried opening a file that is not available and piping it with a word count program. Even though the cat command throws an error, the word count program runs.

If you try to check the exit code of the last run pipe command using $?, you will get zero as the exit code which is from the word count program.

$ cat nofile.txt | wc -l
cat: nofile.txt: No such file or directory
0
$ echo $?
0

When pipefail is enabled in the script, if any command throws a non-zero return code in the pipe, it will be considered as the return code for the entire pipeline. You can enable pipefail by adding the following set property in your script.

set -o pipefail
PIPE Exit status
PIPE Exit status

There is still a problem with this approach. Normally what should be expected is if any command in the pipe is failed, then the script should exit without running the rest of the command in the pipe.

But unfortunately, even if any command fails the subsequent command in the pipe runs. This is because each command in the pipe runs in its own subshell. The shell will wait till all the processes in the pipe get completed then return the result.

Bash Strict Mode

To eliminate all the possible errors we have seen in previous sections it is recommended to add the following options in every script.

We have already discussed all these options in the previous section in detail.

  • -e flag => Exit the script if any command throws Non-Zero exit code.
  • -u flag => Make the script fail if an undefined variable name is used.
  • pipefail => If any command in the pipeline fails then the exit code will be considered for the entire pipeline.
  • IFS => Internal field separator, setting it to newline(\n) and (\t) will make the split happen only in newline and tab.
set -e
set -u
set -o pipefail

Or

set -euo pipefail
IFS=$'\n\t'

Capture signals using TRAP

Trap allows you to capture signals to your bash script and take some actions accordingly.

Think of a scenario where you trigger the script but you want to cancel the script using CTRL+C keystroke. In that case, SIGINT will be sent to your script. You can capture this signal and run some commands or functions.

Take a look at the pseudo code given below. I have created a function called cleanup which will run when SIGINT is passed to the script.

trap 'cleanup' TERM INT
function cleanup(){
    echo "Running cleanup since user initiated CTRL + C"
    <some logic>
}

You can use trap "DEBUG", which can be used to execute a statement repeatedly in the script. The way it behaves is for each statement that runs in the script trap will run the associated function or statement.

You can understand this using the below example.

#!/bin/bash

trap 'printf "${LINENO} ==> DIR_NAME=${D_OBJECT} ; FILE_NAME=${F_OBJECT}; FILE_CREATED=${FILE_C} \n"' DEBUG

read -p "Pass Dir name : " D_OBJECT
read -p "Pass File name : " F_OBJECT

touch ${D_OBJECT}/${F_OBJECT} && FILE_C="Yes"
exit 0

This is a simple program that gets user input and creates a file and a directory. The trap command will run for each statement in the script and will print the passed arguments and file creation status.

Check out the below output. For each line in the script, the trap is triggered and the variables are updated accordingly.

Running TRAP for each statement
Running TRAP for each statement

In verbose mode, the code will be printed out before returning the result. If the program requires an interactive input in that case that line will alone be printed followed by a block of codes.

Take a look at the following program. It is a simple program that gets an object from the user and checks if the passed object is a file or directory using a conditional statement.

#!/bin/bash

read -p "Please provide the object name :  " OBJECT
if [[ -f $OBJECT ]]
then
  echo "$OBJECT is a file"
elif [[ -d $OBJECT ]]
then
  echo "$OBJECT is a directory"
fi

When I run the above code, first it will print the code then wait for user input like shown below.

Verbose Mode
Verbose Mode

Once I passed the object, then the rest of the code will be printed followed by the output.

Print The Code Using Verbose Mode
Print The Code Using Verbose Mode

You can also set the verbose mode using set or in shebang.

set -v
set -o verbose

(or)

#! /bin/bash -v

You can also combine verbose mode with other modes.

set -vx # Verbose and Xtrace Mode
set -uv # Verbose and Unset Mode

Syntax validation - noexec mode

Till now we have seen how to work with logical errors in the script. In this section, let’s discuss about syntax errors.

Syntax errors are very common in programs. You might have missed a quote or failed to exit the loop etc. You can use the "-n" flag which is called noexec mode to validate the syntax before running the program.

I am going to run the below piece of code and validate the syntax.

#!/bin/bash

TOOLS=( htop peek tilix vagrant shutter )
for TOOL in "${TOOLS[@]" 
do
    echo "--------------INSTALLING: ${TOOL}---------------------------"
    apt install ${TOOL} -y
#done

There are two errors in this program. Firstly, I failed to close the curly braces in the "for loop" and secondly done keyword is commented out which should mark the end of the loop.

When I run this program, I get the following error messages pointing to missing curly bracket and done keyword. Sometimes the line number that is pointed in the error message will not contain any errors you should dig around to find the actual error.

$ bash -n ./debugging.sh 

./debugging.sh: line 6: unexpected EOF while looking for matching `"'
./debugging.sh: line 8: syntax error: unexpected end of file

It is to be noted that, by default when you run the script, bash will validate the syntax and throw these errors even without using noexec mode.

Alternatively, you can also use the set command or shebang to use the noexec mode.

set -n 
set -o noexec

Or,

#! /bin/bash -n

There are a few external tools out there worth taking a look at used for debugging the scripts. One such tool is Shellcheck. Shellcheck can also be integrated with popular text editors like vscode, sublime text, Atom.

Conclusion

In this article, I have shown you some of the ways to debug bash scripts. Unlike other programming languages, bash does not have debug tools other than some built-in options. Sometimes these built-in debugging options will be more than enough to get the job done.

You May Also Like

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

This website uses cookies to improve your experience. By using this site, we will assume that you're OK with it. Accept Read More