たとえばまず、出席課題集計表の例を見てみよう。 表計算プログラムを使って手動でちまちま打ち込むのであれば、 おそらくこのような見出し行を作って管理するだろう。
学生ID | 氏名 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 合計 |
---|
そしてこれを念頭に置いて素直に RDB のテーブルを設計をすると以下のような定義をするかもしれない。
点数を横長に保持するテーブル設計
CREATE TABLE scores(
id TEXT PRIMARY KEY, name,
s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14, s15
-- 合計は計算で求めるのでカラムには含めない
);
しかしこのようなテーブル定義は間違いではないが柔軟な対応ができない。 たとえば、15回で終わらず補習のような回が必要になった場合だとか、 最初に決めた回数と異なる場合にどうすべきか困ることになる。 SQLite3では既存テーブルの最後のカラムの後ろにさらにカラムを 追加することはできるが、途中にはできない。
表計算用の見出し行(合計を除く)をよく観察すると、 それぞれの見出しは以下の3群に分類できる。
主キー | 主キーとつねに結びつく値 | 講義の「回」という共通した属性 | ||||||||||||||
学生ID | 氏名 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|
まず、 「更新管理しやすいものにする」 で述べたように主キーとつねに結びつくものは別の表に分ける。
studentsテーブル | ||
---|---|---|
sid | name | |
C110123 | 公益太郎 | |
C110134 | 飯森花子 | |
C110138 | 高見台一 | |
C110140 | 緑智子 |
さて、講義の「回」については、いずれも同じ性質のカラムであるが、 これは「講義の回」を表すカラムのみを作成し、その回に対応する得点を 格納するカラムを作る。つまり、以下のような格納形式とする。
lectptsテーブル | ||
---|---|---|
学生ID sid | 講義回 nlec | 得点 pts |
C110123 | 1 | 10 |
C110123 | 2 | 10 |
: : | ||
C110123 | 15 | 10 |
C110134 | 1 | 10 |
C110134 | 2 | 10 |
: : | ||
C110140 | 15 | 10 |
このようにすることで、合計得点を求めるなどの集計処理もしやすくなる。 上記の表を以下のようなテーブル定義で行なったとする。
CREATE TABLE students(sid PRIMARY KEY, name);
CREATE TABLE lectpts(sid, nlec, pts,
FOREIGN KEY(sid) REFERENCES students(sid));
学生ごとの得点合計を得たければ、集約関数sum()で、 ptsカラムの合計をsidごとに求めればよい(GROUP BY sid)。
SELECT sid,sum(pts) FROM lectpts GROUP BY sid;
C110123,140
C110134,150
C110138,80
C110140,148
SELECT l.sid,s.name,sum(l.pts)
FROM lectpts l JOIN students s ON l.sid=s.sid -- 別名定義: l=lectpts s=students
GROUP BY l.sid;
C110123|公益太郎|140
C110134|飯森花子|150
C110138|高見台一|80
C110140|緑智子|148
もしこれが点数を横長に保持するテーブル設計 の scores テーブルのような設計だとしたら、 学生ごとの合計点は以下のような問い合わせになる。
SELECT id,s1+s2+s3+s4+s5+s6+s7+s8+s9+s10+s11+s12+s13+s14+s15 FROM scores;
回数が15回だから何とかなるものの、さらに増えた場合を考えると 拡張性に乏しいことが想像できる。
表計算プログラムに慣れていると同種の値の並びを横方向に並べがちだが、 集約させたい場合は同じカラムに縦に並べる。
前節をふまえて、得点集計システムを自動的に行なうシェルスクリプト システムを構築しよう。課題の成績集計に限らず、 一つの主体が複数の得点的なものを持つような集計であれば同様の手法で構築できる。
SQLは慣れれば慣れるほどすばやく処理をこなせるようになるが、それでも less や grep コマンドを叩くときの手軽さには敵わない。 そこで、頻繁に必要になるSQL問い合わせをシェルスクリプトに組み込んで コマンドラインでの処理を効率化できるようにする。 また、シェルスクリプトを起動するだけで非対話的にデータの加除ができるようにしておくことで、 Web や Mail を利用した自動化もしやすくなる。
ここでは、作成するシェルスクリプト名を score.sh とし、 データベースに対する以下の処理への対応を考える。
初期化 | -i オプションでテーブルの初期化 |
登録 | 「学生ID」 「回」 「得点」をデータベースに登録 |
削除 | -d 「学生ID」 「回」 の記録を削除 |
集計表示 | -s オプションで学生ごとの得点集計を出力 |
特定学生の点数一覧表示 | -l 「学生ID」 で指定した学生の全得点を出力 |
「初期化」では、students テーブルへの登録学生一覧の読み込みも行なうものとし、 そのときの学生一覧は students.csv から読むものとする。 以上5つの処理はそれぞれ以下のような挙動を想定している。
: 初期化
./score.sh -i
: 登録 → C110123の3回目の得点を10点に
./score.sh C110123 3 10
: 削除 → C110123の4回目の記録を削除
./score.sh -d C110123 4
: 集計表示
./score.sh -l
C110123|公益太郎|140
C110134|飯森花子|150
C110138|高見台一|80
C110140|緑智子|148
: 特定学生の点数一覧
./score.sh -l C110123
sid 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
C110123 5 5 10 0 5 0 0 5 10 5 5 10 4 10 6
: 再度初期化
./score.sh -i
./score.sh -l
: 空になる
集計表のRDB化で示した2つの表を定義する(再掲)。
studentsテーブル | ||
---|---|---|
sid | name | |
C110123 | 公益太郎 | |
C110134 | 飯森花子 | |
C110138 | 高見台一 | |
C110140 | 緑智子 |
lectptsテーブル | ||
---|---|---|
学生ID sid | 講義回 nlec | 得点 pts |
C110123 | 1 | 10 |
C110123 | 2 | 10 |
: : |
ただし、シェルスクリプトからの得点情報の追加と修正が容易に行なえるよう lectptsテーブルにUNIQUE制約を設定する。 この件の場合、一人の学生がある特定の回(たとえば第3回)の得点は1つで なければならない。つまり、学生ID(sid)と講義回(nlec)の同一の組み合わせが 2つ以上記録されるべきではないという性質があるので、 この2つのカラムの組み合わせで UNIQUE 制約を設定する。 また、学生IDは何でもいいというわけではなく、students テーブルに含まれるものでなければならない。 まとめると、lectptsテーブルには以下の制約を付与する。
これを考慮して2つのテーブル定義を以下のように改める。
CREATE TABLE students(sid PRIMARY KEY, name);
CREATE TABLE lectpts(
sid, nlec, pts,
UNIQUE(sid, nlec), -- 制約1
FOREIGN KEY(sid) REFERENCES students(sid) -- 制約2
);
成績を登録するときに、テーブルを作成していない状態ではINSERT文が使えない。 INSERT文の問い合わせを行なう前に、 もしテーブルがない場合にCREATE TABLEをする処理を入れる必要がある。 このような場合、テーブルが存在しているか調べ、存在していなければ CREATE TABLEする処理が必要となる。
なお、SQLite3で特定のテーブルが存在するかだけを調べたい場合は、 SQLite3システム特殊テーブルの sqlite_master を調べるとよい。 sqlite_master テーブルは以下のような構造である。
.schema sqlite_master
CREATE TABLE sqlite_master (
type text,
name text,
tbl_name text,
rootpage integer,
sql text
);
処理中のデータベースファイルに定義されているすべてのテーブル構造が 格納されている。このうち name カラムがテーブル名を保持している。たとえば 上述の students テーブルが定義されているかを調べたいときは 「name='students'」という条件で sqlite_master テーブルを引く。
: ヘッダ出力をONに
.head on
: 存在しない場合は何も出ない
SELECT * FROM sqlite_master WHERE name='students';
: 存在する場合は定義が出る
SELECT * FROM sqlite_master WHERE name='students';
type|name|tbl_name|rootpage|sql
table|students|students|2|CREATE TABLE students(sid PRIMARY KEY, name)
sqlite_master テーブルには、実際に CREATE TABLE したときのSQL文が、空白や改行、コメント文そのままで格納される。
このことを用いて、「students テーブルが定義されていなければ2つのテーブル作成を行ない、 科目登録学生を students.csv ファイルからインポートする」 という処理を書くと以下のようになる。
CREATE TABLE までの処理
#!/bin/sh
db=score.sq3
query() {
sqlite3 -cmd 'PRAGMA foreign_keys=ON' $db "$@"
}
tbl=`query "SELECT name FROM sqlite_master WHERE name='students';"`
if [ -z "$tbl" ]; then # $tbl が空文字列ならテーブル未定義なので作成
query<<EOF
CREATE TABLE students(sid PRIMARY KEY, name);
CREATE TABLE lectpts(
sid, nlec, pts,
UNIQUE(sid, nlec),
FOREIGN KEY(sid) REFERENCES students(sid)
);
EOF
if [ -s students.csv ]; then # students.csv ファイルが空でなければ
query<<EOF # CSVモードに切り替えてインポート
.mode csv
.import students.csv students
EOF
fi
fi
なお、SQLite3では「テーブルが定義されていなければ定義する」 という処理を以下の1文で行なえる。
CREATE TABLE IF NOT EXISTS テーブル定義...
これまで使用した「CREATE TABLE」とテーブル名の間に IF NOT EXISTS を挟むことで、テーブルが存在しない場合のみ CREATE TABLE する。 もし、これを用いて students、lectpts テーブルを作成するなら以下のように書くことになる。
CREATE TABLE IF NOT EXISTS students(sid PRIMARY KEY, name);
CREATE TABLE IF NOT EXISTS lectpts(
sid, nlec, pts,
UNIQUE(sid, nlec),
FOREIGN KEY(sid) REFERENCES students(sid)
);
シェルスクリプト的にはこの方がコンパクトになる。
表「データベースに対する処理への対応」 にあるように、 得点登録のとき、score.sh は3つの引数を、$1=「学生ID」、$2=「回」、$3=「得点」 のように受けるので登録は以下のようなスクリプトとなる。
db=score.sq3
sqlite3 $db "REPLACE INTO lectpts VALUES('$1', $2, $3);"
lectpts テーブルの第1カラム(sid)と第2カラム(nlec)には UNIQUE 制約があるので、同じ学生同じ回の得点を繰り返し入力したときは REPLACE により上書きされる。
「./score.sh -d 「学生ID」 「回」 としたときに、該当するものを削除するのは以下のようになる(シェル関数 query は前述のものを使用)。
query "DELETE FROM lectpts WHERE sid='$1' AND nlec=$2;"
該当する sid と nlec の組み合わせがデータベースに登録されていない場合は 削除が起きないだけでエラーにはならない。
学生ごとの得点合計を求めたとき に用いたSQL文を利用する。
SELECT l.sid,s.name,sum(l.pts)
FROM lectpts l JOIN students s ON l.sid=s.sid
GROUP BY l.sid;
ここまでの部分をシェルスクリプトにまとめると以下のようになる。
#!/bin/sh
db=score.sq3
query() {
sqlite3 -cmd 'PRAGMA foreign_keys=ON' $db "$@"
}
tbl=`query "SELECT name FROM sqlite_master WHERE name='students';"`
if [ -z "$tbl" ]; then # $tbl が空文字列なら
query<<EOF
CREATE TABLE students(sid PRIMARY KEY, name);
CREATE TABLE lectpts(
sid, nlec, pts,
UNIQUE(sid, nlec),
FOREIGN KEY(sid) REFERENCES students(sid)
);
EOF
if [ -s students.csv ]; then # students.csv ファイルが空でなければ
query<<EOF # CSVモードに切り替えてインポート
.mode csv
.import students.csv students
EOF
fi
fi
if [ x"$1" = x"-d" -a -n "$2" ]; then # -d オプションなら削除
query "DELETE FROM lectpts WHERE sid='$1' AND nlec=$2;"
elif [ x"$1" = x"-l" ]; then # -l オプションなら集計出力
query<<-EOF
SELECT l.sid,s.name,sum(l.pts)
FROM lectpts l JOIN students s ON l.sid=s.sid
GROUP BY l.sid;
EOF
elif [ -n "$3" ]; then # 第3引数まで与えられたら登録
sqlite3 $db "REPLACE INTO lectpts VALUES('$1', $2, $3);"
fi
点数一覧表示はすこし考察と工夫を要する。
まず、例としてデータベース初期化状態から、 以下のように点数を登録した状態を考える。
./score.sh C110123 1 10
./score.sh C110138 1 5
./score.sh C110123 2 10
./score.sh C110138 2 5
./score.sh C110123 3 8
./score.sh C110138 3 10
./score.sh C110123 4 10
./score.sh C110123 5 8
./score.sh C110138 5 5
下から2行目と3行目を見ると分かるが、C110138が第4回目を欠席し、 得点が登録されていない状態となっている。つまり、次の表のような状態である。
学生ID\回 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
C110123 | 10 | 10 | 8 | 10 | 8 |
C110138 | 5 | 5 | 10 | NULL | 5 |
状況としては、「第5回まで進んだ状態で、C110123番は全提出、 C110138は第4回を欠席した」という途中経過である。この状態から、 欠席を含むC110138の得点一覧を
sid 1 2 3 4 5
C110138 5 5 10 0 5
のような形式で得るためには、第4回の欠席によってその回の得点が記録されていない ことにうまく対処する必要がある。まず、C110138 の得点一覧を得る問い合わせを行なってみる。
sqlite3 -header -column score.sq3
SELECT * FROM lectpts WHERE sid='C110138';
sid nlec pts
---------- ---------- ----------
C110138 1 5
C110138 2 5
C110138 3 10
C110138 5 5
この学生の記録だけを調べたのでは第4回の値が得られない。 「これまでに点数が記録された講義回すべて」を得るためには、sid を問わずにすべてのレコードからnlecを抽出する。 nlecに格納されているすべての値を重複なしで取り出すには DISTINCT 句を用いる。
DISTINCT を付けない問い合わせも試してみよ。
SELECT DISTINCT nlec FROM lectpts ORDER BY nlec;
nlec
----------
1
2
3
4
5
この問い合わせで「講義回」のすべての値を得てから、 該当学生のその回の得点を得ればよい。そのためには シェルスクリプトで頑張るか、SQLで頑張るかいずれかとなる。
続く節では、シェル変数 id に "C110138" が、 nlecs に上記のSELECT文の結果が
nlecs=`sqlite3 score.sq3 "SELECT DISTINCT nlec FROM lectpts ORDER BY nlec;"`
のように代入されている状態、つまり 1、2、3、4、5 が改行区切りで代入されている状態から、4回目欠席学生 C110138 の得点リスト行を生成する方法2とおりを説明する。
シェル変数 nlecs にある値を1つずつ取り出し、 該当学生のその回の得点を取り出すループを書けばよい。 得点が得られなければ(空文字列ならば)、0点として処理する。
for n in $nlecs; do
p=`sqlite3 score.sq3 "SELECT pts FROM lectpts WHERE sid='$id' AND nlec=$n";`
printf ' %2d' ${p:-0}
done
zshのコマンドラインでこの for 文を試すときは、$nlecs の代わりに ${=nlecs} とする。zshのデフォルトでは変数の値に空白文字を含む場合でも 1単語として展開する。空白ごとに単語分解して欲しいときには変数展開時に = フラグを付加する。
printf(コマンド) ここで用いている printf は、Cライブラリの printf() 関数とほぼ同じ働きをするコマンドで、%2d は後続引数の整数を2桁幅で整形出力してくれる。 後続引数 ${p:-0} は、シェル変数 p の値が空(または未定義)なら "0"、 そうでなければpの値に置換される表記(「パラメータ展開」参照)なので、 今回の場合第4回の得点問い合わせの結果が空になったときに "0" が出力される。 このようにすることで、 上記の for 文からは以下のような出力が得られる。
5 5 10 0 5
先述のように、どこかの回を欠席した学生の得点一覧を得ようとしても、 欠席回が抜けた状態になる。このような場合は、講義回だけを集めたテーブルと 該当学生の講義回と得点を集めたテーブルを結合(JOIN)操作する。
全講義回 t1
講義回 nlec |
---|
1 |
2 |
3 |
4 |
5 |
×
C110138の得点一覧 t2
講義回 nlec | 得点 pts |
---|---|
1 | 5 |
2 | 5 |
3 | 10 |
5 | 5 |
左の表を t1、右の表を t2 としたときに、 t1.nlec=t2.nlec という条件で結合すればよい。このとき、単なる JOIN では、右の表にない値での結合行は生成されないが、 左結合(LEFT JOIN)を使うことで、左の表にあるnlecがすべて選択される。
sqlite3 -header -column score.sq3
SELECT t1.nlec, t2.pts
FROM (SELECT DISTINCT nlec FROM lectpts) t1 -- 講義回のみ抽出して t1 とする
LEFT JOIN
-- 該当学生のnlec,ptsを抽出して t2 とする
(SELECT nlec, pts FROM lectpts WHERE sid='C110138') t2
ON t1.nlec=t2.nlec
ORDER BY t1.nlec;
nlec pts
---------- ----------
1 5
2 5
3 10
4
5 5
nlec=4に相当するptsの値はNULLである。今回は「NULLの場合は0」 で置き換えてもらいたいので、SQLの標準関数 coalesce() を使用する。 coalesce 関数は任意個の引数を取り、初めて現れるNULLでない値を返す。 上記のSQL文 1行目の t2.pts がNULLだった場合に備え、coalesce 経由に置き換える。
SELECT t1.nlec, coalesce(t2.pts, 0)
FROM (SELECT DISTINCT nlec FROM lectpts) t1
LEFT JOIN
(SELECT nlec, pts FROM lectpts WHERE sid='C110138') t2
ON t1.nlec=t2.nlec
ORDER BY t1.nlec;
nlec coalesce(t2.pts, 0)
---------- ----------
1 5
2 5
3 10
4 0
5 5
このSQL文であればどの学生に対しても、欠席により登録のない回の得点を 0で返す。シェルスクリプトで頑張る方法で示したのと 同じ出力を返すように変更して シェルスクリプト化すると以下のようになる。
sqlite3 score.sq3 <<EOF | tr -d '\n' # trで改行削除
SELECT printf(" %2d", coalesce(t2.pts, 0))
FROM (SELECT DISTINCT nlec FROM lectpts) t1
LEFT JOIN
(SELECT nlec, pts FROM lectpts WHERE sid='C110138') t2
ON t1.nlec=t2.nlec
ORDER BY t1.nlec;
EOF
ここで新しく利用した SQLite3 の printf 関数について説明する。 printf(SQLite関数)
SQLite3 の printf は、Cライブラリの printf とほぼ同じ働きをする関数である。カラムの値を printf 書式の %2d つまり2桁幅の10進数で出力する。この出力結果から tr コマンドにより改行文字(\n)を削除することで以下の出力が得られる。
5 5 10 0 5
なお、sqlite3 3.8.6 で導入された -newline オプションで、SQLの問い合わせ結果の行区切り(デフォルトで改行文字) を変更できる。-newline "" とオプション指定して、改行区切りを空文字列に指定することで、tr を利用しなくても複数レコードの出力を1行内に展開できる。
もうひとつ SQLite 固有の集約関数 group_concat を利用する方法を紹介する。
group_concat(X)
group_concat(X, Y)
結果として得られるすべての X を文字列 Y を区切りとして単一文字列化する。第2引数を省略するとカンマ(,)で区切る。
これを用いて上記問い合わせ文を書き変えると以下のようになる。
sqlite3 score.sq3 <<EOF
SELECT group_concat(printf("%2d", coalesce(t2.pts, 0)), ' ')
FROM (SELECT DISTINCT nlec FROM lectpts) t1
LEFT JOIN
(SELECT nlec, pts FROM lectpts WHERE sid='C110138') t2
ON t1.nlec=t2.nlec
ORDER BY t1.nlec;
EOF
最後に、登録データを「ご破算」にする処理を作る。 SQLite3ではテーブルの有無を調べてテーブルの削除を行なえる IF EXISTS を利用すると単純化できる。
DROP TABLE IF EXISTS テーブル...;
テーブル が存在するときのみテーブル削除を行なう。 したがって、以下のようにすればテーブルの有無にかかわらず 2つのテーブルを確実に削除できる。
DROP TABLE IF EXISTS students;
DROP TABLE IF EXISTS lectpts;
以上の考察をまとめて、仕様を満たすシェルスクリプト score.sh の完成例を示す。
#!/bin/sh
db=score.sq3
query() {
sqlite3 -cmd 'PRAGMA foreign_keys=ON' $db "$@"
}
create() { # テーブルの作成と初期データ登録を行なう関数
tbl=`query "SELECT name FROM sqlite_master WHERE name='students';"`
if [ -z "$tbl" ]; then # $tbl が空文字列なら
query<<-EOF
CREATE TABLE students(sid PRIMARY KEY, name);
CREATE TABLE lectpts(
sid, nlec, pts,
UNIQUE(sid, nlec),
FOREIGN KEY(sid) REFERENCES students(sid)
);
EOF
if [ -s students.csv ]; then # students.csv ファイルが空でなければ
query<<-EOF
.mode csv
.import students.csv students
EOF
fi
fi
}
drop() { # テーブル削除を行なう関数
query<<-EOF
-- 外部キー制約があるので子テーブルを先に削除
DROP TABLE IF EXISTS lectpts;
DROP TABLE IF EXISTS students;
EOF
create # 再初期化
}
delete() { # レコードの削除を行なう関数
query "DELETE FROM lectpts WHERE sid='$1' AND nlec=$2;"
}
summary() { # 集計表を出す関数
query<<-EOF
SELECT l.sid,s.name,sum(l.pts)
FROM lectpts l JOIN students s ON l.sid=s.sid
GROUP BY l.sid;
EOF
}
list() { # 指定した学生の得点リストを出す関数
# ヘッダを出す
printf "sid\t"
query<<-EOF
SELECT group_concat(printf('%2d', nlec), ' ')
FROM (SELECT DISTINCT nlec FROM lectpts ORDER BY nlec);
EOF
# 続いて $1 に指定した学生の得点を横並びで出す
printf "$1\t"
query<<-EOF
SELECT group_concat(printf('%2d', coalesce(b.pts,0)), ' ')
FROM (SELECT DISTINCT nlec FROM lectpts ORDER BY nlec) a
LEFT JOIN (SELECT nlec,pts FROM lectpts WHERE sid='$1') b
ON a.nlec=b.nlec;
EOF
}
while getopts ilsd i; do # i l s d オプションが有効
case "$i" in
i) job=drop ;; # -i が指定されたら... 以下同様
l) job=list ;;
s) job=summary ;;
d) job=delete ;;
\?) exit 1 ;; # 無効なオプションは異常終了
esac
done
shift $((OPTIND-1)) # オプション解析の終わった引数をずらす
case $job in
"") # オプション指定なしの場合
if [ -n "$3" ]; then # 第3引数=点数 まで与えられたら登録処理
query "REPLACE INTO lectpts VALUES('$1', $2, $3);"
fi
;;
*)
$job "$@"
esac
簡単に実行例を示す。データベース中のレコードは以下の状態である。
sqlite3 score.sq3 'SELECT * FROM lectpts ORDER BY sid'
C110123|1|10
C110123|2|10
C110123|3|8
C110123|4|10
C110123|5|8
C110138|1|5
C110138|2|5
C110138|3|10
C110138|5|5
sqlite3 score.sq3 'SELECT * FROM students ORDER BY sid'
C110123|公益太郎
C110134|飯森花子
C110138|高見台一
C110140|緑智子
-s オプションでのサマリ出力:
./score.sh -s
C110123|公益太郎|46
C110138|高見台一|25
-l オプションで学生IDを指定してのリスト出力:
./score.sh -l C110138
sid 1 2 3 4 5
C110138 5 5 10 0 5
getopts はしばしば利用するので、ここで練習しておこう。
前述の score.sh に -L オプションを追加し、-l と同じ横長の得点一覧を出せるようにしたスクリプト score2.sh を作成せよ。ただし、学生IDの部分は 氏名に置き換えて出力するものとする。なお、存在しない ID などのエラー処理は特にしなくてよい。
まず -L オプションの追加から。getopts の引数解析に L を追加する。
while getopts iLlsd i; do # i L l s d オプションが有効
case "$i" in
i) job=drop ;; # -i が指定されたら... 以下同様
l) job=list ;;
L) job=list2 ;;
s) job=summary ;;
d) job=delete ;;
\?) exit 1 ;; # 無効なオプションは異常終了
esac
done
続いて、上のリストで名前を決めたシェル関数 list2 を考える。
ほぼ list 関数と同じで、学生IDの部分を氏名に置き換える。 これには score.sh の list 関数ほぼそのままで、見出しと引数出力の部分を変えればよい。
list2() {
# ヘッダを出す
printf "氏名\t"
query<<-EOF
SELECT group_concat(printf('%2d', nlec), ' ')
FROM (SELECT DISTINCT nlec FROM lectpts ORDER BY nlec);
EOF
# $1 に指定した学生の氏名を取得
sname=`query "SELECT name FROM students WHERE sid='$1';"`
printf "$sname\t"
query<<-EOF
SELECT group_concat(printf('%2d', coalesce(b.pts,0)), ' ')
FROM (SELECT DISTINCT nlec FROM lectpts ORDER BY nlec) a
LEFT JOIN (SELECT nlec,pts FROM lectpts WHERE sid='$1') b
ON a.nlec=b.nlec;
EOF
}
この書き変えにより学生 ID の代わりに氏名が見出しに登場するようになる。 実際に実行してみると結果は以下のようになる。
./score2.sh -L C110138
氏名 1 2 3 4 5
高見台一 5 5 10 0 5
これは桁揃えをTABに任せている結果である。「printf "氏名\t"」の部分の TAB(\t)を2個にすればこの例(高見台一)では桁が揃うが、 仮に「氏名\t\t」にしたとすると、逆に3文字の氏名の場合に以下のようにずれる。
氏名 1 2 3 4 5
緑智子 8 9 10 10 7
テキスト出力画面に対して根本的に桁を合わせるのは UTF-8 をベースとしたシステムでは難しい。 桁揃えが重要な場合は HTML 出力モードにするなどの工夫が必要だろう。
yuuji@koeki-u.ac.jp