rsync の複雑怪奇な exclude と include の適用手順を理解しよう
rsync は便利なんだけど、オプションが多くて難しい。特にややこしいのがファイルを選別するための --exclude と --include オプションだ。
man を読んでもイメージがつかみにくかったので、ググったり、-vvv の結果を見たり、ソースを読んだりしつつ調べてみたところ、3 つのルールを理解すれば何とかなりそうなことが分かった。
この記事では、その 3 つのルールをなるべく分かりやすく説明する。
ルール1: 指定順に意味がある
コマンドライン引数は、通常、どの順番に指定しても同じ挙動になることが多い。しかし、rsync の include と exclude に関しては、指定順が意味を持つ。
man にも出てくる例で説明しよう。MP3 だけをコピーするには次のようにする。
rsync -av --include='*/' --include='*.mp3' --exclude='*' src dst
-av はコピーするときのお決まりのオプション。ネットワーク越しにコピーするときは、-avz として圧縮して転送することが多い。-n (dry run) をつければ、どのファイルが同期されるのかを事前に知ることができる。
今回の本題は 3 つのフィルター。それぞれの意味は次の通り。
--include='*/': フォルダーはコピーする--include='*.mp3': 拡張子が MP3 のファイルはコピーする--exclude='*': それ以外はコピーしない
もし、exclude を手前にもってきて
rsync --include='*/' --exclude='*' --include='*.mp3' src dst
とすると、フィルターの優先順位が
--include='*/': ディレクトリーはコピーする--exclude='*': すべてのファイルをコピーしない--include='*.mp3': 拡張子が MP3 のファイルはコピーする
となる。*.mp3 の前に * にマッチしてしまうので、何もコピーしてくれない。
フィルターの指定順には意味があって、先頭から順番に判定していくと覚えておこう。
ルール2: 上の階層から順番に調べる
いきなりクイズ。
次のコマンドを実行したとき、src フォルダーにある public_html/index.html はコピーされるだろうか?
rsync -av --include='index.html' --exclude='public_html/' src dst
このコマンドのフィルターは次の 2 つ。
--include=index.html--exlclude=public_html/
public_html/index.html は両方のフィルターにマッチしそうだ。さきほど「先頭のフィルターを優先する」と説明したので、1 つ目のフィルターを優先してコピーされそうに思える。
しかし、答えは「コピーされない」である。その理由を説明していこう。
まず最初に上の階層をチェック
rsync は public_html/index.html をコピーするか判定する前に、上の階層の public_html をコピーするかどうかを確認する。
public_html に対して、2 つのフィルターを順番に適用する。
--include=index.htmlにはマッチしないので先へ進む。--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 つのフィルターを適用する。
--include=index.htmlにはマッチしないので先へ進む。--exclude=public_html/*にもマッチしない (*は 1 文字以上の任意の文字にしかマッチしない)。
include にマッチしたときと、どのフィルターにもマッチしなかったときは、下の階層のチェックに進む。
ということで、public_html/index.html に対して 2 つのフィルターを適用してみよう。
--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.png や foo/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 を優先すると書いてあるので、一部除外しか指定できないような気がする。