はじめに
POSIX.1-2024 ではヌル文字区切り(正確にはヌル文字終端)が標準的な形式として認められるようになりました。
-
find
コマンド: ヌル文字区切りで出力する-prinf0
が標準化 -
xargs
コマンド: ヌル文字区切りで入力する-0
オプションが標準化 -
read
コマンド: ヌル文字区切りに変更可能な-d
オプションが標準化
しかし、残念なことに awk に関しては POSIX.1-2024 でヌル文字区切りを扱う方法は追加されませんでした。この記事は awk でヌル文字区切りで出力する(移植性のある)方法について解説します。
ヌル文字を出力する4つの方法
ヌル文字を出力する方法には次の4種類が考えられます。前半の 2 つは awk 自身の機能のみで実装していますが、残念ながら移植性はありません。awk 自身ではヌル文字を出力する移植性のある方法はないため、後半の 2 つは printf
コマンドまたはシェルの機能を利用しています。
1. ヌル文字を使う(変数に代入する)
この方法は移植性がありませんが、一部の awk の実装はヌル文字(\0
)をそのまま扱うことができます。ここでいうヌル文字を扱うことができるというのは変数にヌル文字を代入したり文字列の一部として使用することができるという意味です。ヌル文字を扱うことができる実装は、gawk、mawk、goawk です。nawk(BSD 系 Unix や macOS)、Busybox awk、Solaris awk では扱うことができません。
$ gawk 'BEGIN { v="\0"; printf "%s", "test" v }' | od -tx1
0000000 74 65 73 74 00
0000005
$ mawk 'BEGIN { v="\0"; printf "%s", "test" v }' | od -tx1
0000000 74 65 73 74 00
0000005
これらの awk の実装では RS
や ORS
を使ってヌル文字区切りの入力や出力を行うこともできます。
$ printf 'foo\0bar\0baz\0' | awk -v RS='\0' '{print $0}'
foo
bar
baz
$ printf 'foo\nbar\nbaz\n' | awk -v ORS='\0' '{print $0}' | od -tx1
0000000 66 6f 6f 00 62 61 72 00 62 61 7a 00
0000014
2. printfステートメントを使う
この方法は移植性がありませんが、一部の awk の実装は(ヌル文字を変数に代入することができなくても)ヌル文字(\0
)を出力することができます。ヌル文字を扱うことができる実装は gawk、mawk、goawk に加えて、nawk(BSD 系 Unix や macOS)です。Busybox awk や Solaris awk では出力することができません。
$ awk 'BEGIN { printf "%s%c", "test", 0 }' | od -tx1
0000000 74 65 73 74 00
0000005
3. system("printf ..." ) を使う
この方法は移植性があります。system
関数を使って外部コマンドの printf
コマンドを呼び出すだけなので簡単に実装できますが、必要になるたびに外部コマンドを呼び出さなければならないので遅い方法です。
$ busybox awk 'BEGIN { system("printf \\\\0") }' | od -tx1
0000000 00
0000001
4. printfシェルビルトインコマンドを使う
この方法は移植性があります。シェルビルトインコマンドの printf
コマンド(一部のシェルでは print
コマンド)を使用する方法です。system
関数を使ってその都度、外部コマンドの printf
コマンドを呼び出す方法に比べて、シェルに出力を移譲することで高速化させることができます。ただし awk で直接出力するよりかは遅いので注意してください。
$ awk 'BEGIN { print "printf \\\\0" | "sh" }' | od -tx1
0000000 00
0000001
一部のシェル(mksh)では printf
コマンドが外部コマンドなので print
コマンドを代わりに使う必要があります。また実際の利用ではすべての出力をシェル経由で行うために、シェルエスケープ処理が必要となり複雑になります。具体的な実装については次を参照してください。
移植性が高いヌル文字出力の実装例
「4. printfシェルビルトインコマンドを使う」を使用した実装例です。
seq -f "It's small world %g %% \\ " 5 | {
check='(PATH=/dev/null; print "print -n" 2>/dev/null) || printf -- printf'
awk -v SH="sh" -v PRCHECK="$check" '
function prinit(str) {
NUL = "\\0"; CR = "\\r"; LF = "\\n"
(SH " -c \047" PRCHECK "\047") | getline CMD
CMD = CMD == "--" ? "printf " : CMD " -- "
}
function pr(str, eol) {
gsub(/\\/, "&&", str)
gsub(/%/, "\\045", str)
gsub(/\047/, "\047\\\047\047", str)
print CMD "\047" str eol "\047" | SH
}
BEGIN { prinit() }
{ pr($0, NUL) }
'
} | xargs -0 -n1
解説
check='(PATH=/dev/null; print "print -n" 2>/dev/null) || printf -- printf'
上記のコードは printf
コマンドがシェルビルトインコマンドなのかを判定するためのシェルコードです。実際には PATH
に実行ファイルが存在しないパスに設定して print
コマンドがシェルビルトインコマンドとして呼び出せるかどうかで判定しており、print
コマンドがシェルビルトインコマンドとして呼び出せる場合は、print
を出力し、そうでない場合は printf
を出力します。
(通常は考慮する必要はありませんが)ものすごく古いシェルの printf
コマンドは、POSIX で標準化されている --
の仕様に対応しておらず、printf
ではなく --
を出力する場合があります。
(SH " -c \047" PRCHECK "\047") | getline CMD
CMD = CMD == "--" ? "printf " : CMD " -- "
上記のコードで先程の check
のコードを実行してシェルの能力を判定しています。2 行目はもし出力が --
の場合(古いシェル)に printf
コマンドを使用するようにしています。zsh 4.0.4 以前という古いシェルのための回避策なので省略してもほぼ問題ありませんが、1 行程度の差でしかないので対応させています。
function pr(str, eol) {
gsub(/\\/, "&&", str)
gsub(/%/, "\\045", str)
gsub(/\047/, "\047\\\047\047", str)
print CMD "\047" str eol "\047" | SH
}
上記のコードは、printf
(print
) のためのエスケープと、シェルエスケープを行って printf -- test\0
のような文字列を出力して、シェルに実行させるための関数です。文字列の出力は pr($0, NUL)
のように実行することで行末をヌル文字にして出力することができます。シェルは(このコードでは省略していますが)close
するまでは起動した状態であり、pr
関数を呼び出すたびに起動するわけではないことに注意してください。
さいごに
パフォーマンスと移植性とコードの量のバランスで少し悩みましたが、意外とコンパクトに実装できたのではないかと思っています。あとはヌル文字区切りの入力にも対応させたい気がしますが、おそらく(xargs -0
や read -d
をつかわない場合は) od
コマンドを使って前処理するぐらいしか手段はなさそうな気がします。個人的に入力は今すぐに必要なものではないので割愛します。