バージョン1への拡張:動的ロードの使用
とりあえずバージョン0は動きました。しかし、これをさらに改善していきましょう。ファイルの種類をアプリケーションにそのまま直接マッピングするのではなく、ファイルの種類を実行ハンドラにマップします。つまり、どのアプリケーションをどんな引数で実行するかを追加のコマンドライン引数に基づいて決定するようなコードを定義します。
例えば、Web開発を行っていて、HTMLファイルを作成した場合、ほとんどの場合はそのファイルをブラウザで表示します。そのため、プログラムがそのように動作すれば問題ありません。しかし、ときには、特定のブラウザで表示したいこともあります。今の状態では、Launcherに許可されているのは1つのアプリケーションとの関連だけです。ここで実現したいのは、例えば次のようにして「myfile.html」をOpera Webブラウザで起動したり、
$ ./launcher myfile.html opera
次のようにしてHTMLの構文チェックを実行したりする機能です。
$ ./launcher myfile.html syntax
そのためには、何らかの仕掛けを追加する必要があります。
起動ロジックの背後にある仕掛け
ファイル拡張子を特定のアプリケーション名にマップしていた場所で、今度はRubyコードとの関連付けを追加します。具体的には、処理するファイルの種類ごとにカスタムコード化されたRubyクラスが必要です。
まず、「launcherx.rb」(xは拡張を表します)という名前の新しいRubyソースファイルを「launcher.rb」と同じディレクトリに作成します。
#!/usr/local/bin/ruby # File launcherx.rb require 'launcher' class Launcher def handler( file ) get_handler(file) || build_handler(file) end def build_handler file handler = Class.new application = select_app(file) eval "def handler.run system( '#{application} #{file}' ) end" handler end def get_handler(file) begin here = File.expand_path( File.dirname(__FILE__ )) ftype = file_type(file) require "#{here}/handlers/#{ftype }" Object.const_get( ftype.capitalize ).new rescue Exception nil end end # Execute the given file using he associate app def run( file, args = nil ) handler(file).run(file, args) end end
まず注目してほしいのは、最初にrequireを呼び出してLauncherの既存の定義をロードしている点です。しかし、新しいコードでもLauncherという名前のクラスを定義しています。どうなるのでしょうか。Rubyでは、既存のクラス名と同名のクラス定義が検出されると、既存のクラスが新しいクラスで更新されます。最初のバージョンのLauncherで定義されたメソッドは引き続き存在し、追加のコードで定義されたメソッドは新たに追加されます。そして、runの場合と同様に、既存のコードと同じ名前を新しいコード内で使用すると、新しいコードが古いコードを置き換えます。要するに、最初のバージョンに出てきたのと同じコードを重ねて書く必要はありません。追加または変更するだけでかまいません。
これはRubyのコアクラスにも当てはまります。例えば、独自のメソッドを備えたStringクラスを定義することで、デフォルトのStringの動作を拡張したり変更したりできます。文字列の変更(例えば特殊文字の置換)を頻繁に行う場合は、次のように定義することでコードを簡潔にできます。
class String def amp_escape self.gsub( '&', '&' ) end end
これにより、次のようなコードを記述できるようになります。
"This & that".amp_escape
新しいプログラムファイルでは、この新しい動作を処理する必要があります。まずrunメソッドを変更します。この新バージョンでは、シェルコマンドを直接呼び出すのではなく、Rubyコードを呼び出すからです。また、追加の引数を渡すオプションが必要です。従って、このバージョンでは、ファイル名と引数配列(オプション)を使用します。引数配列がオプションなのは、メソッドの引数リストでデフォルト値(nil)を指定しているためです。このように事前に値が割り当てられる引数は、引数リストの最後に指定する必要があります。
最初のバージョンでは、単にファイル拡張子を使ってハッシュからアプリケーション名を取得していたのに対して、このコードでは、handlerメソッドを使って対応するRubyクラスを作成し、指定されたファイル名を処理します。handlerメソッドは短いメソッドです。最初にget_handlerを呼び出して、一致するハンドラクラスが見つかるかどうかを確認します。||メソッドはRubyの論理ORです。get_handlerがfalse(またはnil。Rubyではこれをfalseとして扱います)を返すと、||の右側のコードが呼び出されます。定義されているハンドラクラスがない場合は、コードがこれを作成します。
新バージョンのrunでは、get_handlerが、runメッセージに応答するオブジェクトを返すことに注意してください。このため、build_handlerメソッドは、この動作に基づくクラスを定義する必要があります。これを実現するには、さまざまな方法があります。ここでは、最初にクラスClassの汎用インスタンスを作成し、問題になっている特定のファイル種類の処理方法を理解しているrunメソッドを動的に追加します。
新しいLauncherクラスは、元のアプリケーションマップコードを保持しています。このマッピングは、特殊なRubyコードが失われた場合にファイルを処理するための代替システムとして役割を果たすものであり、新バージョンでも最初のバージョンと同じ処理が行われることを意味します。コードでは、引き続きselect_appを呼び出してデフォルトアプリケーションを取得しています。新しいクラスのメソッドに取り込んでいるのがポイントです。
起動するアプリケーションがわからない場合にこの処理を実行する最も簡単な方法は、文字列を作成する方法です。文字列を作成するコードは各自で記述します。Rubyのevalでこの文字列を評価し、これを現在のプロセスに組み込みます(注意: 任意の文字列でevalを気軽に使用するのは賢明ではありません。サンプルアプリケーションでは効果を発揮し、Rubyの興味深い機能のデモンストレーションに役立ちますが、特にユーザーから入力を処理するコードなど、本格的なアプリケーションでの使用には注意が必要です)。
同様に、build_handlerは、問題となっているある処理の方法、つまりrun要求への応答を理解しているオブジェクト(簡単なものではありますが)を返します。
動的ロード
この仕掛けを追加するには、プログラムを変更して、ファイルの種類を特定のアプリケーションに関連付けるのではなく、Rubyコードに関連付けます。このRubyコードでは、ファイルの種類に応じてどのアプリケーションを起動するかを判断するための起動ロジックを処理します。
runの実装を含んだカスタムRubyクラスを定義するのは非常に楽しい作業です。まず、すべてのクラスは各自が処理するファイル拡張子にちなんだ名前のファイルに格納されるものとします。例えば、HTMLファイルを処理するハンドラクラスは「html.rb」という名前のファイルになります。また、このようなファイルはすべて、「handlers」という名前の相対ディレクトリに格納されます。この2つの規則を組み合わせることで、get_handlerコードは何をどこで検索すればよいかを理解できます。長々とした設定は必要ありません。get_handlerが呼び出されると、次の処理が行われます。- 組み込みの
Fileメソッドを使って、現在のファイルパス位置を求めます(__FILE__はRubyの特別変数で、現在のコードが格納されている実際のファイルを参照します)。 - 事前定義されたハンドラディレクトリを現在のパスに追加します。
- ターゲットファイル名のファイル拡張子を使って、ハンドラクラスコードが格納されているファイルの名前を求めます。
requireに渡します(適切なファイルが存在し、ロード可能であることが前提になります)。すべてがうまくいけば、Rubyはこのファイルをロードおよび解析し、目的のクラスをコードで使用できるようになります。インスタンス化するクラスの名前が事前にわからない場合は、やや動的な起動を再び実行する必要があります。この場合も
evalを使用できますが、Rubyの定数リストを使用して(クラスはRuby定数であることを思い出してください)、newを呼び出すこともできます。うまくいけば、Object.const_getは目的のクラスを返し、newがインスタンスを返します。何か問題がある場合(ロードすべきファイルがない場合や、ファイルの形式が不正な場合)は、例外が発生します。今回のコードでは、
rescueを使って事態に対処します。rescueでは、固有の例外に対して限定的なエラー処理を実行することも可能ですが、この例の目的上、すべての例外をトラップし、単にnilを返すだけにします。get_handlerが、返す値を明示的に指定していないことに気付いたかもしれません。実際のところ、ここで定義したメソッドはいずれも返す値を明示的に指定していません。Rubyでは、最後に実行された式の値がメソッドの戻り値となります(いくつかの例外はあります)。get_handlerの最上位の式はbegin/rescue/endであり、この式の値は、begin/rescueセクション内の最後の式の値か、またはrescue/endで作成される値です。Rubyにもreturnは定義されており、これによって指定した値を返してメソッドを終了させることができますが、ほとんどの場合、メソッドフロー制御だけで明白な戻り値を十分に定義できます。この操作を行う前に、1つ小さな変更を加えます。すべてのコードを1箇所にまとめておくのは便利には違いありませんが、小規模なアプリケーションを除いては実際的ではありません。一般的なクラスコードと、ユーザーとやり取りする部分のコードとを分けて、コードを編成し直します。このためには、ファイル「go.rb」を作成し、実際のLauncherコード以外の部分(つまり、追加したばかりの最後のコード部分)をすべて移動します。
#!/usr/local/bin/ruby require 'launcher' # Script to invoke launcher using command-line args def help print " You must pass in the path to the file to launch. Usage: #{__FILE__} target_file " end unless ARGV.size > 0 help exit else app_map = { 'html' => 'firefox', 'txt' => 'gvim', 'jpg' => 'gimp' } l = Launcher.new( app_map ) target = ARGV.join( ' ' ) l.run( target ) end
先頭近くに新たなコード行があることに注意してください。
require 'launcher'
この行は、現在のスクリプトでLauncherを使用するために必要です。requireメソッドは、指定された文字列に一致するファイルを検索します。ファイル拡張子は省略されていますが、Rubyは最初に.rbファイルを検索し、Rubyファイルが見つからなかった場合は、コンパイル済みのライブラリ(.soなど)も検索します。Rubyは、事前に定義されたロードパスを検索します。これには現在のディレクトリも含まれます。「launcher.rb」と「go.rb」が同じ場所にあれば問題ありませんが、ファイルを移動する場合は、Rubyがファイルを見つけられるように、明示的な指定を行う必要があります。
