セクションの区切り文字列を変更するnlコマンドのdオプション


nlコマンドはdオプションを指定することで、論理ページのセクション区切り文字列`\:`を変更することができます。論理ページについては[前の記事](https://qiita.com/Carol_815/items/d1f52df36b0d39b374b0)をご覧ください。

# dオプションの使い方
`-d`の後に文字列を指定することで、区切り文字列を変更できます。しかしマニュアルの記述が散らばっており少々わかりにくいような気がします。

## info nl (GNU coreutils)

```console
The two characters from which these strings are made can be changed
from ‘\’ and ‘:’ via options (see below), but the pattern and length of
each string cannot be changed.
```
coreutilsの`nl`では、オプションについては2ヶ所に記載されています。文字列は変更できるが長さは変更できないようです。つまり「ヘッダー」は指定した文字列の3連続繰り返し、「ボディ」は2連続、「フッター」は単体ということになります。これは区切り文字列を変更するオプションが`d`ひとつだけなので仕方ないですね。

```console
‘-d CD’
‘--section-delimiter=CD’
     Set the section delimiter characters to CD; default is ‘\:’.  If
     only C is given, the second remains ‘:’.  (Remember to protect ‘\’
     or other metacharacters from shell expansion with quotes or extra
     backslashes.)
```

`-d`の説明では、与える文字列が`CD`と表記されています。「`C`しか与えない場合」といった書き方がされているので、これは`C`と`D`という2文字と解釈するようです。つまりデフォルトの`\:`は`C`=`\`、`D`=`:`であり、1文字しか与えない場合2文字目は`:`のままになるようです。**なんだこの仕様は。**
大変親切に「`\`などのメタ文字のクオート処理に気をつけて」と注意がされています。

## man nl (BSD)

```console
     -d delim     Specify the delimiter characters used to indicate the start of a
                  logical page section in the input file.  At most two characters
                  may be specified; if only one character is specified, the first
                  character is replaced and the second character remains
                  unchanged.  The default delim characters are ``\:''.
```

BSD系の`nl`では「最大2文字」と明記した上で、1文字しか与えない場合についてはcoreutils同様に2文字目は`:`のままになるようです。**なんだこの仕様は。**


## POSIX

>-d  delim
Specify the delimiter characters that indicate the start of a logical page section. These can be changed from the default characters "\\:" to two user-specified characters. If only one character is entered, the second character shall remain the default character ':'.

「指定できるのは2文字」と「1文字のみ指定したら2文字目は`:`」という謎の仕様はPOSIXで決められているようです。現実的に区切り文字列が1文字ではセクションが誤認識されてしまうことがありそうですが、意志を持って変更する分には1文字でも構わないのではと思います。どういった経緯でこのようになっているのかは調査不足につき不明です。


# dオプションを試す

ボディの区切り`\:\:`を`----`に変更して試してみましょう。

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

     1	b
```

```console:BSD
$ echo -e 'a\n----\nb'
a
----
b
$ echo -e 'a\n----\nb' | nl -d '--'
     1	a
     1	b
```

## coreutilsの`nl`のdオプションの仕様

coreutilsの実装ではマニュアル記載の与える文字列の`CD`のうち`D`には長さ制限がないようです。長い文字列も指定できます。POSIXでは明確に2文字と制限しているようなので、この動作はPOSIX外の仕様になります。

```console:coreutils

$ echo -e 'a\n区切り文字列区切り文字列\nb'
a
区切り文字列区切り文字列
b
$ echo -e 'a\n区切り文字列区切り文字列\nb' | nl -d '区切り文字列'
     1	a

     1	b
```

（12/15加筆）
この動作はGNU拡張として、次版（v8.33？）から以下のようにTexinfoに記載されることになりました。
> As a GNU extension more than two characters can be specified,

# coreutilsの`nl`のdオプションの不具合

（12/15加筆：12/9時点では調査後加筆とした内容です）

coreutils v8.32までの`nl`は、「1文字しか与えない場合」にマニュアル通りの動作をしません。指定された区切り文字列を無視します。

```console:coreutils~v8.32
$ echo -e 'a\n@:@:\nb\n@@\nc' | nl -d '@'
     1 a
     2 @:@:
     3 b
     4 @@
     5 c
```
```console:POSIXによる期待値
$ echo -e 'a\n@:@:\nb\n@@\nc' | nl -d '@'
     1 a

     1 b
     2 @@
     3 c
```

これはプログラムの不具合によるものです。各セクションの区切り文字列を生成する処理に誤りがあり、`-d`で与えられる文字列は長さによらずデフォルトの「\:」を上書きしていました。一方で、区切り文字列と入力データのパターンマッチ処理にも誤りがあり、必ず区切り文字列が2文字以上であるとして処理していました。これにより`-d`に2文字未満の文字列を指定した場合、区切り文字列とのパターンマッチは必ず失敗し、未初期化のメモリ領域を参照する恐れがありました。この不具合は既に修正され、次版（v8.33？）以降では安全にPOSIXに準拠した動作になります。

## 不具合修正の余談

私はこの記事で何度も「**何だこの仕様は**」と書いている通り、POSIXの仕様が意味のあるものだとは思っていません。そこで、この際に以下のようなGNU拡張として仕様を変更できないか提案してみました。

```console:GNU拡張の提案
$ echo -e 'a\n@:@:\nb\n@@\nc' | nl -d '@'
     1 a
     2 @:@:
     3 b

     1 c
$ echo -e 'a\n@:@:\nb\n@@\nc' | nl -d '@:'
     1 a

     1 b
     2 @@
     3 c
```

今まで使えなかった1文字の区切り文字列が使えるようになるのがメリットで、従来どおりの動作を期待するなら2文字指定すれば良いという提案です。

しかし残念ながらこの提案はリジェクトされました。1文字の区切り文字列を利用したいという要望がないのに、わざわざPOSIXの互換性を壊すべきではない。というのが理由です。これはGNU coreutilsの一貫した方針だと思いますので、今回のPOSIXに準拠した修正を支持することにしました。

---