1. Overview

A good Bash script usually performs some checking before executing the commands it contains. For example, a script that executes administrative commands should check if the script is called by the root user or with sudo access. If a less-privileged user calls the script, it should immediately call the exit command to stop its execution. However, is it that simple?

In this tutorial, we will see why the exit command is not the best way of exiting from Bash scripts. Although it is indeed the most straightforward method, unfortunately, it is not the safest one. Here, we’ll explore other ways.

2. Two Methods of Running a Bash Script

One of the common methods of running a Bash script is to execute it from a shell session by calling the script filename prefixed with its filesystem path. For example, to execute a script with the filename of myscript.sh located at the current working directory, we prefix the call with a “./”:

$ ./myscript.sh

There is another way of running the script, which is sourcing it by prefixing the script call with the source keyword:

$ source ./myscript.sh

Alternatively, we can also use a dot in the place of the source keyword:

$ . ./myscript.sh

2.1. The Difference Between Executing and Sourcing a Bash Script

When we execute a script, Linux spawns a child process of our current shell session and executes myscript.sh in it. Our shell process will block while waiting for the script child process to exit unless it is executed in the background.

Unlike executing a script, sourcing it will not spawn a child process. Instead, the commands inside the scripts will be executed directly in the current process at which the sourcing happens.

Let’s see some proof.

First, we’ll create a simple script myscript.sh that simply calls the ps command:

#!/bin/bash
ps -f

Next, let’s make it executable before executing it:

$ chmod +x myscript.sh
$ ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+   149   148  0 14:27 tty1     00:00:00 -bash
mlukman+   262   149  0 14:45 tty1     00:00:00 /bin/bash ./myscript.sh
mlukman+   263   262  0 14:45 tty1     00:00:00 ps -f

In the above example, the current shell process has a PID of 149. The execution of my script.sh spawned the Bash child process with a PID of 262. That process, in turn, executed the ps command as a child process with a PID of 263.

Now, let’s try sourcing the script:

$ source ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+   149   148  0 14:27 tty1     00:00:00 -bash
mlukman+   264   149  0 14:59 tty1     00:00:00 ps -f

As we can see, the process for the ps command call was a direct child of the current shell process. There was no intermediate Bash child process of the shell session.

2.2. The Behaviour of the exit Command When Executing vs. When Sourcing

Moving on, let’s see what happens if we have an exit command in the script:

#!/bin/bash
ps -f
exit
echo We should not see this

Executing the script provides the same result as before:

$ ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  1 15:05 tty1     00:00:00 -bash
mlukman+    25    10  0 15:06 tty1     00:00:00 /bin/bash ./myscript.sh
mlukman+    26    25  0 15:06 tty1     00:00:00 ps -f

However, sourcing behaves very differently from before.

The shell session will terminate. Since sourcing a script executes its commands directly in the current process, the exit command in the script terminates the current process, which is the shell session.

The lesson we can take from that little experiment is that the exit command is not always safe to be used inside a Bash script. We’ll need to use some tricks to safely exit from a script.

3. The return Command

To exit from a sourced script, we’ll need a return command instead of the exit command:

#!/bin/bash
ps -f
return
echo We should not see this

If we source the script, we’ll get the same result as the version of the script with just the ps command:

$ source ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  0 15:05 tty1     00:00:00 -bash
mlukman+    28    10  0 21:09 tty1     00:00:00 ps -f

However, executing it will throw an error:

$ ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  0 15:05 tty1     00:00:00 -bash
mlukman+    29    10  0 21:24 tty1     00:00:00 /bin/bash ./myscript.sh
mlukman+    30    29  0 21:24 tty1     00:00:00 ps -f
./myscript.sh: line 3: return: can only `return' from a function or sourced script
We should not see this

As stated in the error message, the return command is only allowed in a function or sourced script and not allowed when executing a script.

3.1. The returnand-exit Combo

To make the script compatible with both executing and sourcing, we have to combine both the return command and the exit command.

First, we make the return command to not output an error message by piping STDERR to /dev/null:

return 2> /dev/null

Next, we add the exit command:

return 2> /dev/null; exit

Applying this line into our myscript.sh:

#!/bin/bash
ps -f
return 2> /dev/null; exit
echo We should not see this

Finally, let’s try both executing and sourcing the script:

$ ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  0 15:05 tty1     00:00:00 -bash
mlukman+    35    10  0 21:31 tty1     00:00:00 /bin/bash ./myscript.sh
mlukman+    36    35  0 21:31 tty1     00:00:00 ps -f
$ source ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  0 15:05 tty1     00:00:00 -bash
mlukman+    37    10  0 21:31 tty1     00:00:00 ps -f

Voila! Our script now works as intended in both methods of calling. But are we done? Not really.

3.2. Incompatibility With Complex Scripts

Remember the error message that appeared when executing the script with the return command?

./myscript.sh: line 3: return: can only `return' from a function or sourced script

Interestingly, the error message mentioned “function”. Are we not curious to see what will happen if we use the return-and-exit combo method in a function? Let’s move the return-and-exit combo line into a function and call that function instead:

#!/bin/bash

exitscript () {
  return 2> /dev/null; exit
}

ps -f
exitscript
echo We should not see this

Next, we’ll see whether or not the script still works:

$ ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  0 15:05 tty1     00:00:00 -bash
mlukman+    40    10  0 21:43 tty1     00:00:00 /bin/bash ./myscript.sh
mlukman+    41    40  0 21:43 tty1     00:00:00 ps -f
We should not see this
$ source ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  0 15:05 tty1     00:00:00 -bash
mlukman+    42    10  0 21:43 tty1     00:00:00 ps -f
We should not see this

Unfortunately, as made apparent from the echoing of the “We should not see this” line, the return-and-exit combo method no longer works if called from inside a function.

4. The kill Command

The return-and-exit combo trick we explored in the previous sections works in both executing and sourcing methods. However, it fails miserably when used in a function. We need to find a new trick.

The trick we are going to try here seems a little scary. It is the kill command.

In particular, we are going to make the script kill itself. Well, not really. We just make it interrupt itself using the kill command:

kill -SIGINT $$

The SIGINT option tells the kill command to send an interrupt signal to the PID that is referred to by $$, which is the current process that runs the command. Similar to how CTRL+C works, sending an interrupt signal just stops whatever is still running and queued in the current process, regardless of whether it is the shell session process or its Bash child process.

Let’s implement it into the script, replacing the return-and-exit combo line:

#!/bin/bash

exitscript () {
  kill -SIGINT $$
}

ps -f
exitscript
echo We should not see this

Let’s test the modified script:

$ ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  0 Feb19 tty1     00:00:00 -bash
mlukman+   113    10  0 07:01 tty1     00:00:00 /bin/bash ./myscript.sh
mlukman+   114   113  0 07:01 tty1     00:00:00 ps -f

$ source ./myscript.sh
UID        PID  PPID  C STIME TTY          TIME CMD
mlukman+    10     9  0 Feb19 tty1     00:00:00 -bash
mlukman+   115    10  0 07:01 tty1     00:00:00 ps -f

Apart from the additional empty line, the script now works, showing that the kill command successfully exits the script from within a function. However, a side effect of using this method is that the script execution is stopped abruptly and thus suppresses it from properly returning an exit code. Therefore, we should limit using this method only if we need to exit from within a function and do not need the exit code.

5. Conclusion

In this article, we’ve demonstrated the difference between executing and sourcing a Bash script and how we cannot simply use the exit command to exit the script. We’ve explored two methods: the return-and-exit combo method and the interrupt method. We recommended using the return-and-exit combo method unless we need to exit from within a function, in which case we need to resort to the interrupt method.