rsync の複雑怪奇な exclude と include の適用手順を理解しよう

rsync は便利なんだけど、オプションが多くて難しい。特にややこしいのがファイルを選別するための --exclude--include オプションだ。

man を読んでもイメージがつかみにくかったので、ググったり、-vvv の結果を見たり、ソースを読んだりしつつ調べてみたところ、3 つのルールを理解すれば何とかなりそうなことが分かった。

この記事では、その 3 つのルールをなるべく分かりやすく説明する。

ルール1: 指定順に意味がある

コマンドライン引数は、通常、どの順番に指定しても同じ挙動になることが多い。しかし、rsync の includeexclude に関しては、指定順が意味を持つ。

man にも出てくる例で説明しよう。MP3 だけをコピーするには次のようにする。

rsync -av --include='*/' --include='*.mp3' --exclude='*' src dst

-av はコピーするときのお決まりのオプション。ネットワーク越しにコピーするときは、-avz として圧縮して転送することが多い。-n (dry run) をつければ、どのファイルが同期されるのかを事前に知ることができる。

今回の本題は 3 つのフィルター。それぞれの意味は次の通り。

  1. --include='*/': フォルダーはコピーする
  2. --include='*.mp3': 拡張子が MP3 のファイルはコピーする
  3. --exclude='*': それ以外はコピーしない

もし、exclude を手前にもってきて

rsync --include='*/' --exclude='*' --include='*.mp3' src dst

とすると、フィルターの優先順位が

  1. --include='*/': ディレクトリーはコピーする
  2. --exclude='*': すべてのファイルをコピーしない
  3. --include='*.mp3': 拡張子が MP3 のファイルはコピーする

となる。*.mp3 の前に * にマッチしてしまうので、何もコピーしてくれない。

フィルターの指定順には意味があって、先頭から順番に判定していくと覚えておこう。

ルール2: 上の階層から順番に調べる

いきなりクイズ。

次のコマンドを実行したとき、src フォルダーにある public_html/index.html はコピーされるだろうか?

rsync -av --include='index.html' --exclude='public_html/' src dst

このコマンドのフィルターは次の 2 つ。

  1. --include=index.html
  2. --exlclude=public_html/

public_html/index.html は両方のフィルターにマッチしそうだ。さきほど「先頭のフィルターを優先する」と説明したので、1 つ目のフィルターを優先してコピーされそうに思える。

しかし、答えは「コピーされない」である。その理由を説明していこう。

まず最初に上の階層をチェック

rsync は public_html/index.html をコピーするか判定する前に、上の階層の public_html をコピーするかどうかを確認する。

public_html に対して、2 つのフィルターを順番に適用する。

  1. --include=index.html にはマッチしないので先へ進む。
  2. --exclude=public_html/ にはマッチする。

exclude になった場合、それより下位のフォルダーは確認しない。

つまり、public_html/index.html は問答無用でコピー対象から除外される。

フォルダー内の特定のファイルのみを転送するには?

では、public_html の下の index.html のみをコピー対象とするにはどう設定すればよいだろうか。

答えはこうなる。

rsync -av --include 'index.html' --exclude 'public_html/*' src dst

順番にみていこう。

まず最初に public_html に対して 2 つのフィルターを適用する。

  1. --include=index.html にはマッチしないので先へ進む。
  2. --exclude=public_html/* にもマッチしない (* は 1 文字以上の任意の文字にしかマッチしない)。

include にマッチしたときと、どのフィルターにもマッチしなかったときは、下の階層のチェックに進む。

ということで、public_html/index.html に対して 2 つのフィルターを適用してみよう。

  1. --include=index.html にはマッチする。

include にマッチしたので、次の階層のチェックに進む。が、これが最後の階層なので、public_html/index.html はコピー対象となる。

同じように考えていけば、public_html/other.html が除外されることも分かるだろう。

(参考) 擬似コード

ここまで文字で長々説明してきたが、プログラムが読める人なら、擬似コードで説明したほうが早いだろう (読めない人は飛ばしてね)。

// public_html/index.html をコピーするか確認 (上の階層から順番にチェックする)
foreach (name in ['public_html', 'public_html/index.html']) {
  // 1 つ目のフィルターから順番にチェックする
  foreach (filter in filters) {
    if (filter_match(name, filter)) {
      if (is_exclude(filter)) {
        // exclude フィルターにマッチしたらその場で中断
        return false;
      } else {
        // include フィルターにマッチしたら、次の階層のチェックに移る
        break;
      }
    }
  }

  // include にマッチしたとき、1 つもマッチしなかったときはここにくる
}

// すべての階層で除外されなければコピーする
return true;

ルール3: 個別のフィルターの記法いろいろ

あとはフィルターの記法を知ってれば完璧に理解できるだろう。

foo は何にマッチするか

単に foo というフィルターを書いたとき、末尾部分が foo になっているファイルにマッチする。

  • ○: foo
  • ×: abcfoo
  • ○: abc/foo
  • ×: foo/abc

正規表現で書くと (^|/)foo$ となる。

/ から始めると

一方、/ から始めて、/foo のようにすると、マッチ対象が先頭のみになる。

  • ○: foo
  • ×: abcfoo
  • ×: abc/foo
  • ×: foo/abc

正規表現で書くと ^foo$ となる。

/ で終わると

末尾に / をつけると、対象がフォルダーのときのみマッチする。

foo/foo がフォルダーのときはマッチするが、ファイルのときにはマッチしない。

* や ** を使ってみる

記号についてもまとめておこう。

記号 意味 対応する正規表現
* / 以外の 1 文字以上 [^/]+
** / を含む 1 文字以上 .+
? / 以外の 1 文字 [^/]

たとえば、*.png を正規表現で書くと (^|/)[^/]+\.png$ となる。abc.pngfoo/abc.png にはマッチするが、abc.png/foo.pngにはマッチしない。

さらに、***foo/*** のように指定することで、フォルダーとその配下のファイルをまとめて指定できる。foo/*** と指定するのは、foo/foo/** の両方を指定することと等しい。

[a-z] を使う

正規表現のように、マッチする文字の範囲を指定できる。[^a-z] のように特定の文字以外にマッチするようにも書ける。POSIX クラスで [[:digit:][:upper:][:space:]] のように書くこともできる。

正規表現のように書けてうれしいのだが、[a-z]* と書いたとしても、単に [a-z][^/]+ にしかマッチしない。間違えないように注意。

まとめ

rsync の複雑な include / exclude の処理について説明した。要点は次の 3 つ。

  • フィルターは指定順に優先されている
  • 判定処理は上の階層から実施して、各階層の中でフィルターを順番に適用していく
  • 個別の記法を理解すべし

これが理解できれば、あとは頭の体操でフィルター条件を作れるはず!

一見複雑怪奇ではあるが、このような仕組みになっているおかげで、一部除外や一部許可を設定しやすくなっている。一方、tar コマンドは man を見ると exclude を優先すると書いてあるので、一部除外しか指定できないような気がする。