2011/02/28
重複メイル削除ツール
友人の awk (1) の勉強支援の第 3 段。
以前 Software
のペイジで公開したスクリプトの中身。
1 メール 1 ファイルの形式(mh 形式)で格納されたメイル本文のうち
Message-Id: ヘッダが重複するものを抽出して削除するツールで、
sort (1) や uniq (1) だけを利用しても実装可能だが、
可読性と拡張性、実装の手間等を考えて簡単に awk (1) で実装した。
Message-Id: ヘッダから取得したメッセージ ID をインデックスとして
ファイル名を連想配列に格納しており、
既にメッセージ ID が連想配列のインデックスとして存在している場合は
メッセージ ID が重複したものとしてファイル名を標準出力に出力するだけの
非常に簡単な内容のスクリプトだ。
1#!/bin/sh 2# 3# All rights reserved, copyright (c) 2009, Mitzyuki IMAIZUMI 4# $Id: DupmailMac,v 1.1 2009/09/30 09:25:09 mitz Exp $ 5# 6 7exec 2> /dev/null 8 9# Mac のメールボックスのデフォルトロケーション 10basedir="${HOME}/Library/Mail" 11 12find ${basedir} -name "*.mbox" -a -type d | 13while read folder 14do 15 if [ -d "${folder}/Messages" ] 16 then 17 awk '{ 18 # 大文字小文字を区別しないために全部小文字に変換 19 if(tolower($1) ~ /^message-id:/){ 20 # 入力行を `:' で分割する (line[2]: message-id) 21 split($0, line, ":"); 22 # message-id から余計なスペース、 <、> を削除 23 gsub("[<> ]", "", line[2]); 24 25 if(message[line[2]] != "") 26 # message-id に対応するファイルがある場合はファイル名を表示 27 print FILENAME; 28 else 29 # message-id に対応するファイルがない場合はファイル名を格納 30 message[line[2]] = FILENAME; 31 } 32 }' ${folder}/Messages/* | 33 # ファイルの削除 34 sort | uniq | xargs rm 35 fi 36done
2011/02/25
awk を利用した構文解析ツール
友人の awk (1) の勉強支援の第 2 段。
今回はちょっと複雑な処理なので追うのが大変かも?
このスクリプトは随分以前(1996年頃)に作成したのだが、
プログラムによって微妙に異る複数の設定ファイルの中身を
解析するために作成したそこそこ汎用の構文解析機だ。
本来は perl (1) などで記述したかったのだが、
よんどころない事情で awk (1) により実装した。
以下に示す構造の状態遷移テーブルで状態(status)とキーワード(token)を定義し、
それぞれの status の時に出現する入力データ中の token により
定義されていてば外部コマンドを実行して次の status への遷移を繰り返す。
状態遷移デーブルは 1カラム目が '#' の行、タブ、
スペースのみの行は無視する。
`%syntax' で始まる行が token の定義となり、
次の行以降が status の時に出現する token 毎の定義で、
実行するコマンドと遷移する status、
もしくはシンタックスエラー(error)を記述する。
予約された状態値として初期状態を示す `start'と
エラー状態を示す `error' が定義されており、
エラー状態では標準エラー出力にメッセージを出力後終了する。
オプションとして開始時に 1 度だけ実行される初期処理コマンドを
`%start' で始まる行に、
終了時に 1 度だけ実行される終了処理コマンドを
`%end' で始まる行にそれぞれ定義できる。
%syntax token1 token2 ... * start status1:command1 error ... status2 status1 status3:command3 error ... statusN:commandN
: statusN error error ... start %start command parm .... %end command parm ....
status 定義行ではその status に遷移した際に実行するコマンドを `:' に続けて記述でき、 コマンドの引数には以下の特殊文字が指定できる。 全ての特殊文字の置き換えを終了するとシェルを通してコマンドを実行する。
- %
- 現在のトークンに置き換えられる
- ,
- スペースに置換えられる
- $0
- 直前のコマンドのリターン値に置き換えられる
- $1 〜 $N
- このコマンドの第 3 引数以降に置き換えられる
実際に使用した状態遷移テーブルの例を示す。 下で示す形式の設定ファイルの解析を実施するためのもので、 開始状態から入力データに応じて状態値を遷移させながら解析処理を実施する。
# 最初に実行されるコマンド %start ${path}/do.start # 最後に実行されるコマンド %end ${path}/do.end $0 # token 定義 %syntax { } , = * # 状態遷移テーブル start error error error error name:${path}/do.name,%,$0,$1,$2 name keyword error error error error keyword error start error error continue:${path}do.keyword,%,$0,$1,$2 continue error error error equal error equal error error error error next:${path}/do.val,%,$0,$1,$2 next error start keyword error error解析させた設定ファイルの形式。
名称1 { キーワード1 = 値1, キーワード2 = 値2, : キーワードN = 値N } : 名称M { キーワード1 = 値1, キーワード2 = 値2, : キーワードN = 値N }この定義ファイルの解析を実行すると 以下の順にコマンドを実行する事と等価な処理が実施できる。
$ ${path}/do.start $ ${path}/do.name 名称1 $? 引数1 引数2 $ ${path}/do.keyword キーワード1 $? 引数1 引数2 $ ${path}/do.val 値1 $? 引数1 引数2 $ ${path}/do.keyword キーワード2 $? 引数1 引数2 $ ${path}/do.val 値2 $? 引数1 引数2 : $ ${path}/do.keyword キーワードN $? 引数1 引数2 $ ${path}/do.val 値N $? 引数1 引数2 : $ ${path}/do.name 名称M $? 引数1 引数2 $ ${path}/do.keyword キーワード1 $? 引数1 引数2 $ ${path}/do.val 値1 $? 引数1 引数2 $ ${path}/do.keyword キーワード2 $? 引数1 引数2 $ ${path}/do.val 値2 $? 引数1 引数2 : $ ${path}/do.keyword キーワードN $? 引数1 引数2 $ ${path}/do.val 値N $? 引数1 引数2 $ ${path}/do.end $?
そしてスクリプト本体。 今見返すと冗長な記述などもあるが敢えてそのままにしておく。
1#!/bin/sh 2# 3# Copyright (c) 1996 Mitzyuki IMAIZUMI, All rights reserved. 4# 5# $Id: parser,v 1.7 1996/02/01 19:33:18 mitz Exp $ 6# 7# 名称 8# parser - 状態遷移テーブルに基づいてシンタックスをチェック 9# 10# 構文 11# parser config input [引数…] 12# 13# 引数 14# config 15# 状態遷移テーブル 16# input 17# 入力ファイル 18# 引数… 19# 各状態で実行するコマンドの引数 20# 21 22# パラメタチェック 23test $# -lt 2 -o ! -f $1 -o ! -f $2 && exit 255 24 25trap '' 1 2 3 5 9 13 15 26 27conf=${1}; file=${2}; shift 2 28 29for i 30do 31 parm="${parm},${i}" 32 shift 33done 34 35# %syntax 行から token を取得(最後の token は除外 36token=` 37 awk '{ 38 if($1 == "%syntax"){ 39 for(i=2; i<NF; i++) 40 printf("%s", $i); 41 exit 42 } 43 }' ${conf}` 44 45# 入力ファイルの token 前後にスペースを付加する 46sed 's/['${token}']/ & /g' ${file} | 47 48awk ' 49 # 50 # 初期処理 51 # 状態遷移テーブルのリード 52 # 53 BEGIN{ 54 55 argc = split("'${parm}'", argv, ","); # 引数を格納 56 argv[1] = 0; 57 58 while(getline < "'${conf}'" > 0){ 59 if(/^#/ || /^[ \t]*$/) # コメント行/空行 60 continue; 61 if($1 == "%start"){ # 初期処理定義行 62 $1 = ""; 63 prolog = $0; 64 } 65 else if($1 == "%end"){ # 終了処理定義行 66 $1 = ""; 67 epilog = $0; 68 } 69 else if($1 == "%syntax") # トークン定義行 70 for(i=2; i<NF; i++) 71 item[i-1] = $i; 72 else # 状態遷移定義行 73 for(i=2; i<NF; i++) 74 if(p = index($i, ":")){ 75 data[$1 item[i-1]] = substr($i, 0, p-1); 76 command[$1 item[i-1]] = substr($i, p+1); 77 } 78 else 79 data[$1 item[i-1]] = $i; 80 } 81 if(prolog != "") 82 argv[1] = exec(prolog, ""); 83 84 status = "start"; 85 86 } 87 88 # 89 # トークンチェック 90 # 91 function isitem(item, token, i) 92 { 93 94 for(i in item) 95 if(item[i] == token) 96 return 1; 97 98 return 0; 99 100 } 101 102 # 103 # コマンド実行 104 # 105 function exec(command, token, buf, i) 106 { 107 108 gsub("%", token, command); 109 for(i=0; i<argc; i++){ 110 buf = sprintf("\\$%d", i); 111 gsub(buf, argv[i+1], command); 112 } 113 gsub(/,/, " ", command); 114 115 i = system(command); 116 close(command); 117 118 return i; 119 120 } 121 122 # 123 # メイン処理 124 # 125 { 126 127 # コメント行スキップ 128 if(/^#/) 129 continue 130 131 for(i=1; i<NF; i++){ 132 if(isitem(item, $i)){ 133 format = command[status $i] 134 status = data[status $i] 135 } 136 else{ 137 format = command[status] 138 status = data[status] 139 } 140 if(status == "error"){ 141 printf("%s: %d: syntax error \"%s\"\n", 142 "'${file}'", NR, $i) | "'cat' >2" 143 ret = 255 144 exit 145 } 146 else if(format != "") 147 if(format == "exit"){ 148 ret = argv[1] 149 exit 150 } 151 else 152 argv[1] = exec(format, $i) 153 } 154 155 } 156 157 # 158 # 終了処理 159 # 160 END{ 161 if(epilog != "") 162 exec(epilog, "") 163 164 exit ret 165 } 166'
2011/02/24
awk スクリプト
知り合いが awk (1) の勉強をしたいと言ってるので、
支援がてら過去に作成したスクリプトをさらしてみる。
このスクリプトは linux 上で稼働している
あるプログラムが取得した共有メモリの残骸を掃除するスクリプトで、
ipcs (8) で取得した共有メモリ一覧のうち、
3 件連続しているキーが存在する場合 ipcrm (8) をコールして
共有メモリを削除する処理を行う。
短いスクリプトながら、外部コマンドの呼出し (system())、
コマンドからの値取得 (getline())、
そして関数内ローカルスコープの変数定義など
awk (1)の持っている機能を結構活用しているので、
多少なりとも参考にして貰えるとうれしい。
1#!/bin/sh 2# 3# Copyright (c) 2010 Mitzyuki IMAIZUMI, All rights reserved. 4# 5# $Id: shmrm,v 1.7 2010/05/08 09:03:15 mitz Exp $ 6# 7# 共有メモリ削除処理 8# 9 10LANG=C /usr/bin/ipcs -m | sort | 11 12awk ' 13 # 14 # 初期処理 15 # 16 BEGIN{ 17 18 max = 0; # 削除対象キーインデックス 19 num = 0; # 削除対象共有メモリインデックス 20 ipcrm = "/usr/bin/ipcrm"; # ipcrm コマンド 21 22 } 23 24 # 25 # 16進数 -> 10進数変換 26 # cmd, dec はローカル変数として利用するために仮引数宣言とする 27 # 28 function h2d(hex, cmd, dec) 29 { 30 31 # printf(1) を利用して 16進数を 10進数に変換 32 cmd = sprintf("printf '%%d' %s", hex); 33 # printf(1) の実行結果を getline で変数 dec に取得 34 cmd | getline dec; 35 close(cmd); 36 37 return dec; 38 39 } 40 41 # 42 # 3連続するキーかのチェック 43 # start, i はローカル変数として利用するために仮引数宣言とする 44 # 45 function cont3(num, start, i) 46 { 47 48 start = h2d(key[num]); 49 50 for(i=1; i<3; i++) 51 if(h2d(key[num+i]) != start + i) 52 return 0; 53 54 return 1; 55 56 } 57 58 # 59 # コマンド実行 60 # 61 function exec(key, arg) 62 { 63 64 system(sprintf("%s -%s %s", ipcrm, arg, key)); 65 66 } 67 68 # 69 # 共用メモリ/セマフォの削除 70 # 71 function shmrm(key, num) 72 { 73 74 # 共有メモリを削除 75 exec(key, "M"); 76 77 # セマフォを削除 78 if(num % 3 == 2) 79 exec(key, "S"); 80 81 } 82 83 # 84 # メイン処理 85 # 86 { 87 88 # owner が root で権限が 666 かつ nattch が 0 の共用メモリを検索 89 if($3 == "root" && $4 == "666" && $6 == 0) 90 key[max++] = $1; 91 92 } 93 94 # 95 # 終了処理(ある意味こっちがメイン) 96 # 97 END{ 98 99 if("'$1'" == "-f"){ 100 # 強制モード -- 3連続していないキーでも削除 101 for(n in key) 102 shmrm(key[n], 2); 103 } 104 else { 105 # 通常モード -- 3連続しているキーが削除対象なので抽出する 106 for(i=0; i<max; i++) 107 if(cont3(i)) 108 for(j=0; j<3; j++) 109 target[num++] = key[i+j]; 110 111 # 削除対象の共有メモリ/セマフォを削除 112 for(n in target) 113 shmrm(target[n], n); 114 } 115 116 } 117'
2011/02/17
ssh
ssh (1) コマンドは接続先のホスト情報をローカルに保存していて、 接続するたびに接続先が正しいホストかを確認する機能を持っている。 これはホスト名の詐称や DNS poisoning などにより 正しくない接続先に接続してしまうという事態を防止するためには 非常に便利で有用な機能ではあるが、 接続先のシステムが再インストールなどでホスト情報が変更になってしまった場合、 ssh で接続しようとすると
$ ssh ホスト名 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)! It is also possible that the RSA host key has just been changed. The fingerprint for the RSA key sent by the remote host is XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX. Please contact your system administrator. Add correct host key in /home/ユーザ/.ssh/known_hosts to get rid of this message. Offending key in /home/ユーザ/.ssh/known_hosts:NN RSA host key for ホスト名 has changed and you have requested strict checking. Host key verification failed. $などと表示されて接続できなくなってしまう。
画面に表示された通り known_hosts ファイルの該当行を削除すれば 再度接続できるのだが、 これを自動で行うためのスクリプトを作成してみた。
1#!/bin/sh 2cat << EOF | ex -s ${HOME}/.ssh/known_hosts 3`ssh $* 2>&1 | sed -n '/^Offending key/s/.*:\(.*\)/\1/p' | tr -d '\r'`d 4wq 5EOFPATH の通ったディレクトリに sshfix の様な名前で保存し 実行権限を付与しておくと、 ssh で接続を試みて上記エラーが発生した際に実行する事で known_host の該当行を削除するので再度接続できる様になる。
$ ssh ホスト名 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!</i> : $ sshfix !$ $ ssh !$くれぐれも本当に正しい接続先か確認してから利用しなければならない。
2011/02/12
シェルスクリプト自身のパス取得
シェルスクリプトを作成していて、
たまにスクリプト自身のパスを取得したい場合がある。
以下の処理でシェルの種類に依存せず(とは言っても bourne shell 系のみだが)
シェルスクリプト自身のパスを取得する事ができる筈(多分)。
PATH 環境変数に従って実行された場合は
${0} にスクリプトの絶対パスが格納されているので
${0} の dirname (1) を取得すれば良いが、
相対パス指定で実行された場合は 1度 cd (1) した上で
pwd (1) を実行してカレントディレクトリを取得する必要がある。
すべてのケースで
"( cd ${0%/*} `dirname ${0}` && pwd )"
しても同じ結果を得られるのだが、
サブシェルの実行や特に pwd (1) の実行はシステムに負荷をかけるので、
効率を考え ${0} の値を確認している。
expr (1) の実行も結構負荷がかかるからあまり意味ないかな…
1#!/bin/sh 2# ${0} の dirname を取得 3# cwd=${0%/*} 4cwd=`dirname ${0}` 5 6# ${0} が 相対パスの場合は cd して pwd を取得 7expr ${0} : "/.*" > /dev/null || cwd=`(cd ${cwd} && pwd)` 8 :
2011/03/17 追記
$ sh fooこの様に実行された場合 ${0} には foo が 格納されているので ${0%/*} では foo に展開されてしまい cd (1) がエラーとなる問題があったので、 dirname (1) を利用する様修正しました。
2011/02/10
2011/02/09
ファイル暗号化/復号化 wrapper スクリプト
openssl (1) を実行してファイルを暗号化/復号化する処理を
簡易実行するための簡単な
シェルスクリプトによる wrapper
を作成した。
暗号化/復号化は openssl (1) の enc コマンドに
暗号化アルゴリズムに aes-128-cbc を指定して実施している。
指定されたファイルの先頭 8 バイトを参照して
Salted__ の場合は暗号化されていると仮定し復号化、
それ以外は暗号化する処理をデフォルトで実行するが、
実行時オプション -d、-e を指定する事で
強制的に暗号化/復号化を指定する事も可能だ。
-p オプションでパスワードを指定してバッチ処理も実行できるが、
コマンド実行履歴等にパスワードが残るのでお勧めできない。
暗号化/復号化が成功した場合は元のファイルを置き換える。
例によって必要に迫られれて適当に作成したスクリプトなので このスクリプトを利用した事で生じるあらゆる損害に対して責は負えない。
2011/02/07
HyperMac
アメリカの eBay オークションで落札した HyperMac キットが届いた。
HyperMac は MacBook などに電源を供給し充電も可能な外部バッテリーだが、
MagSafe という Apple が特許を持っているコネクタの利用に関する問題で
昨年 11月2日 をもって発売を中止してしまっていた。
現在は Apple 純正の Airline アダプタを経由する事で利用するタイプを
発売している様だが、
Airline アダプタとのコネクタが巨大で不格好、
しかも充電ができない等 HyperMac の特徴を全く活かせていないので却下。
当然新品は入手できないので国内外のオークションなどに
出品されるのを待っていたのだが、
今回 eBay オークションに 150Wh の HyperMac が出品されたので、
本当は一番小型の 60Wh のモノが欲しかったのだが
この機会を逃すと又当面入手できない可能性もあると思い落札した。
eBay の出品者はアメリカ国内への発送しか受け付けていなかったので、 今回はアメリカ国内の転送業者を経由して日本に転送してもらった。 アメリカ国内への送料は出品者負担だったので、 転送業者による日本への転送送料+関税+手数料が余計にかかった費用だが、 転送業者の手数料は $7 だったのでそれほど悪くないだろう。
単語の羅列に近いほんのちょっとの英語力とPayPal アカウント、 それから多少の度胸があれば 日本では滅多に入手できないものを手に入れる事が出来てしまう とっても便利な世の中だ。
2011/02/01
Magic Mouse のスクロール
職場の iMac では Apple 純正の Magic Mouse を利用している。
BetterTouchTool と合わせて非常に便利に使っているのだが、
google map を利用している時などに不用意にスクロールしてしまい
敏感すぎるセンサーが却って邪魔になってしまう事がある。
Safari 用には google map を表示している時に
Magic Mouse のスクロールをオフにするという機能拡張を見つけたが、
普段愛用している firefox には同じ機能を持つアドオンを探せなかった。
かといって firefox のアドオンは自作した事がないので勝手もわからない。
丁度、某有名匿名掲示板で環境設定パネルのユニバーサルアクセスを
automater を利用して操作するという話題が出ていたので、
投稿されていたソースコードを参考にしながら簡単なスクリプトを作って見た。
automater を起動してアプリケーションのテンプレートを選択、 AppleScript を実行で表示されるエディタに以下のコードを貼り付けて、 アプリケーションとして保存すれば実行できる様になる。 実行するたびに Magic Mouse のスクロールオン・オフがトグルされるので、 ショートカットを Dock に登録したり Butler などのランチャから起動すると便利だと思う。
1tell application "System Preferences" 2 activate 3 set current pane to pane "com.apple.preference.mouse" 4end tell 5 6tell application "System Events" 7 tell process "System Preferences" 8 tell window 1 9 tell group 1 10 click checkbox "スクロール" 11 end tell 12 end tell 13 end tell 14end tell 15 16tell application "System Preferences" to quit
automater や AppleScript を書いたのは初めてで仕様も良く把握していないので、 無駄な処理やおかしな記述があるかも知れない。 おかしな部分があれば是非指摘して頂けるとありがたい。 なお、 個人的に適当に作ったスクリプトなので他の環境での動作は保証できないし、 勿論このスクリプトを利用した事で生じるあらゆる損害に対して責は負えない。