シェルスクリプト・sed・awk ── テキスト処理の道具立て

はじめに

このコラムではここまで、Makefile や CI の随所でシェルのワンライナーを書いてきました。シェルスクリプトと sed・awk は、ビルドの糊としてだけでなく、原稿の一括置換や簡単な集計・チェックにも効きます。

3 つの道具はざっくり次の分担です。

  • grep … 該当行を探す
  • sed … 行単位で置換・削除する
  • awk … 列を切り出す、条件で処理する、集計する

シェルスクリプトの基礎

先頭にシバンを書き、エラーで止まる設定を入れておきます。

#!/usr/bin/env bash
set -euo pipefail   # 未定義変数・コマンド失敗・パイプ失敗で停止

src="draft.tex"
count=$(grep -c '\\section' "$src")   # コマンド結果を変数へ
echo "セクション数: $count"

for f in chapters/*.tex; do
  echo "processing $f"
done

要点は次のとおりです。

  • 変数展開は必ず "$var" とダブルクォートで囲む(空白や特殊文字で壊れないように)
  • コマンド置換は `...` ではなく $(...)
  • 直前のコマンドの成否は $?、if コマンド; then ... でそのまま分岐できる
  • パイプ | で連結、> で上書き、>> で追記、2>&1 で標準エラーを標準出力へ

sed の実用

sed は「行ごとにルールを適用する」道具です。もっとも使うのは置換 s/検索/置換/。

sed 's/ / /g' in.html                 # 全ての   を空白に
sed -E 's/([0-9]+)年([0-9]+)月/\1-\2/g' f   # 拡張正規表現と後方参照
sed '/^%/d' main.tex                        # % で始まる行(コメント)を削除
sed -n '10,20p' main.tex                    # 10〜20 行目だけ表示
sed -e 's/“/``/g' -e 's/”/'"''"'/g' f       # 複数のルールを続けて

-i(ファイル直接編集)は macOS と Linux で書き方が違います。 GNU sed は sed -i 's/.../.../' f、BSD/macOS の sed は sed -i '' 's/.../.../' f(空の拡張子が必須)。スクリプトを配る場合は、後述のとおりコンテナで GNU 版に揃えると悩まずに済みます。

原稿処理の例:

# 行頭の全角スペースによる字下げをやめる(段落先頭の を削除)
sed -E 's/^ +//' draft.tex

# ハイフン・マイナス・全角ダッシュを 1 種類に寄せる
sed 's/[‐―−]/-/g' draft.tex

awk の実用

awk は入力を「レコード(既定は行)× フィールド(既定は空白区切り)」として扱い、パターン { アクション } を並べます。

awk '{ print $1, $3 }' data.txt           # 1 列目と 3 列目
awk -F'\t' '{ print $2 }' data.tsv        # タブ区切りの 2 列目
awk 'NR % 2 == 0' file                    # 偶数行だけ
awk '/error/ { c++ } END { print c }' log # "error" を含む行数

# 章ごとの行数を数える
awk '/^\\chapter/ { name=$0; next } { n[name]++ } END { for (k in n) print n[k], k }' main.tex

# 数値列の合計と平均
awk '{ sum += $1 } END { print sum, sum/NR }' nums.txt

BEGIN{} は処理前、END{} は処理後に 1 回。NR は行番号、NF はその行のフィールド数、連想配列(n["key"])で集計できます。「1 行 1 レコードで、列を見て何かする/数える」なら awk が素直です。

組み合わせる

複数ファイルにまたがる処理は find や grep -rl と組みます。

# 全 .tex から「表記ゆれ」候補を洗い出す
grep -rn -E '(Web|web)' --include='*.tex' .

# 置換をファイル横断で(まず対象を確認してから実行)
grep -rl 'oldname' --include='*.tex' . | while IFS= read -r f; do
  sed -i 's/oldname/newname/g' "$f"
done

# パイプで受け渡す:抽出 → 整形 → 集計
grep -o '\\cite{[^}]*}' main.tex | sed 's/.*{//; s/}//' | sort | uniq -c | sort -rn

安全に使うために

  • sed -i の前に差分を確認。まずは -i なしで実行して出力を眺め、Git で管理下に置いてから置換して git diff で確認する。
  • 正規表現は最小限に当てる。.* を安易に使うと意図より広く食う。
  • マルチバイト。日本語がからむときは LC_ALL / ロケールの影響を受けることがある。想定と違う挙動なら LC_ALL=C と比べてみる。
  • while read は IFS= read -r。これを省くと行頭末尾の空白が削れ、バックスラッシュが消える。

移植性の話

  • シェル:bash 固有の書き方(配列、[[ ]])を使うならシバンは bash にする。POSIX sh で足りるなら移植性が上がる。
  • sed / awk / grep:macOS は BSD 版、Linux は GNU 版で、sed -i・grep -P・awk の拡張などに差がある。
  • 揃える手:スクリプトを Docker の Linux イメージ内で走らせれば、どの手元でも GNU 版で動く。CI と挙動を一致させたいときにも有効。

関連記事

  • Git と GitHub の基本 — 一括置換を版管理下で差分確認しながら行う
  • Docker の基本 — コンテナ内で実行して GNU 版に揃える
  • docx2tex — Word からの変換後に残るクセを掃除する
  • latexmk — ビルドの下ごしらえをスクリプトでまとめる

まとめ

  • grep で探し、sed で行を置換し、awk で列を見て集計する ── まずこの分担を押さえる。
  • シェルスクリプトは set -euo pipefail と "$var" のクォートを徹底するだけで事故が激減する。
  • 破壊的な一括置換は「確認してから」「Git 管理下で」。sed -i の macOS/Linux 差に注意。
  • 環境差が気になるならコンテナ内で実行して GNU 版に揃える。

原稿の表記ゆれ検出やビルドの下ごしらえは、この 3 つでかなりのところまで手が届きます。

LaTeX組版・教材校正のご依頼・ご相談はコレクトコネクトへお問い合わせください。

← コラム一覧へ