1-5. パイプとリダイレクト - stdin/stdout/stderr, |, >, xargs

所要時間: 25-50分(がっつりなら2セッション分) ゴール: 標準入出力の概念を説明できる。> >> 2> &> | xargs を意図して使い分けられる コミット内容: 今日の操作履歴を ~/log/linux_day05.log に保存

この章が終わるとできること

  • stdin / stdout / stderr の3つを説明できる
  • > / >> / < / 2> / 2>&1 / &>意図して 使い分けられる
  • /dev/null の使い所(エラー破棄)を即答できる
  • cmd1 | cmd2 | cmd3パイプライン が頭の中で組み立てられる
  • tee xargs で「画面 + ファイル」「結果を別コマンドへ」が書ける
  • ヒアドキュメント (<<EOF) で複数行ファイルを生成できる

Day 1-4 とのつながり

  • Day 4 でちらほら出てきた | の正体に、今日ついに踏み込む
  • Day 4 の grep awk sort uniq「パイプで繋ぐ前提」 で設計されている
  • Day 1 で見た echo "x" > file.txt> の正体も今日明らかになる

これができると何が嬉しいか

  • ... | tee -a deploy.log で「進捗を画面に出しつつログに残す」が書ける
  • find ... | xargs rm で「条件で見つけた100ファイルを一括削除」が書ける
  • Day 6 以降の 「コマンドを組み合わせて自動化する」 ためのベース技能が揃う

大前提: Unix哲学とパイプの思想

Unix哲学のキーフレーズ:

“Write programs that do one thing and do it well. Write programs to work together.” (1つのことを上手くやるプログラムを書け。プログラム同士が協調するように書け)

Day4 で見たように、Unix の道具は grep(検索だけ)、sort(並べ替えだけ)、uniq(重複処理だけ)と、1つの仕事に特化している。一見不便に見えるが、**「組み合わせる仕組み」**があるからこそ、無数の処理を作れる。

その「組み合わせる仕組み」こそがパイプとリダイレクト

  • リダイレクト (>, <, 2>) : コマンドの入出力先を「ファイル」に切り替える
  • パイプ (|) : あるコマンドの出力を別のコマンドの入力にする

この2つを使いこなせると、**シェルが「巨大なデータ処理パイプライン」**になる。バックエンドエンジニアの作業効率を一段引き上げる、最重要スキル。


セッション①: 標準入出力とリダイレクト(25-30分)

0. 録画スタート&作業ディレクトリ

mkdir -p ~/log ~/learn/linux/day05
cd ~/learn/linux/day05
script ~/log/linux_day05.log
 
# 演習で使うサンプルログを生成(Day 4 と同内容、章を自己完結させるため)
cat > access.log <<'EOF'
2026-05-14 10:00:01 192.168.1.10 GET /api/users 200
2026-05-14 10:00:02 192.168.1.11 POST /api/login 200
2026-05-14 10:00:03 192.168.1.10 GET /api/users 200
2026-05-14 10:00:05 10.0.0.5 GET /api/products 404
2026-05-14 10:00:08 192.168.1.10 GET /api/orders 500
2026-05-14 10:00:10 10.0.0.5 GET /api/products 404
2026-05-14 10:00:12 192.168.1.12 POST /api/login 401
2026-05-14 10:00:15 192.168.1.10 GET /api/users 200
2026-05-14 10:00:20 10.0.0.5 GET /api/products 404
2026-05-14 10:00:25 192.168.1.13 DELETE /api/orders/1 500
2026-05-14 10:00:30 192.168.1.99 GET /health 200
2026-05-14 10:00:30 192.168.1.99 GET /health 200
2026-05-14 10:00:30 192.168.1.99 GET /health 200
2026-05-14 10:00:31 192.168.1.99 GET /health 200
2026-05-14 10:00:31 192.168.1.99 GET /health 200
EOF

1. リダイレクトとは何か

普段、コマンドを実行すると結果は**画面(ターミナル)**に出る。これがデフォルトの「出力先」。

リダイレクトとは、この出力先を画面以外(ファイルなど)に切り替える操作のこと。記号は > >> < 2> &> などいろいろあるが、やっていることはどれも「入出力の行き先を変える」だけ。

なぜリダイレクトが必要か:

  • ログを保存したい: 長時間動くスクリプトの結果を、後で見返せるようにファイルへ
  • エラーを抑制したい: 大量の権限エラーが画面を埋め尽くすのを防ぐ
  • 設定ファイルを生成したい: コマンドで動的に作った内容をファイルに書き出す
  • ファイルを入力にしたい: コマンドに直接タイプする代わりに、ファイルの中身を流し込む

これからリダイレクト記号を1つずつ、「説明 → いつ使うか → 具体例」 の順に紹介していく。最後に全体まとめ表を置くので、まずは一個ずつ手を動かしながら腹落ちさせるのがコツ。

2. > - 標準出力をファイルへ(上書き)

>コマンドの結果(標準出力)をファイルに書き出すリダイレクト。日常的な「→(こっちに送る)」と同じ感覚。既存ファイルがあれば中身は問答無用で上書きされる。

いつ使うか

  • コマンドの結果を保存したい: ls -la > filelist.txt でファイル一覧をスナップショット
  • 設定ファイルを新規生成したい: echo "port=8080" > config.ini で1行設定を作る
  • 空ファイルを作りたい: > empty.txt だけで0バイトのファイルを作成
  • 前回の結果を上書きしたい: 毎回最新だけ残したいレポート出力など
# 普通: 結果は画面に出る
echo "hello"
 
# リダイレクト: 結果はファイルへ(画面には何も出ない)
echo "hello" > out.txt
cat out.txt    # → "hello"
 
# 上書きされることを確認
echo "1行目" > out.txt
echo "2行目" > out.txt   # 上書き、1行目は消える
cat out.txt              # → 「2行目」だけが残る

3. >> - 標準出力をファイルへ(追記)

>>> とほぼ同じだが、既存ファイルの末尾に追記する点が違う。中身は消えない。

いつ使うか

  • ログを蓄積したい: echo "$(date) deployed" >> deploy.log で毎回末尾に履歴を残す($(...) は「中のコマンドを実行してその出力をここに埋め込む」シェル記法=コマンド置換。$(date)Thu May 21 14:30:00 JST 2026 のような現在日時文字列に置き換わる。詳細は Linux 2-2 で扱う)
  • コマンド履歴を集めたい: 複数コマンドの結果を1つのファイルに順次溜める
  • 既存設定に1行足したい: echo "alias ll='ls -la'" >> ~/.zshrc で末尾に追加
  • 既存ファイルを壊したくない: 上書きで事故るリスクを避けたい時のデフォルト選択
# 上書き vs 追記の違いを目で見る
echo "1行目" > out.txt
echo "2行目" >> out.txt  # 追記、両方残る
cat out.txt              # → 「1行目」と「2行目」
 
# 日時付きで追記していく定番パターン
# ↓ $(date) は前述のコマンド置換: date コマンドの出力(現在日時)が文字列としてここに埋め込まれる
echo "$(date) - 作業開始" >> work.log
echo "$(date) - 作業終了" >> work.log
cat work.log

4. 「画面に出る文字」は実は2種類ある(stdout と stderr)

ここまで使ってきた > >> は「コマンドの結果」をファイルに送る記号だった。でも実は、「コマンドの結果」と一言で言っても、内部には2つの別チャンネルがある。これを知ると、残りのリダイレクト記号がスッと理解できる。

次のコマンドを試してみる:

ls /etc /nonexistent_dir

画面にはこんな風に出る:

ls: /nonexistent_dir: No such file or directory   ← エラーメッセージ
/etc:                                              ← 正常な結果
<...フォルダ一覧...>

人間の目では「全部画面の文字」に見えるけど、OS の中では別々のチャンネル で流れている:

種類略称番号説明
正常な結果stdout1「ちゃんと処理できた結果」
エラー出力stderr2「失敗・警告・進捗メッセージ」

これらは OS 内部にある2本の独立したチャンネル。デフォルトでは両方とも画面に流れているから、混ざって見えるだけ。

なぜ2本に分けてあるか:

  • stdout = 「次のコマンドに渡したい有用なデータ」
  • stderr = 「人間に知らせたいけど、次のコマンドには流したくないメッセージ」

例えば「正常な結果だけファイルに保存、エラーは画面で見たい」が普通にやれる:

# 1番のチャンネル(stdout)だけファイルに送る
ls /etc /nonexistent_dir > result.txt
 
# result.txt には /etc の内容だけ入っている
cat result.txt
 
# エラーメッセージは実行時点で画面に出たまま、ファイルには入っていない

これが Unix の 「2チャンネルを別々に扱える」 という強み。

ファイルディスクリプタ(fd)という概念

Unix では「ファイル」「キーボード」「画面」「ネットワーク」など、入出力できるものはすべて「ファイル」として抽象化される。プロセスはそれぞれ番号(ファイルディスクリプタ)でアクセスする。

番号名前デフォルトの接続先
0stdin(標準入力)キーボード
1stdout(標準出力)画面
2stderr(標準エラー出力)画面
3以降自分でファイルを開いたら割り当てられるユーザー指定

リダイレクト記号は、この番号を使って「どのチャンネルをどこへ送るか」を指定する仕組み。

5. 2> - 標準エラー出力をファイルへ

2>stderr(2番チャンネル)をファイルに送るリダイレクト。> の前に 2 を付けるだけ。「何も書かない > は実は 1> の省略形」だと知ると、2> は自然に読める。

いつ使うか

  • エラーログだけ別ファイルに保存したい: バッチ処理で「正常結果」と「エラー」を分けて記録
  • エラーだけ捨てたい: find / -name "*.log" 2> /dev/null で大量の権限エラーを抑制
  • 失敗時の原因調査をしやすくしたい: stderr を独立したファイルに溜めておけば、後で grep しやすい
  • stdout と stderr を別々に処理したい: 正常結果をパイプで次に流しつつ、エラーはファイルへ
# stderr だけ別ファイルに
ls /nonexistent_dir 2> err.txt
cat err.txt    # → エラーメッセージ
 
# stdout と stderr を別々のファイルに
ls /etc /nonexistent_dir > stdout.txt 2> stderr.txt
cat stdout.txt   # → 正常な結果だけ
cat stderr.txt   # → エラーだけ
 
# 追記版: `2>>`
ls /nonexistent_dir 2>> err.txt
ls /another_missing 2>> err.txt
cat err.txt    # → 2回分のエラーが追記されている

6. 2>&1&> - stdout と stderr の両方を同じ場所へ

「stdout も stderr も両方まとめてファイルに保存したい」場面はめちゃくちゃ多い(デプロイログ、CI出力など)。これを書く方法が2つある。

2>&1 を分解すると: 「チャンネル2を、チャンネル1が今向いている場所と同じ場所へ送る」。 ここで重要なのが & の意味: 「この後ろはファイル名じゃなく、チャンネル番号です」とシェルに教える印。これがないと 2>1 は「stderr を 1 という名前のファイルへ送る」と解釈されてしまう(1 という名のファイルが作られる)。

&> は新しい書き方で、「stdout と stderr の両方をこのファイルへ」を1記号で表現できる。読みやすく、順序ミスも起きないので現代では &> が推奨

いつ使うか

  • デプロイ・ビルドの全出力をログに集約: ./deploy.sh &> deploy.log
  • 長時間タスクを背景実行してログだけ残す: ./batch.sh &> batch.log &
  • CI/CD で完全な実行記録を残したい: 成功も失敗も同じファイルに時系列で並ぶ
  • 「成否を気にせず全部捨てたい」時: command &> /dev/null
# 古い書き方: 2>&1
ls /etc /nonexistent_dir > all.txt 2>&1
cat all.txt    # → 正常結果もエラーも両方入っている
 
# 新しい書き方(推奨): &>
ls /etc /nonexistent_dir &> all.txt
cat all.txt    # → 同じ結果
 
# 追記版
ls /etc /nonexistent_dir &>> all.txt

2>&1 の「書く順序」が大事

command > file 2>&1    # OK: 「[1]をfileへ」→「[2]を[1]の今の場所(=file)へ」=両方fileへ
command 2>&1 > file    # NG: 「[2]を[1]の今の場所(=画面)へ」→「[1]をfileへ」= stderrは画面に残る

リダイレクトは 左から右に順番に処理 されるので、書く順序を逆にすると意図と違う動きになる。混乱の元なので、現代では &> の方が推奨される。

7. < - ファイルを標準入力として読む

ここまで見てきた > >> 2> &> は全部、コマンドの「出力」をファイルに送る 方向だった。 < だけ矢印の向きが逆で、ファイルの「中身」をコマンドの入力にする リダイレクトになる。

>  コマンド ───→ ファイル   (出力を書き出す)
<  ファイル ───→ コマンド   (入力として読み込む)

まず動かしてみる

# wc -l access.log と wc -l < access.log は、どちらも「access.log の行数を数える」
wc -l access.log
# →       15 access.log     ← ファイル名が一緒に表示される
 
wc -l < access.log
# →       15                ← 行数だけ

結果はほぼ同じだけど、< を使った方は「ファイル名」が出力に出てこない。これは:

  • 引数で渡す (wc -l access.log) → コマンドは「access.log というファイルを見ている」ことを知っている
  • < で流し込む (wc -l < access.log) → コマンドは「どこからかデータが流れてきた」としか認識しない(中身だけ来る、名前は来ない)

< がないと困る」場面

多くのコマンドは引数でファイル名を受け取れる(wc, cat, grep などここまで習ったコマンドのほとんど)ので、< を使わなくても困らないことが多い。ただし以下のような 引数でファイルを取らないコマンド には < が必須:

# bc は「計算式を stdin から読み取って結果を返す電卓コマンド」
# 引数でファイル名を受け取らないので < で流し込む必要がある
echo "100 + 200" > calc.txt
echo "300 * 4" >> calc.txt
 
bc < calc.txt
# → 300
#   1200
 
rm calc.txt

bc calc.txt と書いても動かない(bc はファイル名引数を受け付けない)。こういう時に < の出番。

いつ < を使うか

  • 引数でファイル名を取れないコマンドに食わせたい時: bc (電卓) など
  • DB に SQL を流し込みたい時: mysql -u root mydb < schema.sql で SQL ファイルを一括実行
  • シェルスクリプトでファイル内容を「行ごとに処理」したい時: while read line; do ...; done < file.txt(while ループは 2-3 制御構文 で扱う)
  • cat file | cmd を1プロセス減らしたい時: cmd < file の方が cat プロセスを起動しないぶん効率的(“Useless Use of Cat” として有名)

< 使うべき or 引数でいい?」の判断

迷ったら 「引数」を優先 でOK。引数で受け取れるコマンドなら、その方が読みやすく、出力にもファイル名が出る。 < を使うのは:

  1. 引数を取らないコマンドに食わせる時(bc のような)
  2. < file の出力にファイル名を入れたくない時(スクリプトの整形等)
  3. while read のような シェルの構文 で必須な時

これくらいの感覚でOK。

8. リダイレクト記号の全体まとめ表

ここまで紹介した記号は、すべて3パーツの組み合わせとして読める。これを覚えると新しい記号が出てきても怖くない。

 [どのチャンネル]   [向き]   [先]
       2            >       err.txt
  • どのチャンネル: 何も書かなければ 1 (stdout)、2 と書けば stderr、& で「両方」
  • 向き: > 上書き / >> 追記 / < 入力
  • : ファイル名、または &1 (1番が今向いてる場所) / &2 (2番が今向いてる場所)
記号パーツ分解意味
> file[1] > filestdout をファイルへ(上書き)
>> file[1] >> filestdout を追記
2> file[2] > filestderr をファイルへ(上書き)
2>> file[2] >> filestderr を追記
2>&1[2] > &1stderr を、1番が今向いてる場所へ
&> file[1+2] > filestdout と stderr の両方をファイルへ
&>> file[1+2] >> filestdout と stderr の両方を追記
< filestdin < fileファイルを stdin として読む
<< EOFstdin << EOFヒアドキュメント(次節で扱う)

リダイレクトの実務ユースケース総まとめ

  • ログを保存: ./long_running_task.sh &> task.log
  • エラーだけ捨てる: find / -name "*.log" 2>/dev/null
  • 進捗を画面に出しつつログにも残す: tee を使う(セッション② で扱う)
  • デプロイ結果を日付付きで記録: ./deploy.sh &> deploy_$(date +%Y%m%d).log

> で大事故が起きる

  • 既存ファイルが問答無用で上書きされる: cat report.txt > report.txt のような自己参照は 空ファイルになる(シェルが先に report.txt を空にしてから cat する)
  • > のスペースを間違えがち: echo "x">file でも動くが、echo "x" > file の方が安全
  • noclobber オプションで防御: set -o noclobber.zshrc に書くと、既存ファイルへの > はエラーになる。強制上書きは >|set -o <名前> は bash/zsh の動作モードを切り替えるシェル組込みコマンド。詳細は Linux 2-2 で扱う。ここでは「.zshrc に1行足せば上書き事故を防げる安全装置がある」という発想だけ覚えればOK)

9. /dev/null - 出力の捨て場

/dev/null「Unix のゴミ箱」 とも呼ばれる特殊なファイル。書き込んだものはすべて消える、読み出しても何も出ない。

「不要な出力を捨てたい」「進捗メッセージを黙らせたい」時に使う。

# 出力を全部捨てる
command > /dev/null
 
# エラーだけ捨てる(正常な結果は画面に残る)
find / -name "*.log" 2> /dev/null
 
# 全部捨てる(成功・失敗を気にせず実行したい時)
command > /dev/null 2>&1
command &> /dev/null
 
# /dev/null から読み出すと「空」が返る
cat /dev/null    # 何も出ない

/dev/null の使い所

  • 不要なエラーメッセージを消す: find / 2>/dev/null で権限エラーを抑制
  • コマンドの成否だけ知りたい時: if curl -s example.com > /dev/null; then echo "OK"; fiif <コマンド>; then ...; fi は「コマンドが成功(終了ステータス0)なら then 以降を実行」する bash の制御構文。詳細は Linux 2-3 で扱う。ここでは「curl が成功したら OK と表示」のイメージだけ持てればOK)
  • 空ファイルを作る: > file.txt または cat /dev/null > file.txt
  • 進捗表示の煩わしいコマンドを静かにする: wget URL -q > /dev/null 2>&1

似たデバイスファイル

  • /dev/null: 書いたら消える、読んだら空
  • /dev/zero: 読むと永遠に 0x00 が返る(ファイル埋め立て用: dd if=/dev/zero of=test bs=1M count=100 で100MBの空ファイル)
  • /dev/random / /dev/urandom: 乱数の源(パスワード生成などに)

10. ヒアドキュメント << - 長文をコマンドに流し込む

<<「長文の文字列を、コマンドの stdin に流し込む」 リダイレクト。<<EOF から EOF までが文字列としてコマンドに渡される。設定ファイル生成や SQL 実行で頻出。

# 複数行の文字列を一気にコマンドに渡す
cat <<EOF
1行目
2行目
3行目
EOF
 
# 変数展開も働く
NAME="takato"
cat <<EOF > greeting.txt
Hello, $NAME!
Today is $(date).
EOF
cat greeting.txt
 
# 変数展開を止めたい時はクォート('EOF' のようにシングル)
cat <<'EOF' > script.sh
echo "$HOME"   # ← これがそのまま出る(展開されない)
EOF
cat script.sh

ヒアドキュメントの2つのモード

「EOF」は単なる目印(区切り文字)で、好きな名前に変えられる: <<END, <<__SCRIPT__ など。

  • <<EOF (クォートなし): 内部で $VAR$(...) が展開される
  • <<'EOF' (クォート付き): 何も展開されず、そのまま渡される

使い分け: 動的な値を埋め込みたいならクォートなし、スクリプトのテンプレートをそのまま渡したいならクォート付き。

ヒアドキュメントの実務ユースケース

  • 設定ファイルを生成: cat > /etc/nginx/sites-available/myapp <<EOF ... EOF
  • SQLを直接叩く: mysql -u root <<EOF \n SELECT * FROM users; \n EOF
  • 複数行のメッセージ送信: mail -s "Report" admin@example.com <<EOF ... EOF
  • Dockerfile内でmultilineを書く: RUN cat > /etc/config <<EOF ... EOF

ヒアドキュメントの落とし穴

  • 終端のEOFは行頭から書く: スペース・タブを入れるとエラー。<<-EOF (ハイフン付き)にすればタブインデントは無視される
  • $ を含むスクリプトを書くなら必ずクォート: クォートなしだと展開されて意図しないものに置き換わる
  • <<< (ヒアストリング)と混同しない: <<< は単一文字列を渡す: wc -l <<< "hello"1

セッション②: パイプ・tee・xargs(25-30分)

11. パイプ | の本質

|「左のコマンドの stdout を、右のコマンドの stdin に直接繋ぐ」 仕組み。ファイルを経由せず、コマンド同士を直結する。Unix で最も使われる記号の1つ。

# 「pythonプロセスが何個動いているか」
ps aux | grep python | grep -v grep | wc -l
 
# 「ファイル数の多い順にディレクトリを並べる」
find . -type f | awk -F'/' '{print $2}' | sort | uniq -c | sort -rn | head
 
# 「コマンド履歴の頻度ランキング」
history | awk '{print $2}' | sort | uniq -c | sort -rn | head

パイプの仕組み

A | B は「Aの標準出力を、Bの標準入力に繋ぐ」。

     ┌─ stdout ─┐         ┌─ stdin ──┐
[A] -│          │ pipe →  │           │- [B]
     └──────────┘         └───────────┘

重要な性質:

  • AとBは同時に動く(並列実行)。Aが全部終わってからBが動くのではない
  • Aの出力が大きくても、メモリに全部溜め込まずストリーミングで処理される
  • 何個でも繋げる: A | B | C | D | ...

これにより、巨大ファイル(数GB)でも、メモリを使わずに集計できる

リダイレクトとの違い

  • リダイレクト (>, <, 2>) : コマンドの入出力先を ファイル に切り替える
  • パイプ (|) : コマンドの出力を 次のコマンド に直結する

どちらも「コマンドの入出力先を変える」点では同じ仲間だが、相手が ファイルかコマンドか が違う。

パイプの実務ユースケース

  • ログ集計: cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
  • プロセス検索: ps aux | grep nginx
  • ネットワーク監視: netstat -an | grep LISTEN
  • ディスク使用量ランキング: du -sh */ | sort -rh | head

パイプの落とし穴

  • エラーは流れない(stdout だけ): A | B| は stdout のみ繋ぐ。stderr は画面に出る。両方繋ぎたければ A 2>&1 | B または A |& B
  • パイプの終了ステータス: A | B の終了ステータスはデフォルトで B のもの。A の成否を知りたければ set -o pipefail または ${PIPESTATUS[0]} を使う(set -o pipefail は「パイプの途中でコケたら全体を失敗扱いにする」bash のオプション、${PIPESTATUS[N]} はパイプ各段の終了ステータスを格納した配列の参照。どちらも詳細は Linux 2-2 で扱う)
  • grep grep 問題: ps aux | grep nginx には grep nginx 自身もマッチする。grep -v grep を付ける or grep "[n]ginx" のトリック
  • 巨大ファイルでも cat 不要: 入力ファイルを取れるコマンドなら cat file | grep x ではなく grep x file の方が速い(Useless Use of Cat と揶揄される)

12. tee - 「画面表示」と「ファイル保存」を両立

teeT字管のように、入力を2方向に分岐するコマンド。画面(stdout)にそのまま流しつつ、同時に指定ファイルにも書き込む。命名の由来は「T」の形(パイプを2方向に分岐するから)。

# 結果を画面に出しつつファイルにも保存
ls -la | tee filelist.txt
 
# 追記モード
echo "added" | tee -a filelist.txt
 
# パイプの途中経過を覗き見
cat access.log | grep "500" | tee errors.log | wc -l
# ↑ 500エラーの行を errors.log に保存しつつ、件数を画面表示
 
# sudo が必要なファイルに書き込む(リダイレクトでは無理なケース)
echo "127.0.0.1 myapp.local" | sudo tee -a /etc/hosts

tee の実務ユースケース

  • 長時間タスクの進捗を見つつログ保存: ./deploy.sh 2>&1 | tee deploy.log
  • sudo 経由でファイルに書く: > はリダイレクトする側のシェルが処理するので sudo の効果が及ばない。sudo tee ならtee自体がrootで動く
  • パイプの中間結果を保存しつつ次のコマンドへ: ... | tee step1.log | ... | tee step2.log | ...

tee の落とし穴

  • デフォルトは上書き: 追記したいなら -a を忘れずに
  • >> と勘違いしない: tee 単体は上書き、tee -a で追記

13. xargs - 標準入力を「引数」に変換

xargsstdin に流れてきた文字列を、コマンドの「引数」に変換するコマンドrm, mv, cp, echo などの多くのコマンドは 引数からファイル名を受け取る のであって、stdin から読まない。だから find の結果を直接 rm に渡すには xargs が必要になる。

# echo はパイプから入力を受け取らない
ls | echo                # 何も出ない(echo は stdin を読まない)
ls | xargs echo          # ファイル名が引数として echo に渡る
 
# find と組み合わせて削除
find . -name "*.tmp" | xargs rm
 
# 1コマンドずつ実行(並列化)
find . -name "*.log" | xargs -P 4 gzip   # 4並列で圧縮
 
# スペース・特殊文字に強い版(find -print0 と xargs -0)
find . -name "*.log" -print0 | xargs -0 rm
 
# 引数の位置を指定({} で挿入)
ls *.txt | xargs -I {} cp {} backup/
 
# 確認モード(実行前に y/n を聞く)
ls | xargs -p rm

なぜ xargs が必要か

echo "file.txt"          # 「file.txt」と表示
echo < file.txt          # 何も表示しない(echo は stdin を読まないので)

パイプで前のコマンドから渡されるのは stdin。しかし echo, rm, mv などは引数で動く。この「stdin → 引数」の変換が xargs の仕事

echo "file.txt" | xargs cat    # → cat file.txt と同じ動作

xargs の主要オプション

オプション意味
-I {}{} の位置に各入力を埋める(順序入れ替え可能)
-n 11個ずつコマンドを呼ぶ(バッチではなく逐次)
-P 4並列実行(4個同時に)
-0NULL区切りで読む(find -print0 と組み合わせる)
-p各コマンドを実行前に確認
-t実行するコマンドを画面表示してから実行

xargs の実務ユースケース

  • find の結果を一括操作: find . -name "*.tmp" -print0 | xargs -0 rm
  • CSVのIPリストに一斉ping: cat ips.txt | xargs -n 1 -P 10 ping -c 1
  • 複数ファイルを別ディレクトリに移動: ls *.log | xargs -I {} mv {} archive/{}_$(date +%Y%m%d)
  • 大量のDockerイメージを削除: docker images -q | xargs docker rmi

xargs の落とし穴

  • スペース入りファイル名で爆発する: My Document.txt のようなファイルがあると、xargs はスペースを区切りとして扱い「My」と「Document.txt」の2引数だと思って失敗する。find -print0 | xargs -0 をデフォルトに
  • 空入力で「不思議な動作」: 入力が空でも xargs は1回コマンドを呼ぶ場合がある(GNU版 -r で抑制可能、macOSのBSD版は対応していないため空入力チェックを別途)
  • find-exec と xargs の違い:
    • find ... -exec rm {} \; : 1ファイルずつ rm を起動(遅い)
    • find ... -exec rm {} + : まとめて起動(速い)
    • find ... | xargs rm : まとめて起動(速い、ただしスペース問題に注意)
    • 結論: 安全と速度を両立するなら find ... -exec rm {} + が最も安心

14. 実務でよく見る組み合わせパターン

# パターン1: ログから特定IPの500エラーだけを抽出してファイル保存
grep "500" access.log | awk '$3 == "192.168.1.10"' | tee errors_for_ip.log
 
# パターン2: 大量ファイルをまとめて移動(スペース対応)
find /backup -name "*.tar.gz" -mtime +30 -print0 | xargs -0 mv -t /old_backup/
 
# パターン3: ヒアドキュメントで設定ファイル生成
cat > nginx.conf <<EOF
server {
    listen 80;
    server_name myapp.local;
    location / {
        proxy_pass http://localhost:3000;
    }
}
EOF
 
# パターン4: 進捗を見つつ結果をログに残す
#    ↓ `$(コマンド)` は「中のコマンドを実行して、その出力をここに埋め込む」シェル記法=コマンド置換(詳細は Linux 2-2)。
#      `$(date +%Y%m%d_%H%M)` は `20260521_1430` のような日時文字列に展開される。
./long_task.sh 2>&1 | tee task_$(date +%Y%m%d_%H%M).log
 
# パターン5: エラーを捨てつつ正常結果だけパイプ処理
find / -name "*.conf" 2>/dev/null | xargs grep -l "listen" 2>/dev/null

15. 練習課題

# 課題1: 500エラーの行を errors.log に保存しつつ件数を画面に出す
grep "500" access.log | tee errors.log | wc -l
 
# 課題2: アクセスIPの上位3つを抽出して ranking.txt に保存
awk '{print $3}' access.log | sort | uniq -c | sort -rn | head -3 | tee ranking.txt
 
# 課題3: stderr 抑制つきで /etc 配下から ".conf" を探す
find /etc -name "*.conf" 2>/dev/null | head -5
 
# 課題4: 練習用ファイルを5個作って、xargs でまとめて削除
touch trash_{01..05}.tmp
ls trash_*.tmp
ls trash_*.tmp | xargs rm    # 一括削除
ls trash_*.tmp 2>/dev/null   # 何も出なければ成功
 
# 課題5: ヒアドキュメントで簡単なシェルスクリプトを生成
#    ↓ 1行目の `#!/bin/bash` は「shebang」と呼ばれ、「このファイルを bash で実行してね」と OS に教える宣言行(詳細は Linux 2-2)。
#      `$(whoami)` `$(date)` はコマンド置換: 中のコマンドを実行して、その出力を文字列としてここに埋め込む(詳細は Linux 2-2)。
#      クォート付きヒア (<<'EOF') を使っているのでファイル生成時には展開されず、後で ./greet.sh を実行した瞬間に展開される。
cat > greet.sh <<'EOF'
#!/bin/bash
echo "Hello from $(whoami) at $(date)"
EOF
chmod +x greet.sh
./greet.sh

締め: 振り返り(10分)

1. セッション録画を終了

exit

2. 今日の発見

このノートに追記:

- stdout と stderr が分かれている意味、納得できたシーン:
  成功結果とエラー結果を分けたい場合がある。とくにエラーログなどはまとめておきたいので2>を使うのは納得
- xargs を初めて使った感想:
  findのexecでもいい気がする
- 「これパイプで一発じゃん」と気付いた処理:
  アクセスの多いIPアドレスとかをランキングにするとは楽そう
- 明日やりたいこと:

チェックリスト

  • > >> < 2> 2>&1 &> をそれぞれ実際に試した
  • /dev/null でエラー出力を捨てる場面を体験した
  • パイプで3つ以上のコマンドを繋いだ
  • tee で「画面表示+ファイル保存」を同時にやった
  • xargsfind の結果を別コマンドに渡せた
  • ヒアドキュメントで複数行のファイルを生成した

詰まった時のチートシート

やりたいこと書き方
標準出力をファイルへcommand > file
標準出力を追記command >> file
標準エラーをファイルへcommand 2> file
両方を同じファイルへcommand &> file または command > file 2>&1
エラーだけ捨てるcommand 2>/dev/null
全部捨てるcommand &>/dev/null
ファイルを入力にcommand < file
パイプで連結cmdA | cmdB
パイプ+ファイル保存cmdA | tee file | cmdB
パイプ+追記保存cmdA | tee -a file
sudo でファイル書き込みecho "x" | sudo tee -a /etc/file
stdin を引数に変換... | xargs <command>
並列実行... | xargs -P 4 <command>
スペース対応find ... -print0 | xargs -0 ...
ヒアドキュメントcat <<EOF ... EOF
変数展開なしのヒアcat <<'EOF' ... EOF

「実務OK」基準

  • stdin / stdout / stderr の3つを意識して、リダイレクトを書ける
  • 2>/dev/null で不要なエラーを抑制し、欲しい情報だけ拾える
  • 長いパイプチェーン(5段以上)を組み立てて集計できる
  • tee で「進捗を見つつログ保存」のパターンが書ける
  • xargs を使った find との連携が書ける

ここまで来れば「シェルでデータ加工パイプラインを組める人」。


アンチパターン / 初心者やらかし事例

NG 1: command > file 2>&1 の順序を間違える

command 2>&1 > file   # NG: stderr が「画面」へ、stdout だけ file へ行く
command > file 2>&1   # OK: 両方 file へ

→ シェルは左から右に解釈する。2>&1「現時点の stdout の向き先に stderr を流す」 の意味。> を先に書いてから 2>&1 が鉄則。

NG 2: cat file | grep x で「ファイルなくしてる?」

cat large.log | grep ERROR   # 動くが UUOC(Useless Use of Cat)
grep ERROR large.log         # こちらでOK

→ パイプは便利だが、1ファイルなら直接渡せるgrep / awk / sort は引数にファイルを取れる。

NG 3: > で既存ファイルを上書きしてしまう

sort access.log > access.log   # 空になる!シェルが先に file を開いて 0 にしてから sort が読みに行く

→ 対策: 入力と出力を同じファイルにしない。一時ファイル経由sort access.log > tmp && mv tmp access.log。または sort -o access.log access.log-o は sort 専用、安全)。

NG 4: xargs でスペース入りファイル名が壊れる

find . -name "*.log" | xargs rm   # "My Log.txt" が「My」「Log.txt」と分割される

→ 対策: -print0 + xargs -0 をセットで使う: find . -name "*.log" -print0 | xargs -0 rm


自己評価チェックリスト

  • stdoutstderr がなぜ分かれているか、実例で説明できる
  • 2>/dev/null の意味を即答できる
  • tee を使った「画面 + ファイル保存」のシーンが思い浮かぶ
  • xargs を使うべき場面と find -exec で済む場面の区別がつく
  • ヒアドキュメント <<EOF<<'EOF'(変数展開しない版)の違いが言える

次のレッスン: Day 6 - プロセス管理

明日は 動いているプログラムを見る・操る がテーマ。

今日まで「ファイル」と「テキスト」を扱ってきたが、Linux にはもうひとつの主役 ─ 「プロセス」 がいる。ps / top / htop / kill / & (バックグラウンド) で「Webサーバーが落ちた」「重いプロセスが居座っている」みたいな現場対応の基本を覚える。

Day 6: プロセス管理