Linuxでのテキスト操作: grep, sed, awkを活用した処理法

Linux のテキスト処理は、最初から長いパイプを作るより「行を選ぶ」「置換する」「列を計算する」「単純列を切る」「文字を写像する」のどれかに分解すると失敗原因が見えます。grep、sed、awk、cut、tr は重なる機能がありますが、入力契約と終了状態が異なります。以下では一つの固定ログを使い、各段階の入力・正確な出力を確認してから連結します。 確認ポイント:sed -iの前にpreviewとdiffを作り、対象件数が期待範囲外なら原本を変更しません。

目次

grep・sed・awkを一つのlog処理へ分担する

  • grep: 正規表現または固定文字列に一致する行を選ぶ。抽出0件を終了値1で区別できる。文字列そのものなら -F、行全体なら -x。
  • sed: 行を順に読み、置換・削除・範囲表示をする。既定は結果を標準出力へ出し原本を変えない。
  • awk: フィールド分割、数値条件、集計、出力書式を一つのプログラムで扱う。複雑な grep|cut|計算の連鎖を短くできる。
  • cut: 一文字 delimiter の単純な列またはバイト・文字位置を選ぶ。引用符付き CSV や連続空白の解析には使わない。
  • tr: 一文字ずつ置換・削除・圧縮する。old という語を new へ置換するコマンドではない。ファイル引数を取らず標準入力を読む。

再現用ログを作る

work=$(mktemp -d) || exit 1
trap 'rm -r -- "$work"' EXIT
cat >"$work/app log.txt" <<'LOG'
2026-07-17T09:00:00 INFO user=aki latency_ms=120 path=/home
2026-07-17T09:01:00 ERROR user=bo latency_ms=950 path=/api
2026-07-17T09:02:00 WARN user=aki latency_ms=450 path=/api
2026-07-17T09:03:00 ERROR user=aki latency_ms=700 path=/login
LOG
wc -l -- "$work/app log.txt"
sed -n '1p;$p' "$work/app log.txt"

行数は 4 でファイル名が続き、先頭は 09:00 の INFO、末尾は 09:03 の ERROR です。パスに空白があるため常に "$work/app log.txt" と引用します。このベースラインを保存しておけば、後のコマンドが入力を欠落させていないか確認できます。

grep: ERROR 行だけを選ぶ

LC_ALL=C grep -F ' ERROR ' -- "$work/app log.txt"
printf 'grep rc=%d\n' "$?"
LC_ALL=C grep -F ' FATAL ' -- "$work/app log.txt" >/dev/null
printf 'no-match rc=%d\n' "$?"

最初は 09:01 の bo と 09:03 の aki の2行、次に grep rc=0、該当しない FATAL は no-match rc=1 です。GNU grep の 0 は一致あり、1 は一致なし、2 はエラーです。監視で0件を正常とするなら1だけ許可し、読み取り失敗や不正オプションの2を隠しません。検索語を正規表現として扱う必要がないため -F を使い、前後の空白を含めて level フィールドだけに近い一致にしています。厳密な構造判定は後段の awk へ任せます。

sed: ストリーム上で表示名だけ置換する

LC_ALL=C sed 's/ user=/ account=/' "$work/app log.txt" | sed -n '1,2p'

出力先頭2行は ... INFO account=aki ... と ... ERROR account=bo ... です。原本の user= は変わりません。sed の s/old/new/ は各行の最初の一致だけ、末尾 g を付ければ全一致です。区切り文字 / を値に含むパス置換では s|/old|/new|g のように別の一文字を delimiter にします。外部入力を sed program へ直接文字列連結すると &、バックスラッシュ、delimiter が命令として解釈されるため、任意値の置換には専用言語やエスケープ処理を使います。

awk: フィールドを検証して条件・集計する

このログは空白区切りの各フィールド内に空白がない契約です。awk の既定フィールド分割を使い、level が ERROR かつ latency がしきい値以上の行から user と latency を取り出します。フィールド番号だけに依存せず key=value を探索すれば、項目順の変更に少し耐えられます。

threshold=800
LC_ALL=C awk -v min="$threshold" '
$2 == "ERROR" {
  delete v
  for (i=3; i<=NF; i++) { split($i, pair, "="); v[pair[1]]=pair[2] }
  if (v["latency_ms"] + 0 >= min)
    printf "user=%s latency=%d\n", v["user"], v["latency_ms"]
}' "$work/app log.txt"

正確な出力は user=bo latency=950 の1行です。しきい値は shell 文字列へ埋め込まず -v min="$threshold" で awk 変数として渡します。入力値が数値であることも必要なら v["latency_ms"] ~ /^[0-9]+$/ を条件に加え、不正値を標準エラーへ報告して END で非0終了にします。awk は条件に一致する行が0でも既定で成功するため、0件を異常とする処理では found を数えて終了状態を明示します。

cut と tr: 単純な段階だけに使う

cut -d' ' -f1,2 -- "$work/app log.txt"
printf '%s\n' '--- lower-case levels ---'
cut -d' ' -f2 -- "$work/app log.txt" | LC_ALL=C tr 'A-Z' 'a-z'

cut の結果は各行の日時と level、つまり 2026-07-17T09:00:00 INFO から4行です。次は info、error、warn、error です。ここでは入力に連続空白がないため -d' ' が使えます。連続空白なら空フィールドが生じるので awk を選びます。LC_ALL=C tr 'A-Z' 'a-z' は ASCII level を小文字化する例で、自然言語全体の Unicode 大文字小文字変換を保証しません。CRLF の CR を落とす用途なら tr -d '\r' ですが、引用された値内の CR も消すため形式を確認します。

一つのログ抽出タスクとして連結する

要件を「ERROR 行だけを選び、表示上 user を account に変え、account と latency を出す」とします。grep と sed の各段階を上で検証した後、awk で必要フィールドを出します。

LC_ALL=C grep -F ' ERROR ' -- "$work/app log.txt" |
  sed 's/ user=/ account=/' |
  awk '{
    account=latency=""
    for (i=3; i<=NF; i++) {
      split($i, p, "=")
      if (p[1]=="account") account=p[2]
      if (p[1]=="latency_ms") latency=p[2]
    }
    printf "%s\t%s\n", account, latency
  }'

正確な出力は bo、タブ、950、次行が aki、タブ、700 です。出力件数は ... | wc -l で2と確認できますが、確認のためだけに本処理を二度走らせると増加中ログで結果が変わります。実運用では一度ファイルへ出し、wc -l と awk -F'\t' 'NF!=2{bad++} END{exit bad>0}' で検証します。

パイプの終了状態を失わない

Bash の既定ではパイプ全体の終了状態は最後のコマンドです。grep がファイルを読めなくても、空入力を処理した awk が0で終われば成功に見えます。set -o pipefail を有効にすると、全要素が0なら0、失敗があれば最後に非0となった要素の状態を返します。個別値は PIPESTATUS を直後に保存します。

set -o pipefail
LC_ALL=C grep -F ' ERROR ' -- "$work/app log.txt" | sed 's/ user=/ account=/' | awk '{print $3}' >/dev/null
status=("${PIPESTATUS[@]}")
printf 'grep=%s sed=%s awk=%s\n' "${status[0]}" "${status[1]}" "${status[2]}"

この入力では grep=0 sed=0 awk=0 です。PIPESTATUS は別の単純コマンドを実行すると更新されるため、printf より前に配列へコピーします。grep の0件=1を正常としたいパイプでは、grep 段を { grep ...; rc=$?; [[ $rc == 0 || $rc == 1 ]]; } のように包み、2だけ失敗させます。pipefail は Bash 固有なので #!/bin/sh では各段を一時ファイルで分け、終了状態を個別確認する方法が移植しやすくなります。

sed -i の前に必ずプレビューと差分を見る

原本を変更するのは、標準出力プレビューが期待どおりになった後だけです。まず変更候補を別ファイルへ出し、diff -u で対象行と件数を確認します。diff は差があれば終了値1なので、1を「変更あり」として扱い、2以上をエラーとして止めます。

sed 's/ user=/ account=/' "$work/app log.txt" >"$work/app log.preview" || exit 1
diff -u -- "$work/app log.txt" "$work/app log.preview"
diff_rc=$?
if [[ $diff_rc -gt 1 ]]; then exit "$diff_rc"; fi
grep -c ' account=' "$work/app log.preview"

差分は4行すべてで user= が account= へ変わることを示し、最後の件数は4です。ここで初めて in-place を実行します。GNU sed なら sed -i.bak 's/ user=/ account=/' file と空でない接尾辞を付ければ、変更前が file.bak に残ります。

sed -i.bak 's/ user=/ account=/' "$work/app log.txt" || exit 1
cmp -s -- "$work/app log.txt" "$work/app log.preview"
printf 'matches-preview=%d backup-users=%s\n' "$?" "$(grep -c ' user=' "$work/app log.txt.bak")"

出力は matches-preview=0 backup-users=4 です。変更後がプレビューと一致し、バックアップに旧表記が4件あります。検証失敗時の復元は mv -- "$work/app log.txt.bak" "$work/app log.txt" です。成功後も所定の保持期間までは backup を残します。

-i は POSIX sed の必須オプションではなく、GNU/BSD/macOS で引数構文が異なります。特に「バックアップなし」の GNU -i と BSD 系 -i '' を同じコマンドとみなせません。空でない suffix を付けた -i.bak も対象実装の manual と使い捨てファイルで確認します。高い移植性が必要なら sed ... >tmp、差分検証、cp -p file file.bak、同じディレクトリの tmp を mv という明示手順にします。

最終確認

  • 入力の行数、先頭・末尾、encoding、delimiter を固定し、各段を単独で実行した。
  • 検索語は固定文字列なら grep -F、正規表現なら locale と構文を明示した。
  • shell 値は引用し、awk へは -v で渡した。
  • Bash パイプは pipefail と直後の PIPESTATUS、0件の grep=1 を要件に沿って処理した。
  • sed の変更はプレビュー、diff、件数確認後に backup suffix 付きで行い、cmp と backup 内容を確認した。

公式情報・参考資料

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次