はじめに

Linuxの真価は、シンプルな単機能コマンドを組み合わせて複雑な処理を実現できる点にあります。「一つのことをうまくやる」という Unix 哲学に基づき、各コマンドは特定の処理に特化しており、それらを連携させることで強力なデータ処理パイプラインを構築できます。

この連携を可能にするのが、本記事で解説するパイプリダイレクトです。これらの仕組みを理解することで、ログ解析、データ変換、バッチ処理など、あらゆる場面で効率的な作業が可能になります。

本記事では、標準入出力の基礎から始め、リダイレクト演算子の詳細な使い方、パイプによるコマンド連携、そしてteeコマンドやxargsといった高度なテクニックまでを体系的に解説します。

動作確認環境

本記事のコマンドは以下の環境で動作確認しています。

項目 内容
OS Ubuntu 24.04 LTS
シェル bash 5.3
カーネル Linux 6.8
coreutils 9.4

WSL2やVirtualBox上のLinux環境、その他の主要ディストリビューション(AlmaLinux、Debian等)でも同様に動作します。

標準入出力の基礎

3つのストリーム

Linuxでは、すべてのプロセスが起動時に3つの標準ストリーム(データの流れ)を持ちます。これらのストリームを理解することが、リダイレクトとパイプを使いこなす第一歩です。

ストリーム ファイルディスクリプタ 略称 役割
標準入力 0 stdin プログラムへの入力データを受け取る
標準出力 1 stdout プログラムの通常の出力を送る
標準エラー出力 2 stderr エラーメッセージや診断情報を送る

ファイルディスクリプタは、オープンされたファイルやストリームを識別するための整数値です。リダイレクトの際にこの番号を使用します。

graph LR
    A[標準入力<br/>fd=0] --> B[プロセス]
    B --> C[標準出力<br/>fd=1]
    B --> D[標準エラー出力<br/>fd=2]
    
    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#e8f5e9
    style D fill:#ffebee

デフォルトの接続先

デフォルトでは、これらのストリームは以下に接続されています。

  • 標準入力: キーボード(ターミナル)
  • 標準出力: 画面(ターミナル)
  • 標準エラー出力: 画面(ターミナル)

リダイレクトを使うと、これらの接続先をファイルや他のコマンドに変更できます。

標準出力と標準エラー出力の違い

なぜ出力が2種類に分かれているのでしょうか。以下の例で確認してみましょう。

1
2
3
4
5
6
# 存在するファイルと存在しないファイルを両方指定
ls /etc/hostname /nonexistent

# 出力例
# ls: cannot access '/nonexistent': No such file or directory  <- 標準エラー出力
# /etc/hostname                                                 <- 標準出力

正常な結果とエラーメッセージを分離することで、後述するリダイレクトで個別に処理できます。たとえば、正常結果だけをファイルに保存し、エラーは画面に表示するといった制御が可能になります。

出力リダイレクト

上書きリダイレクト(>)

>演算子は、標準出力をファイルに書き込みます。ファイルが存在する場合は上書きされ、存在しない場合は新規作成されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# コマンドの出力をファイルに保存
echo "Hello, World!" > greeting.txt

# 内容を確認
cat greeting.txt
# Hello, World!

# 上書きされることを確認
echo "Goodbye!" > greeting.txt
cat greeting.txt
# Goodbye!
1
2
3
4
5
# lsの結果をファイルに保存
ls -la /etc > etc_list.txt

# ファイル情報をファイルに保存
date > timestamp.txt

追記リダイレクト(»)

>>演算子は、ファイルの末尾に追記します。ログファイルへの書き込みなど、既存の内容を保持したい場合に使用します。

1
2
3
4
5
6
7
8
9
# 追記モードでファイルに書き込み
echo "Line 1" > log.txt
echo "Line 2" >> log.txt
echo "Line 3" >> log.txt

cat log.txt
# Line 1
# Line 2
# Line 3
1
2
3
4
# タイムスタンプ付きログの追記
echo "$(date): Script started" >> app.log
# 何かの処理...
echo "$(date): Script completed" >> app.log

上書き防止オプション(noclobber)

bashには、誤ってファイルを上書きしないための保護機能があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# noclobberオプションを有効化
set -o noclobber

# 既存ファイルへの上書きがエラーになる
echo "test" > existing_file.txt
# bash: existing_file.txt: cannot overwrite existing file

# 強制的に上書きする場合は >| を使用
echo "test" >| existing_file.txt

# noclobberを無効化
set +o noclobber

入力リダイレクト

基本的な入力リダイレクト(<)

<演算子は、ファイルの内容を標準入力として渡します。

1
2
3
4
5
6
# ファイルからの入力を受け取る
wc -l < /etc/passwd
# 45

# catに標準入力を渡す(通常のcat fileと同じ結果)
cat < /etc/hostname

入力リダイレクトの実践的な使い方

1
2
3
4
5
6
7
8
# ソート済みファイルから重複を除去
sort < unsorted.txt | uniq > unique.txt

# メール送信(mailコマンドがある場合)
mail -s "Report" user@example.com < report.txt

# 入力と出力の両方をリダイレクト
tr 'a-z' 'A-Z' < input.txt > output.txt

ヒアドキュメント(«)

ヒアドキュメントを使うと、複数行のテキストを標準入力として渡せます。スクリプト内でファイルを作成したり、対話型コマンドに入力を渡したりする際に便利です。

1
2
3
4
5
6
# ヒアドキュメントで複数行を入力
cat << EOF
This is line 1.
This is line 2.
This is line 3.
EOF
1
2
3
4
5
6
7
# 設定ファイルの作成
cat << 'EOF' > config.ini
[database]
host=localhost
port=5432
name=myapp
EOF

区切り文字(上記ではEOF)をシングルクォートで囲むと、変数展開が行われません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 変数展開あり
NAME="Alice"
cat << EOF
Hello, $NAME!
EOF
# Hello, Alice!

# 変数展開なし(シングルクォートで囲む)
cat << 'EOF'
Hello, $NAME!
EOF
# Hello, $NAME!

ヒアストリング(«<)

ヒアストリングは、単一の文字列を標準入力として渡す簡潔な方法です。bash拡張機能であり、POSIXシェルでは使用できません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 文字列を標準入力として渡す
cat <<< "Hello, World!"
# Hello, World!

# 変数の値を標準入力として渡す
greeting="Good morning"
cat <<< "$greeting"
# Good morning

# コマンドの結果を渡す
bc <<< "2 + 3 * 4"
# 14

標準エラー出力のリダイレクト

標準エラー出力をファイルへ(2>)

2>演算子を使うと、標準エラー出力だけをファイルにリダイレクトできます。

1
2
3
4
5
6
# エラーメッセージをファイルに保存
ls /nonexistent 2> error.log

# エラーログの内容を確認
cat error.log
# ls: cannot access '/nonexistent': No such file or directory
1
2
3
4
5
6
7
8
# 正常出力とエラー出力を別々のファイルに保存
ls /etc/hostname /nonexistent > output.txt 2> error.txt

cat output.txt
# /etc/hostname

cat error.txt
# ls: cannot access '/nonexistent': No such file or directory

標準エラー出力の追記(2»)

1
2
3
4
# エラーログに追記
command1 2>> error.log
command2 2>> error.log
command3 2>> error.log

標準エラー出力を破棄

エラーメッセージを表示させたくない場合は、/dev/nullにリダイレクトします。

1
2
# エラーを無視してコマンドを実行
find /etc -name "*.conf" 2>/dev/null

/dev/nullは特殊なデバイスファイルで、書き込まれたデータはすべて破棄されます。

標準出力と標準エラー出力の統合

両方を同じファイルへ(&>)

&>演算子は、標準出力と標準エラー出力の両方を同じファイルにリダイレクトします。

1
2
3
4
5
6
# 全出力をファイルに保存
ls /etc/hostname /nonexistent &> all_output.txt

cat all_output.txt
# ls: cannot access '/nonexistent': No such file or directory
# /etc/hostname

従来の書き方(2>&1)

&>は比較的新しい記法で、従来は2>&1を使っていました。この記法は今でも広く使われており、POSIX準拠です。

1
2
# 従来の書き方
ls /etc/hostname /nonexistent > all_output.txt 2>&1

2>&1は「ファイルディスクリプタ2(標準エラー出力)をファイルディスクリプタ1(標準出力)と同じ場所に向ける」という意味です。

重要: リダイレクトの順序が重要です。

1
2
3
4
5
# 正しい順序: 標準出力をリダイレクトしてから、標準エラー出力を標準出力に向ける
command > file.txt 2>&1

# 間違った順序: 標準エラー出力を(まだターミナルを指している)標準出力に向けてから、標準出力をリダイレクト
command 2>&1 > file.txt  # エラーはターミナルに表示される
graph TD
    subgraph "正しい順序: command > file.txt 2>&1"
    A1[stdout] -->|1. > file.txt| B1[file.txt]
    A2[stderr] -->|2. 2>&1| B1
    end
    
    subgraph "間違った順序: command 2>&1 > file.txt"
    C1[stderr] -->|1. 2>&1| D1[terminal]
    C2[stdout] -->|2. > file.txt| E1[file.txt]
    end

追記モードで両方を統合(&»)

1
2
3
4
5
# 全出力を追記モードでファイルに保存
command &>> all_output.log

# 従来の書き方
command >> all_output.log 2>&1

パイプによるコマンド連携

パイプの基本

パイプ(|)は、あるコマンドの標準出力を次のコマンドの標準入力に接続します。これにより、複数のコマンドを連鎖させてデータを処理できます。

1
2
3
4
5
# 基本的なパイプの使用
cat /etc/passwd | head -5

# 複数のパイプを連鎖
cat /etc/passwd | grep "bash" | wc -l
graph LR
    A[cat /etc/passwd] -->|パイプ| B[grep bash]
    B -->|パイプ| C[wc -l]
    C --> D[結果: 3]
    
    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#e8f5e9
    style D fill:#f3e5f5

パイプの仕組み

パイプでは、各コマンドが別々のプロセスとして並列に実行されます。データはバッファを介してストリーミングで渡されるため、最初のコマンドがすべての出力を終える前に次のコマンドが処理を開始できます。

1
2
3
# 大きなファイルでも効率的に処理
# (最初の10行を見つけたらheadが終了し、catも停止する)
cat very_large_file.txt | head -10

実践的なパイプの例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# アクセスログからHTTPステータス別にカウント
cat /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn

# プロセス一覧からメモリ使用量順に上位10件を表示
ps aux | sort -k4 -rn | head -10

# ディレクトリ内のファイルをサイズ順に表示
ls -la | sort -k5 -n

# 特定のポートを使用しているプロセスを検索
netstat -tlnp 2>/dev/null | grep ":80"

# ファイル拡張子ごとのファイル数を集計
find . -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn

パイプラインのエラーハンドリング

デフォルトでは、パイプラインの終了ステータスは最後のコマンドの終了ステータスになります。

1
2
3
# 最後のコマンド(true)の終了ステータスが返される
false | true
echo $?  # 0

pipefailオプションを設定すると、パイプライン内のいずれかのコマンドが失敗した場合にエラーを検出できます。

1
2
3
4
5
6
7
8
9
# pipefailを有効化
set -o pipefail

# 最初のコマンドが失敗するとパイプライン全体が失敗
false | true
echo $?  # 1

# pipefailを無効化
set +o pipefail

PIPESTATUS配列を使うと、パイプライン内の各コマンドの終了ステータスを個別に確認できます。

1
2
cat /nonexistent 2>/dev/null | head -5 | wc -l
echo "${PIPESTATUS[@]}"  # 1 0 0

標準エラー出力とパイプ

標準エラー出力をパイプに含める

デフォルトでは、パイプは標準出力のみを次のコマンドに渡します。標準エラー出力もパイプに含めるには、2>&1を使用します。

1
2
3
4
5
6
# 標準エラー出力もパイプに含める
ls /etc/hostname /nonexistent 2>&1 | cat

# 出力例
# ls: cannot access '/nonexistent': No such file or directory
# /etc/hostname

bash 4.0以降では、|&という短縮記法も使用できます。

1
2
# bash 4.0以降の短縮記法
ls /etc/hostname /nonexistent |& cat

標準出力と標準エラー出力を別々に処理

より高度なケースとして、標準出力と標準エラー出力を別々のコマンドに渡すことも可能です。

1
2
# 標準出力はファイルへ、標準エラー出力は別のコマンドへ
command 2>&1 1>output.txt | grep "ERROR" > errors.txt

teeコマンド

teeの基本

teeコマンドは、標準入力を受け取り、それを標準出力とファイルの両方に書き出します。パイプラインの途中でデータを分岐させたい場合に便利です。

1
2
# パイプラインの途中でデータをファイルに保存しつつ、次のコマンドにも渡す
ls -la | tee filelist.txt | head -5
graph LR
    A[ls -la] --> B[tee]
    B --> C[filelist.txt]
    B --> D[head -5]
    D --> E[画面表示]
    
    style B fill:#fff3e0

teeの追記モード

-aオプションで、ファイルへの追記が可能です。

1
2
# ファイルに追記
echo "New entry" | tee -a log.txt

複数ファイルへの同時出力

teeは複数のファイルに同時に書き出すことができます。

1
2
# 3つのファイルに同時に書き出し
echo "Broadcast message" | tee file1.txt file2.txt file3.txt

sudoとteeの組み合わせ

sudoでリダイレクトを使用する場合の問題を解決するのにもteeが役立ちます。

1
2
3
4
5
6
7
8
# これはエラーになる(リダイレクトはsudo権限で実行されない)
sudo echo "127.0.0.1 myhost" >> /etc/hosts  # Permission denied

# teeを使った解決策
echo "127.0.0.1 myhost" | sudo tee -a /etc/hosts

# 標準出力への表示が不要な場合
echo "127.0.0.1 myhost" | sudo tee -a /etc/hosts > /dev/null

プロセス置換との組み合わせ

bashのプロセス置換を使うと、複数のコマンドにデータを同時に渡すことができます。

1
2
# 同じデータを2つの異なるコマンドで処理
echo "test data" | tee >(md5sum > md5.txt) >(sha256sum > sha256.txt) > /dev/null

xargsコマンド

xargsの基本

xargsは、標準入力から読み取ったデータを引数として別のコマンドに渡します。パイプで渡された結果を、引数を受け取るコマンドに渡す際に使用します。

1
2
3
4
5
# findの結果をrmの引数として渡す
find /tmp -name "*.tmp" -mtime +7 | xargs rm -f

# grepで見つかったファイルをlessで開く
grep -l "TODO" *.txt | xargs less

なぜxargsが必要か

多くのコマンドは標準入力からではなく、コマンドライン引数としてファイル名を受け取ります。xargsはこのギャップを埋める役割を果たします。

1
2
3
4
5
# これは動作しない(rmは標準入力からファイル名を読まない)
find /tmp -name "*.tmp" | rm

# xargsを使うと動作する
find /tmp -name "*.tmp" | xargs rm

安全なファイル名の処理

ファイル名にスペースや特殊文字が含まれる場合、デフォルトの動作では問題が発生する可能性があります。-0オプションとfindの-print0を組み合わせることで、安全に処理できます。

1
2
3
4
5
# スペースを含むファイル名も安全に処理
find . -name "*.txt" -print0 | xargs -0 rm

# locateの結果も安全に処理
locate -0 "*.conf" | xargs -0 ls -la

コマンドライン長の制限への対応

xargsは、コマンドライン長の制限を考慮して、必要に応じてコマンドを複数回実行します。

1
2
# 大量のファイルでも問題なく処理
find / -name "*.log" 2>/dev/null | xargs ls -la

xargsの主要オプション

オプション 説明
-0, –null NUL文字を区切りとして使用(スペースや改行を含むファイル名に対応)
-I {} {}をプレースホルダとして、入力項目で置換
-n 数値 1回のコマンド実行で使用する引数の最大数
-P 数値 並列実行するプロセス数(0で最大並列)
-p 各コマンド実行前に確認を求める
-t 実行するコマンドを標準エラー出力に表示
-r 入力が空の場合はコマンドを実行しない

-Iオプションによるプレースホルダ

-Iオプションを使うと、引数をコマンドの任意の位置に挿入できます。

1
2
3
4
5
# 各ファイルを.bakという拡張子でコピー
find . -name "*.txt" | xargs -I {} cp {} {}.bak

# ファイルを特定のディレクトリに移動
find . -name "*.log" -mtime +30 | xargs -I {} mv {} /archive/logs/

並列処理

-Pオプションで複数のプロセスを並列実行できます。

1
2
3
4
5
# 4並列で画像変換を実行
find . -name "*.png" | xargs -P 4 -I {} convert {} -resize 50% resized_{}

# 利用可能なCPUコア数だけ並列実行
find . -name "*.txt" | xargs -P $(nproc) -I {} gzip {}

確認モードとデバッグ

1
2
3
4
5
# 実行前に確認を求める
find . -name "*.tmp" | xargs -p rm

# 実行するコマンドを表示
find . -name "*.tmp" | xargs -t rm

高度なリダイレクト技法

ファイルディスクリプタの操作

bashでは、任意のファイルディスクリプタを作成して操作できます。

1
2
3
4
5
6
7
8
9
# ファイルディスクリプタ3を開く
exec 3> output.txt

# ファイルディスクリプタ3に書き込み
echo "Line 1" >&3
echo "Line 2" >&3

# ファイルディスクリプタ3を閉じる
exec 3>&-

読み書き両用のファイルディスクリプタ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# ファイルを読み書き両用で開く
exec 3<> data.txt

# 読み取り
read line <&3
echo "Read: $line"

# 書き込み
echo "New line" >&3

# 閉じる
exec 3>&-

一時的なファイルディスクリプタの複製

1
2
3
4
5
6
# 標準エラー出力を一時的に保存し、後で復元
exec 3>&2          # fd 3にstderrを複製
exec 2>/dev/null   # stderrを/dev/nullにリダイレクト
noisy_command      # エラー出力が抑制される
exec 2>&3          # stderrを復元
exec 3>&-          # fd 3を閉じる

プロセス置換

プロセス置換は、コマンドの出力をファイルのように扱える機能です。

1
2
3
4
5
6
7
8
# 2つのディレクトリの内容を比較
diff <(ls dir1) <(ls dir2)

# 2つのコマンドの出力を比較
diff <(sort file1.txt) <(sort file2.txt)

# 複数のファイルを結合してソート
sort -m <(sort file1.txt) <(sort file2.txt) <(sort file3.txt)

実践的な活用例

ログ解析パイプライン

1
2
3
4
5
6
7
8
# Nginxアクセスログから上位10件のIPアドレスを抽出
cat /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

# 特定の時間帯のエラーを抽出してファイルに保存
grep "$(date '+%Y-%m-%d') 1[4-7]:" /var/log/app/error.log | tee today_afternoon_errors.log | wc -l

# HTTPステータスコード別の集計
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

システム監視

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ディスク使用率が80%を超えるパーティションを警告
df -h | awk 'NR>1 && int($5) > 80 {print "Warning: " $6 " is " $5 " full"}'

# メモリ使用量の多いプロセス上位5件
ps aux --sort=-%mem | head -6

# 特定のプロセスのCPU使用率を監視
while true; do
    ps aux | grep "[n]ginx" | awk '{sum+=$3} END {print strftime("%H:%M:%S"), sum"%"}'
    sleep 5
done

バッチファイル処理

1
2
3
4
5
6
7
8
# 全てのJPEGファイルをPNGに変換(ImageMagick使用)
find . -name "*.jpg" -print0 | xargs -0 -P 4 -I {} convert {} {}.png

# 古いログファイルを圧縮
find /var/log -name "*.log" -mtime +7 -print0 | xargs -0 gzip

# バックアップ対象ファイルのリストを作成してtar
find /home -type f -mtime -1 -print0 | xargs -0 tar -cvzf backup.tar.gz

設定ファイルの一括編集

1
2
3
4
5
# 全ての設定ファイルで特定の文字列を置換
find /etc/myapp -name "*.conf" -print0 | xargs -0 sed -i 's/old_value/new_value/g'

# コメント行と空行を除外して有効な設定を確認
grep -v '^#' /etc/nginx/nginx.conf | grep -v '^$' | grep -v '^\s*#'

データ変換パイプライン

1
2
3
4
5
6
7
8
# CSVファイルの特定カラムを抽出して重複を除去
cat data.csv | cut -d',' -f2 | sort -u > unique_values.txt

# JSONからキーを抽出(jqがインストールされている場合)
cat data.json | jq -r '.items[].name' | sort | uniq -c

# 複数ファイルのマージとソート
cat file1.txt file2.txt file3.txt | sort -u > merged_unique.txt

パイプラインのデバッグ

中間結果の確認

パイプラインが期待通りに動作しない場合、各段階の出力を確認することが重要です。

1
2
# teeを使って中間結果をファイルに保存
cat data.txt | tee step1.log | grep "pattern" | tee step2.log | sort | tee step3.log | uniq -c

パイプラインの可視化

1
2
# 各ステップの行数を確認
cat data.txt | tee >(wc -l >&2) | grep "pattern" | tee >(wc -l >&2) | sort | uniq -c

実行コマンドの表示

1
2
# xargsで実行されるコマンドを確認
find . -name "*.txt" | xargs -t echo rm

よくある間違いと対処法

リダイレクトの順序

1
2
3
4
5
6
7
# 間違い: エラーがファイルに保存されない
command 2>&1 > file.txt

# 正解: 全ての出力がファイルに保存される
command > file.txt 2>&1
# または
command &> file.txt

変数への出力の保存

1
2
3
# パイプラインの結果を変数に保存
result=$(cat file.txt | grep "pattern" | wc -l)
echo "Found $result matches"

同じファイルへの読み書き

1
2
3
4
5
6
7
8
# 間違い: 入力ファイルが空になる
cat file.txt | sort > file.txt

# 正解: 一時ファイルを使用
cat file.txt | sort > file.txt.tmp && mv file.txt.tmp file.txt

# 正解: spongeコマンドを使用(moreutils)
cat file.txt | sort | sponge file.txt

まとめ

本記事では、Linuxにおけるパイプとリダイレクトの仕組みを詳細に解説しました。

機能 構文 用途
出力リダイレクト > 標準出力をファイルに保存(上書き)
追記リダイレクト >> 標準出力をファイルに追記
入力リダイレクト < ファイルから標準入力を読み込み
エラーリダイレクト 2> 標準エラー出力をファイルに保存
全出力リダイレクト &> 標準出力と標準エラー出力を同じファイルに保存
パイプ | コマンドの標準出力を次のコマンドの標準入力に接続
tee | tee file 出力を分岐してファイルと次のコマンドに渡す
xargs | xargs cmd 標準入力をコマンドの引数に変換

これらの技術を組み合わせることで、シンプルなコマンドから複雑なデータ処理パイプラインを構築できます。日常的な作業で積極的に活用し、効率的なLinux操作を身につけていきましょう。

参考リンク