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
を優先すると書いてあるので、一部除外しか指定できないような気がする。