1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

nlコマンドAdvent Calendar 2020

Day 7

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

Last updated at Posted at 2020-12-06

いままで特に触れてきませんでしたが、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でわかりやすく@@@に変更して検証してみましょう。

coreutils
$ echo -e 'a\n\nb' | nl -s @@@ 
     1@@@a
         
     2@@@b
busybox
$ echo -e 'a\n\nb' | busybox nl -s @@@ 
     1@@@a
         
     2@@@b
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オプションの記事でも引用した「OUTPUT」の一部分の記載になります。
POSIXによると「行番号が抑制されるとき、セパレータもまた抑制される」とあります。つまりBSD系のnlのこの挙動は「POSIXに準拠していない仕様」あるいは「ソフトウェアのバグ」ということになります。

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

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

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

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

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

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

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

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.
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つ重ねて使う場合には想定した挙動と異なる結果になるかもしれません。

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

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

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

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

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

busybox
$ echo -e 'a\n\nb' | busybox nl -w 0 -s '' | xxd
00000000: 3161 0a0a 3262 0a                        1a..2b.

しかし、行番号についてもセパレータが消えてしまいますので実用するのは難しいでしょう。現実的に空行を元に戻したい場合は、sedなどを利用するしかなさそうです。

coreutils
$ 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に反映されるかどうかはわかりません。続報があればまた追記したいと思います。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?