いままで特に触れてきませんでしたが、nl
はデフォルトで空行(改行だけの行)に行番号を出力しません。これは行番号を出力する機能を持つ他のコマンドとは大きく異なる点だと思います。
空行を含むデータの行番号
まずは空行に対してnl
がどんな動作をするのか確かめてみましょう。echo
は-e
を指定することで\n
を改行コード(LF
)として出力します。
$ echo -e 'a\n\nb'
a
b
$ echo -e 'a\n\nb' | nl
1 a
2 b
確かに空行に行番号は出力されません。
空行と判定される条件
スペースやCR
改行(\r
)を含む行など、見た目は空行に見えるデータも試してみましょう。
$ 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オプションの記事で説明したセパレータは出力されているのでしょうか。実はcoreutilsのnl
とBSD系のnl
では結果が異なります。タブのままでは見えないので-s
でわかりやすく@@@
に変更して検証してみましょう。
$ echo -e 'a\n\nb' | nl -s @@@
1@@@a
2@@@b
$ echo -e 'a\n\nb' | busybox nl -s @@@
1@@@a
2@@@b
$ 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オプションの記事でも引用した「OUTPUT」の一部分の記載になります。
POSIXによると「行番号が抑制されるとき、セパレータもまた抑制される」とあります。つまりBSD系のnl
のこの挙動は「POSIXに準拠していない仕様」あるいは「ソフトウェアのバグ」ということになります。
STANDARDS
The nl utility conforms to IEEE Std 1003.1-2001 ("POSIX.1").
さらに、Manpageを参照すると「標準」としてPOSIXが引用されています。おそらくはバグということになるのでしょう。
ちなみに、coreutilsのManpageを参照すると、-s
について以下のように書かれています。
$ man nl | grep -A1 'number-separator'
-s, --number-separator=STRING
add STRING after (possible) line number
この(possible)
という部分が、「行番号をつけるところだけ」という補足のようです。
coreutilsのnl
での空行の取り扱い
デフォルトのタブのときも検証してみましょう。xxd
というvim
に含まれるコマンドユーティリティでバイナリを16進数のテキストで出力します。
$ 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.
$ 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つ重ねて使う場合には想定した挙動と異なる結果になるかもしれません。
$ echo -e 'a\n\nb' | nl | nl
1 1 a
2
3 2 b
空行に付加されるデータを取り除く
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
が指定できるので空行のまま取り扱うことはできるようです。
$ echo -e 'a\n\nb' | busybox nl -w 0 -s '' | xxd
00000000: 3161 0a0a 3262 0a 1a..2b.
しかし、行番号についてもセパレータが消えてしまいますので実用するのは難しいでしょう。現実的に空行を元に戻したい場合は、sed
などを利用するしかなさそうです。
$ echo -e 'a\n\nb' | nl | sed 's/^[ ]*$//' | nl
1 1 a
2 2 b
BSD系のnl
のPOSIX準拠についての報告
(12/23加筆)
NetBSDに問題の報告をしてみました。
macOSのnl
はNetBSD由来のものですが、NetBSDが修正されたとしてもmacOSに反映されるかどうかはわかりません。続報があればまた追記したいと思います。