リーダブルコード

うろ覚えですが、学生の頃、プログラミングの勉強をしていて、どこかのサイトに 1000 行以上のコードを書いてみるとその言語のプログラミングの雰囲気が分かるといった内容が書いてあったような気がします。本書は特定の言語に依存した内容ではありませんが、少しは言語依存の内容もあります。

言語仕様を学び、コーディングに慣れてきたら、その次に気になるのが設計だったり、抽象化だったり、人間にとって自然な概念や分かりやすさを表現したくなってきます。本書はそういった人間のための「リーダブルコード」を書くためのコツやその具体例が簡潔にまとめられています。リーダブルコードを書こうと意識するのもまた違ったプログラミングの醍醐味に触れる機会となるでしょう。

個人的におもしろかったところ、気付きがあったところを抜粋して紹介します。

名前重要、超重要!

面倒だからと言って汎用的な名前に逃げない

tmp・retval・foo のような名前をつけるのは、「名前のことなんて考えていません」と言っているようなものだ。このような「空虚な名前」をつけるのではなく、エンティティの値や目的を表した名前を選ぼう。

がーん。

出鼻からくじかれた気分になります。普段から気を付けていることですが、たまに使ってるときもあったりします。私は名前を付けるのが下手で、リファクタリングで名前を変えたりもちょくちょくします。まさに名前付けに王道なしって気がしました。

範囲指定の名前の付け方

これは英語ネイティブではない日本人だと分からない感覚です。素直にそのまま従うと良さそうです。本書では、実際に境界値を含む含まないを図解で解説しているのでさらに分かりやすいです。

  • 限界値を含めるときは min と max を使う
  • 範囲を指定するときは first と last を使う
  • 含有/排他的範囲には begin と end を使う
式を分割する変数の意義
  • 説明変数: 式の意味を説明する変数
  • 要約変数: 式を説明する必要がない場合でも概念を簡潔に表現する変数

この概念が私にとっては目から鱗でした。

式や関数呼び出しを、そのまま関数の引数に渡したり、戻り値として返すコードをよく書いたりしていました。ローカル変数に代入すべきかどうかを迷うときもよくあったのですが、説明したり要約したりする必要があるかどうかで1つの判断基準ができました。

スコープの意識

スコープが小さければ、短い名前でも良い

以前「名前に迷ったら長い名前を書けば良い」というプラクティスを実践していて、何でも長い名前を付けていたときがありました。

ほんの数行から十数行程度の小さなユーティリティ関数なら、関数名で分かりやすさを表現すれば、内部のローカル変数は短い名前でも良いように私も同意します。Python なら PEP8 を守って 80 行文字以内に収めようとするので、以下のようなコードは状況により名前を変えたりもします。

match = re.search(REGULAR_EXPRESSION, text)
if match:
    ...

m = re.search(REGULAR_EXPRESSION, description_to_be_explained_in_detail)
if m:
    ...
Javascript の「プライベート」変数のサンプル

スコープは、言語によって違うので、その言語の仕様やイディオムを理解しておく必要があります。慣れた言語のスコープが頭の中に残ってしまうので、Javascriptクロージャーを用いたサンプルコードの奇妙さからスコープの配慮の重要性が分かりやすかったです。こういったイディオムは、より深く言語を理解する上での取っ掛かりになりますね。

var submit_form = (function () {
    var submitted = false;
    return function (form_name) {
        if (submitted) {
            return ; // 二重投稿禁止
        }
        ...
        submitted = true;
    };
}());

コメントの書き方

私が唯一意識しているコメントの書き方は「意図を表す内容を書く」ということだけでした。本書では、それ以外にも良さそうな実例が紹介されています。

コメントを書かないモチベーション

優れたコード > ひどいコード + 優れたコメント

おもしろい表現です。ひどいコードを説明するのに労力をかけるなら、ちゃんとしたコードを書きなさいという教訓ですね。

ひどいコードにはマーキングする

さらにコードの修正を促すコメントを付けることも恥ずかしがってはいけないとあります。確かに他人のコードを修正するのはやや躊躇してしまうこともあるので、堂々とコードが汚いことを認めるのが良さそうです。よく使う記法です。XXX って危険という意味だったのですね、私は知りませんでした。

記法 典型的な意味
TODO あとで手をつける
FIXME 既知の不具合があるコード
HACK あまりキレイじゃない解決策
XXX 危険!大きな問題がある
分からなかったらググれ

以下のコメントもおもしろいです。

// ベクタのメモリを解放する (「STL swap 技法」で検索してみよう)
vector().swap(data);

まとめ

名前を分かりやすくすること、コードをキレイに保つこと。

リーダブルコードの最初の取っ掛かりです。たくさんコードを書いて、失敗して、工夫して、もっと良い方法を学ぶ。本書は、シンプルなテクニックを紹介していますが、多くのプログラマーの試行錯誤の上で生き残った実践的なテクニックだと思います。知っていることであっても、普段から注意してコーディングしているか、実践しているかを考えるきっかけにも良いと思います。

Subversion で svn:externals (外部参照) を設定する

Subversion は、シンボリックリンクをサポートしています。例えば、あるユーティリティモジュールがあり、それを複数のプログラムから使いたい場合、シンボリックリンクを使うと便利です。

シンボリックリンクの作成

試してみましょう。テスト用のリポジトリを作成して Subversion サーバーを起動します。

$ svnadmin create svn-repos
$ vi svn-repos/conf/svnserve.conf 
[general]
anon-access = write  # 匿名書き込みアクセスを許可する
$ svnserve --daemon --foreground --root svn-repos/

サーバーが起動しました。クライアントからチェックアウトします。

$ svn co svn://localhost/ ./myrepo
リビジョン 0 をチェックアウトしました。

$ # 初期ディレクトリとシンボリックリンクを作成
$ cd myrepo
$ mkdir utils tool1 tool2
$ vi utils/myfunc.py
print "do something"
$ (cd tool1; ln -s ../utils/myfunc.py)
$ (cd tool2; ln -s ../utils/myfunc.py)
$ ls -l tool1 tool2
tool1:
lrwxrwxrwx 1 t2y t2y 18  98 12:00 myfunc.py -> ../utils/myfunc.py
tool2:
lrwxrwxrwx 1 t2y t2y 18  98 12:00 myfunc.py -> ../utils/myfunc.py

$ svn add utils tool1 tool2
A         utils
A         utils/myfunc.py
A         tool1
A         tool1/myfunc.py
A         tool2
A         tool2/myfunc.py

$ svn commit -m "added"
追加しています              tool1
追加しています              tool1/myfunc.py
追加しています              tool2
追加しています              tool2/myfunc.py
追加しています              utils
追加しています              utils/myfunc.py
ファイルのデータを送信しています ...
Committed revision 1.

# リポジトリの属性を調べる
$ svn proplist -R .
'tool1/myfunc.py' の属性:
  svn:special
'tool2/myfunc.py' の属性:
  svn:special

シンボリックリンクを作成すると、svn:special という属性が設定されます。

別のクライアントからチェックアウトしてみましょう。

$ svn co svn://172.16.55.131/ ./myrepo2
A    myrepo2/tool2
A    myrepo2/tool2/myfunc.py
A    myrepo2/utils
A    myrepo2/utils/myfunc.py
A    myrepo2/tool1
A    myrepo2/tool1/myfunc.py
リビジョン 1 をチェックアウトしました。

$ cd myrepo2/
$ ls -l tool1 tool2
tool1:
lrwxrwxrwx 1 t2y t2y 18  98 12:09 myfunc.py -> ../utils/myfunc.py
tool2:
lrwxrwxrwx 1 t2y t2y 18  98 12:09 myfunc.py -> ../utils/myfunc.py
$ cat tool1/myfunc.py 
print "do something"

ちゃんとシンボリックリンクが有効になっていますね。

svn:externals (外部参照) 属性の設定

Unix 系 OS であれば、これで大丈夫ですが、Windows ユーザーも使いたいときに課題があります。

But that doesn't in any way limit the usability of working copies on systems such as Windows that do not support symlinks. On such systems, Subversion simply creates a regular text file whose contents are the path to which the original symlink pointed. While that file can't be used as a symlink on a Windows system, it also won't prevent Windows users from performing their other Subversion-related activities.

シンボリックリンクのバージョン管理

Windowsシンボリックリンクをサポートしていないため、リンク先を指すテキストファイルを作成して、ユーザーの Subversion 操作においては邪魔しないとあります。

実際に Windows 環境でチェックアウトしてみましょう。

チェックアウト結果をみると、ファイルをコピーして作成してくれているように見えますが、ファイルの中身は "link ../utils/myfunc.py" です。ドキュメント等であれば良いですが、ユーティリティモジュールなどでは使えないですね。

そんなとき、Windows 環境でもシンボリックリンクのように扱える仕組みが svn:externals 属性です。

Fortunately, Subversion provides support for externals definitions. An externals definition is a mapping of a local directory to the URL―and ideally a particular revision―of a versioned directory. In Subversion, you declare externals definitions in groups using the svn:externals property. You can create or modify this property using svn propset or svn propedit (see the section called “Manipulating Properties”). It can be set on any versioned directory, and its value describes both the external repository location and the client-side directory to which that location should be checked out.

svn:externals 属性のドキュメント

svn:externals のドキュメントによると、リポジトリ内に外部リポジトリの URL を指定したり、そのリポジトリ特定バージョンを取得するように設定できるとあります。もともとは外部のリポジトリを取り組むための機能のようですが、同じリポジトリ内においても利用できます。そして、svn propsetsvn propedit で設定できると記述されていますが、残念ながら実際の設定コマンドのサンプルがここでは記述されていません。そのため、どうコマンドを実行して良いか全く分かりません (> <)

svn:externals の設定は直感的に分かりにくいので備忘録としてまとめておきます。また、Suversion 1.4 でディレクトリ単位、1.6 からファイル単位の外部参照ができるようになったので svn クライアントのバージョンにも注意してください。

$ mkdir tool3
$ svn add tool3
A         tool3

$ cd tool3
$ svn propset svn:externals "../utils/myfunc.py linked_myfunc.py" .
属性 'svn:externals''.' に設定しました

$ svn update
Updating '.':
Fetching external item into 'linked_myfunc.py':
A    linked_myfunc.py
外部項目をリビジョン 5 に更新しました。
リビジョン 5 です。

$ cat linked_myfunc.py 
print "do something"

$ svn commit -m "set svn:externals"
送信しています              .
Committed revision 6.

svn propset svn:externals の引数が覚えられないです (> <)
1つのやり方としては、以下のように相対パスで指定します。

$ svn propset svn:externals "カレントディレクトリからの相対パス  取得後のファイル名" 属性設定したいディレクトリ

svn:externals 属性は、ディレクトリに対して設定されます。

$ svn proplist tool3/linked_myfunc.py 
$ svn proplist tool3
'tool3' の属性:
  svn:externals

別のやり方として、リポジトリのルートパスからの相対パスを指定する方法もあります。今度は同じファイル名 myfunc.py で取得しています。

$ svn propset svn:externals "^/utils/myfunc.py myfunc.py" tool4
属性 'svn:externals''tool4' に設定しました
$ svn update tool4/
Updating 'tool4':
Fetching external item into 'tool4/myfunc.py':
リビジョン 7 の外部項目です。

リビジョン 7 です。
$ svn commit -m "set svn:externals"
送信しています              tool4
Committed revision 8.

Windows 環境でもチェックアウトしてみます。

Unix 系 OS 環境と同様にファイル本体がチェックアウトできました!

外部リポジトリ特定ディレクトリを取得したいときは URL を指定します。こういうときは取得した後のディレクトリ名を変更できると便利です。

$ mkdir svn
$ svn add svn/
A         svn
$ svn propset svn:externals "http://svn.apache.org/repos/asf/subversion/trunk/tools/examples/ tool-example" svn/
属性 'svn:externals''svn' に設定しました

$ svn update
Updating '.':

Fetching external item into 'tool3/linked_myfunc.py':
リビジョン 10 の外部項目です。

Fetching external item into 'tool4/myfunc.py':
A    tool4/myfunc.py
外部項目をリビジョン 10 に更新しました。

Fetching external item into 'svn/tool-example':
A    svn/tool-example/svnserve-sgid.c
A    svn/tool-example/svnput.c
A    svn/tool-example/dumpprops.py
A    svn/tool-example/svnlog2html.rb
A    svn/tool-example/check-modified.py
A    svn/tool-example/svnlook.py
A    svn/tool-example/svnshell.py
A    svn/tool-example/geturl.py
A    svn/tool-example/getlocks_test.c
A    svn/tool-example/info.rb
A    svn/tool-example/svnlook.rb
A    svn/tool-example/get-location-segments.py
A    svn/tool-example/headrev.c
A    svn/tool-example/putfile.py
A    svn/tool-example/revplist.py
A    svn/tool-example/svnshell.rb
A    svn/tool-example/minimal_client.c
A    svn/tool-example/getfile.py
A    svn/tool-example/blame.py
A    svn/tool-example/SvnCLBrowse
A    svn/tool-example/testwrite.c
外部項目をリビジョン 1382235 に更新しました。

リビジョン 10 です。

svn update を実行する度に fetch 処理が実行されます。仕方ないですが、外部リポジトリを参照すると実行に時間がかかるので、更新をチェックしたくないときは "--ignore-externals" オプションを指定します。

$ svn update
Updating '.':
Fetching external item into 'tool3/linked_myfunc.py':
リビジョン 11 の外部項目です。
Fetching external item into 'tool4/myfunc.py':
リビジョン 11 の外部項目です。
Fetching external item into 'svn/tool-example':
リビジョン 1382235 の外部項目です。

リビジョン 11 です。

$ svn update --ignore-externals
Updating '.':
リビジョン 11 です。

あまり頻繁に使う機能ではありませんが、使いたいときに実際の設定コマンドのサンプルを見つけられなくて苦労しました。

ちなみに Subversion もいまや Apache プロジェクトのトップレベルプロジェクトなんですね。ホームページも刷新されていて驚きました。

2012/9/10 追記

同じディレクトリに複数ファイルの svn:externals 属性を設定するには、svn propedit を使わないと設定できないようです。

$ svn propedit svn:externals tool5 --editor-cmd=vim  # vim エディタで複数ファイルを設定
^/utils/myfunc.py myfunc.py
^/utils/myfunc2.py myfunc2.py

属性 'svn:externals' の新しい値を 'tool5' に設定しました

アリエル・ネットワークに入社しました

9月からアリエル・ネットワーク (以下アリエル) に入社しました。

以前、アルバイトで3ヶ月半ほどお仕事していました。以下はそのときの手記です。

さて、最近こんなツイートを見ました。


どうやら私がアルバイトをしていた頃から少し経って、最近たるんでいるようです。

これはまた締め直さなければ、、、

と思っていたら入社の初日から先制攻撃を受けてしまいました。ほんとに怖い会社です、、、

寝坊したんじゃないです!初日に入館できる手続きをするから9時に来てと言われてたんです (> <)、朝組イメージを払拭するために、朝何時に来ようが本当はどうでも良いです。生産性を上げるには、人によっては (私もそうですが) 朝型で取り組む方が効率的だったりもしますが、それよりも (自分にあった) 規則正しい生活を続ける方が安定したパフォーマンスを発揮できて、結果的に生産性が上がります。

閑話休題。私はこれまでフリーランスの経験もあわせると7社で働いてみました。それぞれの会社の特徴や文化の一長一短があり、どの会社が優れているとは一概に言えませんが、どの会社が自分の考え方やキャリアの方向性と最も一致しているかということは言えます。私はやはりプログラマーとしてのキャリアをもっと突き詰めたいし、チームメンバーよりもコードを書いて、チケットをクローズしまくるマネージャーが普通にいるアリエルのような職場が魅力的でした。

エンタープライズ分野のパッケージ販売という、ソフトウェア開発の王道とも言える業務に対して、これまでの経験を活かして何ができるか。結果的に過去のキャリアでは、最長3年と、中長期的な展望をもって開発に取り組むことができなかったことが私の中での課題であり、未知の領域でした。それは結果論であって本意ではなかったのですが、次に働くところは中長期的な展望をもって開発に取り組める企業にしようと決めていました。

ひとつ自己努力だけで容易に学べないものがあると思っています。良い開発プロセスを経験できるかどうかです。良い開発プロセスとはウォーターフォールできっちりやるという意味ではありません。社内の開発者がどれだけ合理的にプロセスを改善しようとしているかが重要です。会社にそういう文化があるかどうかです。開発プロセスは日々の行動の積み重ねなので、勉強会で話を聞いたり本を読むだけでは限界があります(わかった気にはなれますが)。

2013年度、アリエル新卒募集が始まりました | ありえるえりあ

実際、私が働いた7社の中で最も優れた開発プロセスをもっていたのもアリエルでした。開発プロセスの改善というのは、技術力やその知識以上に、会社の財務基盤や業務としての開発文化、そして上司の理解が必要になります。いわゆる wikipedia:技術的負債 を抱え込まないために必要なことは誰もが分かってますが、中長期的な開発計画がないと、そもそもそんな投資はできません。また、短期的な利益とのトレードオフになるため、そのバランスを取ることが難しいです。

これまで、私は独りで開発してきたことが多かったので、チームでの開発プロセス改善やその在り方を模索していけることも今後の楽しみです。

最後に1つ。

エキスパート職に関する仙石さんの見解は単純かつ明解でした。エキスパート職の職能は、まわりに良い影響を与えられるか、だと言いきりました。

マネージャになりたくないプログラマのキャリアパス | ありえるえりあ

これは私の中の「すごい人」理論にも通じます。

どこの会社にも優秀な方はたくさんいますが、本当にすごい人はあまりいません。職場にすごい人なんか滅多にいません。私が思うすごい人は、会社の枠や多少の損得なんかの概念を超えて、さらなる高みへ行ってしまうような人たちです。別の言い方をすると、自分でリスクを取って行動し、結果を出しても、さらに先を目指すような人たちです。

アリエルにはそういった人たちが何人もいますし、私もそんな人たちを目標に切磋琢磨していきたいと考えています。

結局のところ、何かおもしろそうだ、という直感は生きていく上で大事なものなんだと思います。

Pyramid ベースの軽量 CMS フレームワーク Kotti の紹介

ここ最近 Kotti アドオンの開発を行っていました。

Kotti は、Pyramid の上位でアプリ開発を支援するフレームワークです。いまのところ、軽量 CMS として開発が進められています。

Kotti を紹介するウェブサイトも構築されています。まだ概要程度の情報量ですが、日本語化もされているのでざっとご覧ください。

個人的にちょっと使ってみた感想としては、Kotti の CMS 機能もその完成度もまだまだベータ開発中といったところです。実用的なレベルまではもう少し時間がかかりそうですが、開発が活発に行われているので将来が楽しみです。私の場合、Pyramid を使ったアプリを開発してみたかったことと、普通の Web アプリにユーザー管理やコンテンツ管理の機能を簡単に追加したかったという動機で Kotti を利用してみました。

開発していると Pyramid の知識も必要になりますが、Node/Content という Kotti がもつリソースモデル、pyramid_tm によるリクエスト単位のトランザクション管理など、Pyramid 流の開発のプラクティスとして、フレームワークでどんなことをやっているのかを学ぶことが多かったです。また、後述する Kotti アドオンの開発も容易でした。

私が開発したものは Amazon Elastic MapReduce (Amazon EMR) を操作する Kotti アドオンです。

現時点の EMR の GUI と比較して、

  • EMR 設定をリソースとして保存・再利用
  • サービス単位にジョブフローをまとめる
  • Keep Alive を有効にしたインスタンスへ追加ステップを実行

といった特徴があります。但し、まだまだ開発ステータスがアルファレベルであり、機能や UI は最低限のものですし、運用面の考慮も全くされていません。実験中といった感じです。

Kotti アドオンを開発する上で慣れが必要なのは、複数プロダクトの仕組みやドキュメントを調べないといけないことです。主なライブラリのドキュメントは以下です。

例えば、INI ファイル の設定においても、

kotti.site_title = Kotti with mapreduce
kotti.secret = qwerty

などの kotti で始まる設定は Kotti 本体の設定ですが、

pyramid.default_locale_name = en
pyramid.includes =
    pyramid_debugtoolbar

などの pyramid で始まる設定は Pyramid のドキュメントを調べないといけません。
また、

mail.default_sender = yourname@yourhost

は、pyramid_mailer の設定であり、これは Message オブジェクトの sender 属性 (envelope-from) に利用されます。

開発していて気になることや不具合があったときに Kotti 本体だけ調べても分からないこともよくあります。そんなとき、この機能を実際に提供しているのはどのプロダクトか、それはどういった仕組みかを理解するために、それぞれのプロダクトについての理解を深め、全体像を把握しないと解決策が分からないこともあります。

最初の取っ掛かりとしては、既に PyPI 上にも10個以上の Kotti アドオンが公開されているので、実際に動くサンプルとしてコードを眺めるのも参考になります。

まだまだ荒削りなフレームワークですが、その分、自分で開発したり、改善したりする余地がたくさんあります。Pyramid アプリ入門として Kotti アドオンを作るのでも良いように思います。もう少し経って Kotti の開発が落ち着いたら、ドキュメント翻訳にも挑戦してみたいところです。

ローカル環境で複数の Sphinx ドキュメントを参照する

最近の Python パッケージは、Sphinx でドキュメントが書かれていることが多いです。

Sphinx でドキュメントが書かれているなら、そのパッケージのソースをクローンしてきて、ローカルでドキュメントをビルドするのも簡単です。私の場合、そのときの開発に用いるフレームワーク・ライブラリ・ツール等は、リポジトリからソースをクローンしてきて、最新版ソースを開発環境へインストールして、自分のアプリケーションを開発する上でデバッグしやすい環境を構築しています。また、そのドキュメントもローカルでビルドしています。

例えば、KottiPyramidフレームワークのドキュメントをローカルでビルドしてみます。

$ cd /path/to/Kotti/
$ python setup.py build_sphinx

$ cd /path/to/pyramid/docs/
$ make html

このとき、前者は Kotti/build/sphinx/html/ に、後者は pyramid/docs/_build/html/ に HTML ドキュメントが生成されます。

ローカルで HTML ドキュメントを参照する際に Sphinx の検索機能を使いたいので HTTP サーバーを介して参照できると便利です。とはいえ、そのためにわざわざ HTTP サーバーを設定・構築するのもちょっと面倒です。前置きが長くなりました、そんな用途に「ちょっとだけ」便利な HTTP サーバーを作ってみました。

その名の表す通り、標準ライブラリの SimpleHTTPServer をちょっとだけカスタマイズしたものです。

先ず SimpleHTTPServer を使って、複数のドキュメントを見ようとすると次のようになります。

$ cd /path/to/Kotti/build/sphinx/html/
$ python -m SimpleHTTPServer 8001
Serving HTTP on 0.0.0.0 port 8001 ...

$ cd /path/to/pyramid/docs/_build/html/
$ python -m SimpleHTTPServer 8002
Serving HTTP on 0.0.0.0 port 8002 ...

SimpleHTTPServer は、カレントディレクトリ配下のみを公開ディレクトリとして扱います。2-3のドキュメントならこれでも良いですが、10個のドキュメントをこの方法で見ようとすると、コマンドラインでサーバー起動するのが面倒だったり、どのドキュメントがどのポート番号か分からなくなったりします。

この煩わしさを解決するのが LittleHTTPServer です。次のようにコマンドラインオプションで指定します。

$ littlehttpserver -p 8003 -d /path/to/pyramid/docs/_build/html/ -d /path/to/Kotti/build/sphinx/html/
INFO: Serving HTTP/1.0 on 0.0.0.0, port: 8003 ...

http://localhost:8003 へアクセスすると、次のように表示されます。

これならローカルで見たいドキュメントが複数になっても煩わしくないですね。

2012/8/22 追記

コメントをもらったので -m オプションで実行できるように修正して再リリースしました。

$ python -m littlehttpserver -i path/to/top -d path/to/pkg1

コマンドラインオプション付きの which コマンドを実装してみた

というツイートを見かけました。

以前、勉強がてらに

を作ったことがありましたが、もうあれから3年も経つんだなとコードを見返しながら感慨に浸っていました。

そんなとき、ふと which コマンドを実行したところ、コマンドラインオプションがあることに気付きました!

$ which
usage: which [-as] program ...

$ man which
WHICH(1)                  BSD General Commands Manual                 WHICH(1)

NAME
     which -- locate a program file in the user's path

SYNOPSIS
     which [-as] program ...

DESCRIPTION
     The which utility takes a list of command names and searches the path for each 
     executable file that would be run had these commands actually been invoked.

     The following options are available:

     -a      List all instances of executables found (instead of just the first one of each).

     -s      No output, just return 0 if any of the executables are found, or 1 if none are found.

     Some shells may provide a builtin which command which is similar or identical
     to this utility.  Consult the builtin(1) manual page.

これは、、、

。。。

過去の私の実装に足りないものを発見してしまいました、もう再実装するしかない! (> <)

ということで、コマンドラインオプションを追加するだけなのですが、結構難しくて悩んでしまいました。

# -*- coding: utf-8 -*-
import glob
import os
import sys
import argparse
from itertools import chain
from os.path import join as pathjoin
from operator import itemgetter

def search(cmd, paths, is_all=False):
    for path in paths:
        for match in glob.glob(pathjoin(path, cmd)):
            if os.access(match, os.X_OK):
                yield match
                if not is_all:
                    raise StopIteration

def parse_argument(args=None):
    parser = argparse.ArgumentParser()
    parser.set_defaults(is_all=False, is_silent=False, commands=[])
    parser.add_argument("-a", dest="is_all", action="store_true",
        help="List all instances of executables found "
             "(instead of just the first one of each).")
    parser.add_argument("-s", dest="is_silent", action="store_true",
        help="No output, just return 0 if any of the executables are found, "
             "or 1 if none are found.")
    parser.add_argument("commands", nargs="*")
    args = parser.parse_args(args or sys.argv[1:])
    return args

def main(cmd_args=None):
    args = parse_argument(cmd_args)
    env_paths = os.environ['PATH'].split(':')
    result = []
    for cmd in args.commands:
        founds = list(search(cmd, env_paths, args.is_all))
        result.append((0, founds) if founds else (1, [cmd]))

    status_code = max(map(itemgetter(0), result))
    if not args.is_silent:
        cmd_paths = [paths for ret_val, paths in result if ret_val == 0]
        for cmd_path in chain.from_iterable(cmd_paths):
            print cmd_path
    return status_code

if __name__ == '__main__':
    sys.exit(main())

実行結果はこんな感じです。

$ python which.py -a ls vi unknown
/bin/ls
/opt/local/bin/vi
/usr/bin/vi

$ python which.py -s ls vi unknown; echo $?
1

ついでに本当に which コマンドと動作が一致しているか、簡単なテストを書いてみました。

# -*- coding: utf-8 -*-
import sys
from subprocess import Popen, PIPE

import pytest
import which

FILESYSTEM_ENCODING = sys.getfilesystemencoding()

def get_system_which_result(args):
    cmds = ["which"]
    cmds.extend(args)
    p = Popen(" ".join(cmds), stdout=PIPE, stderr=PIPE, shell=True)
    out, err = p.communicate()
    return p.returncode, out, err

@pytest.mark.parametrize("args", [
    ["ls"],
    ["cd", "pwd"],
    ["non_existence"],
    ["-a", "vi"],
    ["-a", "ls", "vi"],
    ["-s", "non_existence"],
    ["-a", "-s", "ls", "vi"],
    ["-a", "-s", "ls", "vi", "non_existence"],
])
def test_which_command(args, capsys):
    my_ret = which.main(args)
    my_out, my_err = capsys.readouterr()
    sys_ret, sys_out, sys_err = get_system_which_result(args)
    assert sys_ret == my_ret
    assert sys_out == my_out.encode(FILESYSTEM_ENCODING)
    assert sys_err == my_err.encode(FILESYSTEM_ENCODING)

ソースは以下にあります。

こういったシンプルなツールを作るのはおもしろいですね。

PyCon Taiwan 2012 イベントレポート Day 2

6月9-10日に開催された PyCon Taiwan 2012 に参加してきました。

前回の記事からちょっと間があきましたが、2日目 (10日) のカンファレンスレポートです。私は基調講演について執筆しました。