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 return–and-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.