2018年7月20日

Bash スクリプトの基本あれこれ #2: 区切る、まとめる、無効にする

今回は Bash の「メタキャラクタ」、意味としては「特殊な動作を行う文字」を扱う。 Bash においてはこういった文字が多数用意されており、それによって動作を制御している。 Bash は仕様を正確に理解していないと「何故そう動くのか」が解りづらいスクリプト環境であるが、前回説明したファイルディスクリプタやリダイレクションを踏まえて今回説明するメタキャラクタを理解できれば動作の理解はより容易くなると思う。

・メタキャラクタ

例えば、基本として変数の参照には $ というメタキャラクタを使う。

$ var=hello
$ echo $var
hello

他にも「ファイル名の補完」や前回説明した「リダイレクションやパイプ処理」を行うメタキャラクタ等、様々なものがあるが(詳細はリファレンスとしてこういったサイトを始め、検索して参照していただきたい)、重要なのはコマンドを「区切ったり」、「まとめたり」といった、構造的にスクリプトの動作を制御するためのものだ。こういったメタキャラクタの動作を理解できれば、 Bash においてスクリプトの読み書きの際に制御の流れを把握しやすくなる。

・区切る

1: 改行 スペース タブによる区切り

コマンドの区切りにおいて最も使うであろうメタキャラクタは「改行」である。例えば、 CUI 上でコマンドを入力した際に、 Enter キーを押すことでコマンドは実行されるが、これは改行文字によってコマンドが区切られているためだ。ファイル内でスクリプトを定義する場合、

echo one
echo two

とすれば、 echo one の行の最後の改行文字によってコマンドは区切られ、 echo one と echo two は違うコマンドであると認識される。

「スペース及びタブ」は、文字列を区切るためのメタキャラクタだ。例えば、 mkdir(ディレクトリを作成する)コマンドを実行し、 one と two というディレクトリを作成するとする。

$ mkdir -v one two
mkdir: ディレクトリ 'one' を作成しました
mkdir: ディレクトリ 'two' を作成しました
(この場合 one と two がスペースによって区切られているため、別々の引数として mkdir に渡され、 one と two の2つのディレクトリがカレントディレクトリ下に作成される)

明確に文字列として指定すれば「one two」を一つのディレクトリと出来る(「 '(アポストロフィー)」で囲う事でメタキャラクタとしてのスペースの意味を無効にする。詳しくは下記「無効にする」を参照)。

$ mkdir -v 'one two'
mkdir: ディレクトリ 'one two' を作成しました
(この場合「one two」が一つの引数として mkdir に渡され、「one two」という一つのディレクトリが作成される)

「スペースが挿入されたディレクトリやファイル」をスクリプト上で扱いづらいのは、スペースが区切り文字として扱われるメタキャラクタであるためである。「文字列の区切り」として使う文字は組み込み変数である IFS で定義されているので、この内容を変える事で「文字列の区切り」に使う文字自体を変える事が出来る。

$ PIFS=$IFS
( IFS 変数の中身を PIFS 変数に退避する。変更した後は元に戻さないと通常の処理が行えなくなる。ちなみに標準では IFS の中身はスペース、タブ及び改行となっている)

$ for i in `echo \`ls\``; do echo $i; done
Desktop
Document
Download
Image
Internet
Music
Project
Video
(カレントディレクトリ下で ls した結果の echo を順次 echo として出力する)

$ IFS=$'\n'
( 文字列の区切りを改行のみに変更)

$ for i in `echo \`ls\``; do echo $i; done
Desktop Document Download Image Internet Music Project Video
(スペースが区切り文字として機能しないため、 ls した結果の echo は順次評価されず、一度で全てが出力される)

$ IFS=$PIFS
( IFS を元に戻す)

2: 一行で複数のコマンドを区切って実行する

一行で複数のコマンドを区切って実行する(改行を使わずにコマンドを実行する)には「 ;(セミコロン)」を使う。例えば echo one と echo two のコマンドを一行で続けて実行したい場合、下記のように書ける。

$ echo one; echo two
one
two
(echo one の後に echo two が実行される)

3: コマンドを置換する

例えば、 ls の結果を echo で表示したいとする。

$ echo ls
ls
(単純に echo ls とすると、 echo は素直に ls という文字列を出力する)

こういった場合、「コマンド置換」を使用すると ls コマンドの結果を echo に渡せる。これは「 `(バッククォート)」で置換したいコマンドを囲む事で実現出来る。

$ echo `ls`
( ls コマンドが実行結果として置換され、カレントディレクトリ下の一覧が表示される)

変数にコマンドの結果を代入したい場合はコマンド置換が使える。

$ list=`ls`
$ echo $list
( ls の実行結果を list に代入し、それを echo で表示する)

コマンド置換は実行順が明確に解りづらいコマンド群に、明確な評価順を与える場合にも利用できる。こういった意味では、コマンドの実行を実行順で「区切っている」と言える。例えば、カレントディレクトリ下に Image ディレクトリが存在するとして、次のコマンドを実行してみる。

$ echo ls | grep Image
(文字列 ls に対して Image を含む行を抽出する。結果は抽出されず、表示されない)

$ echo `ls | grep Image`
Image
(この場合 ls | grep Image の結果を echo で出力する)

$ `echo ls` | grep Image
Image
(この場合 ls の結果を echo で出力し、それをパイプで grep に渡す。結果として grep で一致した Image が強調表示する形で出力される)

このように、コマンド置換によってコマンドの実行順が変わるのが確認できる。

Bash においては、コマンド置換は「 #() 」によっても実行できる。ただし、「 `(バッククォート)」によるコマンド置換とは「 \(エスケープ文字、詳細は下記「無効にする」を参照)」の扱い方が異なる。

$ one=hello
$ two=world

$ echo `echo \$one`
hello
(コマンド置換によって評価された $one の結果を echo で出力する。結果は one 変数の中身となる。コマンド置換内の echo によってエスケープされた $one 文字列が、外側の echo に渡される。この時に one 変数が展開される)

$ echo $(echo \$two)
$two
(コマンド置換によって評価された \$two の結果を echo で出力する。結果は $two 文字列となる。コマンド置換によってエスケープ文字が評価されず、 \$two 文字列として外側の echo に渡される。この時に \$two はエスケープされて評価されるため、 $two 変数は展開されず、そのまま文字列として出力される)

・まとめる

1: コマンド群を一つのコマンドにまとめる

複数のコマンド群を一つのコマンドとしてみなしたい場合は、「 () 及び {} 」を使う。この概念を文章として説明するのは非常に難しいが、動作例を通じて説明を試みようと思う。今回は例としてカレントディレクトリ下に Image 及び Document ディレクトリが存在するとする。

$ ls | (cd `grep Image`; touch here)
( ls の結果をパイプして grep に渡し、その結果を基にして cd を実行して、その場所に touch (ファイルを作成する)コマンドで here ファイルを作成する)

$ ls Image
here
( Image ディレクトリ下に here ファイルが作成されている)

この奇妙な動作を説明するのは骨が折れる。 () メタキャラクタによって「 cd `grep Im`; touch here 」コマンド群は一つのコマンドとして定義されている。そのコマンドに対して ls の結果をパイプとして接続する。すると標準入力を入力として認識できる最初のコマンドである grep が ls の結果を受け取り、それが cd への入力として評価され、結果的に Image ディレクトリ下に here ファイルが作成される。

こういった処理はコマンドをまとめる事によって初めて実行可能となる。例えば、下記のコマンドでは期待したように動作しない。

$  ls | cd `grep Image`; touch here
(これは ls の結果を cd コマンドに渡すことになる。cd は標準入力を受け取ることが出来ないコマンドである。 grep Image は入力がないため正しく評価されず、カレントディレクトリ下に here ファイルが作成される)

つまり、 () でまとめたコマンドにパイプを接続すると、まとめたコマンド群を順次実行しつつ、なおかつ最初に標準入力を入力として評価するコマンドにパイプが接続される。

$ ls | (cd `grep Image`; touch one; cd '..'; cd `grep Document`; touch two)
(このコマンドを実行すると、 ls の結果は最初の grep Image に渡される。次点の grep Document は正しく評価されない)

$ ls Image
one
( one ファイルは期待通り Image ディレクトリ下に作成されている)

$ ls
two
( two ファイルはカレントディレクトリ下に作成されている)

$ ls | (touch one; cd `grep Document`; touch two)
(このコマンドは grep Document コマンドが標準入力を受け取ることが出来る最初のコマンドであるため、 ls の結果を受け取る。まずカレントディレクトリ下に one ファイルを作成し、そのあと ls | grep Document の結果を cd への入力とし、結果として Document ディレクトリ下に two ファイルが作成される)

$ ls Document
two

このようにコマンド群をまとめる処理は、コマンド群に対してパイプを用いる際に入力の対象を的確に指定したい場合に効果的に利用出来るものであると言える。それ以外でもコマンドのコンテキストを明示的に表現したい場合に利用できる。

2: () と {} の違い

コマンドをまとめるには () と {} が使用できるが、これらは () の場合はその内部でのみ定義した環境が有効になる、 {} の場合は定義した環境を引き継ぐという違いがある。

$ (cd Image)
(カレントディレクトリ下の Image に移動するが、コマンド実行後には環境は引き継がれず、カレントディレクトリに戻っている)

$ { cd Image; }
(カレントディレクトリ下の Image に移動する。コマンド実行後も環境が引き継がれているため、 Image ディレクトリ下にいる。 {} は文法的に { の後にスペース、 } の前に ; が必要である)

前述した IFS の例で言うと、

$ (IFS=$'\n'; for i in `echo \`ls\``; do echo $i; done)
Desktop Document Download Image Internet Music Project Video

は IFS への代入が () 外には引き継がれないため、 ()  外では標準の IFS を適用した文字列区切り処理がなされる。この場合明示的に変数を退避する必要はない。

$ for i in `echo \`ls\``; do echo $i; done
Desktop
Document
Download
Image
Internet
Music
Project
Video
(上記コマンド実行後でも通常の IFS を利用した文字列区切りで処理されている)

3: 複数行のコマンドを一行のコマンドにまとめる

複数行に渡る長いコマンドを一行のコマンドとしてまとめるには、「 \(バックスラッシュ)」を使う。文法上の注意点として、 \ の後には余計な入力をせず、直後に改行する必要がある。

$ echo one \
> two
one two
(このコマンドは echo one の後の \ によってコマンドの結果が次の行に保留される。続けて次の行に two を入力する事により、結果的に echo one two と同じ結果となる)

・無効にする

「メタキャラクタ」として処理される文字を「ただの文字として使用したい」場合は、メタキャラクタとしての機能を無効に出来る(これは一般的に「エスケープ」と称される)。

1: 一文字のメタキャラクタをエスケープ

明示的に一文字のメタキャラクタをエスケープするには、「 \ (バックスラッシュ)」を使う。

$ var=hello
$ echo \$hello
$hello
(変数参照のメタキャラクタである $ の機能が無効化される(エスケープされる)ことにより、単純に $hello 文字列が出力される)

2: 明示的な文字列の定義

Bash スクリプト内において明示的に文字列を表現したい場合は「 '(アポストロフィー)」及び「 "(ダブルクォーテーション)」を使用する。この場合囲われた文字列はメタキャラクタがエスケープされるが、 ' と " ではエスケープされる範囲が異なる。

「 '(アポストロフィー)」の場合、囲った文字列のメタキャラクタは全てエスケープされる。

$ var='hello world'

$ echo *
( * メタキャラクタはファイル名の補完において任意の文字列の合致を表す。この場合はカレントディレクトリ下に存在するディレクトリとファイルを列挙する(ただし、 . で始まる隠しファイルは除く))

$ echo '$var \$var `ls` *'
$var \$var `ls` *
( 「 '(アポストロフィー)」の処理では、メタキャラクタの処理( $ \ ` * )は全てエスケープされ、純粋な文字列となる)

「 "(ダブルクォーテーション)」の場合、 $ と \ と ` はエスケープされない。これらのメタキャラクタは評価されるが、それ以外のメタキャラクタはエスケープされる。

$ echo "$var \$var `ls` *"
hello world $var カレントディレクトリ下の表示 *
( $var の評価として var 変数の中身を評価し、 \$var の評価として $ をエスケープした  $var を文字列とし、コマンド置換による ls コマンドを評価し、 エスケープした * を文字として表示する echo が実行される)

" の特性を利用すると、変数の内容を連結して新たに変数として代入するといった柔軟な処理が可能となる。

$ one=hello
$ two= world
$ three="$one $two"
$ echo $three
hello world
( $ は "" において評価されるため、 one と two の内容を連結した文字列を three に代入できる)


2018年7月19日

Bash スクリプトの基本あれこれ #1: ファイルディスクリプタとリダイレクション

Bash は Linux のシステムにおいて基本的に使うスクリプト環境であるが、癖が強い故に使いこなすにはコツがいる環境でもある。今回は自分自身の復習もかねて、 Bash スクリプトを使用する上での「キモ的なもの」をおさえる記事を書いてみる。なお、ほとんどの仕様は Sh(Bourne Shell)に基づいたものあるが、これから紹介する機能においては Bash の固有の機能も排除できないので Bash スクリプトの機能として統一して表記する。

ファイルディスクリプタ

ファイルディスクリプタ」は、 Bash のスクリプトの入出力を理解する上で覚えるべき基本的な要素だ。パイプのような強力な機能もこれを基にして実装されているので、これを理解しているか否かで Bash というスクリプトに対する理解度はまるで違うといってもよい。

「C 言語」のような「構造化プログラミング」言語では、関数は引数として特定の型を入力として受け取り、戻り値として特定の型を出力する。この考え方に基づき、語弊を恐れずに単純に言うなら、Bash で実行されるコマンドは「ファイルディスクリプタ型の入力(引数)を受け取ってファイルディスクリプタ型の出力(戻り値)を返す」と考えて良い。こういった考え方は構造化プログラミングを理解しているなら理解し易い。単純にコマンドとして使用していると概念として意識しないかもしれないが、スクリプトとして使用するとなるとこのような構造的な考え方が重要になる。

ファイルディスクリプタには POSIX の仕様に基づいて次の定義がなされている(構造化プログラミング的に言うなら、こういった型があらかじめ用意されている)。

0: 標準入力 (stdin)
1: 標準出力 (stdout)
2: 標準エラー出力 (stderr)
3 以降: 新たにファイルディスクリプタが定義されると作られる(下記リダイレクションを参照)

例えば、 ls コマンド(ディレクトリの内容を表示)を実行すると、現在のディレクトリ下の一覧が表示される。

$ ls
(ディレクトリ下の一覧が出力される)

これは ls コマンドの結果として標準出力(1)というファイルディスクリプタで値を出力することで、結果的に CUI(標準出力)に表示がなされている言える。

・パイプ

Bash の機能のひとつであるパイプは実行したコマンドの結果を次のコマンドへ渡すことが出来る Bash スクリプトを支える強力な機能だ。熟練した Linux ユーザなら当たり前のように使用している機能である。だが、 Bash のコマンドの実行の詳細を理解しないまま使用していると、パイプは「そういう結果となる」といったファジーな感覚で利用しがちである。

この機能を構造化プログラミング的な表現において定義するなら、ファイルディスクリプタ型である標準出力(1)を次のコマンドへのファイルディスクリプタ型である標準入力(0)として渡すことが出来る機能である、と言える。

例えば、 ls コマンドの実行結果を grep コマンド(マッチングする文字列を含む行の抽出)にパイプする場合、

$ ls | grep マッチングする文字列
(ls の結果を grep で抽出した結果が表示される)

と出来るが、これは ls の実行結果をファイルディスクリプタである標準出力(1)とし、 grep がそれをファイルディスクリプタである標準入力(0)として受け取ることで実行される結果となる。

「標準出力(1)を出力するコマンドを標準入力(0)を入力としてサポートするコマンドにパイプとして接続できる」と考えると、厳密な意味でパイプを使用する事が可能となる。つまり、標準入力(0)を受け取る事が出来るコマンドはパイプで接続できるコマンドであると考える事が可能となり、それに基づいてパイプ処理を構築出来る。また、標準入力(0)を入力としてサポートしないコマンドにはパイプは使用できない、と考えることも出来るようになる(ただし、こういったコマンドが何であるかは --help に書かれていない場合は経験則で実行するしかないのが現状ではある)。

・リダイレクション

リダイレクション」は、ファイルディスクリプタによる入出力処理を指定した箇所に変更する機能であり、これもファイルディスクリプタを操作する方法として、 Bash スクリプトでは覚えるべき基本的な機能となるものだ。上記した構造化プログラミングの例で言うなら、入力に使用するファイルディスクリプタ型及び出力に使用するファイルディスクリプタ型を変更できる機能である。「パイプ」も、「標準出力(1)を標準入力(0)とする」というリダイレクションの処理に基づいている。

例えば、 ls コマンドは標準として標準出力(1)に結果を表示するコマンドであるが、この標準出力の位置を別の場所に変更する事で結果を「ファイルに保存」とすることも可能である。この場合新たに「ファイル」に対するアクセスを実現するファイルディスクリプタが定義される。

$ ls > list
(このコマンドは ls の結果をカレントディレクトリ下の list ファイルに保存する。つまり、ファイルディスクリプタが標準出力(1)から list ファイルへのファイルディスクリプタ( 3 以降)に変更されている)

0~2 番のファイルディスクリプタは標準で定義されているが、新たにファイルを開くとなると新たに 3 番以降のファイルディスクリプタが定義される(この例では list ファイルに対して新たにファイルディスクリプタが定義されており、これはシステムによって自動的になされる)。

リダイレクションの処理はファイルディスクリプタの番号を指定することで詳細に指定する事が出来る。例えば、

$ command 2>&1 
(このコマンド(command)を実行すると実行結果の標準エラー出力(2)を標準出力(1)に向け、結果的にコマンドライン上の標準出力(1)に標準エラー出力(2)も表示する)

こういったリダイレクションの処理を応用すれは Bash スクリプトにおける入出力を自由自在に操れるようになる。 Bash スクリプトはこういったファイルディスクリプタの操作を行うコマンドを多数定義しているが、ここでは使用頻度が高いであろうコマンドを紹介しようと思う(詳細はこういったリダイレクションの詳細機能の説明しているサイトを参照)。

$ command > file
( command の実行結果(標準出力(1))を file に対するファイルディスクリプタを使って( 3 以降の番号が自動的に割り当てられている)file への出力として保存する。この場合はファイルに上書きはされず、新たにファイルが作られる)

$ command >> file 
( command の実行結果(標準出力(1))を file に対するファイルディスクリプタを使って( 3 以降の番号が自動的に割り当てられている)file への出力として保存する。この場合は file が存在している場合は上書きして追記する)

$ command < file
( file をあらわすファイルディスクリプタからの入力(3 以降)を標準入力(0)として読み込む。コマンドが標準入力(0)からの入力をサポートしているなら、パイプと同じ処理がなされる)

つまり、機能的に次のコマンドは同一の結果となる。

$ ls | grep 抽出する文字列
( ls を実行した結果を grep した結果を表示。 ls の結果をパイプにより grep に接続する。 ls の結果を標準出力(1)とし、それを grep の標準入力(0)として処理する。結果は grep の標準出力(1)としてコマンドライン上に表示される)

$ ls > temp
$ grep ファイル名 < temp
( ls を実行した結果を grep した結果を表示。 ls の結果(標準出力(1))を temp に対するファイルへの出力(3 以降)とする。その結果を読み込む形で temp からの標準入力(0)として grep を評価して標準出力(1)が出力され、結果がコマンドライン上に表示される)

・ヒアドキュメント

リダイレクションの操作の応用として、「ヒアドキュメント」がある。 Bash スクリプトにおいては「実行の際に独自のプロンプト上で動作し、 対話モードとして動作を停止するまでユーザの入力を待つコマンド」が存在する。そういったコマンドに対してコマンドを注入する場合にヒアドキュメントは使用できる。

$ bc << CALC
> x = 1 + 2
> x = x + 3
> x
> CALC
6
(結果として 6 が表示される。 bc コマンド(演算処理を行うインタプリタ)を実行して開始位置として定義された CALC の位置から終了位置として定義された CALC までの文字列を bc コマンドに注入して評価する)

特定の文字列をファイルに保存する場合もヒアドキュメントは利用できる。

$ cat << FILE > file
> hello world
> hello world !
> FILE

$ cat file
hello world
hello world !
( FILE ヒアドキュメントで指定した文字列が cat コマンド(ファイルの内容を標準出力とする)の標準出力(1)となり、それをファイルへの出力(3 以降)として file に保存する)

次: Bash スクリプトの基本あれこれ #2: 区切る、まとめる、無効にする

2018年7月16日

スクリプトライクに GNU Smalltalk を扱う試み #2: SUnit テスティングフレームワークの実行

前回で基本的な実行環境は構築できたため、今回は SUnit によるテスティングフレームワークの実行環境を整えてみようと思う。 SUnit は自分が敬愛するプログラマの一人であるケント・ベック氏によって書かれ、 xUnit フレームワークの原型となったものであり、 Smalltalk コミュニティの先見性をあらわす代表的なフレームワークの一つと言える。

Simple Smalltalk Testing: With Patterns

※ 文脈には関係ないが、ケント・ベック氏著による下記の書籍は、経験上 Smalltalk プログラマに限らず全てのプログラマにお勧めできる良書なので紹介しておきたい(Ruby のような Smalltalk ライクなクロージャの使い方が出来る言語のプログラマには特にお勧めだ)。

ケント・ベックのSmalltalkベストプラクティス・パターン―シンプル・デザインへの宝石集

GNU Smalltalk ではサポートする SUnit パッケージをイメージにロードすることで SUnit が使用可能になる。自分は現在 OS に Linux Mint 18.3 を使っているが、 GNU Smalltalk においては追加の Debian パッケージ(.deb)をインストールしなくても GNU Smalltalk パッケージ(.star)をロード出来た。 CUI 上で動作を確認してみる。

$ gst
GNU Smalltalk ready
(デフォルトイメージで GNU Smalltalk を実行)

st>  PackageLoader fileInPackage: 'SUnit'
(SUnit パッケージを現在のデフォルトイメージにロード)

st> TestCase
TestCase
(SUnit フレームワークの基底クラスである TestCase が定義されている)

gst-load コマンドを使う場合(前回の投稿を参照)は、次のコマンドで指定するイメージファイルに SUnit をロード出来る。

$ gst-load SUnit -I ロードするイメージファイル名

・テストユニットの実行処理

SUnit の動作の確認も兼ねて GNU Smalltalk 環境において CUI で手動で SUnit を実行してみる。

$ gst
GNU Smalltalk ready
(デフォルトイメージで GNU Smalltalk を実行)

st>  PackageLoader fileInPackage: 'SUnit'
( SUnit パッケージを現在のデフォルトイメージにロード)

st> TestCase subclass: #HelloWorldTest
( TestCase クラスを継承した HelloWorldTest テストケースクラスを作成)

st> HelloWorldTest extend [
st> setUp [
st> super setUp
st> ]
st> ]
( TestCase クラスの setUp メソッドをオーバーライド。 これはテストケース内のテストメソッドが実行される前にコンストラクタ(初期化処理)として実行されるメソッドである。テストメソッドが評価される度に呼び出される)

st> HelloWorldTest extend [
st> tearDown [
st> super tearDown
st> ]
st> ]
(TestCase クラスの tearDown メソッドをオーバーライド。これはテストケース内のテストメソッドが実行された後にデストラクタ(後処理)として実行されるメソッドである。テストメソッドが評価される度に呼び出される)

st> HelloWorldTest extend [
st> testHelloWorld [
st> self should: [ 'Hello World' isString ]
st> ]
st> ]
(テストメソッド testHelloWorld を定義する。 SUnit は test で始まる名前のメソッドはテストメソッドとして自動的に実行する便利な機能がある。 should: メソッドは「〜であると期待する」という意味で、引数として与えられたブロックが flase として評価された場合は例外が発生し、テスト失敗となる。この例の場合は true なのでテスト成功)

st> HelloWorldTest extend [
st> testTrueWorld [
st> self assert: true
st> ]
st> ]
(テストメソッド testTrueWorld を定義する。 assert: メソッドは引数の評価が true であればテスト成功となる)

st > HelloWorldTest suite run
2 run, 2 passes
(定義されたテストメソッドのテストを実行。前述したように特に指定しなくても test で始まるメソッドは全て評価されている。定義した2つのテストメソッドが評価され、2つともテスト成功となる)

st> (HelloWorldTest selector: #testHelloWorld) run
1 run, 1 passes
(特定のテストメソッドのみを指定して実行してみる。この場合は testHelloWorld メソッドのみテストされるため、一つの実行で一つの成功となる)

st> | suite |
st> suite := TestSuite named: 'HelloWorldTests'
st> suite addTest: (HelloWorldTest selector: #testHelloWorld)
st> suite addTest: (HelloWorldTest selector: #testTrueWorld)
st> suite run
2 run, 2 passes
(TestSuite クラスを使うとセレクタで指定して追加したテストメソッドをまとめて実行出来る)

こういった処理を自動化出来ればビルド環境は楽になる、と考えられる。

・SUnit での環境を整える

GNU Smalltalk における SUnit の環境を整えるために前回作ったビルドスクリプト(的なもの)に SUnit をサポートする機能を追加する。

gst-package コマンドに使う package.xml ファイルには SUnit で使用するファイルを明示的に定義出来る。定義としては package.xml の <package> の下に

<test>
<sunit>テストスイート名</sunit>
<file>テストスイートが定義された .st ファイル名</file>
<filein>テストスイートが定義された .st ファイル名</filein>
# ~これ以降にも存在するテストスイート定義の .st ファイルが <file> <filein> で定義される
</test>

で定義する。

SUnit の動作を長々と確認したが、実際のところ GNU Smalltalk には SUnit を簡易的に実行出来る gst-sunit コマンドが用意されている。上記のようにテストスイートを定義した package.xml をパッケージングすると、そのパッケージを適用したイメージに対してクラス単位のユニットテストが可能になる。

$ gst-sunit -p 評価するパケージ -I 評価するイメージファイル TestCase *

このコマンドにおいて、最後のオプションで指定されている TestCase * により、指定したパッケージを適用したイメージファイルにおいて、定義されている全てのテストケースを評価出来る( TestCase クラスを継承したクラス下における test で始まるメソッドを全て評価する)。

・上記の結果を踏まえて Atom 上でビルドスクリプト(的なもの)を実行する

結果は前回の結果を上書きする形で GitHub にアップロードしてある。 -t オプションを指定して実行することで SUnit が動作するようになっている。

# 2つのテストメソッドを実行した場合の実行例
$ ./.build.sh -t
Running SUnit
2 run, 2 passes

前回作成したビルドスクリプト(的なもの)に SUnit の評価機能を追加した

2018年7月13日

スクリプトライクに GNU Smalltalk を扱う試み #1: 単純なビルド環境の構築

Smalltalk は自分にとって最も好きなプログラミング言語だ。今現在ではまるで珍しい考え方ではない OOPMVC をはじめとしたデザインパターン)を始めクロージャやそれに基づく DSL の考え方等を 1980 年代に既に実装していた恐るべき先見性をもったプログラミング言語である。

趣味で使うなら Smalltalk を使いたい。特に Python のように「手軽なスプリクトライク」で使いたい。だが Smalltalk 環境(代表的なもので Squeak 及びその派生である Pharo 等)はその独自性故に、プログラミングするとなると大袈裟になりすぎるきらいがある。

Smalltalk 環境のひとつである GNU Smalltalk は CUI 上で実行出来る手軽さがあり、これを利用して Python ライクなスクリプティング環境を構築出来ないだろうか試してみよう、と思いたった。

GNU Smalltalk
GNU Smalltalk User's Guide: GNU Operating System
GNU Smalltalk: Wikipedia

・GNU Smalltalk の実行環境

例えば「C言語」ならビルドはハードウェアの環境で静的に完結するもので、ビルドした時点でその環境で実行可能な「実行ファイル」がビルドされる。こうしてビルドされた実行ファイルを実行すればそのプログラムが実行される。

Smalltalk は仮想マシン上で動作する言語であり、仮想マシン上で動作するイメージファイルに実行される環境の全てが定義される(これは Smalltalk の独自的な長所であると同時に、理解しづらい特性でもある)。このイメージに定義を追加することでイメージが拡張され、その拡張されたイメージが事実上実行ファイルの役割を果たす。

例えば、コマンドライン上で GNU Smalltalk を実行するとして

$ gst -q
GNU Smalltalk ready
(システムデフォルトのイメージで GNU Smalltalk を実行)

st> Object subclass: #You
( You クラスを定義する)

st> You class extend [ say [ 'Yes'  displayNl ] ]
( Yes と答える You クラスの say クラスメソッドを定義)

st> You say
Yes
( say クラスメソッドを呼び出すと Yes と答える)

st > ObjectMemory snapshot: 'SayYes.im'
(現在のイメージをスナップショットとして保存)

st> ^C
(Ctrl+c でプロセスを終了)

$ gst -q
GNU Smalltalk ready
(再びシステムデフォルトのイメージで GNU Smalltalk を実行)

st> You isNil displayNl
true
(システムデフォルトのイメージで You クラスは定義されていない)

st> ^C
(Ctrl+c でプロセスを終了)

$ gst -qI SayYes.im
GNU Smalltalk ready
(先ほどスナップショットした SayYes.im イメージで GNU Smalltalk を実行)

st> You say
Yes
( スナップショットとして保存した SayYes.im では You クラスが定義されており say クラスメソッドを呼び出すと Yes と答える)

このように、仮想マシンに対するイメージファイルの定義を拡張することで「実行ファイル」に相当する実行環境の拡張が出来る。

・GNU Smalltalk のソースファイル

GNU Smalltalk におけるソースファイルは .st 拡張子で保存できる。コマンドラインで一行ずつ入力しなくても、このようなソースファイルがあればファイル単位での読み込みが可能。ここでは HelloWorld.st を作成し、次のような Hello World コードを書き込む(下記のパッケージの例で使用する)。

Object subclass: World [
    World class >> sayToMe [ 'Hello!' displayNl ]
]

・GNU Smalltalk のパッケージ管理

GNU Smalltalk User's Guide: 3 Packages

gst-package コマンド

GNU Smalltalk はソースファイルの集合をパッケージとして定義し、指定したイメージファイルに一律に適用(ロード)する事が出来る。これには gst-package コマンドを使う。このコマンドは package.xml(フォーマットは上記リンク参照)の XML ファイルで定義されている .st 拡張子のソースファイルを一律にパッケージング及びインストールする機能を持つ。

下記のコマンドはカレントディレクトリに package.xml に基づいた .star パッケージを作成する。このパッケージは配布可能であり、受け取った人は自身の GNU Smalltalk システムにインストール可能。

$ gst-package -t. package.xml

下記のコマンドは実行中のユーザのディレクトリ下(~/.st)に package.xml で定義した .star パッケージを作成してインストールする。

$ gst-package -t ~/.st package.xml

パッケージを直接指定してインストールすることもできる。

$ gst-package -t ~/.st パッケージ名.star

gst-load コマンド

例えば package.xml において

<package>
     <name>HelloWorld</name>
     <file>HelloWorld.st</file>
     <filein>HelloWorld.st</filein>
</package>

と定義されて HelloWorld パッケージとされ、上記の gst-package コマンドによって ~/.st にインストールされていたとする。

パッケージをインストールしたユーザは gst-load コマンドによって任意のイメージにインストールした HelloWorld パッケージ(ソースとしては HelloWorld.st のみ)をロード出来る。 ~/.st ディレクトリは GNU Smalltalk がパッケージの取得のために参照するユーザディレクトリであり、ここにある .star パッケージは実行中のユーザがロード可能になる。

あらかじめシステムにインストールされているコアライブラリ( SQL データベースやネットワークソケットライブラリ等)も gst-load でロード出来る。

$ gst-load HelloWorld -I SayYes.im
( SayYes.im イメージに HelloWorld.st を含む HelloWorld パッケージを適用)

$ gst -qI SayYes.im
GNU Smalltalk ready
(パッケージを適用した SayYes.im を読み込む)

st> World sayToMe
Hello!
( HelloWorld.st で定義されている World クラスがロードされている)

・ここまでの知識を活かしてテキストエディタを扱う

Atom 等のプログラミング用のテキストエディタ上でも、上記の gst-package や gst-load を用いればシェルスクリプト等で簡易的なビルドスクリプト(的なもの)を組む事は容易い。こんな感じで GNU Smalltalk を活用すれば、 Python のようにスクリプトライクに GNU Smalltalk を扱うことが出来る、かもしれない。

なお、作ったスクリプトは GitHub にアップロードしてある

シェルスクリプトによる簡易的な GNU Smalltalk のビルドスクリプト的なもの

次: スクリプトライクに GNU Smalltalk を扱う試み #2: SUnit テスティングフレームワークの実行