重要なファイルを上書き更新するシェルスクリプトを扱う。ファイルを更新するということは「覆水盆に返らず」なので、それなりに気を使うべきだ。
UNIX系OSにおいては、コマンドを実行することで既存の元ファイルの内容を変更するようなコマンドは少数派だ。sed(1)やnkf(1)コマンドは上書きができるが、オプションを指定しないと上書きモードにならない。基本的には新しいファイルに、変更後の内容を書き出す。
$ command file
ファイル上書きをむやみに許してしまうと、次のようになにかと不都合だ。
パイプ接続できるコマンドとできないコマンドの識別する労力が増大なにか失敗すると後戻り不可能。あるいは面倒
ファイルを上書きしたいときは、面倒でも次のような手続きを踏む。
$ command file > file.new $ mv file.new file
もし必要なら、mv(1)の前にdiff(1)をとって確認したり、もとのファイルのバックアップをとったりする。一度別のファイルに結果を出力してからmv(1)する方法は合理的だ。
前回に引き続き、架空の団体UPS友の会の会員管理を扱う。前回は手動で会員リストを操作したが、今回は会員リストへの新会員の追加処理をシェルスクリプト化する。シェルスクリプトでは、会員リストを上書きするときに会員リストを壊さないように、さまざまな仕掛けをする。
ディレクトリを準備をする。適当な場所に、リスト2のようにディレクトリを掘る。
UPSTOMO/ ├── DATA │ └── MEMBER ├── SCR │ └── ADDMEMBER └── newmember
SCR: シェルスクリプト (ADDMEMBERファイル) 置き場DATA:会員リスト (MEMBERファイル) の置き場所
newmemberは、新しく追加する会員のリストで、一時的なのものだ。
$ cat newmember 門田 kadota@paa-league.net 香川 kagawa@dokaben.com $
MEMBERファイルには、次のような既存会員データが記録されている。このファイルが原本となる。
$ head -n 5 ./DATA/MEMBER 10000001 上田 ueda@hogehoge.com 19720103 - 10000002 濱田 hamada@nullnull.com 19831102 - 10000003 武田 takeda@takenaka.com 19930815 20120104 10000004 竹中 takenaka@takeda.com 19980423 - 10000005 田中 tanaka@kakuei.jp 20000111 - $
1:会員番号 2:氏名 (簡略化のため姓のみ) 3:e-mailアドレス 4:入会処理日 5:退会処理日
newmemberファイルのデータに会員番号と入会処理日をつけて、MEMBERファイルに追記するのが、ADDMEMBERの役目となる。MEMBERファイルを壊してはいけないので、入力をチェックしてから追記処理を実施する。
まず、シェルスクリプトADDMEMBERに、リスト5のようにエラーを検知する仕組みを書く。
#!/usr/bin/env bash tmp=/home/ueda/tmp/$$ CHECK(){ [ -z "$(echo ${PIPESTATUS[@]} | tr -d '0 ')" ] && return echo "エラー: $1" 1>&2 echo "処理できませんでした" 1>&2 rm -f $tmp-* exit 1 } #テスト true | true CHECK "trueで成功" true | false CHECK "falseで失敗" rm -f $tmp-* exit 0
5~12行目はbashの関数だ。書き方はリスト5のように、名前(){処理}となる。呼び出し方はコマンドと一緒で、名前を行頭に書く。引数は()内で定義せず、関数内で$1、$2、...と呼び出す。
エラーメッセージは、標準エラー出力させる。8、9行目のように、1>&2と書くことで、echoの出力先を標準エラー出力にリダイレクトできる。
${PIPESTATUS[@]}は、パイプでつながったコマンドの終了ステータスを記録した文字列に置き換わる。終了ステータスは、コマンドが成功したかどうかを示す値で、コマンドが終わると変数$?にセットされる。ただし、$?には1つの終了ステータスしか記録できない。これに対しbashでは、PIPESTATUSという配列に、パイプでつながったコマンドの終了ステータスを記録できるようになっている。
$ true $ echo $? 0 $ false $ echo $? 1 $ true | true | true | true $ echo ${PIPESTATUS[@]} 0 0 0 0 $ true | true | false | true $ echo ${PIPESTATUS[@]} 0 0 1 0 $ true $ echo ${PIPESTATUS[@]} 0 $
$(echo ${PIPESTATUS[@]} | tr -d '0 ')は、「文字列${PIPESTATUS[@]}をtr(1)に送って、0と半角空白を取り除いた文字列」となる。$()は、括弧中のコマンドの標準出力を文字列として置き換えるための表記方法だ。${PIPESTATUS[@]}から0と空白を除去すれば、コマンドの終了ステータスがすべて0ならば空文字列になる。
[ -z "文字列" ] && returnで、「空文字であったら関数を出る」という意味になるので、コマンドにエラーがなければCHECK関数をすぐに出て処理に戻る。
$ [ -z "" ] $ echo $? 0 $ [ -z "12" ] $ echo $? 1 $
&&が指定されているので、左側のコマンドの終了ステータスが0の場合に右側のコマンドが実行される。リスト5の7行目の場合、PIPESTATUSに0以外のものがなければ[が0を返すので、returnが実行されて処理が関数から出る。もし0でない数字が含まれていたら、処理は8行目以降に進み、エラー情報が表示され、中間ファイルが消されて、終了ステータス1でスクリプトが終わる。
[コマンドを使うときは、必ず変数や文字列に置き換わる部分を"で囲むようにする。"で囲っていない変数が空だと、[コマンドが空状態を認識できないため、次のように挙動が変わってしまう。
$ a= $ [ -n "$a" ] $ echo $? 1 $ [ -n $a ] $ echo $? 0 $
[コマンドの使い方については、シェルプログラミングTipsの[と]の間には空白を入れるにも説明があるので参考にしてほしい。
書いたスクリプトを実行する。これまでのことが理解できていたら、リスト9のような出力になることがわかるはずだ。
$ ./ADDMEMBER エラー: falseで失敗 処理できませんでした $
では、ADDMEMBERに次のチェック項目を実装してみよう。
入力のデータがちゃんと2列になっているか
メールアドレスについて、文字列と文字列の間に@がついているか
ある文字列がメールアドレスかどうかという判断は大変だ。厳密にチェックしたい場合は、コマンドを準備して、そこに通して判断させるということを考えないといけない。ここでは簡素に処理しておく。
リスト10が上記2点を実装したものだ。
#!/usr/bin/env bash tmp=/home/ueda/tmp/$$ CHECK(){ (略) } #################################### #標準入力をファイルに書き出す cat < /dev/stdin > $tmp-file #1:名前 2:emailアドレス CHECK 読み込めません #################################### #入力チェック ###入力ファイルが2列か調べる [ "$(retu $tmp-file | gyo)" -eq 1 ] ; CHECK 列数 [ "$(retu $tmp-file)" -eq 2 ] ; CHECK 列数 ###@が文字列と文字列の間に挟まっていること self 2 $tmp-file | grep '^..*@..*$' > $tmp-ok-email [ "$(gyo $tmp-file)" -eq "$(gyo $tmp-ok-email)" ] CHECK email rm -f $tmp-* exit 0
19、20行目でフィールド数を確認する。gyoとretuはTukubaiコマンドで、gyoはレコードの数、retuはフィールド数を出力する。リスト11に使用例を示す。あるファイルのフィールド数が揃っていると、retu file | gyoと書くと1が出力される。リスト10のチェックでは、19行目でそれを利用してフィールドが揃っていることを確認して、20行目でフィールド数が2であることを調べている。
ちなみに、gyoはawk 'END{print NR}'、retuはawk '{print NF}' | uniqと等価だ。
$ cat fuge 1 2 3 1 2 3 1 2 3 1 2 3 $ gyo fuge 4 $ cat fuge | retu 3 $ cat hoge a a a a a a a a $ cat hoge | retu 1 3 2 $
23、24行目では、入力から電子メールのフィールドをselfで切り出して、grepで条件に合うものを抽出している。25行目で、もとのレコード数と抽出された電子メールのレコード数を比較している。
スクリプトを書いたら、挙動を確認してみよう。リスト12のように、エラーメッセージと終了ステータスが適切に出力されることを確認する。
↓ 正しい入力 $ echo 山田 email@email | ./ADDMEMBER $ echo $? 0 $ ↓ emailがない $ echo 山田 | ./ADDMEMBER.CHECK エラー: 列数 処理できませんでした $ echo $? 1 $ ↓ 間違えてtwitterアカウントを入力 $ echo 山田 @usptomo | ./ADDMEMBER.CHECK エラー: email 処理できませんでした $ echo $? 1 $
入力のチェック部分は完成したので、本来やりたいことである新規会員の追加処理を書こう。こちらにもエラーチェックは必要だ。特に、ファイルを更新するときは神経を使わなければならない。
#!/usr/bin/env bash dir="$(dirname $0)/../DATA" tmp=/home/ueda/tmp/$$ (リスト10の5~26行目。関数と入力チェック) #################################### #追記処理 DATE=$(date +%Y%m%d) #1:名前 2:email cat $tmp-file | #MEMBERと形式を合わせる awk -v d="${DATE}" '{print 0,$0,d,"-"}' | #1:会員番号 (仮) 2:名前 3:email 4:登録日 5:"-" #MEMBERとマージ cat $dir/MEMBER - | #1:会員番号 2:名前 3:email 4:登録日 5:退会日 awk '{if($1==0){$1=n};print;n=$1+1}' > $tmp-new CHECK 追加処理失敗 #新しいリストをチェック [ "$(retu $tmp-new | gyo)" -eq 1 ] CHECK フィールド数が不正 [ "$(retu $tmp-new)" -eq 5 ] CHECK フィールド数が不正 #emailの重複チェック DUP=$(self 3 $tmp-new | sort | uniq -d | gyo) [ "${DUP}" -eq 0 ] CHECK email重複 ###################################### #更新 cat $dir/MEMBER > $dir/MEMBER.${DATE}.$$ CHECK 旧リストのバックアップ cat $tmp-new > $dir/MEMBER CHECK 新リストの書き出し ###################################### #diffで確認 echo 変更しました 1>&2 diff $dir/MEMBER.${DATE}.$$ $dir/MEMBER 1>&2 rm -f $tmp-* exit 0
13行目から20行目で、新たなメンバーをMEMBERファイルに追加して、$tmp-newに新しいリストを作成している。
目新しいところとしては、3行目のdirname(1)コマンドの使い方と、15行目のawk(1)の使い方だろう。dirname(1)コマンドは、このスクリプトのあるディレクトリを出力する。このスクリプトでは、MEMBERファイルの場所を特定するために使っている。15行目では、変数をawk(1)に渡すために-vというオプションを使用している。
23行目から31行目までで、しつこくチェックをする。29行目のuniq -dは第1回でも使ったが重複するレコードを抽出するために使っている。
35~38行目での更新では、更新前のファイルのバックアップをとっている。こうしておけば、何かあっても安心だ。元のファイルルさえ残しておけば、処理が多少ルーズであっても致命的なことになりにくい。パイプを使うとファイルを直接上書きすることはないので、スクリプトが途中で止まれば重要なファイルは守られるという特徴もある。
↓ 更新前 $ tail -n 2 ./DATA/MEMBER 10000009 山本 yamamoto@bash.co.jp 20101010 - 10000010 山口 yamaguchi@daioujyou.com 20120401 - $ ↓ 更新実行 $ cat newmember | ./SCR/ADDMEMBER 変更しました 10a11,12 > 10000011 門田 kadota@paa-league.net 20120429 - > 10000012 香川 kagawa@dokaben.com 20120429 - $ ↓ 不正な値を入力してみる $ echo 上田 ueda@hogehoge.com | ./SCR/ADDMEMBER エラー: email重複 処理できませんでした $ ↓ 更新後 $ tail -n 4 ./DATA/MEMBER 10000009 山本 yamamoto@bash.co.jp 20101010 - 10000010 山口 yamaguchi@daioujyou.com 20120401 - 10000011 門田 kadota@paa-league.net 20120429 - 10000012 香川 kagawa@dokaben.com 20120429 - $ ↓ バックアップを確認 $ ls ./DATA/MEMBER* ./DATA/MEMBER ./DATA/MEMBER.20120429.8648 $
ファイルの追記を自動化するためのスクリプトを書いた。シェルスクリプトではファイルと標準出力を相手にプログラムする。これはデータの可視化という面で、配列やメモリなど可視化しにくいものを相手するよりも、直感的に作業できるといえる。
Software Design 2012年7月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【7】安全にファイルを更新する ― エラーチェックの実装」より加筆修正後転載