(いつも通り、ツッコミ歓迎です)
問題
例えば TSV (タブ区切りテキスト)形式の正誤表があり、そこから SQLite のデータベースファイルへ更新をかけたい…
そんな時、皆さんだったらどうしますか? なお、レコード数は1,000行程度とします。
私はこういうケースだと Zsh を使って雑に UPDATE 文の列を作ることが多いです。
ただ、雑に書きすぎると入力の形式によっては上手く行かないので、その辺りをまとめてみました。
TL;DR IFS で分けるな、一行丸々 IFS= read -r line
で読み込んでから col=( "${(@ps:\t:)line}" )
で配列に分けよう。
なお、SQLite は文字列をシングルクォートで囲って表現しますが、その中にシングルクォートが出現する時は ''
のように二重にする必要があります。
今回は、この処理に以下の関数を使います。この SQL文字列をクォートする仕組みの解説は以前の日記を参照して下さい
function zqsql {
local val=$1
print -nr "${${(qq)val}//'\''/''}"
}
部分的にしか動かない方法: IFS を使う
まず、read と IFS の組み合わせを使う方法を書いてみました。
入力ファイル input1.tsv
はこんな内容です。
id old new
1111 ああああ かかかか
2222 いいいい きききき
3333 うううう くくくく
実行
% i=0; while IFS=$'\t' read -r id old new; do
((i++)) || continue
print "update t set name = $(zqsql $new) where id = $(zqsql $id);"
done < input1.tsv
update t set name = 'かかかか' where id = '1111';
update t set name = 'きききき' where id = '2222';
update t set name = 'くくくく' where id = '3333';
残念ながら、このやり方だと次の input2.tsv
のように、入力に空文字列が入るケースで困ります。
id flag1 old flag2 new
1111 foo ああああ bar かかかか
2222 1 いいいい きききき
3333 うううう 2 くくくく
実行の様子です。 1111
は正常ですが、 2222
, 3333
の update 文は期待と違います。
% i=0; while IFS=$'\t' read -r id _ old _ new; do
((i++)) || continue
print "update t set name = $(zqsql $new) where id = $(zqsql $id);"
done < input2.tsv
update t set name = 'かかかか' where id = '1111';
update t set name = '' where id = '2222';
update t set name = '' where id = '3333';
これは $IFS
に(タブなどの)空白文字を設定した場合、複数の空白文字の連なりを一つのフィールド区切りとして認識しているから、でしょう。
% IFS=$'\t' read -A foo <<<$'foo\t\t\tbar'
% print $#foo
2
% print -l "$foo[@]"
foo
bar
%
% IFS=, read -A foo <<<"foo,,,bar"
% print $#foo
4
% print -l "$foo[@]"
foo
bar
%
解決策:IFS では分割せずに、読み込み後に分割する
変数展開フラグの s:string:
を使いました。
以下 zsh 公式マニュアルの引用です。
s:STRING:
Force field splitting at the separator STRING. Note that a STRING
of two or more characters means that all of them must match in
sequence; this differs from the treatment of two or more characters
in the IFS parameter. See also the = flag and the SH_WORD_SPLIT
option. An empty string may also be given in which case every
character will be a separate element.
For historical reasons, the usual behaviour that empty array
elements are retained inside double quotes is disabled for arrays
generated by splitting; hence the following:
line="one::three"
print -l "${(s.:.)line}"
produces two lines of output for one and three and elides the empty
field. To override this behaviour, supply the '(@)' flag as well,
i.e. "${(@s.:.)line}".
今回の例に適用すると、こんな感じになります。
% i=0; while IFS= read -r line; do
((i++)) || continue
col=("${(@ps:\t:)line}")
print "update t set name = $(zqsql $col[5]) where id = $(zqsql $col[1]);"
done < input2.tsv
update t set name = 'かかかか' where id = '1111';
update t set name = 'きききき' where id = '2222';
update t set name = 'くくくく' where id = '3333';