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?

awk でヌル文字区切りで出力する POSIX 準拠の移植性がある方法

Last updated at Posted at 2024-08-06

はじめに

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 の実装では RSORS を使ってヌル文字区切りの入力や出力を行うこともできます。

$ 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 -0read -d をつかわない場合は) od コマンドを使って前処理するぐらいしか手段はなさそうな気がします。個人的に入力は今すぐに必要なものではないので割愛します。

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?