1-4. テキスト処理ツール - grep, awk, sed, sort, uniq, wc

所要時間: 25-50分(がっつりなら2セッション分) ゴール: ログファイルから必要な行を抜き出し、整形・集計してレポートを作れる コミット内容: 今日の操作履歴と作ったワンライナーを ~/log/linux_day04.log に保存

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

  • grep -i / -n / -v / -r / -E を使い分けて、ログから必要な行だけ抜ける
  • awk '{print $N}' で N 列目を取り出し、$5 == "ERROR" で条件抽出できる
  • sed 's/old/new/g' で置換、sed -n '5,10p' で範囲抽出ができる
  • sort | uniq -c | sort -rn「集計三種の神器」 ワンライナーが書ける
  • wc -l / -w / -c で行・単語・バイト数を即カウントできる

Day 1-3 とのつながり

  • Day 1 の cat / less / tail は「ファイル全体を見る」道具だった
  • Day 4 はその先 ─「必要な行・列・パターンだけ取り出す」道具を覚える
  • Day 3 で Permission denied のエラーログを見ても「読めない」と困った人へ、今日からは grep で原因行だけ拾える

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

  • 500MB のログから「ユーザー X の 500 エラーだけ」を秒で抽出できる
  • 「アクセス上位 IP トップ10」みたいなレポートが 1行のコマンド で出せる
  • Day 5(パイプ)以降、これらの道具を | で繋ぐ快感が始まる

大前提: なぜ「テキスト処理」がバックエンドの中核なのか

Unix哲学の有名なフレーズ: “Everything is a file. Every file is text.”(すべてはファイル、すべてのファイルはテキスト)。

実際バックエンドの仕事の大半は テキストを読み書きする こと:

  • ログ: アクセスログ、エラーログ、アプリログ → すべてテキスト
  • 設定ファイル: nginx, systemd, Dockerfile, YAML, JSON → すべてテキスト
  • API のレスポンス: JSON, XML → テキスト
  • データ移行: CSV, TSV のインポート/エクスポート → テキスト
  • メトリクス: Prometheus形式, ログ形式の数値 → テキスト

「テキストを早く・正確に料理する」スキルは、本番サーバーで障害対応する時の差を決定的に分ける。GUI のログビューワーが無い環境で、500MBのログから「特定ユーザーのエラーだけ」を抜き出して時系列に並べる、みたいな作業を秒で終わらせる人と、1時間かかる人の違いは、ここで学ぶ道具を使えるかどうか。

今日は5つの道具を覚える: grep(検索)/ awk(フィールド処理)/ sed(置換)/ sort(並べ替え)/ uniq(重複排除・カウント)/ wc(カウント)。


セッション①: 閲覧の発展 + grep(25-30分)

0. 録画スタート&作業データ準備

mkdir -p ~/log ~/learn/linux/day04
cd ~/learn/linux/day04
script ~/log/linux_day04.log
 
# 練習用のダミーログを作成
# ↓ cat > file <<'EOF' ... EOF は「ヒアドキュメント」と呼ばれる構文。Linux 1-5 で扱う。
#   ここではコピペで OK、目的は access.log というテキストファイルを作ること。
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
 
cat access.log

1. 閲覧コマンドの発展(cat / less / head / tail 再訪)

Day 1 で覚えた cat / less / head / tail「ログ調査でガチに使う」レベルまで掘り下げる。本番でログを追う時には「ファイル全部を読む」より「先頭/末尾/特定行範囲だけを素早く見る」のが圧倒的に多い。ここでは特に重要な以下の使い方を押さえる。

  • tail -f: ファイルに追記される行をリアルタイムで追う(本番ログ監視の最頻出)
  • head -n N / tail -n N: 行数を明示的に指定して先頭/末尾を見る
  • head | tail の組み合わせ: 「途中の行範囲だけ抜き出す」ワザ
  • cat -n: 行番号付きで全表示(短いログの目視確認に)

これらは grep / awk / sed と組み合わせる前提のコマンドなので、ここで再復習して「長いファイルから必要な範囲を一瞬で切り出す」感覚を身体に染み込ませる。

# 連番でファイル名を出力
cat -n access.log
 
# 末尾10行(デフォルト)
tail access.log
 
# 末尾を「現在進行形で」追跡
tail -f access.log    # Ctrl+C で止める
 
# 先頭5行
head -n 5 access.log
 
# 5行目から10行目まで
head -n 10 access.log | tail -n 6

tail -f は本番調査の最頻出コマンド

ログにリアルタイムで出力される新規行を逐次表示する。アプリの動作確認・障害調査の必須スキル。

使い所:

  • デプロイ直後に tail -f /var/log/nginx/error.log でエラー出てないか監視
  • APIを叩いた瞬間に何が起きているかリアルタイムで観察
  • 別ターミナルで動かしながら開発する

-F(大文字)の違い: ファイルがローテーション(古いログがリネームされて新しいログファイルに切り替わる)された時、-f は古いファイルを追い続けるが、-F は新しいファイルに切り替えて追跡する。長時間ログ監視するなら -F

headtail を組み合わせて「途中の行」を取り出す

# 50行目から60行目まで
head -n 60 file.log | tail -n 11
 
# 末尾の100行のうち、先頭10行(=末尾100-91行目)
tail -n 100 file.log | head -n 10

「行範囲指定」がしたい時はこの形が定番。sed でも書けるが、こちらの方が直感的。

2. grep - テキスト検索の基本

grep「ファイルの中からパターンにマッチする行だけを抜き出して表示するコマンド。Unix最古参のテキスト処理ツールで、名前は g/re/p(global / regular expression / print)の略。バックエンド作業で最も叩く回数が多いコマンドの1つで、これが使えるかどうかが「ログ調査スピードの差」を直撃する。

実務での頻出シーン:

  • エラー調査: grep "ERROR" /var/log/app.log で大量ログからエラー行だけ抜く
  • 特定ユーザー/IPの行動追跡: grep "192.168.1.10" access.log
  • コードベース全体から TODO を発掘: grep -rn "TODO" src/
  • 設定ファイルから特定のキー行だけ取り出す: grep -E "^(host|port)" config.yml

文字列マッチだけでなく正規表現にも対応するので、「数字3桁で始まる行」「行末が .log の行」のような複雑な条件も書ける。さらにパイプ |awk sort uniq と繋げば、ログ集計ワンライナーの主役になる。

# 単純検索
grep "200" access.log
 
# 大文字小文字無視
grep -i "delete" access.log
 
# 行番号も表示
grep -n "404" access.log
 
# 一致しない行(反転)
grep -v "200" access.log
 
# 複数ファイルを再帰検索
grep -r "TODO" ~/learn/linux/
 
# マッチした件数だけカウント
grep -c "500" access.log
 
# 拡張正規表現で OR 検索
grep -E "404|500" access.log

grep の本質

ファイルの中から、パターンにマッチする行を抜き出す」コマンド。名前は g/re/p(global / regular expression / print)の略。Unix の最古参テキストツールの一つ。

基本構造:

grep <パターン> <ファイル>

パターンは単なる文字列でも、正規表現でも書ける。

grep の主要オプション

オプション意味用途
-iignore case大文字小文字を区別しない
-nline number行番号を表示(後で sed -i で編集する時の下準備に便利)
-vinvertマッチしない行を出す
-rrecursiveディレクトリ配下を再帰検索
-ccountマッチ数だけ表示(行は出さない)
-lfiles with matchマッチしたファイル名だけ表示
-Eextended regex拡張正規表現(`
-A 3afterマッチ行の後3行も表示
-B 3beforeマッチ行の前3行も表示
-C 3context前後3行表示
--colorカラーマッチ箇所を色付け(多くの環境でデフォルト有効)

grep の実務ユースケース

  • エラー調査: grep "ERROR" /var/log/app.log でエラー行だけ抽出
  • 特定IPのアクセス: grep "192.168.1.10" access.log
  • 404を除いた他のエラーだけ見たい: grep -E "5[0-9]{2}" access.log(5xx エラー)
  • TODOコメントを全部探す: grep -rn "TODO" src/
  • 設定ファイルから特定キーを探す: grep -E "^(host|port)" config.yml

正規表現の最低限

grepは拡張正規表現 -E で以下が使える:

  • ^ : 行の先頭
  • $ : 行の末尾
  • . : 任意の1文字
  • * : 直前の文字の0回以上の繰り返し
  • + : 1回以上の繰り返し(-E が必要)
  • ? : 0回または1回
  • [abc] : aかbかc
  • [0-9] : 数字
  • | : OR(-E が必要)
  • (...) : グループ化

:

  • ^ERROR → 行頭が ERROR の行
  • 200$ → 行末が 200 の行
  • [0-9]+ → 1文字以上の数字
  • (GET|POST) → GET または POST

grep の落とし穴

  • シェルが先にワイルドカードを展開してしまう: grep *.log file.log のように書くとシェルが *.log を展開しておかしくなる。パターンは必ずクォート: grep "*.log" file.log
  • -r で巨大ディレクトリを舐めると遅い: node_modules などを --exclude-dir で除外: grep -r "TODO" . --exclude-dir=node_modules
  • 基本正規表現と拡張正規表現の罠: grep 単体だと |+ がそのままの文字として扱われる。OR や + を使うなら -E を付けるgrep -P(Perl正規表現)を使う
  • macOS の grep は GNU grep と挙動が違うことがある: 困ったら brew install grep で GNU 版を入れる選択肢も

3. wc - 行数・単語数・文字数のカウント

wc (word count) は ファイル(または stdin)の行数・単語数・バイト数・文字数を数えるシンプルなコマンド。地味だが「集計の最後の仕上げ」として超頻出。

実務での使い方:

  • 「このログ何行ある?」: wc -l access.log
  • 「grep でヒットした件数は?」: grep "ERROR" app.log | wc -l(または grep -c でも可)
  • 「ディレクトリ内に .ts ファイル何個ある?」: find . -name "*.ts" | wc -l
  • 「今日のアクセス数」: grep "$(date +%Y-%m-%d)" access.log | wc -l$(...) は「中のコマンドを実行して、その出力を文字列としてここに埋め込む」というシェル記法=コマンド置換。$(date +%Y-%m-%d)2026-05-21 のような今日の日付に置き換わる。詳細は Linux 2-2 で扱う)

オプションは -l(行)/ -w(単語)/ -c(バイト)/ -m(文字)の4つを覚えれば実務OK。マルチバイト文字(日本語等)を扱う時は -c(バイト)と -m(文字)で値が変わるので意識する。

# 全部出す(行数、単語数、バイト数、ファイル名)
wc access.log
 
# 行数だけ
wc -l access.log
 
# 単語数だけ
wc -w access.log
 
# パイプ経由
grep "200" access.log | wc -l    # 200を含む行が何行あるか

wc (word count) の本質

ファイルの 行数 / 単語数 / バイト数 を数える。地味だが超頻出。

オプション:

  • -l (lines): 行数
  • -w (words): 単語数(空白区切り)
  • -c (bytes): バイト数
  • -m (chars): 文字数(マルチバイト考慮)

実務ユースケース:

  • 「このログ何行あるんだ?」: wc -l access.log
  • 「grep でヒットした件数は?」: grep "ERROR" app.log | wc -l(または grep -c "ERROR"
  • 「ディレクトリ内の .ts ファイル数」: find . -name "*.ts" | wc -l

wc -l の地味な罠

末尾に改行が無いファイルは「最後の行」が数えられない。echo -n "abc" > file && wc -l file0 が返る。本番ログは普通改行で終わるので問題ないが、自作テキストでハマることあり。


セッション②: awk / sed / sort / uniq(25-30分)

4. sort - 並べ替え

sortテキストを「行単位で並べ替える」コマンド。ログを時系列順に整える、集計の前処理として並べる、サイズや回数の順位を作る、など使い所は無限にある。

実務で頻出する組み合わせ:

  • sort | uniq -c | sort -rn: 出現頻度ランキング(ログ集計の鉄板パターン、次節で扱う)
  • du -h | sort -h: ディスク使用量を人間が読める単位で大きい順
  • 複数サーバーのログをマージしてから時系列に並べる: cat *.log | sort

最大の落とし穴は 「デフォルトは辞書順(文字列比較)」 という点。2 < 10 は数値だが、文字列としては "10" < "2"(先頭の 12 の比較で 1 が小さい)になる。数値ソートしたい時は -n を必ず付ける。これを忘れて「ソート結果がおかしい」と悩むのは新人あるある。

# アルファベット順
sort access.log
 
# 数値順(重要)
echo -e "10\n2\n100\n3" | sort        # 辞書順: 10, 100, 2, 3
echo -e "10\n2\n100\n3" | sort -n     # 数値順: 2, 3, 10, 100
 
# 逆順
sort -r access.log
 
# 重複削除(ソートしたものに対して)
sort -u access.log
 
# 特定カラムでソート(スペース区切り、3列目=IPで並べる)
sort -k 3 access.log
 
# 数値で逆順、特定カラム
sort -k 6 -n -r access.log    # 6列目(ステータスコード)の数値で大きい順

sort vs sort -n の違い

ざっくり言うと

sortデフォルトで「辞書順」。数字を渡しても 文字列として 比較する。これを知らずに「2 < 100 のはずが逆になる」で詰まる新人あるある。数値で並べたいなら -n 必須

まず普通のパターン

数字を縦に並べてソートしてみる:

echo -e "10\n2\n100\n3" | sort

期待:

2
3
10
100

実際:

10
100
2
3

「は?」となる。なぜこうなる?

なぜ辞書順になるのか

sort1文字目から順に文字コードで比較する。

"10"  ← 1文字目は '1'
"100" ← 1文字目は '1'
"2"   ← 1文字目は '2'
"3"   ← 1文字目は '3'

→ 1文字目の ‘1’ < ‘2’ < ‘3’ で並ぶ。 → ‘1’ で始まる行同士は2文字目で比較、結果 “10” < “100”。

sort から見たら全部「文字列」。数字としては扱われない。

図で見る

sort(デフォルト):
  "10"  → '1' '0'
  "2"   → '2'
  → '1' < '2' なので "10" が先
  → 人間からすると不自然

sort -n:
  "10"  → 10
  "2"   → 2
  → 数値として比較
  → 2 < 10 で人間直感通り

解決策: -n を付ける

echo -e "10\n2\n100\n3" | sort -n

結果:

2
3
10
100

イメージ: -n = numeric。「この入力は数値だと思って比較してね」とソートに教える。

対比表

sortsort -n
比較方法辞書順(文字列)数値順
102 の順10 が先(‘1’ < ‘2’)2 が先(2 < 10)
用途名前、ID、ログ行カウント、サイズ、件数
数字を渡すとバグる正しく動く

よくある「ハマる」シーン

# 「アクセス数の多い順」を出したい
awk '{print $3}' access.log | sort | uniq -c | sort -r

                                       ここに -n が無いと
                                       100 99 より前に来る事故

正解:

awk '{print $3}' access.log | sort | uniq -c | sort -rn

-rn = reverse + numeric。「集計の最後の sort には常に -n と覚える。

関連オプションも整理

オプション意味
-n数値として比較(最重要
-r逆順
-u重複削除
-k <N>N列目をキーに
-t <delim>区切り文字指定
-h1K, 2M のような単位付き数値も理解

一番覚えやすい説明

  • sort は文字列ソート(数字も文字として扱う)
  • 数字でソートしたいなら必ず -n
  • 集計ワンライナーの最後の sort には -rn を付けるのが定石

sort の実務ユースケース

  • ログを時系列に並べる: 複数サーバーから集めたログをマージしてソート
  • du -h | sort -h で人間が読めるサイズの大きい順
  • 集計の前処理: uniq と組み合わせる時は必ず先にsort(後述)

5. uniq - 重複の処理

uniq「連続する重複行を1つにまとめる、または重複回数をカウントする」コマンド。地味な見た目だが、sort と組み合わせると 「同じ値が何回出てきたか」の集計が一発でできる、ログ解析の主力ツール。

最大の注意点は 「隣接行しか見ない」 という性質。離れた場所にある同じ値は別物として扱われるので、必ず sort で隣接させてから uniq に渡すのが鉄則。「sort | uniq -c で集計、もう一回 sort -rn で多い順に並べる」のパターンが、ログ集計の半分以上を占めると言っても過言ではない。

実務での使い所:

  • アクセス数ランキング: awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
  • エラー種類別の発生件数: grep "ERROR" app.log | awk '{print $5}' | sort | uniq -c
  • 重複ファイル名のチェック: find . -name "*.tmp" | sort | uniq -d

-c(カウント)、-d(重複行のみ)、-u(一意な行のみ)の3つのオプションを使い分けるだけで、ほとんどの集計に対応できる。

# 隣接する重複を1つに(必ず sort と組み合わせる)
sort access.log | uniq
 
# 重複行を「何回出現したか」カウント付きで
sort access.log | uniq -c
 
# 重複している行だけ表示
sort access.log | uniq -d
 
# 一度しか出ない行だけ表示
sort access.log | uniq -u

なぜ uniqsort とセットで使うのか?

ざっくり言うと

uniq「隣り合った重複」しか見ない。離れた場所にある同じ値は別物扱い。だから先に sort で並べて、同じものを隣り合わせてから uniq に渡す必要がある。

まず普通のパターン

こういうログがあるとする:

192.168.1.10
192.168.1.20
192.168.1.10
192.168.1.30
192.168.1.10

「重複を消したい」と思って uniq だけ使うと:

cat ips.log | uniq

結果:

192.168.1.10
192.168.1.20
192.168.1.10    ← 残ってる
192.168.1.30
192.168.1.10    ← 残ってる

「重複消えてないじゃん!」となる。なぜ?

uniq の仕様: 「隣り合った」重複だけ消す

uniq は1行ずつ流れてくる入力を見て、直前の行と同じなら捨てる という動き方をする。

入力:
  192.168.1.10  ← 出す
  192.168.1.20  ← 直前と違う、出す
  192.168.1.10  ← 直前(.20)と違う、出す! ← 重複なのに残る
  192.168.1.30  ← 直前と違う、出す
  192.168.1.10  ← 直前(.30)と違う、出す! ← 重複なのに残る

つまり、離れた場所にある同じ値は気付けない

図で見る: なぜ sort が必要か

入力ファイル(バラバラ):
  a
  b
  a       ← この a は b の隣にいるので「別物」扱い
  c
  a       ← この a も「別物」扱い

uniq だけ通す:
  a, b, a, c, a   ← 全部残る

先に sort:
  a
  a       ← 隣同士に並ぶ
  a       ← 隣同士に並ぶ
  b
  c

sort | uniq:
  a, b, c   ← 正しく重複削除

解決策: sort | uniq のパイプ

cat ips.log | sort | uniq

これで初めて「ファイル全体の重複削除」になる。

イメージ:

  • sort = 同じものを隣同士に並べる
  • uniq = 隣り合った重複を消す
  • → セットで初めて「全体の重複削除」が成立

集計の鉄板パターン

どのIPが何回出たか」を多い順に出す:

cat ips.log | sort | uniq -c | sort -rn

各ステップを図で:

入力(バラバラ):
  192.168.1.10
  192.168.1.20
  192.168.1.10
  192.168.1.30
  192.168.1.10
      ↓ sort
ソート済み:
  192.168.1.10
  192.168.1.10
  192.168.1.10
  192.168.1.20
  192.168.1.30
      ↓ uniq -c(カウント付きで重複削除)
カウント:
  3 192.168.1.10
  1 192.168.1.20
  1 192.168.1.30
      ↓ sort -rn(数値で逆順)
ランキング:
  3 192.168.1.10
  1 192.168.1.20
  1 192.168.1.30

対比表: uniq の主要オプション

オプション意味
-c出現回数を頭に付ける(集計の主役
-d重複している行だけ表示
-u重複してない行だけ表示
-i大文字小文字無視

一番覚えやすい説明

  • uniq は「隣の行」しか見ない
  • だから sort | uniq がワンセット
  • ログ集計は sort | uniq -c | sort -rn の三段重ねがテンプレ

uniq オプション

  • -c (count): 各行の出現回数をカウント
  • -d (duplicated): 重複している行だけ表示
  • -u (unique): 重複してない行だけ表示
  • -i (ignore case): 大文字小文字を区別しない

6. awk - フィールド処理の最強ツール

awkテキストを「行」と「列(フィールド)」に分解して処理する小さなプログラミング言語。1行を空白区切りで切って $1, $2, ... で各列にアクセスし、条件マッチ + アクションで集計・抽出・整形を一気にやれる。「ログから3列目だけ取り出したい」「特定カラムが○○の行だけ抜きたい」などで頻出。

# 各行の3列目(IP)を表示
awk '{print $3}' access.log
 
# 1列目(日付)と6列目(ステータスコード)を表示
awk '{print $1, $6}' access.log
 
# 条件付き: 500エラーの行だけ抽出
awk '$6 == 500' access.log
 
# 条件付き + 列指定: 500エラーのIPだけ
awk '$6 == 500 {print $3}' access.log
 
# 区切り文字を変える(CSV処理)
echo "a,b,c,d" | awk -F',' '{print $3}'    # c が出る
 
# 集計(合計)
awk '{sum += $6} END {print sum}' access.log    # ステータスコードの合計(無意味だが構文例)
 
# パターン + アクション
awk '/ERROR/ {print $1, $2}' app.log

awk の本質

「テキストを表として扱う」プログラミング言語。1行を「1つのレコード」、空白区切りの各部分を「フィールド(列)」として扱い、$1, $2, … でアクセスできる。

awk の動作モデル:

awk 'パターン { アクション }' ファイル
  • パターン: マッチ条件(省略可、省略時は全行が対象)
  • アクション: マッチした行に対して何をするか(省略時は {print}=その行を出す)

特殊変数:

  • $0 : 行全体
  • $1, $2, ... : 各フィールド
  • NF : フィールド数(その行に何列あるか)
  • NR : レコード番号(今が何行目か)
  • FS : 入力区切り文字(デフォルトは空白)

awk の主要パターン

やりたいこと書き方
全部の3列目を取り出すawk '{print $3}'
5列目が “ERROR” の行awk '$5 == "ERROR"'
3列目が数値で 100 以上awk '$3 >= 100'
カンマ区切り(CSV)awk -F',' '{...}'
各行の最後の列awk '{print $NF}'
5行目だけ表示awk 'NR == 5'
「合計」を計算awk '{sum += $3} END {print sum}'
行ごとに通し番号awk '{print NR, $0}'

awk の実務ユースケース

  • アクセスログのIPだけ抽出: awk '{print $1}' access.log(nginxのデフォルトログ形式の場合)
  • CSVから特定カラムだけ抜く: awk -F',' '{print $2, $5}' data.csv
  • エラーログの時刻だけリスト化: awk '/ERROR/ {print $1}' app.log
  • 平均値・合計の計算: awk '{sum+=$3; count++} END {print sum/count}' metrics.txt

awk の落とし穴

  • シングルクォート必須: シェル変数展開を防ぐため、awk のプログラムは必ず '...' で囲む
  • === の区別: == が比較、= が代入。$3 == "ERROR"$3 = "ERROR" と書くと「代入」になり全行が真と判定される
  • 数値か文字列か: awk '$3 == 100' で動くこともあるが、フィールドが文字列として扱われる場合がある。明示的に $3+0 == 100 で数値化するテクも
  • macOSのawkはBSD版でGNU版と挙動が違う: 複雑な処理を書くなら brew install gawk で GNU awk を入れて gawk として使う

7. sed - 置換の基本

sed (stream editor) は テキストを1行ずつ流しながら「置換・削除・抽出」を行うコマンドs/置換前/置換後/ の文法で文字列を一括変換するのが最も典型的な使い方。設定ファイルの一括修正やログの整形でよく使う。

# 単純置換(最初の1個だけ)
echo "hello world" | sed 's/world/Linux/'
 
# 行内の全ての一致を置換(g フラグ)
echo "a a a" | sed 's/a/b/g'
 
# ファイル内容を置換して標準出力(元ファイルは変わらない)
sed 's/200/OK/' access.log
 
# ファイルを直接書き換え(-i)
sed -i 's/200/OK/g' access.log         # Linux
sed -i '' 's/200/OK/g' access.log      # macOS(''必須!)
 
# 特定行を削除
sed '3d' access.log                    # 3行目を削除
sed '/404/d' access.log                # 404を含む行を削除
 
# 行範囲を抽出
sed -n '5,10p' access.log              # 5-10行目だけ表示

sed (stream editor) の本質

ストリーム(流れてくるテキスト)に対して、行単位で編集する」ツール。awkがフィールド処理に強いのに対し、sedは置換と削除に特化。

最頻出は置換: s/置換前/置換後/s は substitute)。

sed 's/old/new/'      # 各行の最初の old だけ new に
sed 's/old/new/g'     # 各行の全ての old を new に(g = global)
sed 's/old/new/2'     # 各行の2番目の old を new に
sed 's|old|new|g'     # 区切り文字を | に変更(パスを置換する時に便利)

sed の実務ユースケース

  • 設定ファイルの一括変更: sed -i 's/localhost/prod.example.com/g' config.yml
  • ログから余計な情報を削る: sed 's/\[INFO\] //g' app.log
  • 改行コードの変換: Windows形式(CRLF)を Unix 形式(LF)に: sed -i 's/\r$//' file.txt
  • 特定行の削除: sed -i '/DEBUG/d' app.log で DEBUG ログ行を削除

sed -i の地獄

  • macOS の -i は空文字列を要求する: sed -i '' 's/.../.../' file。Linux と書き方が違う
  • -i は「直接書き換え」なので戻せない: バックアップなしで打って後悔するパターン多発。sed -i.bak 's/.../.../' file でバックアップ付き編集に
  • 置換対象に / が含まれる時: 区切り文字を変える sed 's|/old/path|/new/path|g'
  • 正規表現の特殊文字をエスケープし忘れる: . * [ ] などはエスケープが必要

7.5. grep / awk / sed の使い分け

grep vs awk vs sed どう使い分ける?

ざっくり言うと

3つとも「テキストを処理する」コマンドだが、得意分野が違う

  • grep = 行を抜き出す(フィルター)
  • awk = 列を扱う(フィールド処理 + 集計)
  • sed = 文字を置き換える(置換・削除)

まず普通のパターン

同じログから「500エラーの IP」だけ取り出したい。どのコマンドで書く?

2026-05-14 10:00:08 192.168.1.10 GET /api/orders 500
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

それぞれで書いてみる

grep でやると

grep "500$" access.log

→ 「500 で終わる行」を抜き出す。でも IP だけにはできない。grep の出番はここまで。

awk でやると

awk '$6 == 500 {print $3}' access.log

→ 「6列目が 500 の行から、3列目(IP)だけ表示」。条件 + 列指定を1コマンドで。

sed でやると

sed -n '/500$/s/.*\(192[^ ]*\).*/\1/p' access.log

→ 書ける、けど 読めない。sed は置換に特化したコマンドなので、こういう抽出には向かない。

図で見る: 3つの守備範囲

1行のテキスト:  2026-05-14 10:00:08 192.168.1.10 GET /api/orders 500
                ─────────────────── ──────────── ─── ────────── ───
                                       ↑
                            列に分解できるのが awk
                            
                行全体を見て該当行を抜くのが grep
                
                文字パターンを別の文字に置き換えるのが sed

何をしたいかでコマンドを選ぶ

やりたいこと第一候補
特定の文字列を含む行が欲しいgrepgrep "ERROR" app.log
特定の列だけ取り出したいawkawk '{print $3}' file
条件で行を絞って列を取りたいawkawk '$6 == 500 {print $3}'
文字を別の文字に置換sedsed 's/old/new/g' file
特定行を削除sedsed '/DEBUG/d' file
行数を数えるwc -lgrep "ERROR" | wc -l
集計(カウント)sort | uniq -c(ワンライナーで)

対比表

grepawksed
主な役割行のフィルタリングフィールド処理置換・削除
単位行 + 列行 + 文字
集計できる?できないできるできない
置換できる?できないできる(やや手間)得意
学習コスト
1コマンドの守備範囲狭い広い

イメージ

  • grep = (マッチする行だけ通す)
  • awk = 電卓 + テーブル分解(列を操って計算もできる)
  • sed = 置換マシン(文字を別の文字に交換)

3つを連携させる

実務では パイプで繋いで連携 させるのが普通:

# 「エラー行を抜く → IP列だけ取る → カウント」
grep "ERROR" app.log | awk '{print $3}' | sort | uniq -c | sort -rn

  grep で絞る            awk で列抽出      sort + uniq で集計

それぞれが「1つのことだけうまくやる」という Unix 哲学そのもの。

一番覚えやすい説明

  • grep = 行を抜く(検索
  • awk = 列を扱う(抽出 + 集計
  • sed = 文字を変える(置換
  • 迷ったら grep でフィルタ → awk で列処理 → sort/uniq で集計 が定番

8. パイプで組み合わせる(次章の予告)

# 1. 全アクセスログから
# 2. 500エラーの行を抜き出し
# 3. IP(3列目)を取り出し
# 4. 並べ替えて
# 5. 重複をカウントして
# 6. 多い順にソート
grep "500" access.log | awk '{print $3}' | sort | uniq -c | sort -rn

パイプ | の威力

ここで紹介した道具は どれも「1つの仕事だけ」をする(Unix哲学)。代わりに、パイプ | で連結して任意の複雑な処理を組み立てられる

パイプの仕組みそのものは Day5: パイプとリダイレクト で詳しくやる。今日は「コマンドA | コマンドB で A の出力が B に流れ込む」とだけ覚えておく。

9. 練習課題

# 1. access.log から GET リクエストだけ抜き出す
grep "GET" access.log
 
# 2. ステータスコード別の件数を集計
awk '{print $6}' access.log | sort | uniq -c | sort -rn
 
# 3. 一番アクセスが多いIPトップ3
awk '{print $3}' access.log | sort | uniq -c | sort -rn | head -n 3
 
# 4. 404エラーを起こしたパスだけ抽出
awk '$6 == 404 {print $5}' access.log
 
# 5. ログのステータスコードを「成功/失敗」に置換(成功=2xx, 失敗=4xx/5xx)
sed -E 's/ 2[0-9]{2}$/ SUCCESS/; s/ [45][0-9]{2}$/ FAILURE/' access.log
 
# 6. 「総アクセス数」と「ユニークIP数」を出す
#    ↓ `$(コマンド)` はコマンド置換: 中のコマンドを実行して、その出力結果をここに埋め込む(詳細は Linux 2-2)。
#      `< access.log` は「ファイルの中身をコマンドの標準入力に流し込む」リダイレクト(詳細は Linux 1-5)。
#      ここではコピペで動かしてみるだけでOK、目的は echo で集計結果を1行にまとめて表示すること。
echo "総アクセス: $(wc -l < access.log)"
echo "ユニークIP: $(awk '{print $3}' access.log | sort -u | wc -l)"

締め: 振り返り(10分)

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

exit

2. 今日の発見

このノートに追記:

- 一番「これ便利」だったコマンド:
  grepはかなり便利、使い所が多いと感じた
- awk と sed、それぞれどういう時に使い分けると理解した:
  awkは抽出、集計に強い sedは削除、集計に強い
- 「ワンライナーで集計できる」のヤバさを感じた瞬間:
- 明日やりたいこと:

チェックリスト

  • grep -i / -n / -v / -r / -E を実際に試した
  • awk '{print $1}' で特定列を取り出した
  • awk '$6 == 500' のような条件抽出ができた
  • sed 's/old/new/g' で置換した
  • sort -nsort (辞書順)の違いを観察した
  • sort | uniq -c | sort -rn の「集計ワンライナー」を体感した
  • wc -l で行数カウントした

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

やりたいことコマンド
パターンを含む行を出すgrep "pattern" file
大文字小文字無視grep -i "pattern" file
マッチしない行grep -v "pattern" file
件数カウントgrep -c "pattern" file
ディレクトリを再帰検索grep -r "pattern" dir/
OR検索grep -E "a|b" file
特定列を取り出すawk '{print $3}' file
条件付き抽出awk '$5 == "ERROR"' file
CSV処理awk -F',' '{print $2}' file.csv
文字列置換sed 's/old/new/g' file
行削除sed '/pattern/d' file
行範囲抽出sed -n '5,10p' file
ファイル直接編集sed -i.bak 's/x/y/g' file
並べ替え(辞書順)sort file
数値で並べ替えsort -n file
重複カウントsort file | uniq -c
出現頻度ランキングsort | uniq -c | sort -rn
行数カウントwc -l file

「実務OK」基準

  • grep で「あるパターンが何回出るか」を即答できる
  • awk で「N列目」を抜き出す書き方を覚えている
  • sort | uniq -c | sort -rn の集計パターンを使える
  • sed で「置換」と「削除」の基本構文が書ける
  • これらをパイプで繋いで、ログ集計のワンライナーが書ける

ここまで来れば「ログ集計でハマらない人」。明日 Day5 で パイプ・リダイレクト・xargs を体系的にやって、ここで学んだ道具を本格的に繋げる力を付ける。


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

NG 1: grep で正規表現の特殊文字をエスケープし忘れる

grep "192.168.1.1" access.log   # `.` が「任意の1文字」と解釈されて誤マッチ

→ 対策: 固定文字列なら grep -F、または grep "192\.168\.1\.1" でエスケープ。

NG 2: sed -i を MacOS と Linux で同じ書き方をして失敗

sed -i 's/a/b/g' file.txt   # macOS では「`-i` の後ろに拡張子必須」でエラー

→ 対策: 移植性重視なら sed -i.bak 's/a/b/g' file.txt と書く(macOS でも GNU sed でも動く)。.bak がバックアップを残す。

NG 3: 巨大ログを cat | grep で読む(UUOC)

cat access.log | grep ERROR   # `cat` 不要

cat を介すと 無駄なプロセスとパイプが1個増える(数百MBで体感差)。grep ERROR access.log で十分。Useless Use of Cat (UUOC) と呼ばれる古典的アンチパターン。

NG 4: sort -usort | uniq -c を混同

sort access.log | uniq          # 重複は消える、カウントは出ない
sort access.log | uniq -c       # カウント付き
sort -u access.log              # `uniq` と同じ結果、カウントは出ない

→ 集計したいなら 必ず uniq -c-u はあくまで「重複削除」だけ。


自己評価チェックリスト

  • grep -i / -v / -E の意味をすべて答えられる
  • awk '{print $1}'awk -F',' '{print $1}' の違い(区切り文字)を説明できる
  • sed 's/old/new/g'g を外したら何が起きるか答えられる
  • sort | uniq -c | sort -rn3ステップそれぞれの役割 を口頭で説明できる
  • 「アクセス上位 IP トップ10」のワンライナーをホワイトボードで書ける

次のレッスン: Day 5 - パイプとリダイレクト

今日学んだ grep awk sed sort uniq wc を、|(パイプ)で繋ぐ世界 に進む。

実は Day 4 で何度か出てきた ... | ... という書き方。これを体系的に理解すると、「1行のコマンドで集計・整形・保存まで完結」する Unix の真骨頂が見える。

Day 5: パイプとリダイレクト