コマンドライン操作で主要なデータ操作ができるようになった シェルスクリプトを、電子メイル経由で操作するようにして、 ある程度の自動採点システムを構築する手法を考える。
今回作成するものは、以下のような流れで使用するものの一部とする。
このうち「採点プログラム」は統一的に決められるものではないので作らず、 今回は宛先によって得点を決める単純なものとする。 たとえば、解答者に出された条件は以下のようなものとする。
芋煮発祥の地と言われるのはどこか。以下の6つから選べ。
a. 宮城県塩竃市 b. 青森県八戸市 c. 秋田県横手市
d. 山形県中山町 e. 福島県三春町 f. 岩手県奥州市
解答は taro-report-04-[a-f]@example.com 宛に氏名のみ書いて送信せよ。 [a-f] の部分は選んだ選択肢1つに置き換えること。
つまり、宛先によって処理を切り替えるようにする。このような方法であれば 投票システムなどにも応用できる。ちなみにメイル本文で処理を切り替える方法は、 送信形態の違い(テキストメイルかHTMLメイルか)や、 表記ゆれへの対応が必要で、精度の高いものを作ることは容易でない。
このような条件で送られた課題提出メイルを自動受信スクリプトが受け取る。 これは以下の条件で動くものとする。
電子メイルは詐称が難しくなく、本人からの送信を保証するには なんらかの認証機構を要し簡単な話でなくなる。 これは本書の範疇を大きく外れるため、ここでは性善説に基づき 送信者が正しく自分のメイルアドレスから送信しているものとする。
解答者と解答者のIDは必ず1対1対応するものとする。 解答者のメイルアドレスのローカルパート(@より前の部分)も重複がなく、 ローカルパートを集計用のIDとして利用できるものとする。
この条件で、受信スクリプト receiver.sh を作成する。
軽量スクリプトで効率的に作業を進めるには、 問題を単純化して簡単なアルゴリズムの短いプログラムで済むような 環境を調えることをまず考える。今回の場合はメイルを自動的に解析して データ登録をするというものだが、一般的にテキストの「解析処理」は 簡単ではない。届いた電子メイルのストリームから差出人やサブジェクト、 内容を正確に解析するのは複雑であるため、極力ストリームを見なくて よい形を考える。
今回の場合は自動受信スクリプトが差出人などの情報を得るときに ストリームではなく環境変数を参照するようにする。
自動受信スクリプトを手軽に行なえるように、十分な情報を環境変数の 形でスクリプトに渡す機能を備えたMTAは Postfix と qmail に限られる。 現在では多くのLinuxディストリビューションの標準MTAが Postfix になっているので、それを利用する前提で必要な追加設定を説明する。 使用しているメイルサーバのMTAが qmail の場合はすべての条件が揃っているのでとくに追加設定は必要ない。
Mail Transfer Agent の略でメイルの送受信を受け持つソフトウェアのこと。
ここでは、Postfix を利用する場合の追加設定を示す。システムへの Postfix の導入とサービスの起動は済ませているものとする。 サービスが正常に稼動していれば 自分宛のメイルが以下の操作により届くのが確認できるはずである。
echo Hello | mail -s Test-Mail $USER
from
IIMORI Hanako Test-Mail
この例はユーザのgecos名が IIMORI Hanako である場合を示している。もし、fromコマンドで自分宛のメッセージの Subject が出てこないようであればシステムのメイル配送サービスが稼動していない。 OSのマニュアルなどを参照して正常に設定する必要がある。
パスワードファイルのアカウントの一般的な情報を記述するフィールドの値。 通常は氏名などを記録する。
Postfixでは追加設定を行なうことにより、拡張メイルアドレス機能が使える。 まず、現状の値を確認する。
postconf | grep '^recipient_delimiter'
何も出力されなければ未設定である。この場合は以下のようにして、 拡張メイルアドレスの区切り文字をハイフン(-)に設定する。
sudo postconf -e recipient_delimiter=-
場合によっては、最初のgrep結果が以下の出力となるかもしれない。
recipient_delimiter = +
この場合は、既に拡張メイルアドレスの区切り文字が 「+」の状態で設定されているが、Postfixのバージョンによって対処が異なる。
sudo postconf -e recipient_delimiter=-
として区切り文字を - に変更するか、それができない場合は+のまま運用する。 + のままの場合は以降の説明の区切り文字を適宜 + に読み換える必要がある。
sudo postconf -e recipient_delimiter=+-
として区切り文字にハイフンを追加する。
続いて、拡張アドレスの機能を拡充するスクリプト dot-qmail をインストールする。
wget http://www.gentei.org/~yuuji/software/dotqmail/dotqmail
chmod +x dotqmail
dotqmail は qmail に由来する便利な拡張アドレス機能を Postfix でも使えるようにするスクリプトである。詳細は http://www.gentei.org/~yuuji/software/dotqmail/ にある。 また、wget コマンドがない場合は、「ftp」(BSDの場合)、 「curl -O」で代替できる。
dotqmail は zsh スクリプトなので、zsh もインストールする。
: Debian系(apt-get)の場合
sudo apt-get -y install zsh
: Arch Linuxの場合
sudo pacman -S zsh
: FreeBSDの場合
sudo pkg install zsh
dotqmail スクリプトは zsh が /usr/local/bin にインストールされる前提になっているが、そうではない(たとえば /bin/zsh)場合は dotqmail スクリプトの1行目(shbang行)を、 インストールされた zsh のパスに書き換える。
#!/usr/local/bin/zsh -f
以下に変更する(例)。
#!/bin/zsh -f
この先の説明は dotqmail をホームディレクトリにインストールした例で 進めるが、別の場所に配置したのであればそれに置き換えて構わない。
~/.forward ファイルに dotqmail による拡張アドレス処理を記述する。 ファイルを開き、以下の内容を書き込む。既に .forward ファイルがある場合は末尾の行に追加する。
"| ~/dotqmail"
これにより Postfix が受け取ったメイルアドレスが拡張アドレス形式に なっていた場合に、拡張子部分に応じた別々の宛先(今回の場合はスクリプト) に配送されるようになる。
Postfixの場合は前項の設定を完了すれば、qmailの場合は標準状態で 拡張アドレスに dot-qmail 機構が使えるようになっている。dot-qmail 機構は、一般ユーザ権限で任意個のメイルアドレスを自由に作成することができる。 たとえば、あるユーザのメイルアドレスが user@example.com だとする。user が ~/.qmail-foo というファイルを作成して宛先設定を書くと、 user-foo@example.com という拡張アドレスでメイルを受け取れるようになる。 また、~/.qmail-default というファイルに宛先設定を書くと user-*@example.com の * の部分を任意の単語にしたアドレス(かつ他に適合する ~/.qmail-* ファイルのないもの)でメイルを受け取れるようになる。
たとえば、ホームディレクトリに以下の3つの dot-qmail ファイルがあったとする。
.qmail-abc .qmail-abc-default .qmail-xyz
この場合、送信先アドレスと対応するファイルの関係は以下のようになる。
user-abc | .qmail-abcの記述先に届く |
user-xyz | .qmail-xyzの記述先に届く |
user-foo | 該当ファイルがないのでエラーメイルが返る |
user-abc-aaa | .qmail-abc-defaultの記述先に届く |
dot-qmail ファイルはホームディレクトリに作成する .qmail あるいは .qmail-単語 という名前のものであり、 1行に1つずつ配送先を定義する働きを持つ。 その書式は以下のいずれかである。
行頭文字 | はたらき | 例 |
---|---|---|
# | コメント(無視される) | # あいうえお |
| | プログラムの起動 | | ./program.sh arg |
& | 転送アドレス | &user2@example.co.jp |
. または / (スラッシュで終わらない) |
mbox形式のファイル | ./mbox |
. または / (スラッシュで終わらる) |
maildir形式のファイル | ./Maildir/ |
今回は3つめの「行頭 |」の書式を用いて、メイル自動受信による 自動採点システムの作成にあたる。
メイル受信によるプログラム起動では、以下の環境変数が自動的に設定され、 起動プログラムに渡される。
環境変数 | 値の意味 |
---|---|
SENDER | エンベロープsender |
RECIPIENT | 実受信者のアドレス |
HOST | 受信アドレスのドメイン部(@の後ろ) |
LOCAL | 受信アドレスのローカル部(@の前) |
EXT | 受信アドレスの拡張子部分 |
EXT2 | $EXTの1個目のハイフンより後ろの文字列 |
EXT3 | $EXTの2個目のハイフンより後ろの文字列 |
EXT4 | $EXTの3個目のハイフンより後ろの文字列 |
DEFAULT | dot-qmailファイルの "default" にマッチした文字列 |
この性質を利用すると、電子メイルによる課題提出では 以下の環境変数が有効利用できる。
$SENDER | taro@example.net |
$EXT | report-01-a |
$EXT2 | 01-a |
$EXT3 | a |
$SENDER の @ より前の部分を、提出者の学生ID、 $EXT2 の最初のハイフンより前の部分を講義の回に、 $EXT3 の値を選択肢の中から選んだ値として用いる。
ここでは極端に簡略化した採点規準に基づいたプログラムを作る。 プログラムから参照できる環境変数を利用して、以下のように採点する。
点を付ける学生ID | ${SENDER%%@*} |
講義回 | ${EXT2%%-*} |
選んだ選択肢 | $EXT3 |
得点 | dを選んだら10点、その他は8点 |
本当の採点処理であれば得点決定部分は重い意味を持つだろうが、 今回は大胆に簡略化する。 環境変数の加工で利用している %% と % は、 後続するパターンにマッチする部分の削除で、%% は最長マッチ削除、 % は最短マッチ削除である(「パラメータ置換時の部分文字列取得」参照)。たとえば SENDER の値が user@example.com であるとき、${SENDER%%@*} は、文字列のうち @* にマッチする部分を最長で削除する。@ が1個のときは最短マッチも最長マッチも 同じ結果になるが、@が2個以上あるときは(通常ありえないが)最初の @ 以降を削除する。
${EXT2%%-*} も同様の文字列末尾パターン削除で、たとえば EXT2 の値が 02-a だったときに -* にマッチするパターンを削除するので ${EXT2%%-*} は 02 となる。
以上の結果をデータベースに登録するには score.sh を使えばよい。 ここまでの工程をシェルスクリプト化すると以下のようになる(receiver.sh)。
#!/bin/sh
# EXT=report-01-a
# EXT2=01-a
# EXT3=a
PATH=/usr/local/sqlite3/bin:$PATH # 最新版のSQLite3起動のための設定など
mydir=`dirname $0` # このスクリプト自身の格納ディレクトリ
cd $mydir # そこへcdしておく
nlec=${EXT2%%-*} # 講義回
who=${SENDER%%@*} # 送信者の学生ID
case $EXT3 in # 得点をPTに代入する
d) PT=10 ;;
*) PT=8 ;;
esac
# score.sh 学生ID 講義回 得点
./score.sh $who $nlec $PT
exit 0 # メイル経由で動くプログラムは exit 0 しないとキューに溜る
続いて、ここで作成した receiver.sh を起動する設定に進む。
上記をふまえて提出用アドレスを決定し、それぞれの dot-qmail ファイルを作る。 今回は受信者のユーザ名を hanako とし、課題提出用の拡張子を report- とする。 意味は report-講義回-選択肢の一つとする。 この場合に作成すべき dot-qmail ファイルは以下のとおりである。
.qmail-report-01-default | 第1回の提出用 |
.qmail-report-02-default | 第2回の提出用 |
.qmail-report-03-default | 第3回の提出用 |
: | |
.qmail-report-15-default | 第15回の提出用 |
15回分の提出課題をすべて吸い込むような .qmail-report-default を作成してもよいが、その場合提出者が宛先を間違えてあらぬ講義回の 宛先に出してしまった場合などにもエラーなしで受信操作が行なわれ、 提出者が間違ったことに気づかず終わることになる。 これを好ましいとするならよいが、 間違ったらエラーを返した方がよい場合は上記のような ものにする。また上記の一覧では、選択肢部分を間違えても(たとえば report-01-x 宛に出しても)エラーなしで受信するが、 そこは間違えても正解にならないだけで、 提出したことに変わりはなく最低限の得点は与えるという判断である。
集計ディレクトリを ~/report と仮定して、必要な dot-qmail ファイルを作成する。以下に手順の例を示す。
cd # ホームディレクトリへ
echo "| report/receiver.sh" > .qmail-report-01-default
for n in 02 03 04 05 06 07 08 09 10 11 12 13 14 15; do
ln -s .qmail-report-01-default .qmail-report-$n-default
done
先述のように ~/report/ に集計用ファイルをまとめる。 mkdir ~/report してから以下のファイルを置く。
修正後のデータベース操作シェルスクリプト
学生ID一覧ファイル。このスクリプトの動作実験を行ないやすく するために、自分のメイルアドレスのローカル部をあらかじめ追加しておく。 以下の例では、設置者(つまり得点集計者)のメイルアドレスが hanako@example.com で、テスト用IDとして hanako を足すために、 以下の1行を students.csv に足した場合で説明を進める。
hanako,葛斗花子
読者自身のものをテストIDに足すためには以下のようにするとよいだろう (氏名 の部分は自分の名前にする)。
echo "$USER,氏名" >> students.csv
上記2ファイルを置いてデータベースの初期化を行なう。
./score.sh -i
実際に初期テーブルができているか確認する。
sqlite3 score.sq3
.schema
CREATE TABLE students(sid PRIMARY KEY, name);
CREATE TABLE lectpts(
sid, nlec, pts,
UNIQUE(sid, nlec),
FOREIGN KEY(sid) REFERENCES students(sid)
);
SELECT * FROM students;
C110123|公益太郎
C110134|飯森花子
C110138|高見台一
C110140|緑智子
hanako|葛斗花子
実際に自分の環境で確かめる場合には、自分のログイン名を ID とする行が students テーブルに含まれているか注意する。 同様の状態が確認できたら実際にレポートアドレス宛に送信してみる。
echo dummy report | mail -s Report hanako-report-01-a
宛先は自分のものに変更したうえで起動してみて、~/report/score.sq3 に得点が記録されていれば成功である。
./score.sh -s
hanako|葛斗花子|8
: (↑実際には自分のログイン名になる)
エラーの場合はシステムログ /var/log/mail.log (あるいはmaillogなど) にエラーメッセージが出るので確認する。
今回作成したシステムは採点部分は仮のものだが、 それ以外はほぼ実用に即したものである。データベースを操作するスクリプトと それを呼び出すスクリプトを合わせても100行程度のものにすぎない。 実際には異常入力への対処などで数倍の行数になるかもしれないが、 それでもコンパクトである。シェルスクリプトの記述性の高さもさることながら データの完全性はsqlite3が保証してくれる点、 データ操作記述の簡潔さはSQLがもたらしてくれる点が、 スクリプトのサイズ低減に大きく寄与している。
プログラムは短いほどバグを回避しやすくなる。 メイル自動応答システムを簡潔化している要点をまとめておく。
メイルに書かれているSubjectや本文の内容を見て振り分ける処理を 考えがちだが、人間が書いたテキストはゆらぎが多く、その解析は元来難しい。 課目名、講義回、選択肢など、変数一つで表現できそうな仕分けであれば、 それぞれ別々メイルアドレスで受けるようにしておけば、 複雑な解析処理の必要もなく、 送信者の誤りは送信エラーの形で送信者自身に通知される。
プログラムの挙動を変えるのにオプション指定などが利用されるが、 オプション指定を解析する部分も少なからず行数が必要となる。 動作に必要な値を環境変数から取得するようにすることで、 文法チェックの必要がなくなり、スクリプトを大幅に短縮できる。 ネットワークデーモンの子プロセスとして起動されるプロセスには デーモンが必要な情報を環境変数に設定してくれていることがあるので それを無駄なく使いたい。 ただしセキュリティの観点からいうと、 環境変数の値にはクライアントが送って来た情報に基づくものもあるので、 値を含む文字列を eval しないよう注意が必要である。 基本的には eval 無しで済むよう設計すべきである。
"eval" はシェルの内部コマンドで、引数に指定した文字列をそのまま シェルの実行文として評価するものであり、悪意を持ったユーザの入力した文字列を eval するとシステムを破壊するおそれにもつながる。 よって本稿で設計するシェルスクリプトでは一切 eval を使用していない。
詩作した得点集計システムでは、解答者が学生 ID と同じローカルパートを持つメイルアドレスから送信することを仮定したが、 メイルアドレスは複数持てるものである。あらかじめ自分の ID に結び付くメイルアドレスを登録しておけば、 そちらからも本人名義でレポート送信できるようになれば便利である。
score.sh は得点登録時に第1引数をそのまま ID として利用したがこれを改良し、 あらかじめ登録した文字列(送信者アドレス)に対応する ID があればそれに変換してから得点登録するように改良した score3.sh を作成せよ。なお、ID と送信者アドレスの対応表は以下のCSVファイルのように与えているものとする。
C110123,k.o.e.k.i.tarooooooo@codomo.example.org
C110134,flower-o_o-hanahana@easywww.example.net
C110138,h1h2h3h4hx4x.ab1225@hardbank.example.com
登録メイルアドレスを格納する emails テーブルを作成し、 ID に外部キー制約(students.id)を付けておく。
score.sh 得点登録時の第1引数($1)が学生 ID、 事前登録メイルアドレスどちらでも登録できるようにしたい。 それには
$1 と emails テーブルの第2(email)カラムを比較する:
一致するものがあればその行の第1(ID)カラムを、
なければ(NULLを返すなら) $1 をそのままを使う。
とすればよい。「A または、AがNULLならB」としたいときは SQL の coalesce 関数を利用する。
まずメイルアドレスと学生IDの対応表を作り、 続いて得点登録部分を変更するという流れで進める。
外部キー制約を設定して以下のようにテーブル作成する。
CREATE TABLE emails(
sid, email UNIQUE,
FOREIGN KEY(sid) REFERENCES students(sid));
emails.csv からインポートする。
.mode csv
.import emails.csv emails
ここでは固定的に作成する例を示したが、emails テーブルの値更新も自動化する工夫を考えてみるとよい。
score.sh の元の得点登録部分は次のようなものであった。
query "REPLACE INTO lectpts VALUES('$1', $2, $3);"
この '$1' の部分に値の選択を入れる。 $1 は receiver.sh によって渡される、メイルアドレスのローカルパートである。 よって、emails テーブルからの参照はローカルパートでの比較を LIKE 演算子を用いて行なう。
query "REPLACE INTO lectpts VALUES(
coalesce((SELECT sid FROM emails WHERE email LIKE '$1@%'),
'$1'),
$2, $3);"
以上2つの修正で個人アドレスからのレポート送信が可能となる。 スクリプトの修正点がなるべく少なくなるよう、receiver.sh からの score.sh の第1引数に 送信者アドレスのローカルパートを渡す仕様のままにしたため、 メイルアドレスから学生IDへの変換に LIKE 演算子を用いた。実際には全体表記のメイルアドレスでの比較をした方がよいだろう。
yuuji@koeki-u.ac.jp