携帯万能のバグに対応する

携帯万能で、WX11Kの送信メールを取り込むと文字化けの上無効な引数が発生しましたと表示される問題がずっと出ていた。

この問題、先日対応したつもりだったが、甘かった。

そもそも問題は「WX11Kのメール」ではなく、「UTF-8のメール」にあった。
UTF-8で送られると、その時点でアウトなのだ。そのため、事前に問題のあるエントリを切り出しておく、というのは労力が大きい。

そこで自動判別できるようにしたのだが、新たな事実が判明した。

だが、新たな問題が発覚。
CSVだが、"をエスケープしていないため、Invalid formatとなっている。
このため処理できない箇所があり、力技でなんとかした。

このあたりはGitHubでスクリプト公開と共に解説している。

問題はPLANEXサポートに報告済み。
8時間以上もかけたのだが、甲斐ない結果に終わった。

携帯万能の「送信メールが取り込めない」問題をなんとかする

私はWILLCOMのW11Kを使っているのだが、このメールデータを取り込むのに、「携帯万能」以外の道がない。

そのため、Amazonで携帯万能を購入して使っているのだが(昨日のエントリの通り)、これがまた曲者で、対応するドライバがWindows 2000/XP/Vista/7の32bitのみという。

Windows 8でもWindows 7 64bitでもダメというのは、今時相当きつい。
うちではWindows XPマシンが1台あるだけだ。

さて、Windows XPはトリスターサポートの「まっさらにしろ」という指示により苦労しつつも初期化したのだが、取り込んだところ結局同じ問題だ。
送信メールが正常に取り込めない。

トリスターの人は、Outlookがどうとか、全く違うところに話を持って行ってしまうのだが、明らかに「アップデータでバグを埋め込んだ」話だ。

で、エクスポートしたCSVを実際に見てみた。

lvで見てみると、ある時期を境に文字化けが分かれている。これはまぁ、わかる。
そして、Mousepadで開こうとすると、不正なシーケンスがあるとして開けない。

Shift-JISならShift-JISで開けるソフトなので、これは恐らく違う文字エンコーディングがバイト列をそのままに吐き出している、ということだろう。
だとしたら、まだ手はある。

試しに、headtailcutで文字化けしているところとしていないところだけを切り出してnkf -gしてみると

$ tail send-text.csv | cut -f 5 -d "," | nkf -g
Shift_JIS
$ head send-text.csv | tail -n 9 | cut -f 5 -d "," | nkf -g
UTF-8
$ head send-text.csv | cut -f 4 -d "," | nkf -g
Shift_JIS

やっぱり

  • 以前は全てShift-JISだった
  • 現在は、Subjectまでの4フィールドはShift-JISだが、本文だけUTF-8である

ということが分かった。

さすがにこの混在はきついので、分離する。

$ head -n 3825 send-text.csv >| sendmail-UTF8sec.csv
$ head -n+3826 send-text.csv >| sendmail-SJISsec.csv

通常は後半部分は考えなくていいはずだ。
後半部分は単に結合すれば良いように変換しておく。

$ nkf -w -Lu sendmail-SJISsec.csv sendmail-SJISsec.utf8.csv

で、困ったことにこの状態だとRubyでCSVを切り出すことができない。RegExpが使えないために、CSVライブラリも使えないのだ。

Perlで試したところ

  • perl -nなら黙っていてもできる
  • while (<>)だとuse utf8の必要あり

というわけで、結局こういうコードになった。

#!/usr/bin/zsh

head -n1 "$1" | nkf -w -Lu >| out.csv

tail -n+2 "$1" | perl -n -e 'if ($_ =~ /"(?:[^"]*|"")*"\s*$/) { print STDOUT $&; print STDERR $`; print STDERR "\n" }' >| out.l 2>| out.f
nkf -w -Lu out.l > out.lu
nkf -w -Lu out.f > out.fu

ruby -e 'File.foreach(ARGV[0]).zip(File.foreach(ARGV[1])) {|i,j| puts(i.chomp + "," + j) }' out.fu out.lu | sort -u >> out.csv

単純に,で区切るとエンベロープやSubjectに入っている可能性があるため、せめてこうした。多分これでいけるはずだ。

一時ファイルなしでいきたかったが、そうするとnkfから1行ずつ読む必要があり、ちょっと面倒だった。

ちなみに、これ

  • 1行目はCSVヘッダーでShift-JIS
  • sortしている関係で日付順になる。sort -ruにすればオリジナルと同じ配列

この状態だとまともなCSVなので、MHやemlに変換するのも、それほど難しくない。

ほんとにひどい品質のソフトだと思う。

Pureminder (リマインダ)とZsh socket programming

Tasqueにはリマインダ機能がない。

Taskcoachということも考えはするが、残念ながらTaskcoachは私の環境ではSystem trayに収まってくれないため、非常に邪魔である。

そこで、リマインダを作ってみた。GitHubで公開している。今回はどちらかというとNoddy寄りだが、きちんとした形で公開している。

PureminderはZshで書かれている、クライアント/サーバーである。
サーバーはメッセージを受け取り、SOX(play)で音を再生し、Zenityで画面に表示する。

わざわざクライアントサーバーモデルをとっているのは、atでの利用に問題があるためだ。

単にatでcallすると、Zenityは表示すべきディスプレイサーバーを見つけられない。
$DISPLAY変数によって指定することはできるが、マルチユーザー環境での動作が信用できない。

そこで、確実に機能させるため、現在のデスクトップ上で起動し、ユーザー別に作られるUNIXドメインソケットでメッセージを受け取る、という仕様とした。

だが、PureminderはZshだ。
一般にはなかなか見かけることのない、Zsh socket programmingをちょっと紹介しよう。

まず、モジュールをロードする。

zmodload zsh/net/socket

次にzsocket -lでソケットを作成する。

zsocket -l "$sockfile"

$REPLYにファイルデスクリプタ(Integer)が入るので、とっておく。

typeset -g sock=$REPLY

zsocket -aで接続を待ち受ける。引数はソケットのファイルデスクリプタ。

zsocket -va $sock

$REPLYに接続のファイルデスクリプタが入る。この接続は全二重。
閉じる時は次のようにする。

exec {fd}>& -

Markdownで書かれたスライドをHTMLとポータブルPDFと印刷用PDFにする

Aki SI&EのPR用に書いたものだが、Markdownで書き、Web(HTML)、配布用PDF、印刷用PDFの3種類を生成する考えでいた。

手でやろうかと思っていたのだが、再生成の回数も多そうだったので、自動化できるようにした。

Markdownで記述したドキュメントと、オリジナルサイズの画像ディレクトリ、mogrify -geometry x300でリサイズした画像ディレクトリが用意されている。
Web用のものについてはHTMLで出力し、リサイズされた画像のWebサーバー上のコピーを参照しなければならない。

PDFについては、MarkdownをLaTeXに変換してという方法もあるにはあったが、画像を含めた細かなデザインを施したかったため、HTMLをベースにPDFに変換することにした。

MarkdownコンバータはRubyのKramdownを使用。改ページはないため、wkhtmltopdfを用いてPDFに変換することとした。
参照しているディレクトリの違いで2回ループを回す。

それぞれHTMLは別に出力し、テンポラリファイルとして出力したHTMLを元にPDFのテンポラリファイルを生成、unitepdfを使ってこれを結合する。

専用のコードなので多くをハードコーディングしているが、これで正常に機能する。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

require 'kramdown'
require 'erb'

def treat_md(file, findex)
  $article_body = doc = Kramdown::Document.new(File.read(file)).to_html
  
  $article_title = doc[/<h1[^>]*>(.*?)<\/h1>/, 1]
  
  foot = Array.new
  if findex > 0
    foot << '<a href="' + ( SOURCEFILES[findex - 1].sub(/\.md$/, '')  + ".html" ) + '">前へ</a>'
  end
  
  foot << '<a href="/si/" target="_top">Aki SI&Eトップページへ</a>'
  
  if findex < ( SOURCEFILES.length - 1 )
    foot << '<a href="' + ( SOURCEFILES[findex + 1].sub(/\.md$/, '')  + ".html" ) + '">次へ</a>'
  end
  
  $article_footer = foot.join
end


TEMPLATE = <<'END'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" http://www.w3.org/TR/xhtml1/dtd/xhtml1-transitional.dtd">
<html>
  <head>
    <title><%= $article_title %></title>
    <meta http-equiv="content-language" content="ja" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <style>
body {
% case $mode
% when :Web
  font-size: 130%;
  color: #333;
  font-weight: bold;
  margin: 0px;
  padding: 0px;
% else
  font-size: 18pt;
  color: #000;
  font-weight: bold;
  margin: 2cm auto;
  padding: auto;
% end
  background-color: #fff;
  font-family: "Migu 1C"
}   

h3 {
  font-size: 135%;
}

#Container {
  margin: auto auto 100px;
  max-width: 950px;
  text-align: center;
}

img {
% case $mode
% when :Web
  height: 300px;
% else
  height: 18em;
  max-width: 100%;
% end
}

ul {
  text-align: left;
  margin: auto 1.8em;
}


#ArticleFooter {
% if $mode != :Web
  display:none;
% end
  position: fixed;
  bottom: 0px;
  margin: 0px;
  width: 100%;
  max-height: 100px;
  background-color: #339;
  color: #fff;
  text-align: center;
}

#ArticleFooter a {
  padding: 0 0.8em;
  text-decoration: none;
  color: #fff
}

#ArticleFooter a:hover {
  text-decoration: underline;
}
    </style>
  </head>
  <body>
    <div id="Container">
    <%= $article_body.gsub(
      %r{src="img/},
      %(src="#{$img_type}_img/)
    ) 
    %>
  </div>
    <div id="ArticleFooter">
      <%= $article_footer %>
    </div>
  </body>
</html>
END

$article_body = nil
$article_footer = nil
$article_title = nil

SOURCEFILES = Dir.entries(".").select {|i| i =~ /\.md$/ }.sort

# HTML
$img_type = "/img/si/intro_slideshow/resized"
$mode = :Web

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  File.open("out/#{x.sub(".md", ".html")}", "w") do |f|
    f.puts(ERB.new(TEMPLATE, nil, "%<").result)
  end
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

# Web PDF
$img_type = "resized"
$mode = :PDF

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  fn = "out/_tmp_#{x.sub(".md", ".html")}"
    File.open(fn, "w") do |f|
      f.puts(ERB.new(TEMPLATE, nil, "%<").result)
    end
  system('wkhtmltopdf "%s" "%s"' % [ fn, fn.gsub(".html", ".pdf") ])
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

system 'pdfunite out/_tmp_*.pdf out/portable.pdf'
system 'rm out/_tmp_*'

# Printable
$img_type = "original"
$mode = :PDF

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  fn = "out/_tmp_#{x.sub(".md", ".html")}"
    File.open(fn, "w") do |f|
      f.puts(ERB.new(TEMPLATE, nil, "%<").result)
    end
  system('wkhtmltopdf "%s" "%s"' % [ fn, fn.gsub(".html", ".pdf") ])
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

system 'pdfunite out/_tmp_*.pdf out/printable.pdf'
system 'rm out/_tmp_*'

完成したものについては

Google DOCs Viewerはスライドショー向きではないので、ウェブ版を使うか、PDFをダウンロードするのがお勧め。

Atnow (diary-tools)

本当は一番最初に書くべきコードだったdiary toolsだが、ようやく書いた。

https://github.com/reasonset/diary-tools/
にアップしている。

Diary Toolsと言ってはいるが、ほぼatnowがメインだ。diary.resumeは要は日付別のファイルをエディタで開くだけだからだ。

atnowはタブセパレートのCSVとして理解可能な日記ならぬ瞬間記だ。その瞬間に起きたことを記録していき、これにより検索し、分析することを容易にする。

記憶しておくことが困難な私には欠かせない。

そのデザインが重要なのだが、今回2つのポイントがあった。

ひとつは、atnowは他のスクリプトから呼ばれる可能性もあるため、ファイルは必ずロックされなくてはいけない。だが、それをシェルスクリプトでやるとなるとなかなかハードルが高い。だが、zshには公式モジュールとしてflockが提供されている。

### Lock diary file
(
touch "${basedir}/${atnow_date}"
zsystem flock -t 180 "${basedir}/${atnow_date}"
integer ret=$?
if ((ret)); then
{
echo -n "Can't acquire lock for ${basedir}/${atnow_date}."
((ret==2)) && echo -n " timeout."
echo
} >&2
exit 1
fi

print "${now}\t${atnow_time:-now}\t${desc}" >> "${basedir}/${atnow_date}"
)

zsh/zsystemモジュールをロードすることで使用可能だ。さすが最強のシェル。

そして、zenityの返り値を確認しなくてはいけないのでやってみて気がついたのだが、

desc=$(zenity --entry --title="Input what to do or what happen now!")
if (($?)) || [[ -z $desc ]]
then
exit 127
fi

Command substitionの結果が$?で取れるらしい。

zenityが値として何を返すのかということを今回チェックしたが、基本的に入力した内容、選択なら選択したテキストが返り、キャンセルやいいえを選択した場合、non-zero終了するらしい。

ランチャに配置して完了。

ちなみに、これをメール通知するため、muttとmsmtpをセットアップした。

VHS変換用スクリプト

結局Windowsで記録するようにしたため、MPEGファイルをH.265形式に変換するためのスクリプト。

INDIR/SUBDIR/FILE.mpgファイルをOUTDIR/SUBDIR/SUBDIDR-NN.mkvに変換する。

ちょっと工夫をいれることでだいぶ楽にした。

#!/usr/bin/zsh

# Put your videos on a subdirectory under $WORKER_DIR.
WORKER_DIR=$HOME/mov/usr/vhs_converted/worker
# Videos will be put on a subdirectory under $DEST_DIR.
DEST_DIR=$HOME/mov/usr/vhs_converted/recorded/${dir:t}
# if $1 given, use as bitrate value.
bitrate={$1:512k}

for dir in $WORKER_DIR/*
do

typeset -i index=1

if [[ ! -e $DEST_DIR/${dir:t} ]]
then
mkdir $DEST_DIR/${dir:t}
fi

for i in $dir/*
do
ffmpeg -i "$i" -vcodec hevc -b:v $bitrate -aspect 720:480 $DEST_DIR/${dir:t}/${dir:t}-$(printf "%02d" $index).mkv
(( index++ ))
done
done

新旧のFirefoxを使い分けるスクリプト

新旧のFirefoxを使い分けるスクリプト

MageiaのFirefoxは24ESRだ。最新のFirefox31を~/lib/firefox/firefoxとして置いた。

しかしこのまま起動すると、どちらのバージョンを使うかによって.mozillaのバージョンチェックが行われ、アドオンなどがいじられてしまう。そのため、それぞれの.mozillaを分けたい。なお、ここでは.mozillaをいじっているが、本来なら.firefoxをいじるべきなのかもしれない。

単純に起動するバージョンによって.mozillaを変えることにした。

#!/bin/zsh

mv ~/.mozilla ~/.mozilla.orig
if [[ -e ~/.mozilla.latest ]]
then
mv ~/.mozilla.latest ~/.mozilla
fi

~/lib/firefox/firefox

mv ~/.mozilla ~/.mozilla.latest
mv ~/.mozilla.orig ~/.mozilla

Firefoxはシェルスクリプトとは別プロセスであるため、Firefox起動中にシャットダウンするようなことをしない限りファイルは保たれる。また、同時起動はどのみちできない。

しかしこのままだとbookmarkが共有されないなど不便な点がある。bookmarkやhistoryなどは~/.mozilla/firefox/$profile.default/places.sqliteにあるということだ。これは通常ファイルなので、symbolic linkにしておけばいい。ただし、latest側を、起動時に作られる.origディレクトリへのリンクにする必要がある。

$ ln -sfv ~/.mozilla.orig/firefox/$profile.default/places.sqlite ~/.mozilla/firefox/$profile.default/places.sqlite

bookmarkbackupsディレクトリもリンクしておいたほうがいいかもしれない。

PureDocにYAMLメタデータ機能を追加

これまで仕様としても定義されてこなかった@metaの使い方だが、この度実装された。

文書中の最初にある##–ではじまる行と次にくる##–ではさまれた行が、# (スペース込み)をstripしてYAMLとして解釈され、それが@metaに格納される。

1つのスペースを削除して解釈するのはYAMLにおいてスペースが意味をもつからだ。他のマクロやRubyのmagic commentにひっかからないように使用を決定したつもりだが、異論があれば修正するつもりだ。既にPureDocのリポジトリをアップデートしてある。

例えばこの文書では

##–*–*–*–*–*–*–*–##
# title : PureDocにYAMLメタデータ機能を追加
# since : 2014-09-01 15@29:00 +09:00
# tags: [devel, programming, ruby, utility]
##–*–*–*–*–*–*–*–##

というメタデータを書いてある。もちろん、仕様自体がこのような見た目のよいコメントブロックを書きやすいように考慮したものだ。

このためのコードは

# Process META DATA
# META DATA is start and end line beginning ##--.
# Lines between them is proceed as YAML after strip beginning "# " .
docstr = ARGF.read # Content

begin
if docstr =~ /^##--.*$/ $' =~ /^##--.*$/
yax = $`.each_line.map {|i| i.sub(/^# /, "") }.join
DOC.meta = YAML.load(yax) || Hash.new
end
rescue
DOC.meta = {}
end

正規表現でマッチ位置を決めて、そこから後ろでさらにマッチを探し、そしてその前、という形で「中身」を取りだし、行のEnumerableにしてからstripしている。失敗は例外任せ。

@metaをどう使うかは規定がないが、基本的には文書中で参照するためにある。また、PureDoc自体をparseしないプログラムからでもメタデータだけを読みたい要望があるかもしれないので、メタデータは#isの中ではなくここで定義することが望ましい。

EncMount

EncFSのマウントの引数が長くなりがちで面倒なので、エイリアス的に使えるスクリプトを書いてみた。githubで公開している。

#!/usr/bin/zsh

# This script needs encfs
if ! type encfs /dev/null
then
print "Encfs is no found." > 2
fi

typeset -A encmap # name, path, mountpoint mapping assoc.

# Check configuration file.
if [[ ! -f ~/.yek/encmap ]]
then
print "No mapping file." > 2
exit 1
fi

# Read the configuration file.
while read name epath dest
do
encmap[$name]="$epath::$dest"
done ~/.yek/encmap

# Decryption.
encfs ${encmap[$1]%::*} ${encmap[$1]#*::}

結構丁寧なコメントがついているので分かりやすいだろう。

EncFSディレクトリとマウントポイントをとる必要があり、しかも名前でアクセスするようにしたかったため、3要素となり多重配列の扱えないZshではやや難しい。2つの要素を::でつなぐことにより${…#…}${…%…}で取り出せるようにした。

コマンドやファイルについて検証もするため私としては丁寧なスクリプトだ。もちろん、公開のために配慮した。

IC recorderデータのpick upスクリプト

単純に日付でディレクトリをつくり、そこに移動するだけ。mvよりcp+rmのほうが細かく制御することが可能なので分けている。

#!/usr/bin/zsh

target="$HOME/local/usr/media/sound/voice/record/$(date +%y%m%d)"
if [[ ! -e $target ]]
then
mkdir -p $target
fi

cp **/*.(mp3|MP3|wav|WAV|aiff|ogg) $target && rm **/*.(mp3|MP3|wav|WAV|aiff|ogg)