exec
| Since: | POSIX(sh互換) |
|---|
In shell scripts, the exec command provides two powerful operations: "replacing the current process with another command" and "opening file descriptors under arbitrary numbers to control redirection for the entire script." Process replacement does not create a new child process — it replaces the current shell itself — making it useful when you want to avoid extra processes, such as at a container entry point or when handing off to a final command. File descriptor manipulation is used for advanced script control, such as centralizing output to a log file or pipe-free bidirectional communication.
Two ways to use exec
| Syntax | Description |
|---|---|
exec command [args...] | Replaces the current shell process with command. Lines after exec are not executed. No new process is created. |
exec n> file | Opens file descriptor number n for writing to file. Can be used with &n redirections afterward. |
exec n>> file | Opens file descriptor number n for appending to file. |
exec n< file | Opens file descriptor number n for reading from file. |
exec n<> file | Opens file descriptor number n for both reading and writing. |
exec n>&- | Closes file descriptor number n, explicitly releasing the open resource. |
exec 1> file | Redirects stdout for the entire script to file. All subsequent echo and similar output goes to the file. |
exec 2> file | Redirects stderr for the entire script to file. |
Process replacement with exec
exec command overwrites the current shell process with command. No new child process is created — the shell that ran exec itself becomes command. Once exec succeeds, there is no returning to the original shell, so any code written after exec is never executed.
| Use case | Example | Description |
|---|---|---|
| Hand off to the final command | exec /usr/bin/python3 app.py | Uses the shell script as a launcher and replaces it with the actual command at the end. This is the standard pattern for Docker entry points. |
| Hand off after privilege escalation | exec sudo -u www-data "$@" | Performs environment variable checks and initialization, then runs the command as a different user. |
| Switch to another shell | exec bash --login | Replaces the current sh session with a bash login shell. |
# ----------------------------------------------- # Demonstrates the basic behavior of process # replacement with exec # ----------------------------------------------- #!/bin/bash echo "This line runs (PID: $$)" # Replace the process with exec exec echo "Replaced by exec" # ↓ This line is never reached echo "This line does not run"
$ bash exec_basic.sh This line runs (PID: 12345) Replaced by exec
File descriptor manipulation
The shell provides three file descriptors by default (0: stdin, 1: stdout, 2: stderr). By specifying a number of 3 or higher with exec n> file, you can freely use additional file descriptors within your script. A typical use is to keep a log file open and write only the necessary lines to it using >&3.
| Number | Name | Default connection |
|---|---|---|
0 | Standard input (stdin) | Keyboard. |
1 | Standard output (stdout) | Terminal screen. |
2 | Standard error (stderr) | Terminal screen (an independent stream from stdout). |
3〜9 | User-defined | Can be freely assigned to any file or another descriptor. |
Redirecting all script output to a log file
Writing exec 1> log.txt near the top of a script sends all subsequent stdout to the file. Following it with exec 2>&1 also records stderr to the same file. This saves you from having to write >> log.txt on every command and prevents log output from being missed.
# -----------------------------------------------
# Redirects all script output to a file using exec
# -----------------------------------------------
#!/bin/bash
LOG="script.log"
# Redirect both stdout and stderr to the log file
exec 1> "${LOG}"
exec 2>&1
# All echo and command output from here on is recorded in script.log
echo "Log started: $(date '+%Y-%m-%d %H:%M:%S')"
echo "Running process..."
ls /no_such_dir # Errors are also recorded in the same file
echo "Log ended"
$ bash exec_log_redirect.sh $ cat script.log Log started: 2026-03-27 10:00:00 Running process... ls: cannot access '/no_such_dir': No such file or directory Log ended
Sample code
dragonball_exec_replace.sh
#!/bin/bash
# -----------------------------------------------
# A script that demonstrates process replacement
# with exec using Dragon Ball characters
# -----------------------------------------------
# -----------------------------------------------
# Validates the arguments passed at startup.
# Prints usage and exits if no argument is given.
# -----------------------------------------------
if [ $# -eq 0 ]; then
echo "Usage: $0 <character name>"
echo "Example: $0 Goku"
exit 1
fi
FIGHTER="$1"
echo "=== DRAGON BALL POWER LEVEL SCANNER ==="
echo "Launcher script PID: $$"
echo "Target: ${FIGHTER}"
echo ""
# -----------------------------------------------
# Sets the power level for each character
# -----------------------------------------------
case "${FIGHTER}" in
"Goku")
POWER=150000000
;;
"Vegeta")
POWER=120000000
;;
"Frieza")
POWER=120000000
;;
"Piccolo")
POWER=800000
;;
"Krillin")
POWER=75000
;;
*)
echo "[ERROR] Unknown character: ${FIGHTER}" >&2
exit 1
;;
esac
echo "Power level: ${POWER}"
echo "Switching system to combat mode..."
echo ""
# -----------------------------------------------
# Replaces the process with exec.
# Nothing after this line is executed.
# -----------------------------------------------
exec printf "=== Power level %d confirmed for %s. Storing scouter. ===\n" \
"${POWER}" "${FIGHTER}"
# This line is never reached
echo "This line does not run"
$ chmod +x dragonball_exec_replace.sh $ ./dragonball_exec_replace.sh Goku === DRAGON BALL POWER LEVEL SCANNER === Launcher script PID: 12345 Target: Goku Power level: 150000000 Switching system to combat mode... === Power level 150000000 confirmed for Goku. Storing scouter. === $ ./dragonball_exec_replace.sh Frieza === DRAGON BALL POWER LEVEL SCANNER === Launcher script PID: 12346 Target: Frieza Power level: 120000000 Switching system to combat mode... === Power level 120000000 confirmed for Frieza. Storing scouter. ===
dragonball_fd_log.sh
#!/bin/bash
# -----------------------------------------------
# Manages battle logs for 5 Dragon Ball characters
# using exec and file descriptors
#
# FD 3: Normal log (battle.log)
# FD 4: Error log (battle_error.log)
# stdout remains displayed in the terminal
# -----------------------------------------------
set -uo pipefail
LOG_FILE="battle.log"
ERROR_FILE="battle_error.log"
# -----------------------------------------------
# Opens the file descriptors.
# Assigns FD 3 to the normal log in append mode.
# Assigns FD 4 to the error log in append mode.
# -----------------------------------------------
exec 3>> "${LOG_FILE}"
exec 4>> "${ERROR_FILE}"
# -----------------------------------------------
# Cleanup: closes FDs when the script exits
# -----------------------------------------------
cleanup() {
exec 3>&- # Close FD 3
exec 4>&- # Close FD 4
echo "[cleanup] File descriptors closed."
}
trap cleanup EXIT
# -----------------------------------------------
# Log-writing helper function
# $1: log level (INFO / WARN / ERROR)
# $2: message
# -----------------------------------------------
write_log() {
local level="$1"
local msg="$2"
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
if [ "${level}" = "ERROR" ]; then
# Write ERROR to FD 4 (error log)
printf "[%s] [%s] %s\n" "${ts}" "${level}" "${msg}" >&4
else
# Write INFO / WARN to FD 3 (normal log)
printf "[%s] [%s] %s\n" "${ts}" "${level}" "${msg}" >&3
fi
}
# -----------------------------------------------
# To also print to the terminal, combine with tee
# -----------------------------------------------
log_and_print() {
local level="$1"
local msg="$2"
echo "[${level}] ${msg}" # Print to terminal
write_log "${level}" "${msg}" # Also write to file
}
# -----------------------------------------------
# Processes the battle results for 5 Dragon Ball characters
# -----------------------------------------------
echo "=== DRAGON BALL BATTLE LOG SYSTEM START ==="
echo ""
# Format: "name:power:result"
battles=(
"Goku:150000000:Victory"
"Vegeta:120000000:Victory"
"Frieza:120000000:Defeat"
"Piccolo:800000:Incapacitated"
"Krillin:75000:Retreat"
)
for entry in "${battles[@]}"; do
# Split by colon into name, power, and result
name="${entry%%:*}"
rest="${entry#*:}"
power="${rest%%:*}"
result="${rest##*:}"
if [ "${result}" = "Victory" ]; then
log_and_print "INFO" "${name} (power: ${power}) — ${result}"
elif [ "${result}" = "Defeat" ] || [ "${result}" = "Incapacitated" ]; then
log_and_print "ERROR" "${name} (power: ${power}) — ${result}"
else
log_and_print "WARN" "${name} (power: ${power}) — ${result}"
fi
done
echo ""
echo "=== Processing complete ==="
echo ""
# -----------------------------------------------
# Displays the contents of the recorded log files
# -----------------------------------------------
echo "--- ${LOG_FILE} ---"
cat "${LOG_FILE}"
echo ""
echo "--- ${ERROR_FILE} ---"
cat "${ERROR_FILE}"
# -----------------------------------------------
# Cleanup: removes the log files
# -----------------------------------------------
rm -f "${LOG_FILE}" "${ERROR_FILE}"
$ chmod +x dragonball_fd_log.sh $ ./dragonball_fd_log.sh === DRAGON BALL BATTLE LOG SYSTEM START === [INFO] Goku (power: 150000000) — Victory [INFO] Vegeta (power: 120000000) — Victory [ERROR] Frieza (power: 120000000) — Defeat [ERROR] Piccolo (power: 800000) — Incapacitated [WARN] Krillin (power: 75000) — Retreat === Processing complete === --- battle.log --- [2026-03-27 10:00:00] [INFO] Goku (power: 150000000) — Victory [2026-03-27 10:00:00] [INFO] Vegeta (power: 120000000) — Victory [2026-03-27 10:00:00] [WARN] Krillin (power: 75000) — Retreat --- battle_error.log --- [2026-03-27 10:00:00] [ERROR] Frieza (power: 120000000) — Defeat [2026-03-27 10:00:00] [ERROR] Piccolo (power: 800000) — Incapacitated [cleanup] File descriptors closed.
dragonball_fd_redirect_all.sh (redirect entire script output)
#!/bin/bash
# -----------------------------------------------
# A simple pattern using exec to send all script
# output to a single log file.
# Both stdout and stderr are recorded in dragonball.log.
# -----------------------------------------------
LOG="dragonball.log"
# -----------------------------------------------
# Redirects all stdout and stderr to the log file.
# After these two lines, all echo and command output
# goes into the file.
# -----------------------------------------------
exec 1> "${LOG}"
exec 2>&1
echo "=== DRAGON BALL POWER SCAN START: $(date '+%Y-%m-%d %H:%M:%S') ==="
echo ""
# -----------------------------------------------
# Processes information for 5 characters
# -----------------------------------------------
declare -A fighters
fighters["Goku"]=150000000
fighters["Vegeta"]=120000000
fighters["Frieza"]=120000000
fighters["Piccolo"]=800000
fighters["Krillin"]=75000
for name in "Goku" "Vegeta" "Frieza" "Piccolo" "Krillin"; do
echo "[SCAN] ${name}: power level ${fighters[${name}]}"
done
echo ""
# -----------------------------------------------
# Intentionally triggers an error (stderr also goes to the same file)
# -----------------------------------------------
echo "[TEST] Accessing a non-existent file..."
cat /no_such_file_dragonball 2>&1 || true
echo ""
echo "=== SCAN COMPLETE: $(date '+%Y-%m-%d %H:%M:%S') ==="
$ chmod +x dragonball_fd_redirect_all.sh $ ./dragonball_fd_redirect_all.sh $ cat dragonball.log === DRAGON BALL POWER SCAN START: 2026-03-27 10:00:00 === [SCAN] Goku: power level 150000000 [SCAN] Vegeta: power level 120000000 [SCAN] Frieza: power level 120000000 [SCAN] Piccolo: power level 800000 [SCAN] Krillin: power level 75000 [TEST] Accessing a non-existent file... cat: /no_such_file_dragonball: No such file or directory === SCAN COMPLETE: 2026-03-27 10:00:00 ===
Common uses of exec
| Purpose | Syntax | Notes |
|---|---|---|
| Hand the process off to a final command | exec /path/to/command "$@" | Used at the end of a Docker entry point or startup wrapper script. |
| Redirect all script stdout to a file | exec 1> log.txt | Saves you from writing >> log.txt on every command. |
| Also redirect all script stderr to the same file | exec 1> log.txt; exec 2>&1 | Redirect stderr after stdout, or you will create a circular reference. |
| Open an additional FD as a log file | exec 3>> app.log | Afterward, write to the log with echo "..." >&3. |
| Close an open FD | exec 3>&- | Closing inside a trap is best practice. |
| Open a file as a read-only FD | exec 3< input.txt | Read one line at a time with read line <&3. |
Summary
The exec built-in in shell scripts has two purposes: "process replacement" and "file descriptor manipulation." Writing exec command replaces the current shell process with command, and no code after it runs. Because no child process is created, it is widely used at the end of container entry points or startup wrapper scripts to hand off to the actual command. Using exec with a file descriptor — such as exec 1> log.txt — changes the output destination for the entire script at once, saving you from appending >> log.txt to every command. Opening an additional FD with exec 3>> app.log and writing only the necessary lines with >&3 is a practical pattern for keeping a log file open and writing to it efficiently. Once you are done with an open FD, close it with exec 3>&- and release all of them together inside a trap — that is best practice. For the basics of redirection, see also Redirection (> / >> / 2>).
If you find any errors or copyright issues, please contact us.