このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

[参照用 記事]

Pythonを使ってみた -- あっち側にだけあるファイルをリストする

「ディレクトリを再帰的にコピーするには」において、2つのディレクトリーの内容をだいたい一致させる方法を示しました。でも、この方法は単なるコピーで、完全な同期を取るものではありません。origin/ の内容を target/ にコピーしたとき、状況は次のようになります。

  1. origin/ の配下にあるファイルは全て target/ にもある*1(ファイルの中身も同じ)。
  2. target/ の配下にはあるが、origin/ 側には存在しないファイルもある。

同期(ミラーリング)するときは、「target/ の配下にはあるが、origin/ 側には存在しないファイル」は削除します。僕の当面の用途では、削除するのはダメです(とんでもないことです)。ですが、「target/ の配下にはあるが、origin/ 側には存在しないファイル」がどのくらいあるかは気になるところです。そこで、次のような機能を持つコマンドを探してみました。

  • dir1/ と dir2/ を再帰的に比較して、dir2/ にはあるが、dir1/ にはないファイルをリストする。

dircmp, diff, rsync などにこの機能が含まれていそうですが、manページ読んだりするのが億劫(オイッ)。シェルスクリプトで作ってしまおうか、と。

いや待てよ。シェルで苦労するならスクリプト言語のほうがいいかな。「Linux/Unixコマンドラインでちょっとした事をするには」では次のような啖呵切っていたけど。

「スクリプト言語(例えばperl)を使えばいいじゃねえか」というご意見・ご指摘はゴモットモだと思いますが、今回は聞く耳持ちません。

ゴモットモなご意見を聞き入れることにします。「Pythonを使ってみた -- とりあえずJSONデータを読み込む」以来、Pythonもロクに触ってないので、Python使ってみよう、と。

ちょっと調べたら、filecmpモジュールのdircmpクラスを使えばとってもお手軽にできそう。

ここから先は必要な知識を箇条書きでチャッチャと書き連ねます。


ファイルシステムとパスについて知っておくべきこと:

  1. os.listdir(path), os.getcwd(), os.chdir(path)、これは「Pythonを使ってみた -- とりあえずJSONデータを読み込む」にも書いた。
  2. パスの存在は os.path.exits(path)。
  3. パスがディレクトリであるかどうかは os.path.isdir(path)。

次にdircmpの使い方。leftとrightがディレクトリーのパスだとして、dcmp = filecmp.dircmp(left, right) とすると:

  1. dcmp.right_only でrightにだけ含まれるファイル/ディレクトリーの一覧が取れる。
  2. ファイル/ディレクトリーの一覧は文字列のリスト。
  3. dcmp.subdirs は辞書で、leftとrightに共通なサブディレクトリに対するdircmpオブジェクトが集められている。
  4. dcmp.subdirs のキーは、サブディレクトリの名前。
  5. 辞書のキーの並びはkyes()メソッド(関数じゃない)で取り出せる。

僕にとって、プログラミング言語の機能でとりあえず欲しい*2のは「map関数、ラムダ記法、三項式、リスト内包表記」。

  1. map関数は、map(<function>, <sequence>) 。
  2. lambda記法は、lambda <変数並び>: <式> 。
  3. 三項式は、<then_value> if <predicate> else <else_value> 。
  4. リスト内包表記は、[<式> for <変数> in <列> if <条件>]

リスト内包表記は最終的には使いませんでしたが、途中でお試しのときは、[f + "/" for f in dcmp.right_only if os.path.isdir(f)] とか書いてました。Pythonは記号が少なくて、綴りを持つ語を多用しますが、リスト内包表記ではかえって読みにくい感じですね。[f + "/" <- f in dcmp.righ_only | os.path.isdir(f)] とか、適度に記号が混じっていたほうが視認性がいいと思います。

んで、課題のプログラムはこんな感じ。本質的な作業はdircmpオブジェクトがやってくれるので、たったこれだけです。

# -*- coding: utf-8 -*-
# rightonly.py
import os
import filecmp

SEP = os.path.sep;

def report_right_only(left, right):
    dcmp = filecmp.dircmp(left, right)
    _report_right_only(right, dcmp)

def _report_right_only(right, dcmp):
    # 間違いが含まれるので、すぐ下の追記を見てください
    for p in map(lambda f: right + SEP + f + SEP \
                     if os.path.isdir(f) else right + SEP + f,
                 dcmp.right_only):
        print p
    for k in dcmp.subdirs.keys():
        new_right = right + SEP + k
        new_dcmp  = dcmp.subdirs.get(k)
        _report_right_only(new_right, new_dcmp)

[追記]

使っていて気がついたのですが、ディレクトリのお尻にはスラッシュ(またはバックスラッシュ)が付くはずが付いてませんでした。次のlambda式の部分が間違っていました。

lambda f: right + SEP + f + SEP \
          if os.path.isdir(f) # ここが間違い
          else right + SEP + f

まず、文字列の連結の代わりにos.path.joinを使って見やすくしておきます。

lambda f: os.path.join(right, f) + os.path.sep \
          if os.path.isdir(f) # ここが間違い
          else os.path.join(right, f)

「ここが間違い」の行のfが os.path.join(right, f) でした。

lambda f: os.path.join(right, f) + os.path.sep \
          if os.path.isdir(os.path.join(right, f)) \
          else os.path.join(right, f)

こうなると、os.path.join(right, f) が何度も登場してさすがに三項演算子がバカバカしくなるので、代入を使った手続き的関数を別に定義しておいたほうが良さそうです。

def make_path(parent, name):
    path = os.path.join(parent, name)
    if os.path.isdir(path):
        return path + os.path.sep
    else:
        return path

結局、_report_right_only を次のようにしました。

def _report_right_only(right, dcmp):
    def make_path(parent, name):
        path = os.path.join(parent, name)
        if os.path.isdir(path):
            return path + os.path.sep
        else:
            return path

    for p in map(lambda name: make_path(right, name), dcmp.right_only):
        print p
    for k in dcmp.subdirs.keys():
        new_right = right + SEP + k
        new_dcmp  = dcmp.subdirs.get(k)
        _report_right_only(new_right, new_dcmp)

[/追記]

rightonly.pyをコマンドとして使うときのお約束があるようなので書き足しました。お約束部分のほうが長いような。

if __name__ == "__main__":
    import sys
    prog = os.path.basename(sys.argv[0])

    def usage():
        print >>sys.stderr, \
            "usage: "  + prog +  " <left (source dir)> <right (target dir)>"
        sys.exit(1)

    def die(message):
        print >>sys.stderr, prog +  ": " + message
        sys.exit(1)

    args = sys.argv[1:]
    if len(args) != 2:
        usage()
    if not os.path.isdir(args[0]): 
        die(args[0] + " is not a directory.")
    if not os.path.isdir(args[1]):
        die(args[1] + " is not a directory.")

    report_right_only(args[0], args[1])

その他、気づいたことを。

  1. 対話的インタプリタからモジュールをimportできますが、一回importするとソレッキリなので、reload(module) を使う。
  2. 今回はプラットフォームの判定は要らなかったのだけど(os.path.sepで間に合った)、判定法が色々あってイヤだな。例えば、Windowsであることは: (os.name == 'nt'), (platform.system() == 'Windows'), (sys.platform == 'win32') 。

*1:shiroさんのご指摘のように、漏れがある可能性もあります。

*2:なければないで我慢しますがね。