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

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

linux bash

目的

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 ...

CSVファイル中のカラム数が異なるレコードをLinux上で手軽に検出する

bash awk

目的

CSVファイルの中にある、不正なレコード(カラム数違い)を、できるだけ手軽に検出、修正する

動機

データの初期投入とかで、CSVからデータをインポートしたいが、ファイルの編集ミスやら、
エスケープ漏れなどで、うまくインポートができなくなってしまうことがある。
こういうとき、プログラムの改修やらログレベルの変更やらに頼らずに、簡単なコマンドだけで
不正なレコードを検出して、vimとかでちゃちゃっと直してしまいたい。
せっかくサーバーにあげたのに、エクセルで編集して、SCPで再転送とか、マジで勘弁してほしい

アプローチ

行ごとのカラム数を数えて、カラム数が異なる行を探す

結論

CSVのレコードがすべて正しい(カラム数が同一)ときは、下記のコマンドで「正しいこと」は確認できる

$ awk -F, '{print NF}' < sample.csv | uniq | wc -l
-> 1

このコマンドの実行結果が1であればすべてのカラム数が同じ。2以上なら、何かしら不正なレコードがある ことがわかる。

例えば、下記のように、3行目に不正なレコード(カラム数が5)を含んでいるCSVファイルだと、 結果は1にならない。

1,aaa,bbb,ccc
2,ddd,eee,fff
3,ggg,hhh,iii,jjj
4,kkk,lll,mmm
$ awk -F, '{print NF}' < sample.csv | uniq | wc -l
-> 3

この時、何行目が不正なレコード、つまり、カラム数が4でないレコードであるかを知るには、
下記のコマンドを実行すればいい。

$ awk -F, '{print NF"::"NR}' < sample.csv | grep -v '^4::'
-> 5::3

⇒ 出力が、 ‘5::3’ と出たので、3行目に、カラム数5のレコードが存在していることがわかる。

レコードも一緒に見たい場合は、$0 も出力してやる

$ awk -F, '{print NF"::"NR, $0}' < sample.csv | grep -v '^4::'
-> 5::3 3,ggg,hhh,iii,jjj

解説(やってること)

awk の F オプションにカンマを指定することで、awk に渡す文字列のデリミタをカンマに変えることができる。
そのため、awk -F, '{print NF}'としてあげると、 「カンマ区切りで行を分割し、1行に含まれるフィールドの数を表示する」という動きになるので、
1行毎のカラム数を標準出力に出力してくれる。( NF は、フィールド数)

カラム数が正しいレコード(カラム数4)であれば、 “4:: ” から始まるはずなので、これをgrep -v で
除外してあげれば、不正なレコードの行数だけを取得することができる。

応用編

ダブルクォートでカンマをエスケープしているCSVの場合

こんなCSVの場合

"aaa","bbb","ccc"
"ddd","eee","fff"
"ggg","hhh","iii"
"jjj","kkk","lll,mmm"

デリミタを , ではなく、"," にしてやる。(空レコードもダブルクォートでくくらていることを想定)

$ awk -F'","' '{print NF}' < sample.csv | uniq | wc -l
-> 1

タブ区切り、空白区切りの場合

タブ区切り(tsv)の場合は、ちょっとだけ工夫してあげる必要がある。 タブやスペース区切りの場合、空のカラムが存在しているとき、連続スペースを一つのデリミタとして 認識してしまうため、正しく数えることができない。

そういう場合は、sedコマンドで一度別の文字に置き換えてからawkに渡してやると、うまくいく。 ただし、デリミタにする文字がレコード中にあると、今度は誤検出されてしまうため、
出現する確率が限りなく低い文字(バッククオートとか)にしておきましょう。

$ sed -r 's/\t/``/g' < sample.csv | awk -F'``' {print NF"::"NR} | uniq | wc -l
-> 1

マウスもランチャーも使わず、キーボードだけでお気に入りのファイルを開く方法

便利技

目的

windowsの「ファイル名を指定して実行」機能を利用して、ランチャーっぽい動作を実現する。

動機

ふだんファイルを探すとき、マウスクリックを使ってCドライブからディレクトリをモジモジたどっていくのが煩わしくて嫌いなので、bluewindなどのランチャーソフトを使ってる

。。。のだが、まれにセキュリティルール等々でソフトウェアのダウンロード制限のある環境などではランチャーが使えないことがある

そんなとき、おとなしくマウスクリックを使って辿っていくのはなんか癪だし、普段慣れた動作ができないのはストレスがたまるので、ソフトウェアをダウンロードしないでランチャーっぽい動作ができるようにしたい

手順概略

1.ショートカットを配置しておくディレクトリを作る

2.上記のディレクトリにPATHを通す

3.立ち上げたいディレクトリや、ソフトウェアのショートカットを作成する

4.「ファイル名を指定して実行」機能を使ってショートカット名を指定する

検証OS

windows10 windows7

手順詳細

1.ショートカットを配置しておくディレクトリを作る

どこでもいいので、ショートカットを配置しておく用のディレクトリを作成する。(仮に、C:\Shortcuts とする) 以下、ここで作成したディレクトリを「ショートカットディレクトリ」と呼びます。

f:id:yamatatsu-blog:20170104205621j:plain

2.ショートカットディレクトリにPATHを通す

コンピュータ→プロパティ→システムの詳細設定→環境変数の設定から、PATH環境変数の末尾に、上記で作成した、C:\Shortcuts を追加します。 ※このとき、ショートカットディレクトリは、必ずPATH環境変数の末尾になるようにしてください。

f:id:yamatatsu-blog:20170104210848p:plain

3.立ち上げたいディレクトリや、ソフトウェアのショートカットを作成する

例えば、C:\project\document\phase1\sample という、それなりに階層の深いディレクトリに一発でたどり着きたいとする。(マウスクリックで到達するには、4回ぐらいクリックしなきゃいけない)

ショートカットディレクトリに、C:\project\document\phase1\sampleへのショートカットを作成します。このとき、ショートカット名を仮に、targetとします。

f:id:yamatatsu-blog:20170104211931p:plain

4.「ファイル名を指定して実行」機能を使ってショートカット名を指定する

3.の手順で作成したC:\ShortcutsディレクトリにPATHが通っているので、「ファイル名を指定して実行」機能で"target" と入力すれば、C:\Shortcuts\target.lnk が呼び出され、所望のディレクトリが一発で立ち上がります。

windowsキー + r を押して、「ファイル名を指定して実行」のウィンドウを立ち上げ、targetと入力してください。

f:id:yamatatsu-blog:20170104212113p:plain

応用編~特定のウェブサイトに一発で到達する~

ブラウザでインターネットの特定のページに到達するには、ちょっとひと手間が必要。 まず、テキストエディタで下記のようなcmdファイル(ggl.cmd)を作成してください。

start http://www.google.co.jp

これを同じようにショートカットディレクトリに保存し、「ファイル名を指定して実行」からgglと入力すれば、googleのトップページに到達できる。

注意点

ショートカットの名前にドットが入ると、うまく動作しません。 ショートカット名には、できる限りドットや記号を使わない方がよいです。