「ファイルを渡すと動くのに、パイプで流すと入力が渡らない」
シェルスクリプトをパイプでつなごうとしたとき、こうした問題にぶつかった経験はないでしょうか。
多くの場合、原因はスクリプトの「入出力設計」にあります。コマンドライン上の各ツールは、結果は標準出力へ・エラーや診断メッセージは標準エラーへ・成否は終了ステータスで返すという暗黙の規約に従っています。この規約に沿って自作スクリプトを設計することで、他のコマンドとパイプで自由につなげられる「再利用しやすい部品」になります。
この記事では、RHEL 9系~Rocky Linux 9系を想定環境として、シェルスクリプトをパイプの部品として設計するための入出力設計パターンを解説します。標準出力・標準エラー・終了ステータスの役割分担、ファイル引数と標準入力の両対応、引数「-」の扱い、
[ -t 0 ] での端末判定まで実例を交えて説明します。この記事のポイント
・結果を標準出力・診断は標準エラーに分けることでパイプ連結が安全になる
・引数がなければ標準入力から読む設計で、ファイルとパイプの両方に対応できる
・引数「-」を標準入力として扱う慣用パターンで混在処理が可能になる
・[ -t 1 ] で端末かパイプかを判定し、ヘッダ行や色付けを切り替える
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
シェルスクリプトをパイプの「部品」にする意義
Unixのコマンドラインツールは「一つのことをうまくやる」という設計哲学に基づいています。grep が抽出したテキストを sort に渡し、sort が並べたものを uniq がまとめる──このような連携が成り立つのは、各ツールが共通の規約に従っているからです。自作スクリプトがこの規約から外れると、途端につなげにくくなります。よくあるパターンは次の2つです。
・診断メッセージを標準出力に混在させてしまう:「処理を開始します」「N件処理しました」のような進捗メッセージが結果データに混じり、パイプ先のコマンドが誤動作します
・エラー時に0を返してしまう:スクリプトが失敗しても終了ステータス0を返すため、パイプ先や呼び出し元が成功とみなして処理を続けてしまいます
これを防ぐには、何を標準出力に・何を標準エラーに出すかを意識して設計することが重要です。シェルスクリプトの入出力設計についてより体系的に学びたい方は、シェルスクリプト実践講座でパイプ設計・エラー処理・ログ設計を合わせて習得できます。
シェルスクリプトで標準出力・標準エラー・終了ステータスの役割を分ける
1. 結果は標準出力へ、診断は標準エラーへ
パイプでつなぐことを前提にするなら、次の原則を守ります。・標準出力(fd 1):後続のコマンドに渡す「データ」。CSVの行、JSONオブジェクト、加工済みテキストなど
・標準エラー(fd 2):人間向けの「診断情報」。進捗メッセージ、警告、エラーの詳細など
具体例として、ファイルの行数を数えるスクリプトで比較します。
NG例:診断メッセージが標準出力に混じるパターン
#!/bin/bash # 悪い例:診断メッセージが標準出力に混じる echo "処理を開始します" # これが標準出力に出てしまう wc -l "" echo "完了しました" # これも標準出力に出てしまう
# ./count_lines.sh data.txt | grep -E "^[0-9]+" とすると # 「処理を開始します」「完了しました」の行も通過してしまう 処理を開始します 42 data.txt 完了しました
#!/bin/bash # 良い例:診断メッセージを標準エラーに送る echo "処理を開始します" >&2 # 標準エラーへ wc -l "" # 結果は標準出力へ echo "完了しました" >&2 # 標準エラーへ
>&2 はリダイレクト記号です。標準出力と標準エラーの仕組みや 2>&1 の意味については、それぞれの解説記事を参照してください。2. 終了ステータスで成否を返す
終了ステータスは、スクリプトが「成功したか・失敗したか」を呼び出し元に伝える唯一の手段です。・0:成功
・1以上:失敗(慣習的に1は一般エラー、2はコマンドの使い方の誤り)
#!/bin/bash process_file() { local file="" if [ ! -r "" ]; then echo "エラー: を読めません" >&2 return 1 # 失敗を返す fi wc -l < "" # 結果のみ標準出力へ return 0 } process_file "" exit 0 # 関数の終了ステータスをスクリプトの終了ステータスとして伝播
exit 0 を忘れると、関数が失敗しても最後に実行されたコマンドが成功した場合にスクリプト全体が0で終わってしまいます。パイプラインで && や || を使って後続処理を制御するためにも、正しい終了ステータスの伝播は重要です。シェルスクリプトでファイル引数と標準入力の両対応設計を実装する
コマンドラインツールの多くは、引数としてファイル名を渡す場合と、パイプで標準入力から渡す場合の両方に対応しています。grep "pattern" file.txt と cat file.txt | grep "pattern" が同じように動くのはそのためです。自作スクリプトにもこの二重対応を実装することで、使い勝手が大幅に向上します。1. 引数がなければ標準入力から読む
引数の有無で入力源を切り替えるには、cat コマンドを活用した次のパターンが有効です。#!/bin/bash # 引数があればファイルを、なければ標準入力を処理する n=0 { if [ 0 -gt 0 ]; then # 引数として渡されたファイル群を処理 cat -- "" else # 引数がなければ標準入力をそのまま受け取る cat fi } | while IFS= read -r line; do # 1行ずつ処理(例:行番号を付与) n=1 printf '%d %s ' "" "" done
-- "" の -- はオプション終端記号です。ファイル名が -v のようにハイフンで始まっていても、オプションと誤認されずに扱えます。このパターンで次の2つが同じ結果になります。
# ファイルを渡す場合 ./numbering.sh access.log # パイプで渡す場合 grep "ERROR" /var/log/messages | ./numbering.sh
2. 引数「-」を明示的な標準入力として扱う
多くのUnixコマンドは、引数に- を指定すると「標準入力から読む」という慣用を持っています。tar -xzf - でパイプから展開する、diff - file2 で標準入力と比較するなどが代表例です。自作スクリプトにも同じ慣用を実装することで、ファイルと標準入力を混在させた処理が可能になります。#!/bin/bash # 引数「-」を標準入力として扱う設計 open_input() { local file="" if [ "" = "-" ]; then cat # 標準入力をそのまま出力 else cat -- "" fi } { for arg in ""; do open_input "" done } | while IFS= read -r line; do # 処理例:大文字変換 echo "" | tr '[:lower:]' '[:upper:]' done
# header.txt の後に標準入力のデータを続けて処理する echo "inline data" | ./uppercase.sh header.txt - footer.txt
シェルスクリプトで[ -t 0 ]を使って端末かパイプかを判定する
スクリプトの出力が端末に直接表示される場合とパイプで別のコマンドに渡される場合では、最適な出力形式が異なります。・端末向け:ヘッダ行、色付け、進捗表示などがあると読みやすい
・パイプ向け:ヘッダや色エスケープシーケンスがあると次のコマンドの処理を壊す
[ -t 1 ] は標準出力(fd 1)が端末かどうかを判定します。ls --color=auto や grep --color=auto が端末にのみ色を付ける仕組みもこれと同じです。入力元を知りたいときは [ -t 0 ](標準入力が端末かどうか)を使います。#!/bin/bash # 標準出力が端末のときだけヘッダと色を付ける if [ -t 1 ]; then printf '%-20s %s ' "HOSTNAME" "STATUS" printf '%-20s %s ' "--------" "------" fi while IFS= read -r host; do if ping -c1 -W1 "" >/dev/null 2>&1; then status="UP" else status="DOWN" fi if [ -t 1 ]; then # 端末向け:ANSIカラーコード付き出力 [ "" = "UP" ] && color="[32m" || color="[31m" printf "%-20s %s[0m " "" "" else # パイプ向け:CSVで出力(色なし・ヘッダなし) printf '%s,%s ' "" "" fi done
# 端末で直接実行(ヘッダと色付き) cat hosts.txt | ./check_hosts.sh HOSTNAME STATUS -------- ------ web01.example.com UP db01.example.com DOWN # パイプ先でCSV処理(ヘッダなし・色なし) cat hosts.txt | ./check_hosts.sh | grep "DOWN" | cut -d, -f1 db01.example.com
トラブルシュート
「標準エラーの出力がパイプ先に渡ってしまう」
2>&1 | とした場合、標準エラーが標準出力にマージされてパイプに乗ります。部品設計では通常避けるべき操作です。# 意図せず stderr をパイプに流してしまう例 ./myscript.sh 2>&1 | grep "ERROR" # 標準出力だけをパイプに流し、エラーは端末に残す ./myscript.sh | grep "ERROR" # エラーをファイルに逃がしながら、標準出力をパイプに流す ./myscript.sh 2>error.log | grep "ERROR"
「スクリプトが失敗しても後続処理が動いてしまう」
終了ステータスが正しく伝わっていない場合、スクリプト冒頭にset -e を追加する方法があります。ただし、set -e はパイプライン内のコマンド失敗を拾えないという制限があります。パイプライン内の途中失敗まで検知したい場合は set -o pipefail を合わせて使います。#!/bin/bash set -e # エラー時に即座に終了 set -o pipefail # パイプライン内のエラーも伝播 set -u # 未定義変数を参照したらエラー # 以降のコマンドはいずれかが失敗したら停止する ./step1.sh | ./step2.sh | ./step3.sh
set -e は if 文の条件式や &&・|| の右辺で失敗しても終了しないという仕様があります。また set -o pipefail を使う場合、grep のように「一致なし=終了ステータス1」を正常動作として使うコマンドが予期せずスクリプトを停止させることがあります。意図した失敗には grep ... || true のように明示的に対処してください。「引数なしで起動したスクリプトが固まって見える」
引数なしで起動したスクリプトが標準入力から読む設計の場合、端末から実行すると入力待ちになります。ユーザーが気づかず「フリーズした」と感じることがあります。[ -t 0 ] で端末入力と判定した場合だけ、標準エラーにプロンプトを表示することで解決できます。#!/bin/bash # 端末からの入力待ちをユーザーに知らせる if [ -t 0 ] && [ 0 -eq 0 ]; then echo "標準入力から読みます(入力後に Ctrl+D で終了)" >&2 fi while IFS= read -r line; do echo "" done
本記事のまとめ
| 設計ポイント | 実装方法 |
|---|---|
| 結果は標準出力へ | echo "結果"(デフォルトで標準出力) |
| 診断・エラーは標準エラーへ | echo "メッセージ" >&2 |
| 成否を終了ステータスで返す | exit 0(成功)/ exit 1(失敗)・exit 0 で伝播 |
| ファイルと標準入力を両対応 | 引数があれば cat -- ""、なければ cat |
| 引数「-」を標準入力として扱う | [ "$file" = "-" ] で分岐して cat |
| 端末かパイプかを判定(出力) | [ -t 1 ] で分岐し、端末向けヘッダ・色付けを切り替える |
| 端末かパイプかを判定(入力) | [ -t 0 ] で分岐し、入力待ちプロンプトを標準エラーに表示 |
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、シェルスクリプトの入出力設計・エラー処理・ログ設計を一貫した「型」として習得できるシェルスクリプト実践講座をご活用ください。パイプの部品設計から本番運用スクリプトの品質向上まで、体系的に学べます。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:ファイル名に空白や改行があっても壊れないシェルスクリプト設計|NUL区切り・read -d・配列で安全にループする方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます