bashのexecとファイルディスクリプタ操作|スクリプト全体のログをリダイレクトする設計

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > bashのexecとファイルディスクリプタ操作|スクリプト全体のログをリダイレクトする設計
「シェルスクリプトのすべての出力をログに残したいのに、毎行に >> logfile を書き足すのは手間だ。しかも書き忘れた行の出力がどこへ行ったのかわからなくなる」

cronジョブやバッチ処理を設計したことがある人なら、一度は感じたことがある悩みです。

bash には、スクリプト起動直後にたった2行書くだけで、スクリプト全体のstdout(標準出力)とstderr(標準エラー出力)をまとめてログファイルにリダイレクトできる仕組みがあります。それが、bashの組み込みコマンド exec によるファイルディスクリプタ(FD)操作です。

この記事では、bashのexecを使ったFD操作の基礎から、cronジョブのログ集約・バッチ処理への応用・trapとの組み合わせまで、実務で使える設計パターンを具体的なコード例とともに解説します。

この記事のポイント

・exec 1>>logfile 2>&1 でスクリプト全体の出力を一括ログ保存できる
・exec はbashの組み込みコマンドであり、docker exec とは別ツール
・exec 3>&1 でFDを複製しておけば、途中でstdoutを元に戻せる
・trap EXIT と組み合わせてFDを確実に閉じる設計が現場の定石


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
図解60P/登録10秒/解除も3秒 / 詳細はこちら

ファイルディスクリプタ(FD)の基礎知識

bashでリダイレクトを理解するには、まずファイルディスクリプタ(File Descriptor、以下FD)の概念を押さえておく必要があります。

LinuxのプロセスはI/Oのやりとりを整数値(FD番号)で管理します。プロセスが起動した時点で、以下の3つのFDが自動的にオープンされます。

FD 0(stdin):標準入力(キーボード入力など)
FD 1(stdout):標準出力(通常は端末に表示される)
FD 2(stderr):標準エラー出力(エラーメッセージの出力先)

コマンドラインで command > file と書いたとき、シェルは内部でFD1の接続先を file に差し替えています。つまりリダイレクトとは、FDの接続先を変更する操作にほかなりません。

通常のリダイレクト(command > file)は、そのコマンド1つだけに適用されます。スクリプト内の全コマンドにまとめて適用するには、別のアプローチが必要です。それがexecによるFD操作です。

FDの状態は、Linuxでは /proc/$$$/fd/ ディレクトリ($$$はシェル自身のPID)で確認できます。

# 現在のシェルのFD接続先を確認する ls -la /proc/$$/fd/ # 出力例(RHEL 9 / bash 5.1 環境) # lrwx------. 1 user user 64 Jul 22 10:15 0 -> /dev/pts/0 # lrwx------. 1 user user 64 Jul 22 10:15 1 -> /dev/pts/0 # lrwx------. 1 user user 64 Jul 22 10:15 2 -> /dev/pts/0 # FD 0, 1, 2 すべてが端末(/dev/pts/0)に向いている

bash の exec コマンドとは何か

exec はbashの組み込みコマンド(shell builtin)です。使い方が2種類あるため、混乱しやすい点があります。

使い方①:プロセスの置き換え
exec /usr/bin/python3 myscript.py のように引数にコマンドを渡すと、現在のシェルプロセスを指定コマンドで置き換えます。新しいプロセスは生成されず、PIDはそのまま引き継がれます。

使い方②:FDの操作(コマンド引数なし)
exec 1>logfile のようにリダイレクト記号だけを指定すると、現在のシェルのFDを操作します。プロセスの置き換えは発生せず、スクリプトはそのまま続行します。

本記事で扱うのは②の使い方です。これを使うことで、以降のすべての出力先をまとめて変更できます。

注意:docker exec とは別ツールです

Dockerを使っている方は「exec」という文字列に反応してしまうかもしれませんが、docker exec(実行中のコンテナ内でコマンドを実行する機能)とbash組み込みの exec は完全に別物です。本記事のexecはあくまでbashシェルの組み込み機能であり、Dockerとは無関係です。

exec によるFD操作の基本構文

1. exec 1>logfile でstdoutを切り替える

最も基本的な使い方です。スクリプト冒頭に以下を書くだけで、以降のすべての標準出力がlogfileに書き込まれます。

#!/bin/bash # スクリプト全体のstdoutを切り替える exec 1>/tmp/test-exec.log echo "ここからの出力はすべてlogfileに書かれる" echo "端末には何も表示されない" date hostname

実行後、ターミナルには何も表示されず、/tmp/test-exec.log に以下のように記録されます。

# /tmp/test-exec.log の内容(RHEL 9 実行例) ここからの出力はすべてlogfileに書かれる 端末には何も表示されない 2026年 7月 22日 水曜日 10:30:00 JST web01.example.local

2. exec 2>&1 でstderrをstdoutに統合する

cronジョブなどでは、stdout(FD1)とstderr(FD2)を同じログファイルに記録したい場合がほとんどです。以下の組み合わせが定番パターンです。

#!/bin/bash # stdout と stderr を同じlogfileに追記する exec 1>>/var/log/myscript.log 2>&1 echo "[INFO] スクリプト開始" ls /nonexistent_dir # stderr が出るコマンド echo "[INFO] スクリプト終了"

2>&1 は「FD2をFD1と同じ出力先につなぐ」という意味です。この順番(先にFD1を切り替えてからFD2をFD1に向ける)が重要で、逆に書くと意図どおりに動きません。

正しい順序:exec 1>>logfile 2>&1(先にFD1をlogfileに向け、次にFD2をFD1に向ける)
誤った順序:exec 2>&1 1>>logfile(FD2をFD1に向けた時点ではFD1がまだ端末のまま)

実行後のログ例(/var/log/myscript.log):

# /var/log/myscript.log(RHEL 9 実行例) [INFO] スクリプト開始 ls: '/nonexistent_dir' にアクセスできません: そのようなファイルやディレクトリはありません [INFO] スクリプト終了

3. exec 3>&1 でFDを複製・保存する

「ログに書きながら、特定の出力だけターミナルにも表示したい」という場合は、FD3(ユーザー定義FD)を使って元のstdoutを保存しておきます。FD0~2はシステム予約ですが、FD3以上はユーザーが自由に使えます(慣習的に3~9を使用)。

#!/bin/bash # 元の stdout(端末)をFD3に保存 exec 3>&1 # stdout をログファイルに切り替え exec 1>/var/log/batch.log 2>&1 echo "[LOG] これはログに書かれる" # FD3(元の端末)に直接書く echo "[CONSOLE] これは端末にも表示される" >&3 echo "[LOG] 後続処理もログに書かれる" # 後始末:FD3を閉じる exec 3>&-

FD3を「バックアップ用ソケット」として使うことで、元の出力先を保持しながら必要な箇所だけリダイレクトする柔軟な設計が可能になります。

4. exec N>&- でFDを明示的にクローズする

exec N>&- でFD番号Nを閉じることができます。ファイルへの書き込みを完了してフラッシュしたい場合や、後続プロセスへのFD引き渡しを防ぎたい場合に使います。

# FD1(stdout)を閉じる exec 1>&- # FD2(stderr)を閉じる exec 2>&- # FD3(ユーザー定義)を閉じる exec 3>&-

実務パターン:スクリプト全体のログを一括保存する

1. cronジョブのログ集約パターン

cronで実行するスクリプトにexecを組み込む最も汎用的なパターンです。日付付きのログファイルに、stdout・stderrを追記モードで一括保存します。

#!/bin/bash # cronジョブのログ集約テンプレート # /usr/local/bin/daily-batch.sh SCRIPT_NAME="$(basename "$0")" LOG_DIR="/var/log/batch" LOG_FILE="${LOG_DIR}/${SCRIPT_NAME%.sh}_$(date +%Y%m%d).log" # ログディレクトリがなければ作成(exec より前に必ず実行) mkdir -p "$LOG_DIR" # スクリプト全体のstdout/stderrをログに向ける(追記モード) exec 1>>"$LOG_FILE" 2>&1 echo "==========================================" echo "[$(date '+%Y-%m-%d %H:%M:%S')] START: $SCRIPT_NAME" echo "==========================================" # --- ここから本処理 --- echo "[INFO] データ処理を開始します" # ... 実際の処理 ... echo "[INFO] データ処理が完了しました" # --- 本処理ここまで --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] END: $SCRIPT_NAME"

このパターンのポイントは追記モード(>>)を使っている点です。上書き(>)にすると前回のログが消えてしまいます。crontabには以下のように書きます。

# crontab -e の設定例 # 毎日午前3時に実行(cron自体のメール通知は MAILTO= で無効化) MAILTO="" 0 3 * * * /usr/local/bin/daily-batch.sh

2. バッチスクリプト:stdout/stderrを別ファイルに保存する

エラー調査を容易にするため、stdoutとstderrを別々のファイルに保存したい場合は以下のパターンを使います。実行ごとにタイムスタンプ付きのファイル名が生成されるため、どの実行のログか一目でわかります。

#!/bin/bash LOG_DIR="/var/log/batch" RUN_ID="$(date +%Y%m%d_%H%M%S)" mkdir -p "$LOG_DIR" # stdout と stderr を別ファイルに振り分ける exec 1>>"${LOG_DIR}/out_${RUN_ID}.log" exec 2>>"${LOG_DIR}/err_${RUN_ID}.log" echo "[INFO] バッチ処理を開始" ls /nonexistent_dir # stderrに記録される echo "[INFO] バッチ処理を終了" # 確認方法: # tail /var/log/batch/out_20260722_030000.log # 正常出力 # tail /var/log/batch/err_20260722_030000.log # エラーのみ

3. /dev/null で出力を捨てる設計

cronジョブで「出力は不要・エラーだけメールで受け取りたい」場合や、デーモン化スクリプトで完全サイレントにしたい場合には /dev/null を使います。

#!/bin/bash # パターン1: stdout のみ捨てる(stderr はそのまま → cron がメール通知) exec 1>/dev/null # パターン2: stdout も stderr も捨てる(完全サイレント) exec 1>/dev/null 2>&1 echo "この出力は /dev/null に捨てられる" ls /nonexistent # パターン1では stderr → cron 経由でメール通知される

/dev/null はデータを書き込んでも捨てるだけのキャラクターデバイスです。2>&1 でstderrもstdoutに統合してから /dev/null に向ければ、完全サイレントモードになります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

trap との組み合わせ:終了時にFDを確実に閉じる

スクリプトが途中でエラー終了した場合でも、ログに「スクリプト終了」を記録したい・FDを確実に閉じたいという要件があります。そのために trap コマンドと組み合わせます。

#!/bin/bash LOG_FILE="/var/log/batch/$(date +%Y%m%d).log" mkdir -p "$(dirname "$LOG_FILE")" # stdout/stderr をログに向ける exec 1>>"$LOG_FILE" 2>&1 # EXIT シグナルで終了ログを記録してFDを閉じる cleanup() { local exit_code=$? echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script ended (exit code: ${exit_code})" exec 1>&- 2>&- } trap cleanup EXIT echo "[$(date '+%Y-%m-%d %H:%M:%S')] Script started" # --- 本処理 --- echo "[INFO] 処理を実行中" # エラーが起きても trap EXIT が必ず呼ばれる # set -e を使う場合、コマンドが失敗してもcleanupは実行される

trap cleanup EXIT は、スクリプトが正常終了・エラー終了・set -e によるエラー終了のいずれでも cleanup 関数を呼び出します。ログへの終了記録と後始末を一か所にまとめられるため、長期運用するcronジョブで特に有効です。

FDを元に戻す(stdout の復元)

「特定の関数内だけログに書き、関数が終わったら端末表示に戻したい」という場合の設計パターンです。FD3を「保存ソケット」として利用します。

#!/bin/bash LOG_FILE="/tmp/function_log.log" # 元の stdout(端末)をFD3に保存 exec 3>&1 log_section() { # 関数内だけログファイルに切り替え exec 1>>"$LOG_FILE" 2>&1 echo "[LOG] 関数内の処理" hostname date # FD3(元の端末)に戻す exec 1>&3 } echo "この出力は端末に表示される(FD1=端末)" log_section echo "ここも端末に表示される(FD1が端末に復元済み)" # 後始末:FD3を閉じる exec 3>&-

トラブルシュート

【問題1】exec 後の出力がログに書かれない

exec 実行後にサブシェル(( ) で囲んだブロック)や外部スクリプトを呼んでいると、FDが引き継がれていない場合があります。

確認方法:問題箇所の直前に ls -la /proc/$$/fd/1 を入れてFD1の接続先を確認する
解決方法:サブシェルを使わずスクリプトをフラット構成にするか、子スクリプト内にも同様のexec設定を入れる

【問題2】ログファイルに何も書かれない

ログディレクトリが存在しない場合、execの実行が失敗します(シェルは終了せず、その後の出力が消えます)。

# NG: ディレクトリがなければ exec が失敗して以降の出力が消える exec 1>>/var/log/newdir/batch.log 2>&1 # OK: execより前に必ずディレクトリを作成する mkdir -p /var/log/newdir exec 1>>/var/log/newdir/batch.log 2>&1

【問題3】FD3を使ったら「bad file descriptor」エラー

FD3を閉じた後(exec 3>&-)に再度 >&3 で書こうとするとエラーになります。また、exec でFD3を開く前に >&3 を実行するのも同様です。

対処:FDの開閉順序を設計段階で整理する。閉じたFDを再利用する場合は exec 3>&1 で再オープンしてから使う

【問題4】2>&1 の順序が逆でエラーが端末に出てしまう

exec 2>&1 1>>logfile と書いてしまうと、「FD2をFD1に向けた時点ではFD1がまだ端末」のため、stderrが端末に流れます。

正しい順序:exec 1>>logfile 2>&1(FD1をlogfileに変更してからFD2をFD1に統合)

本記事のまとめ

bashのexecコマンドによるFD操作は、シェルスクリプトの品質を大きく左右するテクニックです。毎行にリダイレクトを書く代わりに、スクリプト先頭の2行でログ設計を完結できます。

やりたいこと exec の書き方
stdout をログに上書き保存 exec 1>/path/to/logfile
stdout をログに追記 exec 1>>/path/to/logfile
stdout と stderr を同じログに記録 exec 1>>/path/to/logfile 2>&1
stdout を捨てる exec 1>/dev/null
すべての出力を捨てる exec 1>/dev/null 2>&1
元の stdout を保存しておく exec 3>&1
保存した stdout を復元する exec 1>&3 3>&-
FD を閉じる exec N>&-
シェルスクリプトの基本的な動作については、Linux 基本コマンドの解説もあわせて参考にしてください。また、シェルの環境変数とプロセスの関係については シェルの環境変数設定の解説 も参照できます。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

登録10秒/合わなければ解除3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。