syslog.confチートシート

毎回syslog.confの文法を思い出すのが面倒になってきたので,チートシートを作ってみました.syslog.conf(5)は読みにくいし.

とりあえずDebian Sargeのsyslog.conf(5)を参考に作成した.

■syslog.conf cheat sheet (for Linux)
1行1ルール.selectorにマッチするruleのactionを全て実行する.

<rule> ::= <selector><delimiter><action>
<delimiter> ::= (TAB | SPACE)+
  ※マッチする全てのselectorのactionを実行する
  ※デリミタはTAB区切り推奨! (古いUNIXのsyslogdはSPACEがダメな事もあるよ!)

= example
*.info;\
  mail.none  /var/log/info-expect-mail.log

= selector
<selector> ::= <selector_elem>
<selector_elem> ::= <facility>.<priority>[;<selector_elem>]*
  ※後続のselector_elemは先行selector_elemを上書き

<facility>:
  auth, authpriv, cron, daemon, kern, lpr, mail, mark, news, syslog,
  user, uucp, local0-7
  *: 全部

<priority>:
  debug, info, notice, warning, err, warn, panic
  包含関係あり: debug⊃info⊃notice⊃warning⊃err⊃warn⊃panic
  none: そのfacilityを除外

  <facility>.prio1,prio2,...
    -> <facility>.prio1;<facility>.prio2と同じ

= action
  /full/path/of/logfile
  -/unbuffered/output/improve/performance/but/sometime/lost/log
  |/named/pipe/create/before/syslogd/starting
  /dev/tty1
  /dev/console
  @loghost.example.com
  @192.168.0.1
  root,user1  (write(1) to the specified users)
  *  (wall(1))

= Extensions
== Linux
<priority>:
  <facility>.=info  (only records info level messages.)
  <facility>.!info  (only records debug.  ignore higher than info level.)
  <facility>.!=info (ignore info.)

= How to restart syslogd with the new configuration
== Linux
  % sudo kill -HUP `cat /var/run/syslogd.pid`
== FreeBSD
  % sudo /etc/rc.d/syslogd reload
   or
  % sudo /etc/rc.d/syslogd restart

恵方巻き

:zu様のところ恵方巻きのエントリに反応.

我が家でも,今年初めて恵方巻きを食しました.

感想: 「恵方の方角確認するのとか何か面倒くさいけど,うまいからゆるす」

こうして内地かぶれしていくのだろうか…….

our変数の動作とか

Perlのmy, local, ourの違いについて,はてブ方面で盛り上がっていましたので,私も5年ぶりぐらいにPerlを触ってみましたが,ourの検証でのっけからはまりました.

まず,「ourはレキシカル変数だ」と思って,次のようなコードを書きました.

#!/usr/local/bin/perl
use strict;
use warnings;

package Mypackage;
our($foo) = 'our in toplevel';

sub bar {
    sub { $foo .= '!'; print $foo, "\n" }
}

my $closure;
{
    my($foo) = 'my in bare block';
    $closure = bar();
    $closure->();
}
$closure->();

print $foo, "\n";

これはok.次のように,期待通りに動きます.

% ./lexical1.pl
our in toplevel!
our in toplevel!!
our in toplevel!!

ここで,無名ブロック(クロージャ)の中のmyをlocalに変更してみる.動作に支障はないはず.

#!/usr/local/bin/perl
use strict;
use warnings;

package Mypackage;
our($foo) = 'our in toplevel';

sub bar {
    sub { $foo .= '!'; print $foo, "\n" }
}

my $closure;
{
    local($foo) = 'local in bare block';  # 変更
    $closure = bar();
    $closure->();
}
$closure->();

print $foo, "\n";

実行結果

% ./lexical2.pl
local in bare block!
our in toplevel!
our in toplevel!

あれ? クロージャの中の$fooがダイナミックスコープ*1になってる.

うーん.これって,our($foo)の,"$foo"という変数名が,local($foo)のところで上書き(シャドウ化)されたってこと? それで,bar()の中に着いた段階では,レキシカル変数であるはずのour($foo)は全然見えないから,変数の束縛も行われなかったってこと?

それならば,と,さらに1行追加.

#!/usr/local/bin/perl
use strict;
use warnings;

package Mypackage;
our($foo) = 'our in toplevel';

sub bar {
    my($foo) = 'my in bar';  # 追加 ...(1)
    sub { $foo .= '!'; print $foo, "\n" }
}

my $closure;
{
    local($foo) = 'local in bare block';
    $closure = bar();
    $closure->();
}
$closure->();

print $foo, "\n";

こちらは,以下のように期待通りの結果.しかし,トップレベルのour($foo)が参照できないことは変わらないので全然うれしくない.

% ./lexical3.pl
my in bar!
my in bar!!
our in toplevel

(1) のところで,our($foo)をどうにか参照できないか,と,my($foo) = $Mypackage::fooとか,苦し紛れにlocal($foo) = $fooとかしてみるが,いずれもlocal($foo)がしゃしゃり出てきて全然だめ.

Perlって難しいなぁ.

*1:この日記を書いた時点では,このような動きを何と呼ぶのかわからなかったのだが,shallow bindingと呼ぶのが正しいらしい.

MH形式フォルダをMaildirに変換

必要にかられ,MH形式のフォルダをMaildirに変換するスクリプトを書きました.Rubyで.

MHからMaildirに移行する場合に便利に使えると思います.

Googleなどの検索エンジン経由で見つかるmh2maildirというツールもあるようですが,以下の点がいまいちに感じましたので,自分で作ってしまいました.

  • Maildir形式のメールを書き込むためだけにprocmailを使っている
  • 未読フラグや返信済みフラグが保存されない(Maildir形式に変更してくれない)

ファイル構成

  • mh.rb
    • mh2maildir.rbが使うクラス "Mail::Reader::MH", "Mail::Message::MH" を定義
  • maildir.rb
    • mh2maildir.rbが使うクラス "Mail::Writer::Maildir" を定義
  • mh2maildir.rb

使い方

(1) mh2maildir.rb の形で起動.
Maildirフォルダは自動的に作成されます.指定されたMaildirフォルダが既に存在する場合は動作を停止します(ぽかよけの為)

% ~/bin/mh2maildir.rb Mail Maildir-fromMH

(2) 必要に応じ,変換後のMaildirフォルダを,既存のMaildirと統合
昨日紹介したmddigest, mdmergeを使います.

% ~/bin/mddigest.rb Maildir-fromMH
% ~/bin/mddigest.rb Maildir
% ~/bin/mdmerge.rb Maildir Maildir-fromMH

実行前にMTAを止めておくこと.

注意点

  • 壊れたメールは処理しません
    • 「壊れたメール」の定義: ヘッダが一行もないMH形式のファイル
  • Courier IMAP専用です
    • 他のIMAPサーバ用のMaildirを作るためには,いくつか修正しないといけないかも
  • MH形式フォルダに,日本語フォルダ名がある場合はうまく処理できません
    • UTF-16に変換して変形Base64の処理をする必要があるようですが,面倒ですので処理してません
    • まぁ,MH形式のフォルダ名に日本語を付けている人なんていないだろうけど…….
  • MH形式フォルダ名に "." (ピリオド) を含む場合は,JIS X 0208「.」(いわゆる全角のピリオド)に変換します.
    • Courier IMAPの場合,ピリオドはIMAPフォルダの区切りに使う文字なので,変形Base64エンコーディングするのが筋ですが,そういうフォルダ名がある場合,Courier IMAP + Thunderbirdの組み合わせで正しく動かないため,やむなくこうしてあります.

ソース

mh.rb
#!/usr/local/bin/ruby
# Mail::Reader::MH class
# Mail::Message::MH class
# $Id: mh.rb 39 2008-01-08 06:37:09Z genta $

require 'time'
require 'find'

class Mail
class Reader
class MH
  attr_reader :basedir
  def initialize(basedir)
    @basedir = basedir
  end

  def traverse
    Find.find(@basedir) do |file|
      next if File.directory?(file)
      rpath = file.sub(%r|^#{Regexp.escape(@basedir)}/?|, '')
      path = rpath.split(File::SEPARATOR)

      next if path.size < 2
      folder = File.join(path[0..-2])
      filename = path[-1]

      next unless filename =~ /^\d+$/

      msg = open(folder, filename)
      next if ! msg.sanity?  # skip broken mail
      yield(msg)
    end
  end
  alias :each :traverse

  def open(folder, filename)
    file = File.join(@basedir, folder, filename)
    message = Mail::Message::MH.new(file)
    message.folder = folder
    return message
  end
end # MH
end # Reader



class Message
class MH
  attr_reader :file, :from, :time, :header, :flag
  attr_accessor :folder
  def initialize(file)
    @header = Hash.new {[]}
    @flag = {}
    @file = file
    open()
  end

  def open(file = @file)
    File.open(file) do |fd|
      key, key_h, value = '', '', ''
      fd.each do |line|
        line.strip!
        break if line =~ /^$/ # End of the header

        if /^From (\S+) *(.+)$/ === line then
          _, @from, time = $~.to_a
          @time = Time.rfc2822(time) rescue Time.parse(time)
          next
        end
        if not /^([^: \000-\037\177]+): *(.*)/ === line then
          # line is the tailer of above line
          @header[key_h][-1] += ' ' + line
          next
        end
        _, key, value = $~.to_a
        key_h = key.downcase.capitalize
        @header[key_h] = [] unless @header.key?(key_h)
        @header[key_h] <<= value
      end
    end

    %w!Seen Replied Forwarded!.
      select {|x| @header[x].size > 0 }.
      map {|x| @flag[x] = true }

    if @time.nil? then
      retryq = [
        lambda { @header['Date'][0] },
        lambda { @header['Received'].first.match(/.*;(.*)/).to_a[1] },
        lambda { File.mtime(file).to_s }
      ]
      begin
        time = retryq.shift.call
        @time = Time.rfc2822(time) rescue Time.parse(time)
      rescue
        if retryq.empty? then
          raise "Mail::Message::MH#open: Can't get the date. (file: #{file})"
        end
        retry
      end
    end
  end

  def each
    fd = File.open(@file)
    fd.each do |buf|
      _, rs = /(\r?\n)\Z/.match(buf).to_a
      next if /^(Seen|Replied|Forwarded): *$/ === buf  # cut MH flags
      if /^From / === buf then
        if not @from.nil? and not @header.key?('Return-path')
          yield("Return-Path: <#{@from}>" + rs)
        end
        next
      end
      yield(buf)
      break if /^$/ === buf
    end

    fd.each do |body|
      yield(body)
    end
    fd.close
  end

  def sanity?
    return false if @header.empty?
    return true
  end
end # MH
end # Message
end # Mail
maildir.rb
#!/usr/local/bin/ruby
# Mail::Writer::Maildir class
# $Id: maildir.rb 38 2008-01-07 18:20:21Z genta $

require 'time'

class Mail
class Writer
class Maildir
  attr_reader :maildir, :logger
  @@host = `hostname`.chomp
  @@pid = $$

  def initialize(maildir, logger = nil)
    @maildir = maildir
    if ! logger.nil? then
      @logger = lambda do
        o = logger
        dir = nil
        lambda do |tmpfile|
          if tmpfile.nil? then
            o.puts
            return
          end
          if dir.nil? or dir != tmpfile[:dir] then
            dir = tmpfile[:dir]
            o.puts
            o.print "#{dir}: "
          end
          o.print "."
        end
      end.call()
    else
      @logger = lambda {|x| x}  # do nothing
    end
    create_maildir(@maildir)
  end

  def write(reader)
    reader.traverse do |mh|
      tmpfile = tmpfile(mh)
      raise 'Internal error' if test(?e, tmpfile[:file])
      @logger.call(tmpfile)

      create_folder(tmpfile[:dir])

      File.open(tmpfile[:file], 'w') do |fd|
        mh.each {|buf| fd.print buf }
      end
      curfile = curfile(tmpfile, mh)
      File.rename(tmpfile[:file], curfile)
      File.utime(mh.time, mh.time, curfile)
    end
    @logger.call(nil)
  end


  private
  def create_maildir(maildir)
    if test(?e, maildir) then
      raise "#{maildir}: Maildir already exists"
    end
    system(*%W<maildirmake #{maildir}>) or raise "maildirmake"
  end

  def tmpfile(mh)
    t = Time.now
    time, usec = t.to_i, t.usec
    pid = @@pid
    host = @@host
    dir = mh2maildir_rpath(mh.folder)
    file = File.join(@maildir, dir, "tmp", "#{time}.M#{usec}P#{pid}_0.#{host}")
    return({:time => time, :usec => usec, :pid => pid, :host => host,
            :dir => dir, :file => file})
  end

  def curfile(t, mh)
    s = File.stat(t[:file])
    info = ''
    [['Seen', 'S'], ['Replied', 'R']].each do |(label, tag)|
      info += tag if mh.flag[label]
    end
    info = ',' + info if info.size > 0
    file = "%d.M%dP%dV%016XI%08X_0.%s,S=%d:2%s" %
      [t[:time], t[:usec], t[:pid], s.dev, s.ino, t[:host], s.size, info]

    curfile = File.join(@maildir, t[:dir], 'cur', file)
    return curfile
  end

  def mh2maildir_rpath(rpath_mh)
    rpath = rpath_mh.gsub('.', '&,w4-') # XXX: replace '.' -> JISX0208 '.'
                                        #      due to Thunderbird + Courier IMAP
                                        #      can't handle '.' in folder name.
    mpath = rpath.split(File::SEPARATOR)
    if mpath.size == 1 and mpath[0] == 'inbox' then
      return ''
    end
    # Convert MH specific folders to IMAP folders.
    [['drafts', 'Drafts'], ['trash', 'Trash']].each do |(mh, imap)|
      mpath[0] = imap if mpath[0] == mh
    end
    '.' + mpath.join('.')
  end

  def create_folder(dir)
    return nil if test(?e, File.join(@maildir, dir))

    _, folder = /^\.(.*)$/.match(dir).to_a  # strip '.' at head of dirname
    path = folder.split('.')
    path.size.times do |i|
      folder = path[0..i].join('.')
      next if test(?e, File.join(@maildir, '.' + folder))
      system(*%W<maildirmake -f #{folder} #{@maildir}>)
    end
  end
end # Maildir
end # Writer
end # Mail
mh2maildir.rb
#!/usr/local/bin/ruby
# $Id: mh2maildir.rb 37 2008-01-07 16:35:10Z genta $

$LOAD_PATH << File.dirname($0)
require 'mh'       # Mail::Reader::MH, Mail::Messgae::MH class
require 'maildir'  # Mail::Writer::Maildir
require 'pp'

def usage
  puts "usage: #{File.basename($0)} <MH directory> <Maildir>"
  puts "Warning: Maildir will be overwritten."
end

begin
  $mhdir =   ARGV[0] or raise
  $maildir = ARGV[1] or raise
rescue
  usage()
  exit(1)
end

mh = Mail::Reader::MH.new($mhdir)
maildir = Mail::Writer::Maildir.new($maildir, $stderr)

maildir.write(mh)