スクリプトの作成と実行(基本)
サンプルプログラム-
次のプログラムを題材に具体的な方法を説明します.以下,ファイル名をhello.scmとします.
;; hello.scm (define (hello-everybody args) (for-each hello-somebody args)) (define (hello-somebody name) (display (string-append "*** Hello," name "!!! ***")) (newline)) (hello-everybody '("Bob" "Carol" "Dana"))
hello-everybody手続きは,文字列からなるリスト(args)を受け取って,そのリストの各文字列に対してhello-somebody手続きを適用します.hello-somebody手続きは,文字列(name)を受け取って,その文字列の前後に"*** Hello,"と"!!! ***"を連結(string-append)して表示(display)して改行(newline)します.
-
プログラムファイルをスクリプト(単独で動作するプログラム)にするには,プログラムファイルの先頭に次の2行を追加します.
#!/usr/bin/guile -s !# ;; hello.scm (define (hello-everybody args) (for-each hello-somebody args)) (define (hello-somebody arg) (display (string-append "*** Hello," arg "!!! ***")) (newline)) (hello-everybody '("Bob" "Carol" "Dana"))
- 上記の /usr/bin/guile はDebian 11(bullseye)におけるguileコマンドの絶対パスを示しています.-sはguileコマンドの-sスイッチです.
- (注意)実行環境(OSやGuileシステム)が異なるとき,guileコマンドの絶対パスも異なるかも知れません.
-
スクリプトを実行するためには,プログラムファイルに実行属性を付ける必要があります.
$ chmod 755 hello.scm ↵ または $ chmod +x hello.scm ↵
これでプログラムファイルをスクリプトとして実行できます.$ ./hello.scm ↵ ..... コンパイルメッセージ ...... *** Hello,Bob!!! *** *** Hello,Carol!!! *** *** Hello,Dana!!! ***
- (参考)スクリプトファイルを上のように実行したとしても,guileコマンドによってファイル内のプログラムが処理されます(下記参照).そのため,REPLのload手続きやguileコマンドによって実行したときと同様に,コンパイル後のバイトコードがキャッシュディレクトリに保存され,スクリプトファイルの中身が変更されない限り,再コンパイルはされずに既存のバイトコードが実行されます.
-
上で追加した1行目(シェバン行)はLinuxによって処理され,次のようなコマンドに変換されて実行されます(なお,シェバン行はLinuxに関する一般的な事項なので特段の説明はしません).
$ /use/bin/guile -s file ↵
ここで,fileはスクリプトのファイル名を示しています.例えば,上記のhello.scmのシェバン行は次のようなコマンドに変換されて実行されます.$ /use/bin/guile -s hello.scm ↵
-
変換後のguileコマンドはスクリプトファイル内のプログラムを先頭から処理していきます.その際,guileコマンドは#!から!#までの部分をブロックコメントとして処理します.つまり,これらによって囲まれた部分を読み飛ばします.追加した2行目に!#があるのは,guileコマンドにとってのブロックコメントを閉じるためです.これを忘れると次のようなエラーが発生します.
$ ./hello.scm ↵ ...... コンパイルメッセージ ...... ERROR: In procedure primitive-load: In procedure skip_block_comment: /home/user/tmp/./hello.scm:12:1: unterminated `#! ... !#' comment
-
(注意)ブロックコメントに関して,リファレンスマニュアル(4.3.1節)に次のような説明文があります.
The second line of the script should contain only the characters '!#' ...
2行目は!#以外を記述しないほうがよいでしょう. -
(参考)#!と!#で囲まれた部分はguileコマンドにとってコメントにすぎないので,スクリプトファイルをguileコマンドを使って普通に実行できます.例えば:
$ guile -s hello.scm ↵ *** Hello,Bob!!! *** *** Hello,Carol!!! *** *** Hello,Dana!!! ***
さらに,REPLの中でload手続きを使って実行することもできます.例えば:$ guile ↵ GNU Guile 3.0.5 ...... 起動メッセージ ...... guile> (load "hello.scm") ↵ *** Hello,Bob!!! *** *** Hello,Carol!!! *** *** Hello,Dana!!! ***
これは,プログラム内の手続きを個別にテストするときなどに役立つだろうと思います.例えば:guile> (hello-somebody "Alonzo") *** Hello,Alonzo!!! *** guile> (hello-everybody '("John" "Gerald" "Guy")) *** Hello,John!!! *** *** Hello,Gerald!!! *** *** Hello,Guy!!! *** guile>
-
冒頭で示した2行の代わりに,次の3行(シェルを経由する方法)を使うこともできます.こちらのほうがより多くの実行環境で利用可能だと思います.
#!/usr/bin/env sh exec guile -s "$0" "$@" !# ;; hello.scm (define (hello-everybody args) (for-each hello-somebody args)) (define (hello-somebody name) (display (string-append "*** Hello," name "!!! ***")) (newline)) (hello-everybody '("Bob" "Carol" "Dana"))
$ ./hello.scm ↵ ..... コンパイルメッセージ ...... *** Hello,Bob!!! *** *** Hello,Carol!!! *** *** Hello,Dana!!! ***
- (注意)!#はguileコマンドにとってのブロックコメントを閉じるために必須です.忘れないで下さい.
エントリーポイントを指定する
- エントリーポイントを指定したスクリプトの作成方法を説明します.
- 筆者の理解不足による技術的な問題がないのであれば,最後に示すポータブルな形式をおすすめします.
- ポータブルな形式を利用するのであれば,以下のメタスイッチを利用する方法に関する説明は読み飛ばしてもかまいません.ただし,リファレンスマニュアル(4.3節)はメタスイッチ推しです.つまり,メタスイッチに関する説明を詳しく行っているものの,ポータブルな形式についてはほとんど何も説明していません.
-
次のプログラム(entry-point.scm)を題材に具体的な方法を説明します.以下では,main手続きをエントリーポイントとするスクリプト(単独で動作するプログラム)を作ります.
;; entry-point.scm (define (main args) (for-each hello-somebody (cdr args))) (define (hello-somebody name) (display (string-append "*** Hello," name "!!! ***")) (newline))
main手続きは,文字列からなるリスト(args)を受け取って,そのリストの各文字列に対してhello-somebody手続きを適用します.hello-somebody手続きは,文字列(name)を受け取って,その文字列の前後に"*** Hello,"と"!!! ***"を連結(string-append)して表示(display)して改行(newline)します. -
main手続きをエントリーポイントとして指定するためには,この資料の冒頭で示した2行の代わりに,次の3行を追加します.
#!/usr/bin/guile \ -e main -s !# ;; entry-point.scm (define (main args) (for-each hello-somebody (cdr args))) (define (hello-somebody name) (display (string-append "*** Hello," name "!!! ***")) (newline))
これでスクリプトは完成です.このあとはファイルに実行属性を与えて実行します.$ chmod +x entry-point.scm ↵ $ ./entry-point.scm Alice Bob Carol ↵ ...... コンパイルメッセージ ...... *** Hello,Alice!!! *** *** Hello,Bob!!! *** *** Hello,Carol!!! ***
この実行例が示しているように,コマンドライン引数はスクリプトファイル名のうしろに指定します.
-
上で追加したシェバン行の最後にあるバックスラッシュ(\)をメタスイッチと呼びます.一般的に,スクリプト内でguileコマンドに複数の構成要素(スイッチやエントリーポイントなど)からなるコマンドライン引数を指定するときにはメタスイッチを使った次の形式を使用します.
#!/usr/bin/guile \ arg ... !# ...... プログラム ......
ここで,argはコマンドライン引数の構成要素を表しています.例えば,エントリーポイント(proc)を指定するときには次の形式を使用します.#!/usr/bin/guile \ -e proc -s !# ...... プログラム ......
-
メタスイッチを使ったときの2行目(arg ...)は次の掟(条件)を守らなければなりません.
-
すべての空白は引数の区切りとして処理されます.
正確を期すために,マニュアルの英文は次の通りです.
Each space character terminates an argument.
別の言い方をすると, 空白の前後は異なる引数として処理されます. 例えば,空白を含むような '(a b)' といった形式の引数を2行目に指定したとき, ターミナル上ではうまく機能したので2行目でも機能するだろうと期待して指定したとき,クォートしてんだから大丈夫さなんて安易な気持ちで指定したとき, その意図に反して '(a と b)' という2つの引数として処理されます. つまり,2行目には,ターミナル上でうまくいったとしても, 空白を含むような引数は決して指定できません. クォートで囲めば大丈夫といった甘い考えも捨てましょう. 2行目の中の空白は,いわば最上級の権能を有しています. - 行の先頭に空白を入れてはいけません. 行の先頭に空白を入れた場合,その空白の直前に空文字列の引数を指定したものとして処理されます.
- タブ文字は(エスケープしない限り)禁止です.
- 2行目の各要素(上の例では,-eやmainや-sのこと)は空白1文字(きっかり1文字)で区切らなければなりません,空白を2文字以上入れると, 空白の間に空文字列の引数を指定したものとして処理されます.
- 最後の構成要素(上の例では-sのこと)の直後で改行しなければなりません. 空白1文字を入れて改行してもギリギリ大丈夫ですが, 上で述べたように空白を2文字以上入れてはいけません.
- 1つの救いとして,バックスラッシュ(\)を使って, バクスラッシュ自身(\\),空白(\ ),タブ文字(\タブキー), 改行文字(\改行)がエスケープできます. ANSI Cのエスケープ文字(\nや\tなど)をサポートしています. さらに,\NNN (NNNはASCIIコードの8進表示)をサポートしています. これら以外はサポートしません.
-
すべての空白は引数の区切りとして処理されます.
正確を期すために,マニュアルの英文は次の通りです.
-
この掟を守らなかったときにはエラーが発生します.
#!/usr/bin/guile \ -e main -s !# ;; secondline.scm (define (main args) (write args) (newline))
例えば,上のスクリプト(secondline.scm)は2行目のmainのあとに2文字以上の空白を入れているのですが,これを実行すると以下のようなエラーが発生します.$ ./secondline.scm ↵ ...... コンパイルメッセージ ...... Backtrace: 0 (primitive-load "/home/user/tmp/") ERROR: In procedure primitive-load: In procedure fport_read: ディレクトリです
とても分かりにくいエラーなので注意が必要です. - リファレンスマニュアル(4.3.2節)によると,2行目に指定したスイッチや引数の区切りは空白1文字(きっかり1文字)にしているのだそうです.そのため,先頭に空白があったり2文字以上の連続した空白があったりすると,空の文字列""が引数として指定されたと判断し,実行時のコマンドライン引数に加えるそうです.先頭に空白があるときには,その空白の直前に空文字列が指定されていると判断し,空白2文字があるときには,それらの空白の間に空文字列が指定されていると判断するようです.guileコマンドは,この空文字列をプログラムファイルの名前として処理します.でも,そんなファイルは有り得ないのでエラーが発生するのです.
-
例えば,上の具体例は次のように処理されます.まず,Linuxによってシェバン行が処理され,次のguileコマンドに変換されて実行されます.ただし,以下の'\'は,ターミナル(シェル)において,バックスラッシュを通常文字として処理するためのものです.
$ /usr/bin/guile '\' /home/user/secondline.scm ↵
次に,guileコマンドは,第1引数としてメタスイッチが指定されたとき,その直後に指定されているファイル内の2行目を解析して,guileコマンド自身を以下のようなコマンドに変更します.ここで,""という空の文字列がコマンドライン引数に加えられていることに注意して下さい.$ /usr/bin/guile -e main "" -s scondline.scm ↵
変更後のguileコマンドは,空の文字列("")をプログラムファイルの名前として処理しようとします(注:-sスイッチが省略されたものと見なして,空文字列をプログラムファイル名として処理します).上の例では/home/user/tmpというディレクトリの上で作業しているので,"/home/user/tmp/"(注:最後のスラッシュのあとに空文字列が付加されていることに注意)というファイルを処理しようとします.でも,それはディレクトリであってプログラムファイルではありません.そのため,上で示しているようなエラーが発生します. - 以上の処理は(Linuxではなく)guileコマンドが行っています.なんでそんなことになっているのか理由は不明です.空白を読み飛ばせばいいだけじゃないか,手抜きじゃないかと疑いたくなりますが,何か大人の事情があるのでしょう(その事情は筆者には分かりません).
-
メタスイッチを使う代わりに,次の形式(シェルを経由する方法)を使うこともできます.
#!/usr/bin/env sh exec guile -e proc -s "$0" "$@" !# ...... プログラム ......
例えば,上で示した具体例は次のようにすることもできます.#!/usr/bin/env sh exec guile -e main -s "$0" "$@" !# ;; entry-point.scm (define (main args) (for-each hello-somebody (cdr args))) (define (hello-somebody name) (display (string-append "*** Hello," name "!!! ***")) (newline))
$ ./entry-point.scm Alice Bob Carol ↵ ...... コンパイルメッセージ ...... *** Hello,Alice!!! *** *** Hello,Bob!!! *** *** Hello,Carol!!! ***
- この形式では,2行目はguileコマンドではなくシェル(sh)によって処理されるので,2行目の掟を気にする必要はありません.この点で安全な方法だと思います.さらに,これはより多くの実行環境で利用可能な形式だと思います.でも,リファレンスマニュアルは全力をあげてメタスイッチを説明していて,上記の形式は一つの具体例を示すことだけに留めています(しかもちょっと分かりにくい).上記の形式はLinuxの一般的な事項に属することなので,みんな分かってるだろうということなのかも知れません.