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/09
ファイル暗号化/復号化 wrapper スクリプト
openssl (1) を実行してファイルを暗号化/復号化する処理を
簡易実行するための簡単な
シェルスクリプトによる wrapper
を作成した。
暗号化/復号化は openssl (1) の enc コマンドに
暗号化アルゴリズムに aes-128-cbc を指定して実施している。
指定されたファイルの先頭 8 バイトを参照して
Salted__ の場合は暗号化されていると仮定し復号化、
それ以外は暗号化する処理をデフォルトで実行するが、
実行時オプション -d、-e を指定する事で
強制的に暗号化/復号化を指定する事も可能だ。
-p オプションでパスワードを指定してバッチ処理も実行できるが、
コマンド実行履歴等にパスワードが残るのでお勧めできない。
暗号化/復号化が成功した場合は元のファイルを置き換える。
例によって必要に迫られれて適当に作成したスクリプトなので このスクリプトを利用した事で生じるあらゆる損害に対して責は負えない。
2011/01/17
シェルスクリプトで疑似乱数を取得する
gnu bash には ${RANDOM} というシェル変数が用意されていて、 アクセスするたびに疑似乱数(風)の値が取得できる様だ。 しかし posix 準拠の機能ではなく bash 独自の機能なので拡張性がない。 そこで汎用的な疑似乱数取得の方法として awk (1) を利用した方法を使ってみる。
awk (1) には rand() という関数が組込まれているので
乱数生成ができる。
その際に利用される種(seed)は
srand() という関数で初期化できるので、
これらを利用する事でそこそこの精度の疑似乱数は取得できる。
たとえば 0 〜 m までの疑似乱数は
取得した値から m の剰余を取ることで取得できる。
ただし awk (1) の rand() は仕様として
0 〜 1 までの桁数不定の少数を返すので、
乱数として利用する場合には整数に変換する必要がでてくる。
整数にするために 10n を乗すればよいのだが、
小数点以下の桁数が不定のために小さすぎる n を乗すると
全ての桁が整数化されずに乱数の精度が低くなってしまい、
逆に大きすぎる n を乗すると値が xxx000 の様になってしまい
剰余を取得しても無意味になってしまう。
そこで awk (1) により疑似乱数を取得する場合は rand() で取得した値の前 2 文字 ("0.") を文字列として除外した値から m の剰余とする事で 0 〜 m までの乱数が取得できる。
1#!/bin/sh 2 3awk 'BEGIN{ 4 srand(); 5 print substr(rand(), 3) % '${1:-1}' 6}'
データの型が存在しない(文字列であり数字である) awk (1) ならではの裏技とも言えるだろう。
2011/01/06
htaccess で特定のファイルのみパスワードを要求しない
Web コンテンツを保護するために
.htaccess ファイルを用いて Basic 認証などを実施する場合は多い。
通常 .htaccess はディレクトリ単位で有効になってしまうのだが、
あるディレクトリのアクセスを Basic 認証でアクセス制御したいが
特定のファイルについては認証を要求しない設定を行いたい場合は
Files ディレクティブと
Satisfy ディレクティブで実現できる。
AuthType Basic
AuthUserFile 認証用ファイル
AuthName "Enter password"
Require valid-user
<Files "認証要求しないファイル">
Satisfy Any
Allow from all
</Files>
Satisfy ディレクティブは Allow ディレクティブと
Require ディレクティブがどちらも使われている場合の
アクセスポリシーを制御する。
Any が指定された場合は Allow か Require
のどちらかの条件を満たせばアクセスが許可されるので、
Files ディレクティブで指定したファイルに対しては
Allow 指定が有効になり認証なしでアクセスが許可される。
逆にある特定のファイルだけの Basic 認証によるアクセス制御は Files ディレクティブのみで可能である。
AuthType Basic
AuthUserFile 認証用ファイル
AuthName "Enter password"
<Files "認証要求するファイル">
Require valid-user
</Files>
どちらの場合も Files ディレクティブは以下の様に
~ を使う事でファイル名を正規表現で記述できる。
<Files ~ "\.(gif|jpe?g|png)$"> : </Files>apache では PCRE - Perl Compatible Regular Expressions を 利用しているので複雑な正規表現の記述ができる様だ。
2010/12/15
DHCP
先日購入した MacBook Air を自宅のネットワークに接続しようとした所、 何故か DHCP での IP アドレス取得に失敗してしまう現象が発生した。 Windows Xp や FreeBSD がインストールされた PC では正しく取得できるが、 MacOS X ではどうやっても IP アドレスが取得できない。 無線 LAN 接続のアクセスポイント(TimeCapsule)の設定を変更したり、 アクセス制御を解除してみても全然 IP アドレスが取得できず DHCP サーバの設定を何度も確認しても症状はいっこうに改善されない。
そこでネットワーク障害はパケットキャプチャが基本だと思い立ち、
IP アドレスが取得できない MacBook Air で tcpdump (1) を実行すると、
クライアントからの DHCP DISCOVER に対して
サーバは DHCP OFFER を応答しているのだが、
パケットの Source IP Address が
127.0.0.1 となっている事を発見した。
なぜ DHCP OFFER パケットの Source IP Address が
127.0.0.1 だったのか、
結論から言うと /usr/local/etc/dhcp.conf ファイル中の
option domain-name に設定した FQDN が
/etc/hosts に 127.0.0.1 で登録されていたため、
DHCP サーバがパケットを送信する際に
domain-name を逆引きした結果として 127.0.0.1 を
Source IP Address に設定してしまった様だ。
/etc/hosts から該当する FQDN のエントリーを削除して、
DHCP サーバを再起動した所 IP アドレスが取得できた。
FQDN を 127.0.0.1 で /etc/hosts に登録する事は
Linux の一部のディストリビューションなどでは普通に行われている様だが、
メイルが送信できない、DHCP が機能しないなどの障害発生の原因になるので
是非とも辞めて欲しい。
それにしてもこの MacOS X の DHCP クライアントは Source IP Address が 127.0.0.1 の DHCP OFFER を無視しちゃうのね。
2010/09/01
mh で画像を添付したマルチメディアメイルを送信する
最近の mh(1) は標準で画像等を添付した
MIMEマルチパート形式のメイルが送信できる。
mh(1) では mhn(1) コマンドを利用して
マルチメディアメッセージを操作するので、
マルチメディアメイルを送信するためには、
本文中にマルチメディアメイルである事を通知する行
(ここでは mhn 通知行 と呼ぶ)を記述した上で
mhn(1) コマンドを実行し、
添付ファイルを base64 エンコードしてマルチパート形式に変換すればよい。
mhn 通知行は本文の 1 カラム目から以下の形式で記述する。
#MIME タイプ; name="ファイル名" [ファイルの説明] ファイル名MIME タイプは jpeg 形式の画像であれば image/jpeg、 gif 形式の画像であれば image/gif などと指定する。
comp(1) や repl(1) を利用してメイル本文を編集する場合、 mhn 通知行を含む本文を作成した後で What now? プロンプトから edit mhn などとして mhn(1) コマンドを実行してから 送信すればマルチメディアメイルが送信できる。
最近の日本語パッチが適用された mh(1) は、
mhn(1) コマンドがメイル本文の文字コードを参照して
適切な charset を Content-Type: ヘッダに付加するので、
メイル送信前に必ず mhn(1) を実行しても良いだろう。
その場合は $HOME/.mh_profile に以下の行を記述しておくと
送信時に自動で mhn が実行されて便利。
automhnproc: mhn
ちなみに 1 カラム目が # な行は mhn 通知行 とみなされるので、 本文中で # が行頭になるメイルを作成する場合は ## とする必要がある。
この様に mh(1) で MIME マルチパート形式のメイルを送信するのは
最近の高機能な MUA に比較すると若干面倒なので、
多少なりとも簡単に送信するために
mhmime スクリプトを作成した。
このスクリプトを利用してマルチメディアメイルを作成するためには
メイル本文の 1 カラム目から以下の行を記述する。
#MIME ファイル名 ファイルの説明このスクリプトは記述されたファイル名から MIME タイプを推測して mhn 指示行 を生成し、 #MIME 以外で 1 カラム目が # の場合は ## に置換した上でmhn(1) コマンドを実行する。
メイル本文を作成した後で What now? プロンプトから
edit mhmime などとしてこのスクリプトを実行した上で
送信すればマルチメディアメイルが送信できる。
$HOME/.mh_profile の automhnproc: に指定しておけば、
送信時に自動で実行されるので多少は便利になるかもしれない。
しかし、今時 mh を素で利用している人は果たしてどれほどいるのだろうか?

