【Mac】ふたつのフォルダの中身を同期する

2023年1月3日

フォルダ間を同期する

ふたつのフォルダの中身を同期するなら、rsync というコマンドを用いるのが簡単だ。

rsync コピー元 コピー先

しかし、これを毎回、手動でやるのはいかにもダルい。フォルダを監視し、中身の変更があった時にすぐ同期する、という仕組みが作れれば楽できるはずだ。

fswatch

fswatch は特定のパスを監視してくれるコマンドである。Mac においては最初から入っているわけではないが、Homebrew 等で簡単にインストールできる。

brew install fswatch

使い方は、引数として監視したいパスを指示するだけだ。そのパスの中身に変化があれば、それを感知して変更のあったファイルを淡々と返してくれる。
 デスクトップに folderA というフォルダを作って監視させてみる。

fswatch ~/Desktop/folderA

ファインダーに戻って、folderA のなかに新規テキストファイルを作ったり、テキストファイルを編集したり、デスクトップにあった適当なファイルを出し入れしてみた。ターミナルを確認すると、

> fswatch ~/Desktop/folderA
/Users/bakurou/Desktop/folderA/aho.txt
/Users/bakurou/Desktop/folderA/.DS_Store
/Users/bakurou/Desktop/folderA/baka.txt
/Users/bakurou/Desktop/folderA/.DS_Store
/Users/bakurou/Desktop/folderA/list.html
/Users/bakurou/Desktop/folderA/list.html
/Users/bakurou/Desktop/folderA/.DS_Store
/Users/bakurou/Desktop/folderA/japanese_girl.jpg
/Users/bakurou/Desktop/folderA/japanese_girl.jpg

という具合にファイルパスだけを黙々と返してくれる。
 なお、fswatch を終了したい時は、 ctrl + c を押す。

この出力を xargs コマンドで受け止めて、さまざまに利用できる。
 xargs はよくわからないコマンドで、前の処理で出てきた出力を後方のコマンドに渡す、という働きをするらしい。パイプでいいじゃん、と思うものの、パイプより細やかな渡し方が出来るのだろう、おそらく。
 fswatch においては、こんな風に使うようだ。

> fswatch ~/Desktop/folderA | xargs -I{} echo {} >> ~/Desktop/watch_log.txt

上記の場合、fswatch の出力を、echo で watch_log.txt というファイルに書き出している。
 あくまでイメージの話で、正しい解釈ではないとは思うのだが、上記の例だと xargs は、fswatch の出力を、 -I{} で受けとめ、後方の {} に代入している、みたいな感じだと思う。

fswatch と xargs

これを使えば、同期とはいわないまでも cp コマンドでバックアップはできるはずだ。

> fswatch ~/Desktop/folderA | xargs -I{} cp {} ~/Desktop/folderB

こんな感じになろうかと思う。さらにログを出さないために、 2> /dev/null をつけ、バックグラウンドで走らせるために & をつける。

> fswatch ~/Desktop/folderA | xargs -I{} cp {} ~/Desktop/folderB 2> /dev/null &

実行してみるとうまい具合に動く。
 特に、CotEditor などの自動保存してくれるエディタでファイルを編集してくれる時、リアルタイムでコピー先のファイルも更新されて感動する。
 監視を終了させる時は、 ps -a で PID の数値を調べて kill する。

> ps -a
  PID TTY           TIME CMD
82408 ttys000    0:00.01 login -fp bakurou
82418 ttys000    0:26.52 zsh (figterm)
10516 ttys001    0:00.01 login -fp bakurou
10517 ttys001    0:00.45 -zsh
89726 ttys001    0:00.01 fswatch /Users/bakurou/Desktop/folderA
89727 ttys001    0:00.00 xargs -I{} cp {} /Users/bakurou/Desktop/folderB
93869 ttys001    0:00.01 ps -a
82442 ttys002    0:00.02 /bin/zsh --login

fswatch の PID は 89726 となっている。

> kill 89726

同期を試す

fswatch コマンドは以下のようにも書ける。

fswatch -0 $watch_dir | while read -d "" event; do
     cp "$event" $backup_dir 2> /dev/null
done &

fswatch のオプション -0 は Null 文字を区切りに使う、というもの。よくわからないが、read -d と関係していると思われる。read -d も区切り文字を指定するオプションだ。例えば read -d ";" だったら、; が出てきた時点で入力が終了し、ひとつ区切る。
 これを利用し、rsync のシェルスクリプトを作るとしたら、こんな風になると思う。

📝 syncdir.sh

#!/bin/zsh

watch_dir=~/Desktop/folderA
backup_dir=~/Documents
watch_dir2=~/Documents/folderA
backup_dir2=~/Desktop

fswatch -0 $watch_dir | while read -d "" event; do
    rsync -au --delete $watch_dir $backup_dir
done &

fswatch -0 $watch_dir2 | while read -d "" event; do
    rsync -au --delete $watch_dir2 $backup_dir2
done &

デスクトップの folderA を、書類フォルダの folderA と同期する。デスクトップの folderA と、書類の folderA、どっちのフォルダをいじっても片方に同期される。
 されるこたぁされるんだが、一見してわかるようにちょっと危い。 無限ループに落ち入る可能性がある。 A を更新すると、B も更新される。B の更新を受けてまた A が更新され、ふたたび B が……みたいな。
 そうならないために、rsync にオプションを設定した。
  -a はオーナーやパーミッション、タイムスタンプをそのままコピーする。
  -u は追加・更新されたファイルやフォルダだけを同期の対象にする。
  --delete は、片方でファイルが削除されたら、もう片方も同じファイルを削除する。

rsync のコピー先のパスには、コピーされるフォルダの親フォルダを指定する、というところも間違わないようにしたいところだ。
 試してみると一応動作する。きびきび同期してくれる。

Hammerspoon

Hammerspoon は、いろんなきっかけで lua スクリプトを走らせ、Mac を使いやすくしてくれるユーティリティだ。その Hammerspoon にも、pathwatcher というのがある。

hammerspoon アイコン

🔗 Getting Started

📝 init.lua

function pathWatch(files)
    for _,file in pairs(files) do
        hs.execute("cp " .. file .. " /Users/USERNAME/Desktop/folderB")
    end
end
myWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/Desktop/folderA/", pathWatch):start()

監視したいフォルダを hs.pathwatcher.new(path, func) の path に設定する。変更があったファイルは、files というテーブルかなんかで pathWatch 関数へ送られる。files は for で file に分けられ、順番に処理される。

こちらのスクリプトも良好に動作する。
 Hammerspoon を使えば、起動時にこのスクリプトが実行されるので便利だ。
 fswatch のスクリプトも、launchd の plist を作れば起動時に実行できるはずなのだが、試してもうまくいったことがない。

pathwatcher を使って同期を試みるなら、以下のようなものが考えられる。まずはシェルスクリプトを準備するのがいいのではないかと思う。

📝 syncdir2.sh

#!/bin/zsh

rsync -au ~/Desktop/folderA  ~/Documents
rsync -au ~/Documents/folderA  ~/Desktop

不用意にファイルを消さないよう最初は --delete オプションをつけないでおく。
 上記のスクリプトを Hammerspoon で実行する。

📝 init.lua

function pathWatch(files)
    for _,file in pairs(files) do
        hs.execute("sh /Users/USERNAME/bin/syncdir2.sh")
    end
end
deskWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/Desktop/folderA/", pathWatch):start()
docWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/Documents/folderA/", pathWatch):start()

このスクリプトでも、片方のフォルダをいじれば、もう片方にただちに同期される。
 とりあえず 2022 年 12 月現在、ふたつのフォルダ間のファイル同期には以上のようなものが考えられるのではないかと思った。

この記事は、以下の記事で勉強させてもらい書きました。ありがとうございます。

fswatch
🔗 [macOS] Terminalでファイルを監視し更新されたら指定の処理をする – fswatch
🔗 fswatchで変更されたファイルを監視する | 404 motivation not found
🔗 【Mac・ファイル同期】rsync・sshfs・fswatchを使ってローカルとサーバーを同期したら色々便利だった | 感情的プログラミング伝記 | タウン情報誌 AIR函館 – 北海道函館市の食・呑・遊をご紹介!

xargs
🔗 【Linuxコマンド基礎】xargsがわからない!→基礎中の基礎を解説します。 | Web Apps Labo
🔗 【Linux】xargs コマンドの使い方がよく分からない – きゃまなかのブログ
🔗 xargsコマンドで覚えておきたい使い方・組み合わせ7個(+1個) | 俺的備忘録 〜なんかいろいろ〜

rsync
🔗 Linuxコマンド【 rsync 】高速なファイル同期(バックアップ) – Linux入門 – Webkaru