wc -l が数えるのは、画面上で見える「文章の行」ではなく入力に含まれる改行区切りです。GNU wc の行数は newline の個数なので、最後が改行で終わらないファイルでは、人が二行と読む内容でも結果が 1 になることがあります。ログ件数、CSV レコード数、ソースコード行数を求める前に、「改行の数」と「最終行を含む論理レコード数」のどちらが要件かを決めます。 確認ポイント:wc -lはnewline byteを数えるため、末尾newlineのない最終recordを行数と見なさない場合があります。
wc -lが数えるnewlineを固定入力で確認する
一時ディレクトリに内容が確定した二つのファイルを作ります。echo は実装やオプションでバックスラッシュ処理が変わるため、検証データには printf を使います。
work=$(mktemp -d) || exit 1
trap 'rm -r -- "$work"' EXIT
printf 'one\ntwo\n' >"$work/complete.txt"
printf 'one\ntwo' >"$work/no-final.txt"
wc -l -- "$work/complete.txt" "$work/no-final.txt"
printf '%s\n' '--- stdin ---'
wc -l <"$work/complete.txt"
printf '%s\n' '--- logical records with awk ---'
awk 'END { print NR }' "$work/complete.txt" "$work/no-final.txt"
GNU wc の数値前の空白幅や一時パスは環境で変わりますが、対応する結果は 2 .../complete.txt、1 .../no-final.txt、3 total です。標準入力へリダイレクトした wc -l はファイル名を付けず 2 だけを出します。最後の awk は二ファイルを連続入力として読むため 4 です。各ファイル別なら awk 'FNR==1 && NR!=1 {print previous} {previous=FNR} END{print FNR}' のような処理が要りますが、単純な行数なら目的を変えず wc を使う方が明確です。
一ファイル、標準入力、複数ファイル
基本形は wc -l -- file です。-- は以後をオプションとして解釈させないため、名前がハイフンで始まるファイルにも有効です。シェル変数は必ず引用し、wc -l $file ではなく wc -l -- "$file" とします。ファイル名が不要なら入力リダイレクトを使い、コマンド出力ならパイプで渡します。
file=$work/complete.txt
wc -l -- "$file"
wc -l <"$file"
printf 'x\ny\nz\n' | wc -l
printf 'x\ny' | wc -l
結果の数値は順に 2、2、3、1 です。最初だけファイル名が続きます。cat "$file" | wc -l は動作しますが、単一ファイルでは余分なプロセスを増やすので入力リダイレクトで十分です。パイプ元の失敗も検知するスクリプトでは Bash の set -o pipefail を有効にするか、パイプを避けて各終了状態を明示確認します。wc が 0 でも、パイプ元が成功したとは限りません。
複数ファイルを同時指定すると各ファイルの後に合計 total が出ます。たとえば内容が2行、1行なら合計は3です。ただしスクリプトで数値だけを読む際に total 行を誤って一ファイル分として処理しないよう、ファイルごとに呼ぶか出力形式を明示的に解析します。ロケールによって total の表示語が変わる可能性があるため、その文字列を固定キーにしない方が安全です。
空ファイルと「改行なし一行」を区別する
: >"$work/empty.txt"
printf 'only-one-record' >"$work/one-record.txt"
for f in "$work/empty.txt" "$work/one-record.txt"; do
bytes=$(wc -c <"$f")
newlines=$(wc -l <"$f")
records=$(awk 'END {print NR}' "$f")
printf '%s bytes=%s newlines=%s records=%s\n' "$(basename "$f")" "$bytes" "$newlines" "$records"
done
ASCII/UTF-8 のこのデータでは empty.txt bytes=0 newlines=0 records=0、one-record.txt bytes=15 newlines=0 records=1 です。受入条件が「空でないこと」なら wc -l の 0 だけで空ファイルと判定できません。[[ -s $file ]] または wc -c でバイト有無を別に確認します。反対に、POSIX テキストファイルとして各行が改行で終わることを要求するなら、最終改行欠落は入力不備として報告し、勝手に補完して原本を変更しません。
空白・改行・ハイフンを含むファイル名を安全に渡す
ファイル名一覧を改行で作って xargs wc -l に渡すと、空白、引用符、改行を含む名前で分割されます。find ... -exec wc -l -- {} + は検索結果を引数配列として直接渡すため、区切り文字の衝突がありません。{} は引用してもよく、末尾の + は一回に複数パスをまとめます。
mkdir "$work/logs"
printf 'a\nb\n' >"$work/logs/a one.txt"
printf 'c\n' >"$work/logs/b.txt"
printf 'd\ne' >"$work/logs/line
break.txt"
find "$work/logs" -type f -name '*.txt' -exec wc -l -- {} +
出力の順序は保証されませんが、行の集合は 2 .../a one.txt、1 .../b.txt、1 .../line に続いてファイル名中の改行で次の表示行が break.txt、そして 4 total です。名前に改行があっても wc へ渡る一つの引数は壊れていません。ただし人間向け表示は複数行に見えるため、機械連携では GNU wc --files0-from=- と find -print0 の組合せを検討します。
find "$work/logs" -type f -name '*.txt' -print0 | wc -l --files0-from=-
GNU coreutils では NUL 区切りの各パスを読み、同じ各件数と合計を出します。--files0-from は GNU 拡張です。-exec ... {} + は広く使える安全な基本形ですが、ファイル数が多いと OS の引数長上限に合わせて複数回 wc が起動し、各回ごとの total が現れます。ディレクトリ全体の一個の合計が必要なら、出力の total を合算するのではなく、要件に応じて各数値を awk で合算するか全パスを --files0-from へ渡します。
grep -c は「一致する行」であり、総行数ではない
grep -c pattern file は pattern に一致した行数です。一行に pattern が10回あっても 1 と数え、総行数ではありません。空行以外を数える grep -cve '^[[:space:]]*$' も、空白定義は locale と文字集合に影響されます。単純な総改行数は wc -l、レコード条件を評価するなら awk、特定パターンを含む行数なら grep と役割を分けます。
バイナリ、エンコーディング、大容量ファイル
wc -l は文字コードを検証するコマンドではありません。バイナリ中の改行バイト相当を行区切りとして数えるため、画像や圧縮ファイルへ実行した値に「テキスト行」の意味はありません。UTF-8、UTF-16、CRLF が混在するデータでは、業務上のレコード区切りと結果が一致するか小さな既知サンプルで確認します。CRLF では LF があるため通常は一行として数えますが、単独 CR 区切りは wc -l に数えられません。
巨大ファイルでも wc は全内容をメモリへ載せず順次読みます。ただし結果を得るには入力全体を読む必要があり、ネットワークファイルや増加中のログでは実行中に内容が変わると時点の整合した件数になりません。スナップショットやローテーション済みファイルを対象にし、stat --printf='%s %Y\n' -- file を前後で比較すると、サイズ・更新時刻が変わったか確認できます。読み取り専用のカウントにはロールバックはありません。
検証チェック
- 末尾改行あり・なし・空ファイルの三つを
printfで作り、期待値を固定した。 - ファイル名は
-- "$file"で渡し、検索結果はfind -exec ... {} +または NUL 区切りにした。 - 複数ファイルの
totalを一ファイルの件数として取り込んでいない。 - バイナリや未知エンコーディングを「テキスト行」と断定していない。
- 増加中のログではサイズ・更新時刻を前後比較し、一貫したスナップショットでないことを記録した。

コメント