この章の目標

準備体操

まず始めにシェルスクリプト設計のウォーミングアップとして、 単純なテキストファイルで表現できるものでありながら、 重複データの排除や排他制御の必要を考えることで幅広い視野が得られる題材として 簡単な電子投票システムを作ってみる。この章の目標としては RDB を組み込んだシステムが前提となるが、 まずはシェルスクリプトとフィルタコマンドのみでデータを管理する簡単な練習をしておく。

想定する投票操作

まずはどのような仕様で投票を集計するのかまとめておく。

集計仕様

実際の選挙は無記名投票が多いが、 ここで設計する電子投票の場合は多重投票を防ぐために、投票者の ID を記録しておく。例として、投票者には id001、id002、id003、... のような投票用 ID を割り当てるものとする。 また、投票対象となる候補は「赤」、「青」、「黄」の3つのうち1つであるとする。 この条件で、「投票用ID」と「投票対象」を受け取り、 どの候補がどれだけ得票したかを集計するシステムを作りたい。

コマンドライン仕様

簡単なインタフェースで始める。コマンドラインで次のように 投票者と候補を指定して起動することを想定する。

vote.sh 投票用ID 候補

テキストファイルによる集計スクリプト例

いきなりSQLに入る前に、 仮にこの集計をテキストファイルのみでやったらどうなるか考えてみよう。 多重投票を考慮しないのであれば、「投票者」、「投票対象」を列挙した CSV ファイルを作ればよい。

vote.csv

id002,青
id004,赤
id001,赤
id007,黄
id005,青

これをシェルスクリプトで作るなら、たとえば

echo $voter,$cand >> vote.csv

のような1行スクリプトでデータベースができ上がる。 ただしこれは多重投票が起きたときの上書きができない。つまり、 id002 の投票者が再投票で「赤」に投票したときに、

vote-dup.csv

id002,青
id004,赤
id001,赤
id007,黄
id005,青
id002,赤

となるのだが、1行目の「id002,青」を無効化しなければならない。 これへの対処を異なる側面から考えてみる。

先に除外してからCSVファイルに追加

id002 からの2回目の投票を記録する前に、第1カラムが "id002" の行を削っておく。これには grep コマンドや sed が使える。 シェル変数 voter に投票用IDが入っているとすると、

grep -v "^$voter," vote.csv > vote-new.csv

と grep の -v オプションで該当行を除外するか、

sed "/^$voter,/d" vote.csv > vote-new.csv

と、sed の d コマンドで該当行削除するか いずれかの方法で投票者の過去の投票が削除されたものが vote-new.csv に入る。このファイルに最新投票を追記すればよい。 これらの手順をまとめると以下のようになる(sed コマンドを使用)。

{ sed "/^$voter,/d" vote.csv
  echo "$voter,$cand"; } > vote-new.csv

(以上いずれも変数 voter と cand が適切な値であることを確認済みとする)

vote-new.csv を元の vote.csv に書き戻せば処理は完了する。

テキストエディタで重複削除してから書き込む

sed は Stream Editor の略で、 あくまでも入力ストリームに対する「編集」用であり、 非対話的に用いるのに向いている。 いっぽう、ファイルそのものを編集する対話的エディタは自動処理用に 非対話的に使えないかというとそうではない。Stream でないエディタ、つまり ed は標準入力にコマンドを流し込むことで非対話的にファイル編集ができる。

たとえば、ファイルが上掲の vote-dup.csv のような状態のときに、voter="id002"、cand="黄" の投票が来たとする。 このときに ed を使って以下の手順で操作すると重複なき更新ができる

ed の種類によっては保存終了を wq で行なえず、 w と q に分けなければならないものもある。

手順edコマンド
行頭に "id002," とある行をすべて削除 g/^id002,/d
ファイル末尾に "id002,黄" を追加 a
id002,黄
.
保存終了 wq

これをシェル変数 voter、cand で書き直すと次のようになる。

ed vote.csv<<EOF 2> /dev/null
g/^$voter,/d
a
$voter,$cand
.
wq
EOF

ed は保存終了時に、書き込みバイト数などの統計情報を標準エラー出力する。 これを捨てるため 2>/dev/null している(※)

ed -s オプションで統計情報出力を抑止できるものもある。

冒頭のコマンドライン仕様に沿うよう以上の仕組みをまとめてスクリプト化する。

vote-text.sh

#!/bin/sh
if [ -z "$1" -o -z "$2" ]; then	# 第1、第2引数ともにNULLでないことを確認
  cat<<-USAGE
	Usage: $0 "投票用ID" "候補"
	例: $0 id000 黒
	USAGE
  exit 1			# 引数不備時にはUsageを出して終了
fi
voter=$1
cand=$2
# 次の行: 「-EOF」とすると行頭のTABを除いたものが標準入力となる
if ed vote.csv<<-EOF 2> /dev/null
	g/^$voter,/d
	a
	$voter,$cand
	.
	wq
	EOF
then echo "投票処理完了"	# edコマンド正常終了
else echo "投票処理失敗"	# edコマンド異常終了
fi 1>&2		# if文内のecho(上記2つ)を標準エラー出力に向ける

ただし、このスクリプトには排他制御がないという問題点がある。 このスクリプトを動かすのが1人だけであれば問題ない。 複数人で、もしくは自動的に起動する仕組の一部として動くなら 同時に2つのプロセスがファイル更新しないよう排他機構を付ける必要がある。 ed コマンドの代わりに、ロック機構を備えた ex コマンドを用いることも可能で、その場合ファイルへの同時アクセスでは 一瞬遅い方がエラーとなる。ただし、エラーとなっても投票を捨てるわけには いかないので、再試行する処理などを盛り込まねばならない。

このような問題を抱えない形式のデータ管理方式を次で見てみよう。

値の重複が起きないデータ形式の利用

単一ファイルにデータを記録するから重複登録が起きるのであって、 最初から重複が起きないよう別ファイルに登録すればよい。 投票用IDは英数字のみで構成されているためファイル名として利用できる。 この性質を利用して、投票用IDと同名のファイルに投票対象を記録する。

[ -d ballotdir ] || mkdir ballotdir	# 投票箱ディレクトリ、なければ作る
echo "$cand" > ballotdir/$voter	# 票は投票者ごとの別ファイルに保存

このようにすることで、投票者が再投票を行なう度に票は上書きされ、 1人1票は確実に保証できる。また、排他制御について、 投票を保持するファイルは別々なので同時投票があっても問題は起きない。 問題が起こるとすれば、1つの投票ファイルに複数プロセスが書き込む場合で、 それは1人の人が2箇所から同時に更新した場合であり、 「本人投票」という常識からは通常考えられず、 起きたとしたらその世界での投票そのものを根本から見直す必要があり、 それは処理スクリプトが面倒を見るべきレベルではない。

テキストファイル法の難しさ

テキストファイルでデータを管理するスクリプトを示したが、 これらの作りには問題点がある。

いずれも、取り扱う集計の性質をしっかり考慮すればテキストファイル管理のままでも解決できる問題である。 しかしここでは、規模の大小にかかわらず統一的に対処できる方法の一つとして SQLite を使った方法を示していく。

yuuji@koeki-u.ac.jp