株式会社クリアコード > ククログ

ククログ


ノータブルコード5 - Lispとシェルスクリプトのキマイラ

今回紹介するコードは、エディンバラ大学が開発した音声合成ライブラリFestivalからのコードです。このライブラリには、テキストファイルを音声に変換する「text2wave」という実行ファイルが付属しています。このファイルの冒頭部分を引用すると次のようになっています。

#!/bin/sh
"true" ; exec /bin/festival --script $0 $*
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-*-mode:scheme-*-
;;                                                                       ;;
;;                Centre for Speech Technology Research                  ;;
;;                     University of Edinburgh, UK                       ;;
;;                       Copyright (c) 1996,1997                         ;;
;;                        All Rights Reserved.                           ;;
...

;;; Because this is a --script type file I has to explicitly
;;; load the initfiles: init.scm and user's .festivalrc
(load (path-append libdir "init.scm"))

;;; Process command line arguments
(define (text2wave_help)
  (format t "%s\n"
  "text2wave [options] textfile
  Convert a textfile to a waveform
  Options

最初の2行を見て「ああこれはシェルスクリプトなんだな」と思っていると、突然3行目から Lisp/Scheme のコードが始まってビックリさせられます。それ以降のコードはSchemeのプログラムになっていて、シェルスクリプトの面影は影も形もなくなってしまいます。上の抜粋からも、ごく普通のシェルスクリプトの書き出しから、シームレスにSchemeのS式の世界に突入していることがご確認いただけると思います。

これはどういう仕組みで動いているんだろう?というのが、今回の記事のテーマです。

シェルスクリプトとしての実行プロセス

この謎を解き明かすカギは、最初の2行にあります。このスクリプトを「/bin/text2wave test.txt」として実行した時の流れを追ってみましょう。

#!/bin/sh
"true" ; exec /bin/festival --script $0 $*

まず、オペレーティングシステムのプログラムローダーは最初の1行目を見て「これは/bin/shで実行するコードだ」と判断します。ですので、このファイル全体が(最初の見立て通り)まずはシェルスクリプトとして解釈されることになります。

そこで2行目に目を移すと、シェルスクリプトではセミコロン ; はコマンドの区切りを示すので、この行は2つのコマンドから構成されていることが分かります。このうちの、前半部分は特に何もせず成功ステータスコードを返すtrueコマンドなので*1、実行だけされて次に移ります。

これに対して、後半部分にはもっと意味のありそうなコマンドが書かれています。まずこのコマンドの中のシェル変数に注目すると $0 は実行されているプログラムを表すので「/bin/text2wave」に、$* はコマンドライン引数を表すので「test.txt」にそれぞれ置換されます。いいかえると、この部分は次のようなコマンド行に展開されることになります。

exec /bin/festival --script /bin/text2wave test.txt

冒頭のexecはシェル標準のコマンドで、指定した実行プログラムで実行中のプロセスの内容を置き換えます。ですので、このコマンドが実行されると、シェルスクリプトとしての実行はひとまず終了になります。

Schemeスクリプトとしての評価

シェルスクリプトの最後で実行された /bin/festival に着目しましょう。これは、音声合成ライブラリの実行バイナリで、引数「--script」で与えられたファイルをSchemeスクリプトとして処理します。ここでは /bin/text2wave を指定しているので、最初にシェルスクリプトとして評価したスクリプトが(今度はSchemeのスクリプトとして)再び頭から解釈されることになります。

このSchemeインタプリタによる実行評価は次のように進行します。

  • まず、大抵のLisp処理系は#!をコメント行として扱います。
    • これは言語の仕様ではありませんが、スクリプト言語としての用途を考慮して、そのように実装されていることが多いです。
    • FestivalのSchemeインタプリタも同様の振る舞いをするので、1行目はコメントとして無視されます。
  • 2行目の前半部分については、Schemeでは"true"は文字列リテラルなので、評価だけして素通りします。
  • 2行目の後半部分は重要なポイントで、実はSchemeではセミコロンはコメントの開始を表します。 従って、execから行末までの部分はコメントとして無視されます。

そこから後の部分は普通のSchemeプログラムなので、普通に実行されることになります。

話をまとめると、このスクリプトは (1) シェルスクリプトとして実行することもできるし、(2) Schemeスクリプトとしても実行できるという実に不思議なプログラムなのです。この特筆すべき性質によって、Schemeとシェルスクリプトというまったく性質の異なる言語が、1つのファイルの中でなめらかに共存できているのです。

なぜこんなことをしているのか?

「確かに面白いテクニックだけど、なぜこんなことをしているの? 普通に最初の行で#!/bin/festival --scriptと指定するだけではダメなの?」というのは当然思い浮かぶ疑問です。このFestivalのコードは20年以上前のコードで、すでに実装の背景は分からなくなってしまってるのですが、この構成を採用するメリットは少なくとも2つあります。

  1. システム互換性の問題を回避できる。 Unixでは一般に「#!/path/to/program」という一行をスクリプトの冒頭につけることで、実行するプログラムを指定できますが、実は、この仕組みには明確な仕様がなく、具体的な振る舞いはシステムによってまちまちです(例えば、古いOSだと実行ファイルのパスの後に引数を指定できなかったりします)。この方式だと#!/bin/shさえ上手く解釈されれば、あとはシェルスクリプトで呼び出しを制御するので、非常に広い範囲のUnixシステムでポータブルに動作することが期待できます。
  2. 実行プログラムに渡す引数を柔軟に調整できる。 最終的に本体ファイル /bin/festival を呼び出すのはシェルスクリプトなので、実行時のパラメータをいかようにも制御できます。シェルの制御構文を使えば、条件分岐で与える引数を動的に変えることもできます。これは通常のやり方では実現できないポイントです。

まとめ

今回は音声合成ライブラリのFestivalから、Lispとシェルがなめらかに統合された、ギリシャ神話の怪物キマイラのようなコード(頭部はシェルスクリプトで胴体はSchemeです!)をご紹介しました。

皆さんはこのコードを読んでどのような感想を抱きましたか? ノータブルコードのコーナーでは皆さんの寄稿も受け付けていますので、ご意見や「これをぜひ紹介したい」というコードがあれば、ぜひともお寄せください。

*1  (3/17追記) 最初の公開版では「単なる文字列リテラルで、特に変数に代入もされていないので」と説明していましたが、コメントの指摘の通り、誤りのため修正しています。ご指摘ありがとうございます!

2020-03-10

«前の記事: ノータブルコード4 - NULLよりも名前付きの番兵オブジェクト 最新記事 次の記事: ノータブルフィードバック6 - 初回使用時のつまずきを減らす提案 »
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|