UNIX について語る: Squirrel という移植可能なシェル・スクリプト言語

複数のプラットフォームに対応したオブジェクト指向シェル・スクリプトを作成する

特定のプラットフォームで特定のシェルを実行するという特異性に縛られたくなかったら、Squirrel Shell を試してください。Squirrel Shell は、UNIX®、Linux®、Mac OS X™、そして Windows® のどのシステムでも同じように有効に機能する高度なオブジェクト指向のスクリプト言語を提供します。つまり、一度スクリプトを作成すれば、そのスクリプトをどのシステムでも実行できるということです。

Martin Streicher, Software Developer, Pixels, Bytes, and Commas

Photo of Martin StreicherMartin Streicher is a freelance Ruby on Rails developer and the former Editor-in-Chief of Linux Magazine. Martin holds a Masters of Science degree in computer science from Purdue University and has programmed UNIX-like systems since 1986. He collects art and toys. You can reach Martin at martin.streicher@gmail.com.


developerWorks 貢献著者レベル

2009年 3月 17日

1799年、フランス軍のエンジニアが驚くべき発見をしました。それは、フォアグラでもカマンベールでもありません。はたまた低温殺菌法でも、サルトルでもありません。その発見とは、エジプト古代象形文字の大半を解読する鍵となったロゼッタ・ストーンです (図 1 を参照)。

図 1. ロゼッタ・ストーン。重さ 1100 ポンドのこの石には、司祭職に対する減税宣言が 3 種類の文字で記されている
ロゼッタ・ストーン。重さ 1100 ポンドのこの石には、司祭職に対する減税宣言が 3 種類の文字で記されている

紀元前 196年に作られたロゼッタ・ストーンには、同じ内容がヒエログリフ (神聖文字)、デモティク (民衆文字)、そして古代ギリシャ文字の 3 通りに翻訳されて記されています。このロゼッタ・ストーンの翻訳を比較することで (つまり、あるフレーズをそれぞれの翻訳のあいだで対応付けることで)、かつては解読不可能だった象形文字の多くの意味が明らかになりました。

つまり、ロゼッタ・ストーンは今で言うと、(重さ 0.5 トンの) BabelFish (訳注: BabelFish はテキストや Web ページをさまざまな言語に機械翻訳する Web アプリケーション) のようなものだと考えられます。紀元前 196年の昔も、1 つのことを表現する方法は 1 つだけではありませんでした。

それから 2000 年後、ソフトウェア開発者は同じような問題に直面しています。それは、同じ 1 つのことを多数のプログラミング言語で何通りにも表現できるということです。コマンドラインを使うにしても、各種のシェルやさまざまなコマンドの組み合わせを含め、同じような選択肢がいくつもあります。

一般に、種類が多いことは良しとされますが、その逆に厄介な問題にもなり得ます。どのソリューションを選択すべきなのか、要件に対応する技術であるのか、時間をかけて作業をする価値があるのか、そして巧妙に作られている言語 (あるいは Perl で使われている機能) でも、今後サポートされなくなるのかどうかを考えなければなりません。さらに手に負えないことには、他の環境に対応させるために、すべてを変換 (再作成) する必要があるかどうかについても検討が必要です。

Fish シェル、Bash シェル、Z シェル、Windows オペレーティング・システムの cmd.exe、あるいはその他のシェル・スクリプト言語が持つ特異性に縛られたくなければ、Squirrel Shell を試してください。Squirrel Shell は、UNIX、Linux、Mac OS X、そして Windows のどのシステムでも同じように有効に機能する高度なオブジェクト指向のスクリプト言語を提供します。つまり、一度スクリプトを作成すれば、そのスクリプトをどのシステムでも実行できるということです。

さらに都合のよいことに、スクリプトを使うために 0.5 トンの石に書かれている内容を理解する必要もありません。

Squirrel の入手

Squirrel Shell は簡単に入手できて、GNU Public License バージョン 3 (GPLv3) の使用条件に従って無料で使用することができます。最新リリースは、2008年10月11日付の 1.2.2 です。Squirrel Shell の作成者兼保守管理者は、Constantin “Dinosaur” Makshin です。

Squirrel Shell のダウンロード・ページ (「参考文献」にリンクを記載) には、32-bit および64-bit Windows 対応のソース・コードとバイナリーが用意されています。UNIX または Linux 系のオペレーティング・システムを使用している場合には、ディストリビューションのリポジトリーで適切なバイナリーを見つけるか、Squirrel Shell をスクラッチからビルドしてください。

スクラッチからのビルドは至って簡単な作業です。まずはソースの tarball をダウンロードして解凍し、ソース・ディレクトリーに移動してから、お馴染みのビルドの呪文を入力してください (リスト 1 を参照)。

リスト 1. ソースからの Squirrel Shell のビルド
$ ./configure --with-pcre=system && make && sudo make install
Checking CPU architecture...   x86
Checking for install...   /usr/bin/install
...
Configuration has been completed successfully.
   Build for x86 CPU architecture
   Installation prefix: /usr/local
   Allow debugging: no
   Build static libraries
   Use system PCRE 6.7 library
   Install MIME information: auto
   Create symbolic link: no
   Compile C code with 'gcc'
   Compile C++ code with 'g++'
   Create static libraries with 'ar rc'
   Create executables and shared libraries with 'g++'
   Install files with 'install'

パッケージに固有の構成オプションの一覧を調べるには、コマンドラインで ./configure --help と入力します。

便宜上、Squirrel Shell にはプログラムで広範に使用されている PCRE (Perl Compatible Regular Expression) ライブラリーのソース・コードがバンドルされています。システムに PCRE がない場合には、バンドルされたこのコードによってビルドが素早く簡単になります。一方、システムにすでに PCRE がある場合、その PCRE を使用するように選択するには、--with-pcre=system オプションを指定してください。特定の PCRE を使用する必要がなければ、--with-pcre=auto を指定して、システム・ライブラリーまたは Squirrel Shell のコピーのうち、いずれか新しい方にリンクするようにします。

ビルドの結果、squirrelsh という適切な名前が付けられた新しいバイナリーが作成されます。このバイナリーが PATH 変数に含まれるディレクトリー (例えば、/usr/local/bin) にインストールされていれば、squirrelsh と入力するとシェルが起動します。コマンド・プロンプトでコマンド printl(getenv("HOME")); と入力すると、ホーム・ディレクトリーのパスが出力されます。

$ squirrelsh
> printl( getenv( "HOME" ) );
/home/strike
> exit();

Squirrel Shell がベースとしているのは、Squirrel プログラミング言語です (詳細については、「参考文献」に記載したリンクにアクセスしてください)。Squirrel は C++ ライクな言語ですが、提供する機能は Python や Ruby などのオブジェクト指向のスクリプト言語と似ています。Squirrel Shell には、Squirrel のすべての機能とデータ型が組み込まれているだけでなく、新しい関数も数多く追加されています。これらの新規関数は、ファイルのコピーや環境変数の読み取りなどといった共通シェル・スクリプト・タスク専用に作成されたものです。

Squirrel Shell の構文は日常的なコマンドラインでの使用には冗長過ぎますが (Squirrel Shell の printl( "~") は、Bash での echo $HOME に相当します)、その威力はスクリプトで発揮されます。Squirrel Shell で一度スクリプトを作成すれば、そのスクリプトはどこででも実行することができます。UNIX で 1 度作成して、さらに Windows 用にもう一度作成する必要はありません。Dinosaur は自らの作品を称して、「Squirrel Shell は主としてスクリプト・インタープリターである」としています。


Squirrel でのスクリプト作成

Squirrel Shell スクリプトの一例を調べてみましょう。リスト 2 に記載する listing2.nut ファイルは、ホーム・ディレクトリーのコンテンツを再帰的にリストアップするスクリプトです。

リスト 2. listing2.nut
#!/usr/bin/env squirrelsh

function reveal( filedir ) { 
  if ( !exist( filedir ) ) {
    return;
  }
    
  if ( filename( filedir ) == ".." || filename( filedir ) == "." ) {
    return;
  }
  		
  if ( filetype( filedir ) == FILE ) {
  	printl( filename( filedir, true ) );
  	return;
  }
  
  printl("directory: " + filename( filedir, true) );
  local names = readdir( filedir );
  
  foreach( index, name in names ) {
    reveal( name );
  }
}

local previous = getcwd();

chdir( "~" );

reveal( getcwd() );

chdir( previous );

exit( 0 );

規約にならって、すべてのシェル・スクリプトの先頭行では、スクリプトを解釈するために起動するプログラムをオペレーティング・システムに対して指示します。通常は、特定のロケーションから特定のシェルまたはインタープリターを起動するために、この行は #! /usr/bin/bash または #! /bin/zsh となっているはずです。

リスト 2 の #!/usr/bin/env squirrelsh は、通常とは少し異なります。この行は特殊なプログラム、env を起動し、このプログラムから、PATH 変数に含まれる squirrelsh の最初のインスタンスを起動します。したがって、特定プログラムのローカル・バージョン (例えば $HOME/bin/squirrelsh にある、独自に変更した squirrelsh のコピー) を優先するには、PATH 変数を変更すれだけばよいだけのことで、シェル・スクリプトの内容を変更する必要はありません。

注: この芸当は、あらゆる種類のインタープリターに効き目があります。例えば、#!/usr/bin/env ruby の場合、PATH 設定で指定された Ruby の優先バージョンが起動されることになります。一般に、配布する予定のシェル・スクリプトを作成する場合には、先頭行でより「移植」しやすい #!/usr/bin/env application の形を使用することをお勧めします。こうすれば、ユーザーがそれぞれの PATH 変数に構成したアプリケーションのバージョンが実行されます。

リスト 2 の残りの部分は、少なくとも手法の点ではお馴染みの内容のはずです。reveal() は以下に示す動作をする再帰的関数です。

  • reveal() に対して無効なパス、あるいはいつもの「ドット (.)」(カレント・ディレクトリー) や「ドット・ドット (..)」(親ディレクトリー) を渡すと、再帰的検索が終了します。
  • または、引数 filedir がファイルの場合には、コードはその名前を出力して戻り、それ以降の再帰的検索を中止します。filename() 関数では引数を 1 つ、または 2 つにすることができます。引数が 1 つの場合、または 2 番目の引数が false の場合には、ファイル名の拡張子が省略されます。2 番目の引数に true を指定すると、完全なファイル名が返されます。
  • 引数がディレクトリーの場合、コードはディレクトリーの名前を出力した後、ディレクトリーの中身をスキャンします (この場合の処理は、必ずしも深さ優先である必要はありません。ディレクトリーの中身は特別なシーケンスで順序付けられていないためです。この出力は、次の例で改善します)。

ここで興味深い点が 1 つあります。それは、reveal() の呼び出しはこの関数自体での最後のステートメントなので、スクリプト・コードを実行するエンジンである Squirrel 仮想マシン (VM) が末尾再帰と呼ばれる手法によって再帰的検索を反復処理に変えられるということです。基本的に、末尾再帰では再帰的検索に呼び出しスタックを使用しないため、任意の深さの再帰的検索が可能になるとともに、スタックがオーバーフローすることもありません。

Squirrel の構文はかなり簡潔なので、CC++、あるいはこれよりも高級な言語でコードを作成したことがあれば尚更のこと、Squirrel でコードを作成するのは簡単です。

何よりの利点は、このシェル・コードは移植できることです。このコードを Windows マシンに移し、Windows 対応の Squirrel Shell をインストールして、コードを実行してみてください。


テーブルの操作

Squirrel の優れた特徴の 1 つは、典型的なシェルと比べ、データ構造が豊富に揃っていることです。データを上手く編成することができるだけで、複雑な問題でも簡単に解決できる場合はよくあります。Squirrel には、真のオブジェクト、異種混合配列、そして連想配列 (Squirrel ではテーブルと呼びます) があります。

Squirrel でのテーブルを構成するのはスロットで、スロットとは (キーと値) のペアのことです。Null 以外のすべての値がキーの役割を果たすことが可能で、スロットには任意の値を割り当てることができます。新しいスロットを作成するには、「矢印」の演算子 (<-) を使用します。

これからリスト 2 のコードを改善して、サブディレクトリーに進む前に、ディレクトリーの中身を表示するようにします。その方法はと言うと、ローカル・テーブルを使ってファイルとサブディレクトリーを別のスロットに累積し、それから両方のカテゴリーをそれぞれに適した方法で処理します。リスト 3 に、このコードの新しいバージョンを記載します。

リスト 3. ディレクトリーの中身をまず出力してからサブディレクトリーを再帰的に検索するようにしたリスト 2 の拡張バージョン
#!/usr/bin/env squirrelsh

function reveal( filedir ) { 
  local tally = {};
  tally[FILE] <- [];
  tally[DIR] <- [];
  
  if ( !exist( filedir ) ) {
    return;
  }
    
  if ( filename( filedir ) == ".." || filename( filedir ) == "." ) {
    return;
  }
  		
  local names = readdir( filedir );
  
  foreach( index, name in names ) {
    tally[ filetype( name ) ].append( name ) ;
  }

  foreach( index, file in tally[FILE] ) {
    printl( file );
  }
 
  foreach( index, dir in tally[DIR] ) {
    printl( filename( dir ) + "/" );
  }
 	
  foreach( index, dir in tally[DIR] ) {
    reveal( dir );
  }

}

local entries = readdir( (__argc >= 2) ? __argv[1] : "." );

exit( 0 );

テーブルは、この場合に使うには理想的なデータ構造です。reveal() に含まれるテーブルには、ファイル用とディレクトリー用に 2 つのスロットがあります。関数 filetype( name ) の戻り値 (定数 FILE または定数 DIR のいずれか) は、ファイル・システム内の各項目をそれぞれに対応するスロット内に並べます。

さらに、各スロットは、tally[FILE] <- []tally[DIR] <- []; という 2 つの文によって作成される配列です ([] は空の配列)。tally は関数内のローカル変数であるため、呼び出しごとに改めて作成されてスコープから除外され、それぞれの呼び出しから返ってきた時点で自動的に破棄されます。

配列関数 append( arg ) は、配列の終わりに arg を追加し、それによってプロセス内でリストを蓄積していきます。foreach( index, name in names ) ループが完了すると、すべての項目がどちらか一方のスロットのリストに配置された状態になります。この関数にある残りのコードは、ファイルに続いてディレクトリーを出力した後、再帰的検索を続けます。

当然、コマンドライン引数がなければシェル・スクリプトはある意味役に立ちません。そのため、特殊な Squirrel Shell 変数、__argc および __argv にはそれぞれ、コマンドライン引数の個数、ストリングの配列としての引数のリストが含まれます。ここでも規約通り、__argv[0] は常にシェル・スクリプトの名前となるので、__argc が 2 以上であれば、追加の引数が指定されたということになります。簡単にするために、このスクリプトでは最初の追加引数、argv[1] だけを処理します。

参考のために、機能的にはリスト 3 とまったく同じ Ruby スクリプト (Makshin 氏が作成) をリスト 4 に記載します。Ruby をどれほど簡潔にできたとしても、Squirrel Shell コードの簡潔さには及びません。

リスト 4. Ruby でのリスト 3 の再実装
!/usr/bin/ruby

# List directory contents.

path = ARGV[0] == nil ? "." : ARGV[0].dup

# Remove trailing slashes
while path =~ /\/$/
  path.chop!
end

entries = Dir.open(path)
for entry in entries
  unless entry == "." || entry == ".."
    filePath    = "#{path}/#{entry}"
    fileStat = File.stat(filePath)
    if fileStat.directory?
      puts "dir : #{filePath}"
    elsif fileStat.file?
      puts "file: #{filePath}"
    end
  end
end

entries.close()

Squirrel 言語についての詳細は、Squirrel Programming Language Reference を参照してください (「参考文献」にリンクを記載)。

賢いことに、Squirrel Shell の実質上すべての関数は、基礎となるオペレーティング・システムごとの詳細を抽象化します。例えば、filename() 関数 (最初の 2 つのリストで使用した関数) はファイル・パス名から先頭部分のパスを取り去るため (例えば、/home/example/some/directory/file.txt を file.txt に短縮)、どのプラットフォームを使っているかは関係なくなります。同様に、readdir()filetype() を使用することで、基礎となっている実際のオペレーティング・システムとファイル・システムのたくらみと罠を知らないままでいられます。一般に、通常のシェルではこのような抽象化を行いません (このような抽象化を行うのは、より高度なスクリプト言語です)。

以上の他、プラットフォームに依存しない便利な関数としては、パス名をネイティブなパス名のフォーマットに変換する convpath()、別の実行ファイルを呼び出す run() などがあります。convpath() 関数は両方向の変換ができるので、クロス・プラットフォーム・スクリプトを作成する際に大いに役立ちます。


正規表現の大きな役割

シェル・スクリプトは、一般にシステムの管理および保守作業を自動化するために使用されています。この自動化の大部分を支えているのは、正規表現、つまりストリングを検出し、突き合わせ、分解する正真正銘の符号の一式です。記事の初めで説明したように、Squirrel Shell には PCRE ライブラリーが必要ですが、PCRE ライブラリーは Perl、PHP、Ruby、そしてその他多くのインタープリターとプログラムにもあります。PCRE はいわば、データを切り刻むサムライの刀のようなものです。

完成されているとは言え、Squirrel Shell の正規表現の実装は少し異なり、PHP の実装を思わせるかもしれません。Squirrel Shell で正規表現を使うには、正規表現を定義し、コンパイルし、比較を行い、それから (結果がある場合には) 結果を繰り返し処理します。

リスト 5 に、Squirrel Shell での正規表現の使い方を説明するサンプル・プログラムを記載します (このコードは Makshin 氏によって作成されたもので、彼の許可を得て使用しています)。

リスト 5. Squirrel Shell での正規表現の使い方
#!/usr/bin/env squirrelsh

// Match a regular expression against text

print("Text: ");
local text = scan();

print("Pattern: ");
local pattern = scan();

local re = regcompile(pattern);
if (!re)
{
	printl("Failed to compile regular expression - " + regerror());
	exit(1);
}

local matches = regmatch(re, text);
if (!matches)
{
	printl("Failed to match regular expression - " + regerror());
	regfree(re);
	exit(1);
}

regfree(re);
printl("Matches found:");
foreach (match in matches)
	printl("\t\"" + substr(text, match[0], match[1]) + "\"");

上記では、scan() が標準入力からテキストとパターンを読み取りますが、通常は正規表現の始めと終わりを区切るために使用される先頭と末尾のスラッシュ (/) 文字は使われていません。

パターンを指定すると、regcompile() 関数はそのパターンをコンパイルして繰り返し行われる突合せの処理を迅速化します。大/小文字の区別は、フラグを立てて regcompile() 関数を呼び出すことによって有効または無効にすることができます (PCRE の /i 修飾子に相当)。また、突き合わせの対象を 1 行または複数の行に指定できるオプションもあります (PCRE の /m 修飾子に相当)。正規表現をコンパイルしない場合、すべての突き合わせは失敗します。

regmatch(re, text) 関数は正規表現をテキストと比較し、その結果、一致がない場合には Null、それ以外の場合は整数ペアの配列 (2 つの要素からなる配列) を出力します。ペアの最初の整数は一致の始まりの位置、2 番目の整数は一致の終わりの位置を表します。そのため、コードの最終行では substr(text, match[0], match[1]) が使用されているというわけです。

比較が完了すると、結果の繰り返し処理を実行することができます。コンパイルされた正規表現が必要なくなったら、その時点で regfree() を使って不要な正規表現を破棄してください。また、コンパイル済み正規表現が保持するすべてのリソースを破棄する、regfreeall() という関数もあります。


Squirrel Shell の便利な機能

同じプログラミング・ロジックを UNIX、Linux、および Windows に適用できれば理想的であり、仮に生産性は変らなくてもプログラマーはとても助かることでしょう。しかし悲しいことに、オペレーティング・システムはそれぞれに異なるため、特定のシステムに合わせてコードをカスタマイズするしか手段がない場合もあります。

Squirrel Shell あるいはプログラマーのいずれの力を借りてもプラットフォームを抽象化できない場合に備え、Squirrel Shell ではオペレーティング・システムを調べてコードが適切なブランチを流れるようにする便利な関数を用意しています。

リスト 6 に、platform() 関数を使用してプラットフォームを判断する方法を示します。この関数は、値が未知 (Unknown) になる可能性があるとしても、常に値を返します。

リスト 6. オペレーティング・システムのタイプを出力する platform() 関数
print( "Made by ... ");

local platform = platform();

switch ( platform ) {
  case "linux":
    printl( "Linus." );
    break;

  case "macintosh":
    printl( "Steve." );
    break;

  case "win32":
  case "win64":
    printl( "Bill." );
    break;

  default:
  	printl( "Unknown" );
}

Squirrel Shell の環境変数 PLATFORMによって、現行プラットフォームのタイプを調べることもできます。

> printl( PLATFORM );
linux

環境変数 CPU_ARCH を使うと、シェルをコンパイルした対象のプロセッサーが出力されます。

> printl( CPU_ARCH );
x86

最後に一言

ここで取り上げていない残りの Squirrel Shell の関数は、ファイルの管理、環境の操作、そして計算を行うための関数です。三角法に関する組み込み関数に至っては、実に 20 近くもあります。現在計画中のバージョン 2.0 には、さらに多くのクラス、Unicode のサポート、改善された対話モード、そしてモジュール式のプラグイン・アーキテクチャーが導入される予定です。

Squirrel Shell は対話型シェルとしては大したものではありませんが、それはそれで問題ありません。このカテゴリーで使うには、他にも多くの方法を選べます。Squirrel Shell は、スクリプト・ランナーとして遙かに大きな力を発揮します。そのデータ構造は従来のシェルより有能で、構文はお馴染みで、ベースとなる仮想エンジンは列挙型からスレッドに至るまでのあらゆるものをサポートします。さらに、Squirrel エンジンは小さく、6000 行にも満たないコードなので、Squirrel をそっくりそのまま別のアプリケーションに組み込むことさえできます。

2 つのプラットフォームに対応するコードを作成しなければならなくなったら、Squirrel Shell を試してみてください。Squirrel Shell の力が作業をはかどらせてくれるはずです。

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。プロフィールで選択した情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=381404
ArticleTitle=UNIX について語る: Squirrel という移植可能なシェル・スクリプト言語
publish-date=03172009