Exec command in Linux: Replace Shells and Redirect Output

Exec command in Linux: Replace Shells and Redirect Output

In the redirection example, exec-output.log receives both output streams, then “back on terminal” appears on the terminal. I appreciate how the file descriptors make the shell’s output destinations visible.

You’ll connect that result to the scope of Bash’s builtin, rather than treating exec as a command that always launches a program. You can see whether Bash replaces its current process or keeps running with changed file descriptors.

What the exec command changes

In Bash, exec is a shell builtin with two distinct jobs. With a command argument, it replaces the current shell process with that program.

With redirections and no command, it changes the current shell’s file descriptors.

The kernel’s execve system call loads a program into an existing process, while Bash’s exec builtin requests that handoff from the shell. Bash’s builtin reference describes the shell form, and the execve manual page describes the kernel interface, not a shell command to type.

FormWhat changes
exec command argumentsThe current shell program is replaced by the requested program.
exec with redirections onlyThe current shell keeps running with its file descriptors changed.
find -exec commandThe find utility runs a command for matched paths. This is not the Bash builtin.

A normal shell command leaves the shell available for the next prompt. A successful exec handoff does not, so the process ID stays while the program running under it changes.

Python’s subprocess interface launches programs through a different API. See running Linux commands from Python for that workflow.

Before you run these examples

The samples use Bash and write log files in the current directory. Run them from a directory where you can create those files, and use a child script for process replacement so your interactive shell stays available.

Replace the shell with a command

When exec receives a command, Bash stops interpreting the current script and runs that program in the same process. The process ID remains the same, but the old shell cannot resume after the new program exits.

This script prints its Bash process ID, replaces Bash with another Bash process, then includes a line that should never run:

#!/usr/bin/env bash
printf 'before: PID %s\n' "$$"
exec bash -c 'printf "after: PID %s\n" "$$"'
printf 'This line does not run.\n'

Keep the code in exec-process-demo.sh. Bash must run it as a child so the test leaves your interactive shell available:

bash exec-process-demo.sh

I ran this script, and the PID printed before and after exec matched. The final printf did not appear.

Terminal showing the same PID before and after exec replaces the shell process
The process keeps the same PID, and the final printf is skipped after exec.

The test runs inside a script, so its exec replaces that script’s Bash process rather than the terminal’s interactive shell. If you type exec followed by a program directly at an interactive prompt, Bash hands that terminal session to the program instead.

Keep both output streams in a log

When exec appears without a command argument, its redirections change the file descriptors of the shell that evaluates them. Later commands inherit those destinations.

This example saves the current stdout and stderr destinations as descriptors 3 and 4. That preserves them before the script moves both streams to one file and restores them afterward.

#!/usr/bin/env bash
exec 3>&1 4>&2
exec > exec-output.log 2>&1
printf 'stdout: recorded\n'
printf 'stderr: recorded\n' >&2
exec 1>&3 2>&4 3>&- 4>&-
printf 'back on terminal\n'

Run the command below after saving the source as exec-redirect-demo.sh:

bash exec-redirect-demo.sh && printf '%s\n' '--- log ---' && cat exec-output.log
Terminal showing stdout and stderr recorded in a file before the console is restored
Bash sends both streams to the log, then restores the original terminal.
RedirectionEffect
> fileCreate the file or replace its current contents.
>> fileAppend output to the file.
Stderr duplication, 2>&1Point stderr at the destination stdout uses at that point.

I saw both streams in the log, and the final line returned to the terminal after the script restored the saved descriptors.

A truncating output redirect replaces the log’s contents, while the append form preserves earlier lines. See how shell redirection creates a file and how to read the saved log with cat.

Open and close a file descriptor

Descriptors 0, 1, and 2 conventionally represent standard input, standard output, and standard error. You can use another number for a separate file handle, then close it with a redirection ending in an ampersand and a hyphen.

This script writes through descriptor 3, closes it, and asks a child Bash to try the closed handle. The failed write is caught by the if statement, while the earlier line remains in the file:

#!/usr/bin/env bash
exec 3> descriptor-output.log
printf 'written through descriptor 3\n' >&3
exec 3>&-
if bash -c 'printf "this cannot use descriptor 3\\n" >&3' 2>/dev/null; then
    printf 'descriptor 3 is still open\n'
else
    printf 'descriptor 3 is closed\n'
fi

Save the test as exec-descriptor-demo.sh, then run it with this command:

bash exec-descriptor-demo.sh && cat descriptor-output.log
Terminal showing descriptor 3 closed and its earlier file output
The extra descriptor accepts a write, then reports closed after exec 3>&-.

I wrote through descriptor 3 before closing it, and its saved output stayed readable after the child Bash failed to write to that handle.

When a command cannot replace the shell

A successful exec never returns to the shell that called it. A failed exec has a different result, and Bash’s behavior depends on whether the shell is interactive or running a script.

  • A missing command produces status 127 in this example.
  • A non-interactive Bash exits after exec fails unless the execfail shell option is enabled.
  • Put a replacement test in a child script when you need the calling shell to remain available.

This wrapper records the child’s status after Bash reports that the requested command is missing:

#!/usr/bin/env bash
bash -c 'exec command-that-does-not-exist'
status=$?
printf 'non-interactive shell status: %s\n' "$status"

Save the wrapper as exec-failure-demo.sh. Run it with:

bash exec-failure-demo.sh
Terminal showing Bash could not find a command and reports status 127
A failed exec returns command-not-found status in a non-interactive child Bash.

Redirections can fail before the program starts too. If Bash cannot open the requested file, exec reports the redirection error instead of handing off control.

I got status 127 from the child Bash, while the wrapper completed with status 0. Its command-not-found diagnostic went to stderr.

Choose whether the shell should stay

Use exec with a command only when the current shell should hand control to that program and run nothing afterward. Use exec with redirections alone when the shell or script must keep running with different input or output destinations.

For the exact flags supported by your Bash, run its builtin help:

help exec

Frequently asked questions

What do the Bash exec options -c, -a, and -l do?

The -c option clears the environment passed to the command. The -a name option sets argv[0], and -l puts a dash before argv[0].

Is the exec command the same as find -exec?

find’s -exec action belongs to the find utility and runs a command for matched paths, while Bash exec changes shell descriptors or replaces the shell.

Can the old shell resume after exec finishes?

No, the shell that exec replaces has no commands left to resume. Use a child script when the parent shell must stay available.

What is the difference between exec and execve?

Bash’s exec builtin requests the handoff, and execve is the Linux system call that replaces the process image.