7. さらなる言語例

この章では、一連の例を通して言語の核心へと迫ります(ビルドシステムのサンプルについては 3. OMakeビルドサンプル を参照してください)。

私たちはこれらの例のほとんどに osh インタープリターを用いています。また、簡単にするため、 osh によって出力された値は省略されています。

7.1 文字列と配列

OMakeの基本となる型は文字列とシーケンス、そして値からなる配列です。シーケンスはホワイトスペースによって分割された配列のようなもので、関数の要求に応じて分割されます。

osh> X = 1 2
- : "1 2" : Sequence
osh> addsuffix(.c, $X)
- : <array 1.c 2.c> : Array

時々あなたは明示的に配列を定義したいと思うでしょう。この場合、 [] を変数名の後に追加し、配列の成分をそれぞれインデントされた行に書くことで実現できます。

osh> A[] =
        Hello world
        $(getenv HOME)
- : <array "Hello world" "/home/jyh"> : Array

配列は成分にホワイトスペースを含めることができます。これは配列の主要な特徴の一つであり、重要な役割を担います。また、これはホワイトスペースを含むファイル名にとって特に役立ちます。

# ディレクトリ中の現在のファイルを並べる
 osh> ls -Q
 "fee"  "fi"  "foo"  "fum"
 osh> NAME[] =
         Hello world
 - : <array "Hello world"> : Array
 osh> touch $(NAME)
 osh> ls -Q
 "fee"  "fi"  "foo"  "fum"  "Hello world"

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 "a b c" : Sequence ".c" : Sequence> : Sequence
osh> value $(nth 2, $(B))
- : "c.c" : String
osh> value $(length $(B))
- : 3 : Int

配列は異なります。配列の成分はどのような方法を用いても文字列とくっつけることができません。配列は括弧 [] を変数名につけ、インデントした内容を記述することで成分を定義できます。各成分にはホワイトスペースを含めることもできます。

osh> A[] =
        a b
        foo bar
- : <array
       "a b" : Sequence
       "foo bar" : Sequence>
       : 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

7.3 ファイルとディレクトリ

複数のディレクトリにまたがっており、かつ異なったパートに分かれているOMakeのプロジェクト上では、まったく違うディレクトリ上でコマンドが実行されます。これはファイル、あるいはディレクトリの名前が位置的に独立して定義されている必要があることを表しています。

この問題は $(file <names>)$(dir <names>) 関数を用いて解決できます。

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について詳しく知りたい方は “10.1.1 file, dir” を参照してください。

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

mapprefixaddprefix 関数は全く異なります( addsuffixmapsuffix 関数も同様です)。 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 $< > $@

もちろん、これらのルールを記述することは関数を呼ぶことより好ましいものではありません。関数を抽象化することによる普通の特性は、普通の利点となります。ビルドルールを定義するためのコードが一つだけで済み、コードはさらに短くなります。後でもし私たちがルールを修正したりアップデートしたいと思ったときも、単純に一つのルールを修正するだけでよいのです。

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と評価されました。二回目の BC の定義時に評価されているため、なんの影響も与えていません。

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))

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

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 が渡されます。これはエイリアスの名前には 含まれていません

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

目次

前のトピックへ

6. 式と値

次のトピックへ

8. ビルドルール

このページ

SourceForge.JP

SourceForge.JP

このドキュメントはsourceforge.jpのサーバを利用して提供しています。