1. Overview

When we create Bash scripts, we may want to redirect the output of all the echo statements into a log file without explicitly specifying the redirection operator and log file name after every echo statement. The Bash exec command is a powerful built-in utility that can be utilized for this purpose.

In this tutorial, we’ll take a look at how we can use the exec command for adding error and output logging to shell scripts. We’ll also explore other uses of this command within shell scripts.

2. The Basics

Whenever we run any command in a Bash shell, a subshell is created by default, and a new child process is spawned (forked) to execute the command. When using exec, however, the command following exec replaces the current shell. This means no subshell is created and the current process is replaced with this new command.

Let’s run an experiment to understand it better:

$ pstree -p
init(1)─┬─init(52)───bash(53)
        ├─init(78)───bash(79)───pstree(108)
        └─{init}(7)
$ echo $$
79
$ exec sleep 300

We checked the PID of the current shell using echo $$ and pstree commands and then executed a sleep of 300 seconds using exec. Let’s now switch to a different terminal to check the process list:

$ ps -aef | grep $USER
user1       53    52  0 23:37 tty2     00:00:00 -bash
user1       79    78  0 23:39 tty1     00:00:00 sleep 300
user1      111    53  0 23:40 tty2     00:00:00 ps -aef
user1      112    53  0 23:40 tty2     00:00:00 grep --color=auto 

As we can see, PID 79, which was initially assigned to Bash, is now assigned to the sleep command.

We’ll also observe that after 300-second sleep finishes, the session (terminal), which had PID 79, exited since the shell was replaced by exec command, and its execution has completed.

3. Process Replacement Using the exec Command

Overriding an existing process with a different command can be a powerful tool. Let’s explore this idea with some other examples.

3.1. User Login Profile

Let’s assume Bash is not the default shell of our Linux box. Interestingly, using exec command, we can replace the default shell in memory with the Bash shell by adding it to the user’s login profile:

exec bash

There are scenarios where we would want to add a specific program or a menu to the user’s login profile (.bashrc or .bash_profile), and in such cases, we can prevent the user to have Bash prompt access after the program exits irrespective of its exit status:

exec operations_menu.sh

3.2. Program Calls Within Scripts

We can call scripts or other programs within a script using exec to override the existing process in memory. This saves the number of processes created and hence the systems resources. This implementation is particularly useful in cases when we don’t want to return to the main script once the sub-script or program is executed:

#! /bin/bash

while true
do
   echo "1. Disk Stats "
   echo "2. Send Evening Report "
   read Input
   case "$Input" in
      1) exec df -kh ;;
      2) exec /home/SendReport.sh  ;;
   esac
done

In this simple user input driven script, we executed the df command and a script using exec within different menu options.

4. File Descriptors and Logging in Shell Scripts Using the exec Command

The exec command is a powerful tool for manipulating file-descriptors (FD), creating output and error logging within scripts with a minimal change. In Linux, by default, file descriptor 0 is stdin (the standard input), 1 is stdout (the standard output), and 2 is stderr (the standard error).

4.1. Logging Within Scripts

We can dynamically open, close, and copy stdout to achieve logging operations. Let’s redirect stdout (FD 1) to the log file:

#! /bin/bash
script_log="/tmp/log_`date +%F`.log"
exec 1>>$script_log
echo "This will be written into log file rather than terminal.."
echo "This too.."

We checked how we can write standard output to files, now let’s check how we can also write the standard error to the same file:

#! /bin/bash
script_log="/home/shubh/log_`date +%F`.log"
exec 1>>$script_log
exec 2>&1
datee
echo "Above command is wrong, error will be logged in log file"
date
echo "Output of correct date command will also be logged in log file, including these echo statements"

Here we copied the stderr (2) to stdout (1), and stdout was already changed to write into the log file.

4.2. Changing Stdin to Read From a File

Let’s create a sample input file:

$ cat input_csv
SNo,Quantity,Price,Value
1,2,20,40
2,5,10,50
3,1,70,70

Let’s now run an example to read from the file we created:

#! /bin/bash
exec < input_csv
read row1
echo -n "The contents of first line are: "
echo $row1
echo -n "The contents of second line are: "
read row2
echo $row2

And this is what we get:

The contents of first line are: SNo,Quantity,Price,Value
The contents of second line are: 1,2,20,40

4.3. Changing File Descriptors and Restoring the Defaults

We can also open and close new file descriptors for reading and writing from files. Let’s use the same input file as in the previous section to demonstrate this:

#! /bin/bash
exec 3< input_csv
read -u 3 row1
echo $row1
exec 3<&-
exec 4>&1
exec > out.txt
echo "Output will be logged in out.txt"
exec 4>&-

Here we first opened the input file on FD 3, read from the file, and printed its contents on the terminal (which defaults to stdout). Finally, we used &- to close FD 3. When This script executes with the following output:

SNo,Quantity,Price,Value

Additionally, the script also creates an output file out.txt:

$ cat out.txt
Output will be logged in out.txt

Note that the output of the script contains only the first echo statement. Interestingly, after the first echo statement, we copied stdout to FD 4 and redirected it to another file. Later, in the last line, we regained access to stdout by closing FD 4.

5. Running Scripts in a Clean Environment

We can reset all the environment variables for a clean run using the -c option:

exec -c printenv

As printenv command lists the environment variables, giving it as an argument to exec command here prints an empty output.

6. Usage With the Find Command

The exec option can be used to perform operations like grep, cat, mv, cp, rm, and many more on the files found by the find command. Let’s use an example from our article on the find command to find all .java files containing the word “interface” in the “src” directory:

find src -name "*.java" -type f -exec grep -l interface {} \;

Here we specified the option -type f  to find only the regular files. Thereafter, we used the -exec option to execute the grep command on the list of files returned by the find command. Note the semi-colon at the end causes the grep command to be executed for each file, one at a time, as the {} is replaced by the current file name. Note also a backslash is required to escape the semi-colon from being interpreted by the shell.

7. Conclusion

In this tutorial, we’ve seen the various usages of the Bash built-in exec command in shell scripts.

Simply put, it’s a powerful tool for process replacements. Specifically, its usage with file descriptors in scripts makes it one of the most powerful tools available for flexible script logging.