.. 7-language-examples .. index: single: osh .. _label7: 7. さらなる言語例 ======================================= この章では、一連の例を通して言語の核心へと迫ります(ビルドシステムのサンプルについては :ref:`label3` を参照してください)。 私たちはこれらの例のほとんどに ``osh`` インタープリターを用いています。また、簡単にするため、 ``osh`` によって出力された値は省略されています。 .. _label7.1: 7.1 文字列と配列 --------------------------------------- OMakeの基本となる型は文字列とシーケンス、そして値からなる配列です。シーケンスはホワイトスペースによって分割された配列のようなもので、関数の要求に応じて分割されます。 :: osh> X = 1 2 - : "1 2" : Sequence osh> addsuffix(.c, $X) - : : Array 時々あなたは明示的に配列を定義したいと思うでしょう。この場合、 ``[]`` を変数名の後に追加し、配列の成分をそれぞれインデントした行に書くことで実現できます。 :: osh> A[] = Hello world $(getenv HOME) - : : Array 成分にホワイトスペースを含めることができるという配列の主要機能は重要です。これはホワイトスペースを含むファイル名にとって特に役立ちます。 :: # ディレクトリ中の現在のファイルを並べる osh> ls -Q "fee" "fi" "foo" "fum" osh> NAME[] = Hello world - : : Array osh> touch $(NAME) osh> ls -Q "fee" "fi" "foo" "fum" "Hello world" .. _label7.2: 7.2 クオート文字列 --------------------------------------- ``String`` は単一の値です。文字列の中でホワイトスペースは重要な意味を持っています(訳注: 英語圏ではそうだと思います)。文字列をクオートするには4通りの方法があります。まず一番目にクオートをつけることが挙げられるでしょう。シンボル ``'`` (シングルクオート)と ``"`` (ダブルクオート)を用いることで、シェルを扱うときのように成分をクオートすることができます。クオーテーションシンボルは結果の文字列に **含まれます** 。変数は常に内部にクオートを含んだ状態で展開されます。 ``osh(1)`` (15章で説明)は文字列にダブルクオートをつけて出力しますが、これは出力時だけであり、文字列の中には含まれていないことに注意してください。 :: osh> A = 'Hello "world"' - : "'Hello \"world\"'" : String osh> B = "$(A)" - : "\"'Hello \"world\"'\"" : String osh> C = 'Hello \'world\'' - : "'Hello 'world''" : String 二番目の方法は ``$'`` と ``$"`` クオートを導入することです。始まりと終わりのクオートシンボルの数は任意です。また、これらのクオーテーションはいくつかの性質をもっています。 * クオートデリミタは文字列の一部では **ありません** 。 * 文字列中のバックスラッシュ ``\`` 文字は通常の文字として扱われます。 * 文字列は何行にわたって書くこともできます。 * 変数は ``$"`` を用いて展開することができます。ただし、 ``$'`` では展開できません。 :: osh> A = $'''Here $(IS) an '''' \(example\) string[''' - : "Here $(IS) an '''' \\(example\\) string[" : String osh> B = $""""A is "$(A)" """" - : "A is \"Here $(IS) an '''' \\(example\\) string[\" " : String osh> value $(A.length) - : 38 : Int osh> value $(A.nth 5) - : "$" : String osh> value $(A.rev) - : "[gnirts )\\elpmaxe(\\ '''' na )SI($ ereH" : String 文字列とシーケンスの両方はホワイトスペースが含まれていない文字列とくっつけることができます。 :: osh> A = a b c - : "a b c" : Sequence osh> B = $(A).c - : : Sequence osh> value $(nth 2, $(B)) - : "c.c" : String osh> value $(length $(B)) - : 3 : Int 配列は異なります。配列の成分はどのような方法を用いても文字列とくっつけることができません。配列は括弧 ``[]`` を変数名につけ、インデントした中身を記述することで成分を定義できます。各成分にはホワイトスペースを含めることもできます。 :: osh> A[] = a b foo bar - : : Array osh> echo $(A).c a b foo bar .c osh> value $(A.length) - : 2 : Int osh> value $(A.nth 1) - : "foo bar" : Sequence 配列はしばしばシステム上にホワイトスペースを含んだファイル名を使う場合において、非常に有用なツールとなります。 :: osh> FILES[] = c:\Documents and Settings\jyh\one file c:\Program Files\omake\second file osh> CFILES = $(addsuffix .c, $(FILES)) osh> echo $(CFILES) c:\Documents and Settings\jyh\one file.c c:\Program Files\omake\second file.c .. _label7.3: 7.3 ファイルとディレクトリ --------------------------------------- 複数のディレクトリにまたがっており、かつ異なったパートに分かれているOMakeのプロジェクト上では、まったく違うディレクトリ上でコマンドが実行されます。これはファイル、あるいはディレクトリの名前が位置的に独立して定義されている必要があることを表しています。 この問題は ``$(file )`` や ``$(dir )`` 関数を用いて解決できます。 :: osh> mkdir tmp osh> F = $(file fee) osh> section: cd tmp echo $F ../fee osh> echo $F fee ``section:`` を用いて ``cd`` コマンドのスコープに制限を加えていることに注意してください。このセクションでは一時的に ``tmp`` ディレクトリに移動しているので、ファイルの名前には ``../fee`` が用いられます。このセクションが終了してカレントディレクトリに戻ってきたとき、ファイルの名前には ``fee`` が用いられます。 ファイル関数を使う主な目的は、あなたのプロジェクトの ``OMakefile`` で、ファイル名が正しく定義されるようにするためです。これを用いればプロジェクト上の様々なパートに移動したとしても、変数は同一のファイルを指し示します。 :: osh> cat OMakefile ROOT = $(dir .) TMP = $(dir tmp) BIN = $(dir bin) ... (訳注: file, dirについて詳しく知りたい方は ":ref:`label4.15`" を参照してください。) .. index:: single: mapprefix() single: addprefix() single: addsuffix() single: mapsuffix() single: foreach() .. _label7.4: 7.4 イテレーション、マップ、foreach --------------------------------------- ほとんどのビルドイン関数では配列を何も考慮することなく処理できます。 :: osh> addprefix(-D, DEBUG WIN32) - : -DDEBUG -DWIN32 : Array osh> mapprefix(-I, /etc /tmp) - : -I /etc -I /tmp : Array osh> uppercase(fee fi foo fum) - : FEE FI FOO FUM : Array ``mapprefix`` と ``addprefix`` 関数は全く異なります( ``addsuffix`` と ``mapsuffix`` 関数も同様です)。 ``addprefix`` 関数は接頭辞を各々の成分くっつけます。 ``mapprefix`` 関数は元の成分の前に新しい接頭辞を成分として加えるので、配列の長さは2倍になります。 ほとんどの関数は配列でも動きますが、あなた自身が配列にも対応した関数を作りたいと思うこともあるでしょう。 ``foreach`` 関数はその要望を実現します。 ``foreach`` 関数は二つの表記を使いますが、この中身を伴った表記法はとても便利です。この関数は二つの引数と中身が必要です。まず、一つ目の引数は変数で、二つ目のは配列を指定します。そして ``foreach`` の中身は各々の成分を対象に、引数で指定された変数がその成分に束縛された状態で、配列の長さぶんだけ実行されます。さあ、それでは値を持った配列の各々の成分に1を加える関数を定義してみましょう。 :: osh> add1(l) = foreach(i, $l): add($i, 1) osh> add1(7 21 75) - : 8 22 76 : Array あなたはファイル名を持った配列を持ち、そのそれぞれにビルドルールを定義したいと思うこともあるでしょう。ビルドルールは特別なものではなく、あなたはどの箇所でもビルドルールを定義することができます。さて、私たちは配列にある、各々のファイルの処理について記述し、さらに結果を ``tmp/`` ディレクトリの中に置く関数を書きたいものとします。 :: TMP = $(dir tmp) my-special-rule(files) = foreach(name, $(files)) $(TMP)/$(name): $(name) process $< > $@ 後に、プロジェクトの他の部分で、私たちはこの関数を用いていくつかのファイルを処理することを決めたとしましょう。 :: # src/libに処理するためのファイルが入っています MY_SPECIAL_FILES[] = fee.src fi.src file with spaces in its name.src my-special-rule($(MY_SPECIAL_FILES)) ``my-special-rule`` を呼んだ結果は、以下の3つのルールを明示的に書いた場合と全く同じになります。 :: $(TMP)/fee.src: fee.src process fee > $@ $(TMP)/fi.src: fi.src process fi.src > $@ $(TMP)/$"file with spaces in its name.src": $"file with spaces in its name.src" process $< > $@ もちろん、これらのルールを記述することは関数を呼ぶことより好ましいものではありません。関数を抽象化することによる普通の特性は、普通の利点となります。ビルドルールを定義するためのコードが一つだけで済み、コードはさらに短くなります。後でもし私たちがルールを修正したりアップデートしたいと思ったときも、単純に一つのルールを修正するだけでよいのです。 .. index:: single: 遅延評価変数 .. _label7.5: 7.5 遅延評価式 --------------------------------------- omakeでの評価は通常の場合先行して行われます。これは、omakeが式に遭遇した場合、即座に評価が行われることを意味してみます。この効果の一つとして、変数定義式の右側は、変数が定義される時点で展開されることが挙げられます。 この振る舞いをコントロールするための2つの方法があります。 ``$`(v)`` は遅延評価を行うための、 ``$,(v)`` は先行評価に戻すための表記法です。以下のシーケンスについて考えてみましょう。 :: osh> A = 1 - : "1" : Sequence osh> B = 2 - : "2" : Sequence osh> C = $`(add $(A), $,(B)) - : $(apply add $(apply A) "2" : Sequence) osh> println(C = $(C)) C = 3 osh> A = 5 - : "5" : Sequence osh> B = 6 - : "6" : Sequence osh> println(C = $(C)) C = 7 ``C = $`(add $(A), $,(B))`` では遅延評価を定義しています。 ``add`` 関数はこの場合、実際に値が必要となるときまで評価しません。上の式を見てみると、 ``$,(B)`` は ``B`` が即座に評価する変数であることを指定します。これが遅延評価式の中で定義されているにもかかわらずです。 最初私たちが ``C`` の値を出力したとき、 ``A`` は1で ``B`` が2であったので、結果は3と評価されました。次に私たちが同様に ``C`` を出力したとき、 ``A`` は5に再定義されていたので、結果は7と評価されました。二回目の ``B`` は ``C`` の定義時に評価されているため、なんの影響も与えていません。 .. _label7.5.1: 7.5.1 遅延評価式についての追加例 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 遅延評価式は実際に結果が必要とされるときまで評価されません。筆者を含む結構な人数のプログラマは、遅延評価が多用しているコードを見ると眉をひそめます。なぜならこれは実際にどこで評価が行われているのか分かりにくくなるからです。しかしながら、これらの難点をペイオフできるケースも確かに存在します。 一つの例としてオプションの処理が挙げられます。Cコンパイラに"include"ディレクトリの指定をコマンドライン上から指定する場合を考えましょう。もし私たちが"/home/jyh/include"と"../foo"上のファイルをインクルードしたい場合、コマンドラインにはオプション ``-I/home/jyh/include -I../foo`` を指定する必要があります。 Cファイルをビルドするための通常のルールを定義する場合について考えましょう。私たちはインクルードされるディレクトリを指定するため、 ``INCLUDES`` 配列を定義し、ルートのOMakefileに通常用いる暗黙のルールを定義しました。 :: # Cファイルをコンパイルする通常の設定 CFLAGS = -g INCLUDES[] = %.o: %.c $(CC) $(CFLAGS) $(INCLUDES) -c $< # srcディレクトリは4つのソースファイルからmy_widgetをビルドします。 # これはインクルードディレクトリからインクルードファイルを読み込みます。 .SUBDIRS: src FILES = fee fi foo fum OFILES = $(addsuffix .o, $(FILES)) INCLUDES[] += -I../include my_widget: $(OFILES) $(CC) $(CFLAGS) -o $@ $(OFILES) しかしこれは全く正しいというわけではありません。問題としては、 ``INCLUDES`` はオプションが格納されてある配列であり、ディレクトリではないという点です。もし私たちが後にディレクトリ名を変更したい場合は、まず ``-I`` 接頭辞を配列から分割する必要があり、これは混乱の元となります。さらに、私たちはディレクトリの絶対パスを使用していません。この問題を解決する方法は、遅延評価式を使うことです。まず私たちは ``INCLUDES`` をディレクトリの配列として定義し、さらに新しい変数 ``PREFIXED_INCLUDES`` を定義することによって ``-I`` 接頭辞を追加します。 ``PREFIXED_INCLUDES`` は遅延評価を行うことで、最新のINCLUDES変数値が使われることを保証してくれます。 :: # Cファイルをコンパイルする通常の設定 CFLAGS = -g INCLUDES[] = PREFIXED_INCLUDES[] = $`(addprefix -I, $(INCLUDES)) %.o: %.c $(CC) $(CFLAGS) $(PREFIXED_INCLUDES) -c $< # 今回の例では、私たちはインクルードディレクトリの絶対パスを定義しました。 STDINCLUDE = $(dir include) # srcディレクトリは4つのソースファイルからmy_widgetをビルドします。 # これはインクルードディレクトリからインクルードファイルを読み込みます。 .SUBDIRS: src FILES = fee fi foo fum OFILES = $(addsuffix .o, $(FILES)) INCLUDES[] += $(STDINCLUDE) my_widget: $(OFILES) $(CC) $(CFLAGS) -o $@ $(OFILES) 遅延する値と関数が密接に繋がっていることに注目してください。上の例では、私たちは ``PREFIXED_INCLUDES`` を、引数を持たない関数として定義しているのと同じことを行っています。 :: PREFIXED_INCLUDES() = addprefix(-I, $(INCLUDES)) .. index:: single: while .. _label7.6: 7.6 スコープとエクスポート --------------------------------------- OMakeの言語は(IOとシェルコマンドは除きますが)関数型言語となっています。これは、まず関数が最上級のものであることと、変数が不変なものである(代入という操作が存在しない)という2つから、そうであると言えるでしょう。後者に関しては、おそらく従来のGNU makeを使っていたユーザからすると奇妙なものに思えるかもしれません。しかしこれは実際にOMakeを使う上で非常に重要な点となります。変数は修正できませんので、プロジェクトの一部分が他の部分に干渉することは不可能(あるいは非常に困難)です。 これを従来の純粋な関数型言語のように実装してしまうと、非常に使いにくいものになるかもしれません。OMakeでは、インデントすることでレベルを一つ挙げた場合、新しいスコープが導入されます。そしてスコープが終わると、そのスコープで新しく定義された変数は消去されます。もしOMakeが馬鹿真面目にスコープに関して厳格な仕様であったならば、おそらくコードはもっと複雑なものになったでしょう。 :: osh> X = 1 osh> setenv(BOO, 12) osh> if $(equal $(OSTYPE), Win32) setenv(BOO, 17) X = 2 osh> println($X $(getenv BOO)) 1 12 ``export`` コマンドはこの制限を外に出します。このコマンドは内部のスコープの値(あるいは全体の変数環境)を外部に「エクスポート」するお世話をします。 :: osh> X = 1 osh> setenv(BOO, 12) osh> if $(equal $(OSTYPE), Win32) setenv(BOO, 17) X = 2 export osh> println($X $(getenv BOO)) 2 17 エクスポートは、ループ中のイテレーションから次のイテレーションへ値をエクスポートするのに特に役立ちます。 :: # オーケー、それでは配列の各成分を足し合わせてみよう osh>sum(l) = total = 0 foreach(i, $l) total = $(add $(total), $i) value $(total) osh>sum(1 2 3) - : 0 : Int # うーん、正常に動いていないじゃないか! osh>sum(l) = total = 0 foreach(i, $l) total = $(add $(total), $i) export value $(total) osh>sum(1 2 3) - : 6 : Int ``while`` ループは自動的にエクスポートしてくれる、別の形のループ文です。 :: osh>i = 0 osh>total = 0 osh>while $(lt $i, 10) total = $(add $(total), $i) i = $(add $i, 1) osh>println($(total)) 45 .. index:: single: シェルエイリアス single: Shell .. _label7.7: 7.7 シェルエイリアス --------------------------------------- ときどきあなたは *エイリアス* を定義したいと思うことがあるかもしれません。そのためにOMakeでは、実際にシェルコマンドが存在しているかのようにふるまうコマンドが存在します。あなたはこれを、 ``Shell`` オブジェクトに対象の関数を追加することで実現できます。 例えば、 ``awk`` 関数を用いて、あるファイル中のすべてのコメントを出力する場合について考えてみましょう。 :: osh>cat comment.om # Comment function comments(filename) = awk($(filename)) case $'^#' println($0) # File finished osh>include comment osh>comments(comment.om) # Comment function # File finished これをエイリアスとして追加するには、 ``Shell`` オブジェクトにメソッドを追加します。 ``+=`` を用いて、シェルの既存の内容を保存している点に注意してください。 :: osh>Shell. += printcom(argv) = comments($(nth 0, $(argv))) osh>printcom comment.om > output.txt osh>cat output.txt # Comment function # File finished シェルコマンドは引数として配列 ``argv`` が渡されます。これはエイリアスの名前には *含まれていません* 。 .. index:: single: リダイレクション single: stdin single: stdout single: stderr .. _label7.8: 7.8 簡単に入出力のリダイレクションを行う ----------------------------------------- 結果的に、スコーピングによってリダイレクションの実行に関しての良い代替案も表れることとなりました。それでは、既に標準の出力先に出力するコードが大量にあるが、この出力先のリダイレクションを行いたいというような場合について考えてみましょう。まず一つ目の方法としては、前回のテクニックを用いることが挙げられます。具体的には、関数をエイリアスとして定義し、あなたが望む出力先にすることでシェルのリダイレクションを行うといった方法です。 別の方法については、前者の方法よりも簡単な場合があります。変数 ``stdin`` ``stdout`` ``stderr`` は標準I/Oの出力先について定義しています。出力先をリダイレクトするには、これらの変数をあなたが望むように再定義します。もちろん、あなたはこれを普通にネストされたスコープ上で行うことができるので、外部の出力先に影響を与えることはありません。 :: osh>f() = println(Hello world) osh>f() Hello world osh>section: stdout = $(fopen output.txt, w) f() close($(stdout)) osh>cat output.txt Hello world これはシェルコマンドに対しても同様です。もしあなたがギャンブル好きであるならば、以下の例を試してみるのもいいでしょう。 :: osh>f() = println(Hello world) osh>f() Hello world osh>section: stdout = $(fopen output.txt, w) f() cat output.txt close($(stdout)) osh>cat output.txt Hello world Hello world