はじめに
2023年、長い時を経て awk がとうとう Unicode (UTF-8) と CSV に対応しました 🎉🎉🎉
awk で日本語がうまく扱えない(場合がある)、Excel が出力する CSV ファイルが扱えない(場合がある)、といった問題が解決に向けて一歩に進みます。
去年、本家 awk (One True Awk, nawk) に Unicode サポートが Brian Kernighan の手によって追加されたと話題になった(参照)ことを覚えているでしょうか? Brian Kernighan が誰だか知らない方がいるかもしれないので説明すると、オリジナルの awk の開発者の一人で awk の頭文字、Alfred Aho、Peter Weinberger、Brian Kernighan の一人です。通称「K&R」の「プログラミング言語C」や「プログラミング言語AWK」「プログラミング言語Go」の著者としても有名です。
そして 2023年9月11日、とうとう UTF-8 対応が master にマージされました。それだけではなく、CSV 対応も行われました(元々 CSV 対応のブランチに UTF-8 対応が含まれたっぽい?)。念のためですが UTF-8 と CSV に対応したのは本家の awk の話であり、その他の実装とは関係ありません。しかし本家が対応した以上、その他の実装も UTF-8 と CSV 実装に対応する可能性は高くなったのではないでしょうか。
この記事では、本家 One True Awk とその他の awk の UTF-8 と CSV の対応状況についてまとめました。
書籍「プログラミング言語AWK」について
「プログラミング言語AWK」は日本で 1989 年に出版された後、2001年、2004年、2010年にも出版されていますが、これらは別の出版社からの復刊で内容は同じようです(参考)。また洋書ですが 2023/9/16(明後日!) に「The AWK Programming Language」の第二版が出版されます(公式ページ)。非常に楽しみですが日本語版はでないんでしょうか? 目次はこちらにあります。追加された「Exploratory Data Analysis」の章では CSV ファイルについて書かれているようなことがレビューを行った goawk の開発者のコメントからわかります。目次から「Unicode Data」についても扱っていることがわかります。新しい One True Awk には 2ndEdition というタグがつけられているので、この本のために作られたのかもしれません。ちなみに日本 Amazon では Kindle 版 5154円(紙 6481 円)ですが、アメリカ Amazon なら Kindle 版 $31.99(紙 $39.99)です。(最初 Kindle 版 $23.99 と書いていたのですが、これは Pre-order Price でした。)
awkの実装にはどのようなものがある?
awk の実装は本家 One True Awk だけではありません。現在広く使われている有名な awk の実装には以下のようなものがあります。
- 【参考】歴史的な awk (1977年)
- ※古すぎるバージョンで基本的に無視して良い
- 最初の awk でユーザー定義関数などの標準的な機能がない
- POSIX awk の仕様にも準拠していない
- Solaris 10 の /bin/awk (/usr/bin/awk も同じ) で使われている
-
本家の awk (One True Awk、1985年頃)
- 歴史的な awk に対して 1985 年から 1988年頃にかけてに大幅に拡張されたもの
- 別名 nawk: new awk(歴史的な awk に対して新しい awk だから)
- 「プログラミング言語AWK」が対象としている awk
- ソースコードが公開されたのは 1990 年代後半
- バージョン番号について
- version 20230909: UTF-8 と CSV に対応しない最後のバージョン
- version 20230911: UTF-8 と CSV に対応する最初のバージョン
-
macOS、BSD 系 OS、(おそらく)Solaris 11 で使用されている
- それぞれ個別の修正が入っているので全く同じなわけでは無い
- Debian 系では original-awk で使える
- 見分け方
awk --version
またはawk -V
で「awk version 20230911」のような出力- Solaris 11 版はバージョン出力がないので
strings /usr/bin/awk
で判断
- Solaris 11 版はバージョン出力がないので
- GNU awk (1988年)
- Red Hat 系 Linux でのデフォルトの awk
- 見分け方
awk --version
で「GNU awk ・・・」と出力される
- mawk (1991年頃 main.c の Copyright より)
- Debian / Ubuntu でのデフォルトの awk
- 見分け方
awk -W version
で「mawk ・・・」と出力される
- BusyBox awk (2002年)
- Dmitry Zakharov による独自実装
- 組み込み向け
- Alpine Linux などでのデフォルトの awk
- 見分け方
awk
で「BusyBox ・・・」と出力される
- goawk (2018年 URL: https://github.com/benhoyt/goawk)
- Go 言語で実装された awk
- 見分け方
awk --version
で「v1.24.0」のような出力
その他にも商用の awk などがあるようです。goawk はあまり有名ではないかもしれませんが今回の話に関係があるので含めています。その他の awk の実装については GNU awk のドキュメントの B.5 Other Freely Available awk Implementations を参照してください。
awkのUTF-8対応について
新しいOne True Awkはいくつかの関数で挙動が変わる
ドキュメントによると新しい One True Awk は以下の関数でバイトではなく Unicode のコードポイントを扱うように変更されたようです。
length
, substr
, index
, match
, split
, sub
, gsub
, and others.
Unicode コードポイントのリテラルもサポートされています。
$ nawk 'BEGIN { printf "\u3042\n" }'
あ
今までのawkは(gawk以外)UTF-8を正しく扱えなかった
今までのほとんどの awk は UTF-8(日本語など)を正しく扱えませんでした。UTF-8 を扱えないと言っても UTF-8 は ASCII 互換であるためバイナリとして扱うことはできましたが、UTF-8 文字列として適切な処理が行われていませんでした。例えばひらがなの「あ」は 1 文字ではなく、16進数で「E3 81 82」の 3 バイトとして扱われ、正規表現の「.」は 1 文字ではなく 1 バイトにマッチしていました。
すべての awk の実装が UTF-8 に対応していなかったわけではありません。すでに GNU awk は UTF-8 に対応しています。各 awk で UTF-8 がどのように扱われるかを文字列の長さを出力する length
関数と正規表現の置換を例に示します。なおロケールは UTF-8 に設定されていることを確認しています。
【macOS 版の One True Awk(length が適切ではない)】
$ echo あ | /usr/bin/awk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
3 [あ]
【FreeBSD、NetBSD、OpenBSD、Solaris の One True Awk】
$ echo あ | original-awk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
3 [][][]
【Debian 版 の One True Awk】
$ echo あ | original-awk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
3 [][][]
$ echo あ | gawk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
1 [あ]
$ echo あ | mawk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
3 [][][]
$ echo あ | busybox awk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
3 [][][]
【goawk は正規表現は UTF-8 を扱えているが length は適切ではない】
$ echo あ | goawk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
3 [あ]
この中で完全に UTF-8 に対応していると言えるものは GNU awk だけです。その他の awk は何かしら問題があります。こんな状況では awk で日本語データを扱うのは不可能とまでは言いませんがためらいます。日本語を扱うのであれば GNU awk を使いましょうというのが現状です。GNU awk は移植性が高いので(インストールという簡単なお仕事をすれば)どの環境でも動きます。ちなみに POSIX では UTF-8 自体が(まだ)標準化されてないので、どのような動作でも POSIX 準拠違反ではありません。 POSIX ではバイトではなく文字として扱うように規定されているようです(参照 11.2.7.1 Modern Character Sets)。
それでは新しくなった One True Awk はどうなるか?
$ nawk --version
awk version 20230911
$ echo あ | nawk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
1 [あ]
GNU awk と同じような挙動になっていることがわかります。ただし全てを調べたわけではないので異なる部分があるかもしれません。
新しいOne True AwkはLC_ALL=Cに対応していない
2023-10-18 追記 この問題のパッチが提供されています。
→ 2023-10-31 にマージされました
新しい awk の UTF-8 対応は嬉しいのですが、ちょっと(私としては)問題があって、どうも LC_ALL=C
を設定しても、以前のようにバイナリでは扱ってくれないようなのです。
$ LC_ALL=C LANG=C LANGUAGE=C locale # LC_ALL=C だけで良いのだけど念のため
LANG=C
LANGUAGE=C
LC_CTYPE="C"
LC_NUMERIC="C"
LC_TIME="C"
LC_COLLATE="C"
LC_MONETARY="C"
LC_MESSAGES="C"
LC_PAPER="C"
LC_NAME="C"
LC_ADDRESS="C"
LC_TELEPHONE="C"
LC_MEASUREMENT="C"
LC_IDENTIFICATION="C"
LC_ALL=C
$ echo あ | nawk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
1 [あ]
$ # LC_ALL=C の時はバイト単位で扱って欲しいのにそうならない
$ echo あ | LC_ALL=C LANG=C LANGUAGE=C nawk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
1 [あ]
もちろん GNU awk ならちゃんと切り替わります。
$ echo あ | gawk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
1 [あ]
$ echo あ | LC_ALL=C gawk '{ l = length($0); gsub(/./,"[&]"); print l " " $0 }'
3 [][][]
これは困りました。互換性が保てません。
ちょうどこの前、シェルスクリプト用に URL エンコードを行う「url コマンド」を作ったのですが、パーセントエンコードを行うために意図的に LC_ALL=C
を使って一バイトずつ文字を置き換えているのです。私の場合は od
コマンドを使った回避方法とか思いつきますが、既存の awk スクリプトが動かなくなってしまう可能性があります。見事に「url コマンド」は新しい awk で動かなくなってしまいました……
この問題について問い合わせてみたのですが、新しい One True Awk は UTF-8 専用 で、変更可能ではあるが今すぐ行う予定はないそうです。実際にソースコード確認したのですが UTF-8 専用のコードに置き換えられていました。One True Awk は BSD 系 OS や macOS で採用されていますが、影響範囲が大きそうなので、このままではせっかくの UTF-8 対応と CSV 対応が取り込まれない可能性が高いと危惧しています。Homebrew 版は気にせずアップデートしそうですが。
One True Awk の UTF-8 対応への変更内容は小さくはないのですが大きすぎるということもなく(やっぱり大きい)、修正パッチを投げたいところですが、UTF-8 対応と CSV 対応が混ざり気味でちょっと面倒なのと、私が C 言語の国際化をやったことがないので、以前のコードと UTF-8 対応コードを切り替えられるようにするべきか(でもコードの二重管理はしたくない)、ちゃんとした国際化対応をするべきか、その場合の修正範囲はどれくらいで型は何を使うべきか見当がつかず考えあぐねています1。
awkのCSV対応状況について
さてもう一つの話、awk の CSV 対応です。こちらは Brian Kernighan は関わっていないと思っていたのですが、Brian Kernighan のコミットで CSV 対応に関係ありそうなものがありますね。
今までのawkのCSV の対応
awk に CSV 対応がなんて必要なの?普通に読み込めるじゃん?と思う人は、おそらく Excel などが出力する CSV ファイルを読み取ったことがないでしょう。例えば以下の CSV は正しい「一レコードの」CSV です。
aaa aaa,"bbb
bbb","ccc,ccc"
このような CSV ファイルをを正しく扱うのは大変です。
ちなみに CSV 形式には標準化された仕様は存在しません。一応 2005 年に公開された RFC 4180 に仕様が記載されていますが、標準化過程を経ていない「情報 (Informational)」なので厳密には標準仕様ではありません。RFC の分類についてはこちらを参照してください。CSV ファイルの歴史は古く 1970 年代からあります。Excel は 1985 年に誕生しました。最近の実装の多くは RFC 4180 に準拠していると思われますが、古いソフトウェアが後から作られた RFC 4180 に準拠していなくても不思議ではありません。後方互換性を維持するという正当な理由があるため RFC 4180 に従っていなくとも間違っているとは言えません。
GNU awkのFPATはCSVを正しく扱えない
GNU awk には FPAT
という組込変数があり、それを使うとカンマ区切りファイルが読み込めます。しかしダブルクォートの中に改行が含まれた値には対応できません。このことは GNU awk のドキュメントにも書かれています。
NOTE: Some programs export CSV data that contains embedded newlines between the double quotes. gawk provides no way to deal with this. Even though a formal specification for CSV data exists, there isn’t much more to be done; the FPAT mechanism provides an elegant solution for the majority of cases, and the gawk developers are satisfied with that.
CSV には標準仕様がないわけでこれでも CSV 対応といえますが、Excel などから出力した CSV を正しく扱うことはできないため実用性から見ると少し不安が残ります。
新しいOne True Awkは--csvオプションでCSVを読み込める
新しい One True Awk で CSV ファイルを読み込むのは簡単です。単に --csv
オプションを付けるだけです。
$ cat test.csv
aaa aaa,"bbb
bbb","ccc,ccc"
$ nawk --csv '{printf "%d: [%s] [%s] [%s]\n", NR, $1, $2, $3 }' test.csv
1: [aaa aaa] [bbb
bbb] [ccc,ccc]
レコード数は 1 レコード(NR = 1)となっており、ダブルクォートが使われていたりフィールドに改行が含まれていたりしても、それぞれのフィールドが正しく読み込まれているということがわかります。ちなみに CSV モードでは FS
変数は無視されるようです。なお TSV には対応していないようです。
余談ですが、One True Awk の Issue を眺めていたら One True Awk は -F 't'
は t
という文字でフィールドを区切るのではなく -F '\t'
と同じ意味でタブ区切りとして解釈することを知りました。もちろんこのような仕様は他の awk にも POSIX awk にもありません。t
を区切り文字として使うことは少ないとは言えなかなかに酷い罠です。ちなみにこの罠は長く存在し使用例もあるため修正されないそうです。
$ printf 'Uranus\tNeptune' | nawk -F 't' '{print $1}'
Uranus
$ printf 'Uranus\tNeptune' | mawk -F 't' '{print $1}'
Uranus Nep
$ printf 'Uranus\tNeptune' | gawk -F 't' '{print $1}'
Uranus Nep
$ # GNU awk は --traditional オプションを付けると同じ挙動になる
$ printf 'Uranus\tNeptune' | gawk --traditional -F 't' '{print $1}'
Uranus
goawkも--csvオプション対応
今回 goawk の話を含めたのは、goawk も --csv
オプションに対応しているからです。--csv
オプションは One True Awk との互換性のために後から追加されたオプションですが、それよりも前に -i csv
オプションとして対応しており、CSV に対応したのは goawk の方が先のはずです。構想自体はあったようですが 2022 年 5 月に「最近追加した」と書かれているのでそんなに前のことではなさそうです。また goawk は他の awk のような $1
、$2
といったフィールド番号だけではなく、@"name"
のような CSV のヘッダ名でフィールドを参照することが出来ます。これは awk の可読性を向上させます。さらに goawk は CSV での出力や TSV にも対応しています。詳細は「Modernizing AWK, a 45-year old language, by adding CSV support」を参照してください。
awk で CSVファイルを読み込みたいのであれば、goawk を使うのが一番良いのではないかと思います。Go 言語製で移植性が高く、macOS、Linux、Windows ならバイナリが用意されているのでコピーするだけで使えます。
GNU awk 5.3.0も--csvオプション対応
さて GNU awk の FPAT
は CSV を正しく扱えないという話をしましたが、それはもうすぐ過去のものとなります。近くリリースされると思われる次バージョンの gawk 5.3.0 も --csv
オプションに対応するからです。これは One True Awk の新機能に合わせた対応であることがドキュメントに記載されています。
Changes from 5.2.2 to 5.3.0
---------------------------
︙
2. In keeping with new features in BWK awk, gawk now has built-in
CSV file parsing. The behavior is intended to be identical to
that of the "One True AWK", when --csv is applied. See the
manual for details.
︙
4. Also in keeping with BWK awk, gawk now supports a new \u escape
sequence. This should be followed by 1-8 hexadecimal digits. The
given code point is converted to its corresponding multibyte encoding
for storage inside gawk. See the manual.
5. Because of the additional `do_csv' variable in the API, which breaks
binary compatibility, the API major version was updated to 4 and
the minor version was reset to zero. The API remains source code
compatible; that is, existing extensions should only require recompilation.
さいごに
ということで awk が UTF-8 と CSV に対応するという話でした。One True Awk は現時点では後方互換性に問題があるので少し微妙な感じですが、こうやって古いコマンドもバージョンアップされてより便利になるというのは素晴らしいことですね。時代が変わっているのにいつまでも古いままというのはいけません。Unix コマンドもそれを使う人も変わっていかなければです。新しい時代に古いやり方は通用しません。
余談ですが Brian Kernighan は最初 git が分からなかったそうで、最初のコミットはメールでメンテナにパッチを送ったそうです。しかし最近のコミットを見ると Brian Kernighan の名前があるので、どうやら今は git を使っているようです。もしやメールアドレスが分かるのでは?と思って見てみたのですが、いかにも偽物なメールアドレスが使われていました。残念。もっとも検索したら普通にメールアドレスは公開されていましたが。
もう一つ余談ですが、私は既存の awk を使い勝手を変えずに RFC4180 準拠の CSV ファイルに対応させるのは可能だと思っていて、いつか作ろうかなと考えていたりします。優先順位は高くないのですぐに手を付けようとは思っていませんが。
おまけ Miller (mlr) 使えばよくね?
さて、ここまで awk の話をしといてなんなんですが、CSV ファイルを扱うなら Miller (mlr コマンド)を使えばよくね?ということでリンクを貼り付けておきます。
Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON
awk に似た DSL を搭載しているので、おそらく awk で出来ることのほとんどのことが出来ます。awk よりももっと多くの関数を実装しています。awk とは異なり DSL を使わずに他のコマンドと同じような使い方もできます。つまり Miller は awk とテキストフィルタ系 Unix コマンドの完全上位互換です。
-
正直に言うと UTF-8 対応と CSV 対応は明確に分離させるべきで、CSV 対応だけを入れて Brian Kernighan の作業はやり直した方がいい思う。今のは UTF-8 対応ではなく UTF-8 専用の別バージョンになってしまっている。互換性が保たれてないので BSD 系 OS や macOS に取り込まれる可能性は低いし、コードが混ざってるので CSV 対応すらも取り込まれないだろう。今までなくても問題なかった機能だから取り込む理由がない。The AWK Programming Language の第二版の出版に間に合わせたかったのだろうが、急ぎ過ぎだし互換性を考慮しておらず今後メンテナンスしていく気もなさそう。どうみても第二版のネタにするための機能追加にしか見えないし、そのためにメンテナになったように思える。このままだと本家の One True Awk は放棄されるか uawk みたいな名前で UTF-8 版 awk という別物として扱われ、FreeBSD 版の One True Awk が本家をフォークした正当な後継として使われていくだろう。gawk に CSV 対応の拡張機能を入れる口実としてなら評価できる。 ↩