(いつも通り、ツッコミ歓迎です)
問題
例えば 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 にタブを設定した場合 % IFS=$'\t' read -A foo <<<$'foo\t\t\tbar' # 配列の中は2要素 % print $#foo 2 % print -l "$foo[@]" foo bar % # IFS にカンマを設定した場合 % IFS=, read -A foo <<<"foo,,,bar" # 配列の中は 4 要素 % 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 # 変数 $line の中身を s(split) フラグで分割 # タブをエスケープシーケンスで \t と書きたいので p (print) フラグを立てる # 空文字列を残したいので全体を "" で囲みつつ @ フラグも立てる 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';