OOCP

Directory Synchronizer

oocp.rbは、2つのディレクトリを同期(内容を等しく)させ るためのコピーコマンドです。 またこのパッケージには好きなポリシーでコピーコマンドを作るため のライブラリdir-compare.rbが同梱されています。

インストール

dir-compare.rb を ruby が require 可能なディレクトリ に置いてください。また、起動コマンドは oocp.rb です。

使い方

2 つのディレクトリ source-dirtarget-dir を同期させたい 場合は、

ruby oocp.rb source-dir target-dir

としてください。このとき source-dir のファイルと ディレクトリが target-dir に次の条件の下に コピーされます。

なお、ファイルやディレクトリの「削除」とは target-dir と 同位のディレクトリに作る trash というディレクトリへの移動です。 (例: target-dirabc/efg なら abc/trash 。) このディレクトリはコピーコマンドへの第3引数として次のように指定する こともできます。

ruby oocp.rb source-dir target-dir trash-dir

また、-s オプションを使って、

ruby oocp.rb -s source-dir target-dir

とすると、実際にコピーは行わず、実行されるはずの手続きを表示します。

dir-compare.rb

oocp.rb は複数のディレクトリを比較するライブラリ dir-compare.rb のサンプルアプリケーションです。 ここでは以下の dir-compare.rb の使用例があげられています。

  1. Copy
  2. Dir-Sync
  3. Dir-Common
  4. Rm-r
  5. Mv-o

また以下がこのライブラリで定義されているクラスです。

  1. DirCompare
  2. DirWalker
  3. DirRunner
  4. DirPairWalker
  5. DirPairRunner
  6. QuasiFile

サンプル

Copy

ファイルのコピーコマンド: cp.rb

次のコマンドはファイルとディレクトリの再帰的なコピーを行います。

#!/usr/local/bin/ruby
require "dir-compare"
require "ftools"

class Cp < DirPairWalker
  def file_file(s, t)
    File.cp(s, t)
  end

  def file_non(s, t)
    File.cp(s, t)
  end

  def dir_non(s, t)
    File.mkpath(t)
  end
end

source, target = ARGV
visitor = Cp.new
dc = DirCompare.new(visitor)
dc.run(source, target)

コピーコマンドを作るにはまず、 DirPairWalker というクラスを継承して新しいクラス (例えば Cp) を 作り、そのインスタンスを DirCompare.new の引数にして あるオブジェクト(例えば dc) を作ります。 そして、操作の対象になるディレクトリは、その dc.run の引数として与えます。

この run メソッドで実行される処理は Cp の 決められた名前のメソッドとして記述します。ここでの file_file(s, t)file_non(s, t)dir_non(s, t) という名前のメソッドは、コピー元のファイル名が s コピー先のファイル名が t として起動され、 それぞれ、st も存在する場合と、t のみ存在しない 場合と、s がディレクトリで t が存在しない場合に 起動されます。 このペア (s, t) は (source, taget) のサブディレクトリ内の全てのファイル を再帰的に走ります。 詳細は DirPairWalkerのリファレンスを見てください。

Dir-Sync

ディレクトリの同期コマンド: dir-sync.rb

次のコマンドは、2つのディレクトリを比較し、 両方を最新の状態に更新します。

#!/usr/local/bin/ruby
require "dir-compare"
require "ftools"

class DirSync < DirPairWalker
  def file_lt_file(s, t)
    File.cp(t, s)
  end

  def file_gt_file(s, t)
    File.cp(s, t)
  end

  def file_non(s, t)
    File.cp(s, t)
  end

  def non_file(s, t)
    File.cp(t, s)
  end

  def dir_non(s, t)
    File.mkpath(t)
  end

  def non_dir(s, t)
    File.mkpath(s)
  end
end

source, target = ARGV
visitor = DirSync.new
dc = DirCompare.new(visitor)
dc.run(source, target)

file_lt_file(s, t)file_gt_file(s, t) というメソッドは、s より t が、 新しい場合、古い場合にそれぞれ起動されます。

Dir-Common

ディレクトリの共通部分の表示: dir-common.rb

次のコマンドは、2つのディレクトリを比較し、 共通のファイルを表示します。

#!/usr/local/bin/ruby
require "dir-compare"
require "ftools"

class DirCommon < DirPairWalker
  def file_file(s, t)
    puts "#{s} <-> #{t}"
  end

  def dir_file(s, t)
    throw :prune
  end

  def dir_non(s, t)
    throw :prune
  end

  def file_dir(s, t)
    throw :prune
  end

  def non_dir(s, t)
    throw :prune
  end
end

source, target = ARGV
visitor = DirCommon.new
dc = DirCompare.new(visitor)
dc.run(source, target)

ここでは、一方のディレクトリにのみサブディレクトリがある場合、 その中への探索を throw :prune で中断することにより、無駄な処理を させないようにしています。

Rm-r

ディレクトリの削除: rm-r.rb

次のコマンドは、ディレクトリを削除します。

#!/usr/local/bin/ruby
require "dir-compare"
require "ftools"

class Rm_r < DirWalker
  def file(s)
    File.unlink(s)
  end

  def dir_out(s)
    Dir.unlink(s)
  end
end

rm = Rm_r.new
dc = DirCompare.new(rm)
path = ARGV.shift
dc.run(path)

dir_out は、ディレクトリに入るときでは なく、出るときに起動されます。DirWalker を参照してください。

Mv-o

ディレクトリの上書き移動: mv-o.rb

このコマンドは、ディレクトリを移動させます。すなわち、 "mv-o.rb source target" は "rm -r target; mv source target" とほぼ同等ですが、 target にのみあるファイルと ディレクトリはそのまま残されます。 また、もし sourcetarget がディレクトリなら、このコマンドは "cp -r source/* target; rm -r source" ともほぼ同等ですが、可能な限り rename (File.mv) を用います。

#!/usr/local/bin/ruby
require "dir-compare"
require "ftools"

class Mv_o < DirPairWalker
  def initialize
    @file_dir = false
  end

  def file_file(s, t)
    File.mv(s, t)
  end

  def file_dir(s, t)
    @file_dir = true
  end

  def file_dir_out(s, t)
    @file_dir = false
    Dir.unlink(t)
    File.mv(s, t)
  end

  def file_non(s, t)
    dir = File.dirname(t)
    File.mkpath(dir) unless File.directory?(dir)
    File.mv(s, t)
  end

  def non_file(s, t)
    if @file_dir
      File.unlink(t)
    end
  end

  def dir_file(s, t)
    File.unlink(t)
    File.mv(s, t)
    throw :prune
  end

  def dir_dir_out(s, t)
    Dir.rmdir(s)
  end

  def dir_non(s, t)
    dir = File.dirname(t)
    File.mkpath(dir) unless File.directory?(dir)
    File.mv(s, t)
    throw :prune
  end
end

visitor = Mv_o.new
dc = DirCompare.new(visitor)
source, target = ARGV
dc.compare(source, target)

*_out という名前のメソッドは、ディレクトリに入るときでは なく、出るときに起動されます。DirPairWalker を参照してください。

DirWalker

このクラスは DirCompare オブジェクト生成時に渡される ビジターのクラスです。ビジターとは、目的のファイルやディレクトリに 出会うごとにすべき処理が定義されたオブジェクトです。 実際に起動されるのは DirCompare オブジェ クトのメソッド run が起動された時です。

メソッド

file(s)

s がファイルの時起動される

dir(s)

s がディレクトリの時起動される

dir_out(s)

s がディレクトリで、そこから出て行くとき起動される。

run(mod, sqf)

DirCompare クラスの run が内部で直接起動するメソッドです。 このメソッドが上記 3 つのメソッドを再度呼び出しています。 この sqfQuasiFile オブジェクトです。mod はディレクトリに入るとき :in、出るとき :out、 ファイルのとき :empty です。

DirRunner

このクラスは file(s)dir(s)dir_out(s)s にファイル名(String)ではなく、それをラップした QuasiFile が渡るだけで、あとは DirPairWalker と 同じです。

DirPairWalker

このクラスは DirCompare オブジェクト生成時に渡される ビジターのクラスです。ビジターとは、目的のファイルやディレクトリに 出会うごとにすべき処理が定義されたオブジェクトです。 実際に起動されるのは DirCompare オブジェ クトのメソッド run が起動された時です。

起動の順番はサブディレクトリの「深さ優先」で行われます。

メソッド

st は、ソースとターゲットになるファイルあるいは ディレクトリを表します。

dir_dir(s, t)

st がディレクトリであるときに起動される

dir_dir_out(s, t)

st がディレクトリで、そこから 出て行くとき起動される

dir_file(s, t)

s がディレクトリ、t がファイルであるときに起動される

dir_file_out(s, t)

s がディレクトリ、t がファイルで、その ディレクトリから出るとき起動される

dir_non(s, t)

s がディレクトリ、t が存在しないときに起動される

dir_non_out(s, t)

s がディレクトリで t は存在せず、そのディレクトリから 出るとき起動される

file_dir(s, t)

s がファイル、t がディレクトリのときに起動される

file_dir_out(s, t)

s がファイル、t がディレクトリで、そのディレクトリ から出るとき起動される

file_file(s, t)

st がファイルでのとき file_eq_filefile_gt_filefile_lt_file に起動される。

file_eq_file(s, t)

st がファイルで、更新時刻が等しいときに起動され、 更に file_file を起動する

file_gt_file(s, t)

st がファイルで、s の方が新しいとき起動され、 更に file_file を起動する

file_lt_file(s, t)

st がファイルで、t の方が新しいとき起動され、 更に file_file を起動する

file_non(s, t)

s がファイルで、t が存在しないとき起動される

non_dir(s, t)

s が存在せず、t がディレクトリのとき起動される

non_dir_out(s, t)

s が存在せず、t がディレクトリで、そのディレクトリから 出るとき起動される

non_file(s, t)

s が存在せず、t がファイルのとき起動される

non_non(s, t)

st も存在しないとき起動される。

run(mod, sqf, tqf)

DirCompare クラスの run が内部で直接起動するメソッドです。 このメソッドが上記 17 メソッドを再度呼び出しています。 この sqftqfQuasiFile オブジェクトです。 mod については DirCompare#parse を見てください。

DirPairRunner

このクラスは file_gt_file(s, t)s, t にファイル名(String)ではなく、それをラップした QuasiFile が渡るだけで、あとは DirPairWalker と 同じです。

DirCompare

このクラスは複数のディレクトリを比較するためのクラスです。

クラスメソッド

DirCompare.new(visitor)

visitor をビジターとしてインスタンスを生成します。

メソッド

run([path1, [path2, [path3,..]]])
compare([path1, [path2, [path3,..]]])

ビジターのメソッド run(mod, qf1, qf2, qf3,..) を起動します。 ここで、qf1, qf2, qf3,.. は path1, path2, path3,.. から作られた QuasiFile オブジェクトです。 mod については parse を見てください。 結局、このメソッドはこの次のように作られています。

def run(*paths)
  qfs = paths.map{|path| QuasiFile.new(path)}
  parse(*qfs) do |mod, *qfs0|
    @visitor.run(mod, *qfs0)
  end
end

ここでインスタンス変数 @visitor は、自身が生成された とき添えられたビジターを指しています。

parse([qf1, [qf2, [qf3,..]]])

ディレクトリ QuasiFile オブジェクト qf1, qf2, qf3, ... を引数にして起動し、ブロックパラメータに、modQuasiFile オブジェクトとして表された それぞれの内容(content)を代入しながら、イテレートします。 イテレートの順番は「深さ優先」です。

mod は次のように決まります。

mod

:in ... ブロックパラメータのどれかがディレクトリであり、そこに入る

:out ... そこから出る

:empty ... ブロックパラメータのどれもディレクトリでない(すなわちファイルか存在しないパスである)

複数のディレクトリのファイルを同期させる例

#!/usr/local/bin/ruby
require "dir-compare"
require "ftools"
qfa = ARGV.map{|dir| QuasiFile.new(dir)}
dc = DirCompare.new
dc.parse(*qfa) do |mod, *qfb|
  if mod == :empty
    m = (0...qfb.size).max{|i, j| qfb[i] <=> qfb[j]}
    qfb.each_with_index do |qf, i|
      File.cp(qfb[m].path, qf.path) if i != m
    end
  end
end

ここでは、存在しないファイルは比較によって最も小さいとされる <=>の性質を利用しています。

throw :prune をすると、より深いサブディレクトリへの 探索を中断できます。

QuasiFile

これはディレクトリあるいはファイルの情報を保持するオブジェクト のためのクラスです。具体的には StringFile::Stat のラッパーで、更に exist?content を持つクラスです。

インクルードしているモジュール

クラスメソッド

QuasiFile.new(path)

ディレクトリあるいはファイル path の情報を保持するインスタンスを 生成する。

メソッド

このクラスは、 Stringの 以下のメソッド:

%, *, +, <<, [], []=, capitalize, capitalize!, center, chomp, chomp!, chop, chop!, collect, concat, count, crypt, delete, delete!, detect, downcase, downcase!, dump, each, each_byte, each_line, each_with_index, empty?, entries, find, find_all, grep, gsub, gsub!, hex, include?, index, intern, length, ljust, map, max, member?, min, next, next!, oct, reject, replace, reverse, reverse!, rindex, rjust, scan, select, slice, slice!, sort, split, squeeze, squeeze!, strip, strip!, sub, sub!, succ, succ!, sum, swapcase, swapcase!, to_f, to_i, to_str, tr, tr!, tr_s, tr_s!, unpack, upcase, upcase!, upto, ~

と、File::Stat の以下のメソッド:

atime, blksize, blockdev?, blocks, chardev?, ctime, dev, directory?, executable?, executable_real?, file?, ftype, gid, grpowned?, ino, mode, mtime, nlink, owned?, pipe?, rdev, readable?, readable_real?, setgid?, setuid?, size, size?, socket?, sticky?, symlink?, uid, writable?, writable_real?, zero?

を備えています。ただし、存在しないファイル に対しての File::Stat 系のメソッドの値は nil です。 それ以外のメソッドは以下の通りです。

path

自分の名前を返します。

stat

自分の File::Stat オブジェクトを返します。

<=>(o)

o も QuasiFile オブジェクトとして、ファイルの更新時刻を比較する。 ただし、存在しないファイルはもっとも古いと解釈する。

content

自分のディレクトリとしての内容の配列。自分がファイルであるか存在しないパスなら空配列。

exist?

実際に存在するファイルあるいはディレクトリであるとき真を返す。

変更履歴