シェルで行番号を付けるなら nl ではなく cat -n を使おう！

## はじめに

シェルまたはシェルスクリプトである出力のすべての行に行番号を付けたいと思った時、どのコマンドを使いますか？ その候補は複数ありますが、その一つに `nl` コマンドを使う方法があります。しかし `nl` コマンドを使って行番号を付けるのは良くない考えです。なぜならおそらくあなたが期待したとおりに行番号が付けられない場合があるからです。この記事では `nl` コマンドとは本来どのようなコマンドであるかを解説します。

## なぜnlコマンドを使って行番号を付けてはいけないのか？

論より証拠というわけで、`nl` コマンドを使うとどのように行番号をつくのかを示しましょう。その答えなら知ってるよ、デフォルトでは空行に行番号が付かないんでしょ？ という人のために `-ba` オプションを指定して空行にも行番号を付けるようにします。

まず次のようなファイル (file.txt) を用意し・・・

```txt:file.txt
\:\:\:
ページ1ヘッダ
\:\:
foo
bar
baz
\:
ページ1フッタ
\:\:\:
ページ2ヘッダ
\:\:
FOO

BAR

BAZ
\:
ページ2フッタ
```

`nl -ba`を実行すると次のような出力が得られますが、おそらくこれはあなたが期待した通りの結果ではありませんよね？

```console
$ nl -ba file.txt

       ページ1ヘッダ

     1  foo
     2  bar
     3  baz

       ページ1フッタ

       ページ2ヘッダ

     1  FOO
     2
     3  BAR
     4
     5  BAZ

       ページ2フッタ
```

## nlコマンドは論理ページごとに行番号を付けるコマンド

`nl` コマンドは、本来すべての行に行番号をつけるコマンドではなく「論理ページ」というページの概念を前提として、ページごとに行番号を付けるコマンドです。デフォルトでは次のような区切り行を使って、ヘッダとボディとフッタを区別します。

| 区切り行   | 意味 |
| -------- | ------ |
| `\:\:\:` | Header |
| `\:\:`   | Body   |
| `\:`     | Footer |

もちろんこのような区切り行を使うことはめったにないので、だいたい上手く動くでしょう。わかっていてやっているのなら良いのですが、`nl` コマンドを紹介している記事などで、こういう問題があることを注意喚起しているものを見かけません。ドキュメントをしっかり読めばこのような仕様は分かるはずなのですが、動かしてみた、動いた、これで良しと考える人が多いように感じます。

もちろん論理ページごとに行番号を付けたいのであれば、`nl` コマンドを使用してください。それは `nl` コマンドの本来の想定している使い方です。

## 区切り行は無効にできない

ここで区切り行で区切られるというのなら、`-d` オプションに空文字を指定して区切り行を無効にすればいいじゃないか？と考えるかもしれません。

```console
$ nl -ba -d '' file.txt
     1  \:\:\:
     2  ページ1ヘッダ
     3  \:\:
     4  foo
     5  bar
     6  baz
     7  \:
     8  ページ1フッタ
     9  \:\:\:
    10  ページ2ヘッダ
    11  \:\:
    12  FOO
    13
    14  BAR
    15
    16  BAZ
    17  \:
    18  ページ2フッタ
```

たしかに動いているように思えます。**ただし「GNU 版では」です**。

`-d` オプションに空文字を指定した場合、BSD 系 Unix ではデフォルトの「`\:\:\:`」「`\:\:`」「`\:`」を指定した意味となり、Solaris 11 ではエラーになります。

移植性なんか知ったことじゃない。BSD 系 Unix や Solaris 11 で動かなくても構わない。というのであれば別に `nl` コマンドを使っても構いませんが、移植性を考えないならもっといい方法がありますね。

## 行番号を付けるなら「cat -n」を使う

すべての行に行番号を付ける場合、最も覚えやすく簡単に書けるのは `cat -n` です。ちなみに空行を省く場合は `cat -b`（AIXを考慮するなら `cat -nb`）を使います。

```console
$ cat -n file.txt
     1  \:\:\:
     2  ページ1ヘッダ
     3  \:\:
     4  foo
     5  bar
     6  baz
     7  \:
     8  ページ1フッタ
     9  \:\:\:
    10  ページ2ヘッダ
    11  \:\:
    12  FOO
    13
    14  BAR
    15
    16  BAZ
    17  \:
    18  ページ2フッタ
```

`-n` オプションも `-b` オプションも POSIX では標準化されていません。しかし GNU、BSD 系 Unix、System V 系 Unix すべてで動作します。移植性は高いです。

## nlはPOSIXで必須のコマンドではない

`cat` コマンドの `-n` や `-b` オプションは POSIX に準拠してないじゃないか！とこだわる人には、`nl` コマンドも POSIX で必須となっていませんよと伝えておきます。

[POSIX の nl コマンドの仕様](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/nl.html) をよく確認しましょう。XSI というマークが付いていますよね？ これはオプション機能であり POSIX では実装が必須となっておらず、UNIX の認証を取るときだけ必要なコマンドの証です。UNIX の認証を取るというのは事実上 System V 系 Unix 用の仕様で GNU や BSD 系 Unix では `nl` コマンドは実装されていなくてもよく、POSIX の規定通りに実装しなくても良いという意味です。

つまり POSIX に本当にこだわるのであれば `nl` コマンドに依存しないほうが良いということです。現実的な問題を一つ示しておきます。**BusyBox の `nl` コマンドには `-d` オプションが実装されていません**。もっとも Busybox の `nl` コマンドは区切り文字の仕様自体に対応しておらずヘッダやフッタの概念がないため **Busybox は `nl -ba` ですべての行に行番号が追加されるという違いがあります**。

## POSIXにこだわりたいならawkで書く

どうしても POSIX で標準化されている範囲で実装したいというの人ために awk の実装を書いておきますが、`cat` コマンドの `-n` や `-b` オプションは POSIX で標準化されていないと言うだけで移植性があるので、使う必要はないでしょう。

```console:cat -n と同等の処理
$ awk '{printf "%6d\t%s\n", NR, $0}'
```

```console:cat -b と同等の処理
$ awk '/^$/{print; next}; {printf "%6d\t%s\n", ++i, $0}'
```

## まとめ

シェルまたはシェルスクリプトですべての行に行番号を付けたいなら `cat -n` を使用しましょう。