Python 40行でbash/zshのヒストリファイルをマージするスクリプト

自分はzshユーザなのですが、色々なマシンで作業していると、各マシンのヒストリファイル.zsh-historyをマージしたくなることってありますよね。「あれ、あのコマンド、前に使ったはずなのに、コマンドヒストリにない...」と思ったら、他のマシンで作業した時に入力したコマンドだった、というような場合です。

Pythonのgeneratorやiterator周りの練習として、zshのコマンドヒストリファイル(.zsh-historyや.bash_history)をマージするmergehist.pyを、短めに40行で書いてみました。多分、書ける人が書けば、もっとずっと短く書けると思います。

コマンドヒストリファイルの構造は単純で、例えば、
: 1443359084:0;ls
といったように、基本、各行は
: UNIXタイムスタンプ:0;打ったコマンド
の羅列です。が、必ずUNIXタイムスタンプの昇順に並んでいなければなりません。また、末尾に、バックスラッシュを入れて複数行のコマンドを入力した場合は、「打ったコマンド」の部分に複数行がそのまま記録されますから、その辺も処理してやる必要があります。

使い方は、
python mergehist.py ヒストリファイル1 ヒストリファイル2 ...
という具合に、マージしたいヒストリファイルを複数(3個以上でもよい)、ちゃんとUNIXタイムスタンプの昇順などを考慮してマージされたものが標準出力に出てくる...はずです。

#!/usr/bin/env python
import sys

def histkey(line): return int(line[2:12])

def concat_multiline(it):
  it = iter(it)
  toyield = ''
  for line in it:
    toyield += line 
    if line.strip().endswith('\\'):
      continue
    yield toyield
    toyield = ''

def merger(*iterables, **kwds):
  iters    = map(iter, iterables)
  tonext   = [1] * len(iters)
  stack    = []
  prevpopi = -1

  mykeyf   = lambda x: kwds.get('key')(x[0]) if kwds.get('key') else None
  while True:
    for i, it in enumerate(iters):
      try:
        if tonext[i] > 0:
          stack.append((it.next(),i))
          tonext[i] = 0
      except StopIteration:
        tonext[i] = 0
    stack.sort(key = mykeyf)
    if stack:
      x, prevpopi = stack.pop(0)
      tonext[prevpopi] = 1
      yield x
    else:
      raise StopIteration()

sys.stdout.writelines(l for l in merger(*[concat_multiline(line for line in \
    open(sys.argv[i], r'r')) for i in range(1,len(sys.argv))], key=histkey))