nlコマンドでの空行の取り扱い

いままで特に触れてきませんでしたが、`nl`はデフォルトで空行（改行だけの行）に行番号を出力しません。これは行番号を出力する機能を持つ他のコマンドとは大きく異なる点だと思います。

# 空行を含むデータの行番号

まずは空行に対して`nl`がどんな動作をするのか確かめてみましょう。`echo`は`-e`を指定することで`\n`を改行コード（`LF`）として出力します。

```console
$ echo -e 'a\n\nb'
a

b
$ echo -e 'a\n\nb' | nl
     1	a
       
     2	b
```

確かに空行に行番号は出力されません。

## 空行と判定される条件

スペースや`CR`改行（`\r`）を含む行など、見た目は空行に見えるデータも試してみましょう。

```console
$ echo -e 'a\n \nb' | nl
     1	a
     2	 
     3	b
$ echo -e 'a\r\n\r\nb\r' | nl
     1	a
     2	
     3	b
```

一見空行に見えても行番号が振られています。`LF`以外の文字が含まれる行は「空行」とはみなされないようです。UNIX系のコマンドなので、`LF`のみを改行として取り扱うのは当然の結果です。Windowsなど改行が`CRLF`で保存されたデータを扱う場合は気をつける必要がありそうです。

# 空行に付加されるデータ

## セパレータ

では、[-sオプションの記事](https://qiita.com/Carol_815/items/b7aa0a4530041c79b7fd)で説明したセパレータは出力されているのでしょうか。実はcoreutilsの`nl`とBSD系の`nl`では結果が異なります。タブのままでは見えないので`-s`でわかりやすく`@@@`に変更して検証してみましょう。

```console:coreutils
$ echo -e 'a\n\nb' | nl -s @@@ 
     1@@@a
         
     2@@@b
```

```console:busybox
$ echo -e 'a\n\nb' | busybox nl -s @@@ 
     1@@@a
         
     2@@@b
```

```console:BSD(macOS)
$ echo -e 'a\n\nb' | nl -s @@@ 
     1@@@a
      @@@
     2@@@b
```

BSD系の`nl`だけが空行でもセパレータを出力しています。

## BSD系の`nl`のセパレータとPOSIX

このように実装によって異なる挙動を発見した場合は、標準であるPOSIXを確認してみましょう。

> \<empty\>
When line numbers are suppressed for a portion of the page; the \<separator\> is also suppressed.

これは、[-nオプションの記事](https://qiita.com/Carol_815/items/f3ebf5005a94f58f6055)でも引用した「OUTPUT」の一部分の記載になります。
POSIXによると「行番号が抑制されるとき、セパレータもまた抑制される」とあります。つまりBSD系の`nl`のこの挙動は「POSIXに準拠していない仕様」あるいは「ソフトウェアのバグ」ということになります。

> STANDARDS
     The nl utility conforms to	IEEE Std 1003.1-2001 ("POSIX.1").

さらに、Manpageを参照すると「標準」としてPOSIXが引用されています。おそらくはバグということになるのでしょう。

ちなみに、coreutilsのManpageを参照すると、`-s`について以下のように書かれています。

```console:coreutils
$ man nl | grep -A1 'number-separator'
       -s, --number-separator=STRING
              add STRING after (possible) line number
```

この`(possible)`という部分が、「行番号をつけるところだけ」という補足のようです。


## coreutilsの`nl`での空行の取り扱い

デフォルトのタブのときも検証してみましょう。`xxd`という`vim`に含まれるコマンドユーティリティでバイナリを16進数のテキストで出力します。

```console:coreutils
$ echo -e 'a\n\nb' | nl | xxd
00000000: 2020 2020 2031 0961 0a20 2020 2020 2020       1.a.       
00000010: 0a20 2020 2020 3209 620a                 .     2.b.
```
```console:BSD(macOS)
$ echo -e 'a\n\nb' | nl | xxd
00000000: 2020 2020 2031 0961 0a20 2020 2020 2009       1.a.      .
00000010: 0a20 2020 2020 3209 620a                 .     2.b.
```

16進数表示されている真ん中のブロックの1行目右端7バイトに注目してください。coreutilsは`20 2020 2020 2020`でBSD系は`20 2020 2020 2009`ですが、この部分がちょうど元データの2行目の文字列（改行`0x0a`は含まず）にあたります。つまり、coreutilsの`nl`でも空行を一切変更しないわけではなく、行番号とセパレータを表示していないだけです。右揃えにするためのスペース`0x20`が出力されており、さらにセパレータのタブ`0x09`の分まで1文字余計に付加されています。

あまり大きな問題は無いかもしれませんが、これは`nl`を2つ重ねて使う場合には想定した挙動と異なる結果になるかもしれません。

```console:coreutils
$ echo -e 'a\n\nb' | nl | nl
     1	     1	a
     2	       
     3	     2	b
```

## 空行に付加されるデータを取り除く

coreutilsの空行に付加されるデータを消せるのか、他のオプションとの組み合わせを試してみましたが難しいようです。

```console:coreutils
$ echo -e 'a\n\nb' | nl -n ln -w 1 -s '' | xxd
00000000: 3161 0a20 0a32 620a                      1a. .2b.
```

coreutilsでは`-w`は`1`以上の値しか受け付けませんので`0`を指定することはできません。一方、busyboxもほとんど同じ挙動ですが、`-w`に`0`が指定できるので空行のまま取り扱うことはできるようです。

```console:busybox
$ echo -e 'a\n\nb' | busybox nl -w 0 -s '' | xxd
00000000: 3161 0a0a 3262 0a                        1a..2b.
```
しかし、行番号についてもセパレータが消えてしまいますので実用するのは難しいでしょう。現実的に空行を元に戻したい場合は、`sed`などを利用するしかなさそうです。

```console:coreutils
$ echo -e 'a\n\nb' | nl | sed 's/^[ ]*$//' | nl
     1	     1	a
       
     2	     2	b
```


## BSD系の`nl`のPOSIX準拠についての報告

（12/23加筆）
[NetBSDに問題の報告](https://qiita.com/Carol_815/items/c81a278788978b5901d0)をしてみました。
macOSの`nl`はNetBSD由来のものですが、NetBSDが修正されたとしてもmacOSに反映されるかどうかはわかりません。続報があればまた追記したいと思います。