To query OS distribution with Common Lisp especially UIOP.

Introduction

このような記事を読み、脊髄反射でこのように返信してしまったのだけど、よく読んでみたらUIOP:RUN-PROGRAM要らなくね?ってなったので、自分ならこう書くかなってのをメモ程度に書きとどめておくと同時にUIOPの各種機能を紹介していこうと思う。

対象読者。

Useful UIOP functions.

僕が書くならGET-DISTは以下のようになる。

(defun get-dist ()
  (loop :for file :in (uiop:directory-files "/etc/" "*-release")
        :do (loop :for line :in (uiop:read-file-lines file)
                  :if (uiop:string-prefix-p "ID=" line)
                  :do (return-from get-dist (subseq line 3)))))

イカれたメンバー達を紹介しよう!

UIOP:DIRECTORY-FILES

第一引数で指定されたディレクトリにあるファイルpathnameをリストでくくって返すぜ! オプショナルな第二引数にパターンを渡せばそのパターンにマッチするファイルのみを返してくれるぜ! ここで言う「ファイル」はいわゆるファイルでディレクトリはファイルに含まれないぜ!

* (uiop:directory-files "/etc/" "*-release")

(#P"/etc/lsb-release" #P"/etc/os-release")

UIOP:READ-FILE-LINES

pathnameを受け取ってその各行をリストにくくって返すぜ!

* (uiop:read-file-lines (car *))

("DISTRIB_ID=Ubuntu" "DISTRIB_RELEASE=18.04" "DISTRIB_CODENAME=bionic"
 "DISTRIB_DESCRIPTION=\"Ubuntu 18.04.4 LTS\"")

このファミリーとして、ファイルの最初の行だけを返すUIOP:READ-FILE-LINE、ファイル内の各S式をリストにくくって返すUIOP:READ-FILE-FORMS、最初のS式だけを返すUIOP:READ-FILE-FORM、ファイルの内容を文字列として返すUIOP:READ-FILE-STRINGなどがあるぜ!

なかでもUIOP:READ-FILE-STRINGはプロジェクトのREADMEの中身をASDFのLONG-DESCRIPTIONとして読み込むのに使われたりしているから要チェックだ!

UIOP:STRING-PREFIX-P

第二引数に渡した文字列指定子が第一引数で指定したプリフィックスで始まっているかテストするぜ!

* (uiop:string-prefix-p "ID=" (car *))

NIL

元記事では(CL:SEARCH "ID=" i)という形でテストしてあったけど、CL:SEARCHは文字列に含まれるか否かをテストするものだから、元記事の文脈には沿わないバグとなっているぜ! 現に僕の環境で元記事のGET-DISTは以下のような返り値となるぜ!

* (get-dist)

"TRIB_ID=Ubuntu"

これは期待とは異なる振る舞いのはずだ。 僕の環境では“ID=”より先に“DISTRIB_ID=”が先に現れ、これは(CL:SEARCH "ID=" ...)を満足させるので結果このような振る舞いになってしまっているんだ。

せめてCL:SUBSEQに渡す3をハードコーディングせず以下のようにしていればよかったのだけれど。

(defun get-dist ()
  (let ((os-data (split (string #\Newline)
                        (system "cat /etc/*-release"))))
    (loop :for i :in os-data
          :for position := (search "ID=" i)
          :if position
          :do (return (subseq i (+ position 3))))))

とはいえ上記のコードもけしてパーフェクトとは言い難く、というのも、例えば僕の環境には他に“VERSION_ID="18.04"”なんて行もあるからだ。 まがり間違ってこの行が先に現れた場合、返り値は“18.04”となる。 GET-DISTという関数の返り値としてこれは不適切だろう。

でもUIOP:STRING-PREFIX-Pを使えばこんなバグとはおさらばだ!

ちな、ファミリーとしてUIOP:STRING-SUFFIX-Pもあるゾ!

UIOP:SPLIT-STRING

元記事ではSPLIT関数が作られてるけど、UIOPはすでに同じものをもっているぜ!

使い方はだいたい以下の通りだ!

* (uiop:split-string string :separator #.(string #\newline))

TRIVIAL-FEATURES

CL:*FEATURES*に入っている値は処理系依存で、これは割とやっかいな問題だ。 MACであるかどうかをある処理系は:DARWINで表し、ある処理系は:MACOSで表し、またある処理系は:MACOSXで表したりしている。 それは困るということで、ある程度CL:*FEATURES*の中身をポータブルにしようという試みがある。 それがTRIVIAL-FEATURESだ!

ちな、TRIVIAL-FEATURESは世にも珍しいPACKAGEを作らないライブラリだ!

Conclusion.

見てきたようにUIOPは多くの便利関数を提供してくれているぜ! 他のライブラリと違ってUIOPASDFが提供しているものであり、ASDFは多くの処理系にバンドルされているものであり、すなわちUIOPはインストールしなくても使える(場合が多い)ぜ!

一つ一つの機能は小さく(比較的)把握しやすいので、一通り入門記事は読み終えたけど次は何をしようかな?と迷っているような初級CLerが読み始めるにはおすすめのライブラリだ!

機能自体は小さいのに処理系可搬性のためにクソデカコードになってしまっている関数もあり、その労力には涙と感謝を禁じ得ないぜ!

最後に注意点を。

UIOPのコードは読みやすさやメンテナンスのしやすさが重要視されているようで(推測)、効率を追求する場合は他のライブラリを使ったほうがいい場合がある。

例えばUIOP:WHILE-COLLECTINGはいわゆるPUSH/REVERSENREVESRSEですらない!)に変換されるマクロだ。 効率を求めるならTCONCで実装されているCL-UTILITIES:WITH-COLLECTORSの方がいいだろう。

また、UIOPはあくまでASDFのためのものだ。 幾つかのAPIはASDFから使いやすいやすいように実装されている。

例えば上記UIOP:SPLIT-STRINGはキーワード引数MAXを受け付ける。 その振る舞いは何も知らなければ若干奇妙だ。

* (uiop:split-string "ototo" :separator "t" :max 2)

("oto" "o")

先頭からではなく後ろから切り分ける振る舞いとなっている。 これはファイル名“hoge.fuga.piyo”をファイル名と拡張子とに切り分けるのに使われたり、package-infered-systemのsystem名“hoge/fuga/piyo”からコンポネント名(ここでは“piyo”)を切り分けたりするのに使われるためのものだ。

こういった恣意的なAPIを快く思わない場合も別なライブラリが魅力的に見えることとなろう。

それでもUIOPはめちゃくちゃ便利だ。 ENSURE-LISTIF-LETのためだけに巨大なALEXANDRIAに依存したくないなんて場合もUIOPが使えるぜ!(どちらもUIOPにあるのさ!)

さぁ君もUIOPをマスターして一歩上のCler(謎)になろう!