読者です 読者をやめる 読者になる 読者になる

bashのselect文を使ってwhile readの自動処理を半自動化する

目的

bash の while read 文を実行しているとき、決まった場所で処理を中断して、 続行か否かを判断する処理を挟めるようにする

動機

while read文で処理を回している最中、特定の場所で処理を中断して、目視による 確認手順を差し込みたい場合がある。
そういうときに、ループを回しながら、確認を求めてくる、「半自動化」みたいな処理が できると便利だったりする。

結論

bash の select文を使って、こんな感じのコードにする

#!/bin/bash
exec 254<&0
while read PARAM;do
  echo $PARAM # ループしたい処理
  select answer in "continue" "exit";do
    case "$answer" in
        continue) break ;;
        exit)     exit  ;;
    esac
  done <&254
done <<PARAM
param1
param2
param3
PARAM

上のコードは、echo文でparam1,param2,param3を順次標準出力に出力していくループの 中で、次のecho文を実行するか、中断するか、確認を求めてくれる。

解説(やってること)

while read 文でヒアドキュメントを読み込んで順次処理を実行している中で、select文を読み込んであげているだけなのだが、一つだけ注意しなければいけない点がある。

ファイルディスクリプタの複製を使う

大事なのが、2行目の exec 254<&0 と、11行目の done <&254
このふたつをやっておかないと、while read文の中でselect文を実行しても、キーボードからの入力を求めてくれないばかりか、ループ処理もparam1を処理しただけで終わってしまう。

これは、while read文のdoからdoneまでの間に実行されるプロセスの、ファイルディスクリプタの0番(標準入力)が、 12行目のdone <<PARAMによって、ヒアドキュメントに切り替えられているために起きる現象である。(select文が、ヒアドキュメントの方を読み込みに行ってしまう)

なので、事前にキーボード入力のファイルディスクリプタを、別の番号(ここでは254番)に複製しておいてあげて、 select文を実行するときに、あらかじめ複製しておいたファイルディスクリプタを指定してあげれば、select文の実行時にキーボードから入力を求めてくれるようになる。

応用編

(いまどき使う場面があるか不明だが)たとえば↓みたいなコードを使えば、postgreでSQLを順次実行しながら、 コミットするかロールバックするか確認を取ってくれるようになる。

#!/bin/bash

# 事前処理(オートコミットをOFFにする)
prepare(){ echo "\set AUTOCOMIIT off";}

# トランザクション開始
begin(){ echo "BEGIN;"; }

# コミットして続行か、ロールバックか、任意のコマンドを実行するか選ばせる
menu(){
  select cmd in "continue" "rollback" "exit";do
    if [ -z "$cmd" ];then
      echo "$REPLY"
      continue
    fi
    case "$cmd" in
      continue)
        echo "COMMIT;"
        return 0
        ;;
      rollback)
        echo "ROLLBACK;"
        echo "\q"
        return 1
        ;;
      exit)
        echo "\q"
        return 1
        ;;
    esac
  done <&254
}

# update_tbl1.sql, update_tbl2.sql(同一トランザクション)を実行したのち、
# 確認を求めてからupdate_tbl3.sqlを実行する
output_sql(){
  prepare
  while read sqlfiles;do
    cat <(begin) $sqlfiles
    echo "done $sqlfiles !" >&2
    menu || return 1
  done <<FILES
  update_tbl1.sql update_tbl2.sql
  update_tbl3.sql
FILES
}

# ファイルディスクリプタを複製する
exec 254<&0

# 標準出力にSQLを出力する
output_sql

上で作ったsample.shを、下記のように、psqlコマンドに渡してあげれば、psqlで順次SQLを実行してくれる。

$ bash sample.sh | psql -U USER -d DBNAME

下記のような選択肢が出てくるので、コミットして続行する場合は1を、ロールバックして終了する場合は2を選択する。 番号ではなく、例えばselect * from tbl1;みたいなコマンドを入力すると、その通り実行してくれる。

1) continue
2) rollback
3) exit

psqlの標準出力と、select文のプロンプトが混ざって見づらい場合は、
多少めんどくさいが、psql側の標準出力をファイルにリダイレクトしてあげて、
別ターミナルからtail -f コマンドなどでみるようにするといい

$ bash sample.sh | psql -U USER -d DBNAME > dbconsole 2>&1
$ tail -f dbconsole

蛇足

実行したSQLの記録を取りたい場合は、間にteeコマンドを挟んであげる

$ bash sample.sh | tee backup.sql | psql ...