opscape

Shell builtins · bash builtin

exec

Replace the shell with another program, or, with only redirections, change where the shell's own input and output go.

exec [-cl] [-a NAME] [COMMAND [ARGUMENT...]] [REDIRECTION...]

10 of 10 examples run in Debian 13.7 on 2026-10-31 · manual: bash(1), SHELL BUILTIN COMMANDS

exec has two distinct uses. Given a command, it replaces the current shell process with that program: same PID, no fork, and nothing after the exec line ever runs, because the shell is gone. Wrapper scripts and container entrypoints end with exec "$@" or exec program so the real program receives signals directly and no idle shell is left waiting.

Given only redirections, exec runs nothing and instead applies them to the current shell permanently. exec >log 2>&1 sends everything the script prints from then on into a file; exec 3<file opens file descriptor 3 for reading so a loop can use it; exec 3>&- closes it. This is the cleanest way to log a whole script without wrapping every command.

If the program cannot be found, a non-interactive shell exits (status 127) rather than carrying on, unless the execfail shell option is set.

Examples

Replace the shell with another program

echo "shell pid: $$"
exec bash -c 'echo "new program pid: $$"'
echo "this line never runs"
output
shell pid: 728
new program pid: 728

The PID stays the same: exec did not start a child, it turned the current process into the new program. When that program exits, there is no shell to return to, so the last echo never runs.

End a wrapper script with exec

run-greet.sh
#!/usr/bin/env bash
# set up the environment, then hand over to the real program
export GREETING_STYLE=formal
cd "$(dirname "$0")/scripts" || exit 1
exec ./greet.sh "$@"
bash run-greet.sh Alice
echo "wrapper exit status: $?"
output
Hello, Alice!
wrapper exit status: 0

The wrapper prepares the environment and directory, then exec replaces itself with the program, passing the arguments through with "$@". The exit status is the program's own. In containers this makes the program PID 1 so it receives docker stop's signal.

Send the rest of a script's output to a log file

job.sh
#!/usr/bin/env bash
echo "starting, details in run.log"
exec >run.log 2>&1
echo "step 1 done"
ls missing-file
echo "step 3 done"
bash job.sh
echo "--- run.log:"
cat run.log
output
starting, details in run.log
--- run.log:
step 1 done
ls: cannot access 'missing-file': No such file or directory
step 3 done

Only the first line reaches the terminal. After exec >run.log 2>&1, every later command in the script writes both stdout and stderr to run.log, including the error from ls. The redirection lasts until the script ends; the calling shell is not affected.

Redirect temporarily and restore the original output

exec 3>&1
exec >capture.txt
echo "written to the file"
exec 1>&3 3>&-
echo "back on the terminal"
echo "file contains: $(cat capture.txt)"
output
back on the terminal
file contains: written to the file

exec 3>&1 saves the current stdout as descriptor 3. After redirecting stdout to the file, exec 1>&3 3>&- restores it and closes the spare descriptor.

Log to a file and the terminal at the same time

exec > >(tee build.log) 2>&1
echo "compiling"
echo "warning: unused variable" >&2
echo "done"
sleep 0.2
echo "--- log has $(wc -l < build.log) lines"
output
compiling
warning: unused variable
done
--- log has 3 lines

Process substitution starts tee, and exec points stdout and stderr at it, so output still appears on screen and is also saved in build.log. tee runs asynchronously; the short sleep lets it catch up before the log is counted.

Open a file on a descriptor and read from it

exec 3< data/hosts.txt
read -r first <&3
read -r second <&3
exec 3<&-
echo "1: $first"
echo "2: $second"
output
1: web01 10.0.0.11
2: web02 10.0.0.12

`exec 3

Keep a file open for appending

exec 4>>events.log
for i in 1 2 3; do echo "event $i" >&4; done
exec 4>&-
cat events.log
output
event 1
event 2
event 3

Opening the log once with 4>> and writing with >&4 avoids reopening the file on every iteration, which matters in long loops.

A failed exec ends a script

exec no-such-program
echo "never printed"
outputexit 127
/bin/bash: line 1: exec: no-such-program: not found

In a non-interactive shell, an exec whose program cannot be found ends the shell with status 127. shopt -s execfail makes it return an error instead.

Set the program's name with -a

exec -a my-worker bash -c 'echo "\$0 is $0"; ps -o pid,args -p $$ | tail -n 1'
explain
output
$0 is my-worker
    745 my-worker -c echo "\$0 is $0"; ps -o pid,args -p $$ | tail -n 1

-a NAME sets the argv[0] the program sees, which is what ps shows. Some programs change behavior based on it; it is also a way to give a long-running worker a recognizable name.

Run a program with an empty environment with -c

export SECRET=abc
exec -c /usr/bin/env
output

(no output)

-c clears the environment before running the program, so env prints nothing at all. env -i program does the same without replacing the shell.

Common pitfalls

  • Putting exec in the middle of a script by mistake: everything after it is never run.

  • Running exec cmd in an interactive terminal: when cmd exits, the terminal window closes, because the shell it belonged to is gone.

  • exec >file without saving the original descriptor first (exec 3>&1) leaves no way to write to the terminal again.

  • Forgetting "$@" (quoted) in exec program "$@": unquoted, arguments with spaces are split.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • exec with a command and with redirections is POSIX; -a, -c and -l are bash options that dash does not have. In POSIX sh, file descriptors above 9 are not guaranteed.

Options worth knowing

A selection, described in our own words. For the full list, see bash(1), SHELL BUILTIN COMMANDS.

OptionWhat it does
-a NAMEPass NAME to the program as its zeroth argument (argv[0]).
-cRun the program with an empty environment.
-lPut a dash in front of argv[0], as login does, so shells start as login shells.

Related