jq、xmllintコマンドさようなら。俺はパイプが好きだから

  • 187
    いいね
  • 8
    コメント

2016-09-15 New!
この記事で紹介するコマンドの逆変換コマンド(JSONのみ)も作り、記事化しました。次の相手はjoです。
joコマンドさようなら、俺はパイプが好きだから。PART 1

郵便番号→緯度経度→日の出時刻 コマンド

jqxmllintといったコマンドの使い勝手についカッとなって、独自JSON&XML解析コマンドと、その応用品“getsunrise.sh”を作った。なぜカッとなったのかは次のセクションで言いたい放題することにして、まぁとりあえず遊んでみてもらいたい。

curlnkfが入っているUNIX環境ならほとんどの環境で動く。もちろん jqとかxmllintとか不要。JSONとXMLの解析はsedやAWKで済ませてる。 なのでこいつをダウンロードして、解凍・実行するだけだ。(コンパイルも不要)

実行例
$ ./getsunrise.sh 288-0012 2014/01/01
千葉県銚子市犬吠埼、20140101の日の出・日の入時刻を調べます。
(15秒くらい待ってね...)

千葉県銚子市犬吠埼(緯度=35.706987,経度=140.861803)における、
2014年1月1日の 日の出時刻は6:46、日の入時刻は16:34、みたいですよ。
$ ./getsunrise.sh 100-2100 2014/01/01 # ←日本一早い初日の出の時刻は?
#(実際に動かしてみてください)
$ 

このコマンドがやっている事は下記のとおりだ。要するにWebAPIを渡り歩いている。

1) コマンド引数から郵便番号を受け取る。
2) 郵便番号検索WebAPIに掛けて、地名と緯度・経度を得る。
(ちなみにAPI紹介ページのクレジット表記が“HeartRails Inc. Some Rights Reserved.”になってるんですけど、気のせいですか?)
3) さらに、日の出・日の入時刻WebAPIに掛けて、日の出・日の入り時刻を得る。

ソースコードに興味がある方は是非、ご覧いただきたい。
コメントや空行を除くと、たかだか69行のシェルスクリプトである。

jqやxmllint等は、UNIX哲学に染まりきっていない

上記のコマンドで利用した2つのWebAPIは、結果をそれぞれJSONとXMLで返してくれる。

シェルスクリプトでJSONを処理したいといったらjqコマンド、XMLを処理したいといったらxmllintや(あるいはMacOS Xのxpath)といったコマンドを思い浮かべるかもしれない。

「いやぁー、これ超便利」という声をWeb上でも目にするし、ホラホラ、ここの
右サイドメニューの「関連投稿」⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒⇒
でもベタ褒めしてる投稿あるじゃない。でも、私はそんなに便利だとは思えない。「なぜこれで満足できる?」とさえ思う。せっかくUNIX向けに作ってくれるのなら、もっとUNIX哲学に染まった仕様にしてもらいたい。

理由1. 一つのことをうまくやっていない

UNIX哲学の一つとしてよく引用されるマイク・ガンカーズの教義に

1.小さいものは美しい。
2.1つのプログラムには1つのことをうまくやらせよ。

というのがあるが、まずこれができていない。jqやxmllint等は、データの正規化(都合の良い形式に変換する)機能とデータの欲しい部分だけを抽出する部分抽出機能を分けていない。むしろ前者をすっ飛ばして後者だけやっているように思う。

でもUNIX使いとしては、 部分抽出といったらgrepやAWK を使い慣れているわけで、それらでできるようにしてもらいたいと思う。部分抽出をするために、 jqやxmllint等独自の文法をわざわざ覚えたくない し。

だから、正規化だけをやるようなコマンドであってほしかった。

理由2. フィルターとして振る舞うようになりきれてない

同じく教義の一つに

9.全てのプログラムはフィルターとして振る舞うように作れ。

というものがある。フィルターとは、入力されたものに何らかの加工を施して出力するものをいうが、jqやxmllint等は、出力の部分に注目するとフィルターと呼ぶには心もとない気がする。

なぜなら、これらのプログラムはどれも JSONやXML形式のまま出力 される。しかし、他の標準UNIXコマンドからは扱いづらい。AWK,sed,grep,sort,head,tail,……などなど、 標準UNIXコマンドの多くは、行単位あるいは列単位(空白区切り)のデータを加工するのに向いた仕様 になっているため、JSONやXML形式のままだと結局扱いづらい。

だから、行や列の形に正規化するコマンドであってほしかった。

無いものは作る。既存コマンドの組み合わせだけでも相当な威力

無いので作った。
JSONパーサー
XMLパーサー
JSONもXMLも、「値のある場所」と「値(属性)」の2つの情報に意味があるので、前者をkey、後者をvalueとし、この2列を1行としたデータを出力する。

例えば、次のようなJSONデータがあったら

文具購入リスト(bungu_meisai.json)
{"会員名" : "文具 太郎",
 "購入品" : [ "はさみ",
              "ノート(A4,無地)",
              "シャープペンシル",
              {"取寄商品" : "替え芯"},
              "クリアファイル",
              {"取寄商品" : "6穴パンチ"}
            ]
}

値のある場所をJSONPathで表し、半角スペース1つあけて右に値を添えることができる。

拙作JSONパーサーコマンドに通す
$ ./parsrj.sh bungu_meisai.json
$.会員名 文具 太郎
$.購入品[0] はさみ
$.購入品[1] ノート(A4,無地)
$.購入品[2] シャープペンシル
$.購入品[3].取寄商品 替え芯
$.購入品[4] クリアファイル
$.購入品[5].取寄商品 6穴パンチ

同じデータがXMLで下記のように表現されていたら

bungu_meisai.xml
<文具購入リスト 会員名="文具 太郎">
  <購入品>はさみ</購入品>
  <購入品>ノート(A4,無地)</購入品>
  <購入品>シャープペンシル</購入品>
  <購入品><取寄商品>替え芯</取寄商品></購入品>
  <購入品>クリアファイル</購入品>
  <購入品><取寄商品>6穴パンチ</取寄商品></購入品>
</文具購入リスト>

値のある場所をXPathで表し、半角スペース1つあけて右に値を添えることができる。

拙作XMLパーサーコマンドに通す
$ ./parsrx.sh bungu_meisai.xml
/文具購入リスト/@会員名 文具 太郎
/文具購入リスト/購入品 はさみ
/文具購入リスト/購入品 ノート(A4,無地)
/文具購入リスト/購入品 シャープペンシル
/文具購入リスト/購入品/取寄商品 替え芯
/文具購入リスト/購入品
/文具購入リスト/購入品 クリアファイル
/文具購入リスト/購入品/取寄商品 6穴パンチ
/文具購入リスト/購入品
/文具購入リスト \n  \n  \n  \n  \n  \n  \n

こういうふうにkey-value形式に変換されていれば、後で使いまわすのがとても便利だ。例えば、"取寄商品"だけ抽出したいなら、後ろにパイプでgrep '取寄商品'と書けばいいし、更にその取寄商品がいくつあったかを知りたければ、更にwc -lをパイプで繋げばよい。

こんな具合にUNIXコマンドとの相性もばっちりだ。冒頭で紹介したプログラムも、パーサーの後ろにAWKやsedやgrepなどを付けまくっている。

これらのパーサーもまたパイプで繋いだコマンドの塊

ソースコードをみれば明白だが、これらのパーサーもまた、シェルスクリプトを用い、AWK,sed,grep,trなどをパイプで繋ぐだけで実装した。

これまたUNIX哲学の教義だが、

6.ソフトウェアは「てこ」。最小の労力で最大の効果を得よ。
7.効率と移植性を高めるため、シェルスクリプトを活用せよ。

というのがあって、この思想が込められた、シェルスクリプトやUNIXコマンド、パイプというものがいかに偉大な発明であるか思い知らされた。

なにせ、普段作業で使っているUNIXコマンドやシェルスクリプトそれだけで、JSONやXMLの処理まで、ちゃーんとできてしまうのだから。

質疑応答

この記事はUNIX哲学に基づいてjqの類をdisってるのに、UNIX哲学を理解せずにこの記事をdisる人がいる。だから、質疑応答形式で反論しておく。

jqはJSONだけ扱ってる。だから1つの事をうまくやってる、でしょ?

この意見はUNIX哲学のことまるで分かってない。「1つの事」とは、 一つの動詞で言い換えられる程原始的な動作 のことを言っているのに。

  • catは何するコマンド? → catinateする、すなわち結合するコマンド
  • echoは何するコマンド? → echoする、すなわち(書かれたことを)オウム返しするコマンド
  • mkdirは何するコマンド? → dirをmakeする、すなわち(ディレクトリを)作るコマンド
  • :

というように、1つのコマンドは1つの動詞にほぼ写像できることが理想とされるのだ。それが直感的な理解を促すから。

逆にUNIX哲学本において、反面教師的に晒し者にされているコマンドもある。それはlsだ。

lsにはオプションが何十個もある。例えばその中に、-c-f-r-tというオプションがあるが、一体これらが何を表しているかみなさんは知っているだろうか? 共通しているのはファイル表示順を変えるという機能である。 そんなもんsortでいいだろ! と思わないか? もしあなたが、lsコマンドのオプションを知り尽くす「lsマスター」なら何も言うまいが。

あなたのパーサーで変換すると、JSONの情報が色々欠損する

これはべつにdisりではなく、ある視点でみると正しい指摘だった。

もちろん拙作のJSONパーサーが、JSONが格納している値やその位置情報を欠損させてしまうわけではない。なにせ制作当初から、将来は逆変換コマンドを作ることを想定していたくらいだから。

しかし指摘の意図がわかった。JSONは、JSONのままで可読性を確保できるよう、ある程度自由にスペースや改行を付けられるようになっているわけだが、それを切り落としているという指摘だ。確かに逆変換コマンドを作る際にも、そのような付随的な情報の復元は全く想定していなかったし、よっぽどの需要が無い限りは今後もたぶん対応しない。

理由は、これまたUNIX哲学に基づいている。

あなたはUNIXに出会った当初、 「なんて無愛想なOSなんだ」と思ったことはないか? 例えば、コマンドが成功した時「成功しました」というメッセージは一切表示されない。ファイルを検索して、1個も見つからなかったら「見つかりませんでした」と言わない。標準エラー出力で結果が知らされることはあるが、標準出力には絶対にそういうメッセージを流し込んでこない。

これは、UNIX哲学に基づいて作られた伝統的な UNIXコマンドは、出力結果を利用する相手が人ではなく機械であること前提 だからだ。例えば、ファイルを検索して、検索結果が0件だった時に、"not found"という情報が標準出力に送られたらどうだろう。人なら「あぁ、見つからなかったんだね」と気がつくが、 機械は馬鹿正直なので"not found"という名前のファイル名が見つかったと思って処理 をしてしまう。されたくなければ、"not found"という文字列だけ個別に対応するコードをわざわざ書いてやらなければならない。

UNIXコマンドが「不言実行」なのは、このように相手が機械であること前提なためであり、このことはやはりUNIX哲学本にも書かれている。拙作パーサーもその立場に基づいて作っているので、人間に対する気遣いでしかない部分の実装は積極的には行っていない。