rsyncでサーバーに送ったはずがパソコン内にコピーされていたとき|zshが「$変数:」を修飾子として読む仕様と、宛先を波かっこで囲む書き方・転送後の実測
rsyncでサーバーに送ったはずがパソコン内にコピーされていたとき|zshが「$変数:」を修飾子として読む仕様と、宛先を波かっこで囲む書き方・転送後の実測
ホームページの更新ファイルをサーバーへ上げるとき、FTPソフトの代わりに rsync(ファイルをまとめて転送・同期するコマンド)を使う場面が増えています。SSH(サーバーへ暗号化して接続する仕組み)経由で、変更のあったファイルだけを一括で送れる便利な道具です。
ところが Mac のターミナルでは、「送信は正常に終わったのに、サーバーに何も届かず、作業フォルダに見慣れないフォルダができている」という現象が起きることがあります。
先に結論を書きます。
- 原因は、Mac の標準シェル(入力したコマンドを解釈するプログラム)である zsh が「$変数:」の直後の特定の英字(a・h・t など)を「修飾子」として読むことです
- たとえば接続先を
H=user@sv.example.comと変数に入れ、$H:abc-salon.jp/public_html/と書くと、:aが修飾子として消費され、宛先からコロンが消えます - rsync は宛先にコロンが無いと「パソコン内のフォルダ」と判断するため、Mac 付属の rsync ではエラーを出さずに手元へ丸ごとコピーして正常終了します
- 正しい書き方は
${H}:abc-salon.jp/public_html/のように変数名を波かっこで囲むか、接続先を直接書くことです。ダブルクォートで囲むだけでは防げません - 送った後は、終了表示ではなくサーバー側のファイル数・容量・表示(HTTP 200)・中身の一致を実測して完了とします
正しい書き方:接続先の変数は波かっこで区切る
接続先(ユーザー名とサーバー名)を変数に入れる場合は、変数名の終わりを波かっこで明示します。
H=user@sv.example.com
# 正しい書き方(コロンが残る)
rsync -av site/ ${H}:abc-salon.jp/public_html/
# 宛先を1つの変数にまとめておく書き方
DEST="${H}:abc-salon.jp/public_html/"
rsync -av site/ "$DEST"
接続先を直接書いても問題は起きません。SSH の設定ファイル(~/.ssh/config)に接続先の別名を登録し、rsync -av site/ mysite:abc-salon.jp/public_html/ のように別名で書く方法もあります。ポート番号や鍵の指定もまとまり、打ち間違いを減らせます。
逆に、次のような書き方は zsh では安全ではありません。
| 書き方(H=user@sv.example.com) | zshが展開した結果 | rsyncの判断 |
|---|---|---|
$H:abc-salon.jp/public_html/ |
/Users/(作業フォルダ)/user@sv.example.combc-salon.jp/public_html/ |
パソコン内へコピー |
"$H:abc-salon.jp/public_html/"(ダブルクォート) |
上と同じ | パソコン内へコピー |
$H:hair-salon-sample.jp/public_html/ |
.air-salon-sample.jp/public_html/ |
パソコン内へコピー(隠しフォルダ) |
${H}:abc-salon.jp/public_html/ |
user@sv.example.com:abc-salon.jp/public_html/ |
サーバーへ送信 |
表の展開結果は、macOS 26.6 付属の zsh 5.9 で実際に確かめたものです。
なぜ起きるのか:zshの修飾子とrsyncの判定
zsh には、変数の後ろに「:1文字」を付けると、その値をファイルのパスとして加工する機能があります。これを修飾子と呼びます。たとえば $file:t はファイル名の部分だけ、$file:h はフォルダの部分だけを取り出します。本来はファイル名を扱うための便利な機能です。
問題は、この加工が波かっこを付けない $変数:文字 の形でも働くことです。$H:abc-salon.jp と書くと、zsh は :a(絶対パスに変換)を修飾子として読み、残りの bc-salon.jp をその後ろにつなげます。コロンと先頭の1文字が、コマンドから消えてしまうわけです。
同じ macOS 環境で英字の大文字・小文字をすべて試したところ、修飾子として読まれたのは次の13文字でした。
| コロン直後の文字 | 起きること |
|---|---|
| a / A / P | 作業フォルダを付けた絶対パスになる |
| h | フォルダ部分だけになり、先頭がドットの隠しフォルダ名になる |
| t / l / c / q / Q | 接続先のような値では変化せず、コロンと1文字だけが消える |
| e / r | 拡張子部分だけ、または拡張子を除いた部分になる |
| u | すべて大文字になる |
| s | エラーで止まる(この場合は気づける) |
一方、p で始まる public_html/ や、/ や ~/ で始まるパスはコロンが残ります。レンタルサーバーによっては、ホームフォルダの下に「ドメイン名/public_html」という形で公開フォルダがあるため、同じ書き方のスクリプトでも、ドメイン名の頭文字によって起きたり起きなかったりします。以前は問題なく動いていたコマンドを別のサイトに使い回したときに初めて表に出ることがある、というのがこの問題の厄介なところです。
rsync 側の判断は単純で、宛先に「接続先:パス」の形でコロンがあればサーバーへ、無ければパソコン内のパスとして扱います。zsh がコロンを消した時点で、ごく普通の「手元へのコピー」になるわけです。
なお、Linux サーバーなどで広く使われている bash というシェルでは、この修飾子は働きません。同じ条件で bash を使うと、$H:abc-salon.jp/public_html/ はそのままコロン付きで渡されます。ネット上の手順記事のコマンドを Mac のターミナルにそのまま貼ると結果が変わるのは、このシェルの違いが理由です。Mac の標準シェルは macOS Catalina 以降 zsh になっています。
厄介なのは「成功したように見える」こと
Mac 付属の rsync の場合、このときの表示は正常に送れたときとほとんど見分けがつきません。ファイルの一覧と送信バイト数が表示され、終了コード(コマンドの成否を表す数字)も成功を表す0です。手元へのコピーなので、むしろ速く終わります(本家の rsync は宛先の途中のフォルダが無いとエラーで止まる場合があります)。
実際の公開作業で確認された例では、サイト一式(58ファイル・約118MB)を SSH 経由でサーバーへ送るコマンドで、ドメイン名が a で始まっていたために、宛先が作業フォルダ内の「ユーザー名@サーバー名 にドメイン名の2文字目以降が続いた名前」のフォルダになり、rsync はそのまま正常終了していました。FTPのログインが通らない状態(530エラー)で、SSH に切り替えた直後でした。FTP から rsync へ移るときは、後述の権限の持ち込みのように、FTPでは起きなかった問題が出やすいタイミングでもあります。
反対に、サーバーから取ってくるときに同じ書き方をすると、存在しない手元のフォルダを読みに行き、「ファイルが無い」というエラー(終了コード23)で止まります。取ってくる方向は失敗が表に出るのに、送る方向だけは静かに成功してしまう。ここが見落としやすい理由です。
コロン直後が h の場合は、ドットで始まる隠しフォルダが作られ、Finder や通常の ls では見えません。
送る前の確認:展開後のコマンドを目で見る
変数を使ったコマンドを実行する前に、zsh が実際に何を rsync に渡すのかを表示して確認します。
# 実行せずに、展開後の宛先だけを表示する
print -r -- ${H}:abc-salon.jp/public_html/
# 実行するコマンドを1行ずつ表示しながら動かす
setopt xtrace
rsync -av site/ ${H}:abc-salon.jp/public_html/
setopt xtrace を有効にすると、実行直前に展開後のコマンドが表示されます。見るべき点は、サーバー名の直後にコロンが残っているかです。宛先が /Users/ で始まっていたり、サーバー名とドメイン名がつながっていたりしたら止めます。
スクリプトを bash で動かす方法もありますが、ターミナルに貼って使えば同じことが起きます。どちらのシェルでも正しく動く波かっこの書き方にそろえるのが確実です。
送った後の確認:終了表示ではなくサーバー側を実測する
コマンドが正常終了したことは、ファイルがサーバーに届いたことの証明になりません。転送のあとは、次の4点をサーバー側で確かめてから完了とします。
- ファイル数と容量:
find site -type f | wc -lで数えた手元の件数と、ssh 接続先 'find abc-salon.jp/public_html -type f | wc -l'で数えたサーバー側の件数を比べる。元からサーバーにあるファイルの分は差し引く。容量はdu -shで比べる - 表示できるか:送ったファイルのURLを全件確認し、HTTPのステータスが200(正常に表示)であることを確かめる。403が混ざっていたら、ファイルはあるのに読めない状態=まず権限を疑います
- 中身が一致しているか:トップページなど主要なファイルについて、手元とサーバーのハッシュ値(ファイルの中身から計算する指紋のような値)を比べる。Mac では
md5、サーバーではmd5sumで計算できます - パソコン側に取り違えのフォルダができていないか:作業フォルダを
ls -laで表示し、名前に@を含むフォルダや、ドットで始まる見覚えのないフォルダが無いかを見る
先ほどの58ファイルの例でも、公開の判断材料にしたのは、サーバー側で58ファイルすべてがHTTP 200を返し、主要な2ページのハッシュ値が転送元のデータと一致したことでした。この確認を手順として入れておけば、宛先の取り違えはその場で見つかります。「作業が完了した」と「サイトに届いている」を分けて確かめる考え方は、「できた」と「届いている」は違うでも整理しています。
誤って手元にできたフォルダは、名前にサーバー名が含まれ、作成日時が転送時刻であることを確かめてから削除し、波かっこの書き方で送り直します。
あわせて注意:Mac付属のrsyncと権限の持ち込み
Mac から rsync で送るときは、権限(ファイルを誰が読めるかの設定)にも注意が必要です。
rsync でよく使う -a という指定は、ファイルの権限をそのまま保ったまま送ります。Mac で作ったファイルには、持ち主しか読めない「600」という権限になっているものがあり、それがそのままサーバーに届くと、Webサーバーがファイルを読めず、その画像やページだけが403エラーで表示されません。実際に、画像46ファイルが600のまま本番まで届き、一部の画像だけが表示されないという形で起きています。症状の見分け方は画像だけ表示されないときは403を疑うで詳しく解説しています。
本家の rsync(Linux サーバーに入っているものや、Homebrew で追加したもの)なら、送るときに --chmod=D755,F644 を付けて、フォルダとファイルの権限を公開用にそろえられます。ところが最近の macOS に付属している rsync(openrsync。表示上は「rsync version 2.6.9 compatible」)はこの指定に対応しておらず、「invalid argument」というエラーで止まります。
Mac 付属の rsync を使う場合は、転送後にサーバー側で読めないファイルが無いかを数えます。
# 他人が読めないファイルの数(0件ならOK)
find abc-salon.jp/public_html -type f ! -perm -004 | wc -l
# 0件でなければ、読めるように直す
find abc-salon.jp/public_html -type f ! -perm -o+r -exec chmod 644 {} +
find abc-salon.jp/public_html -type d ! -perm -o+x -exec chmod 755 {} +
本番へ上げる前に手元とサーバーのどちらが新しい版かを確かめる手順は、手元のファイルを本番へ上げる前に「どちらが新しいか」を確かめるにまとめています。
まとめ
- Mac の標準シェル zsh は、
$変数:の直後の1文字(a・h・t・e・r など13文字)を修飾子として読み、コロンを消してしまう - コロンが消えると rsync は宛先をパソコン内のフォルダと判断し、エラーを出さずに手元へコピーして正常終了する
- 接続先を変数に入れるなら
${H}:パスと波かっこで区切る。ダブルクォートだけでは防げない。接続先の直書きやSSH設定の別名でもよい - 実行前は
print -r --やsetopt xtraceで展開後の宛先を見て、サーバー名の直後にコロンが残っているかを確かめる - 実行後は、サーバー側のファイル数・容量・HTTP 200・ハッシュ値を実測し、手元に
@を含むフォルダができていないかも見る - Mac 付属の rsync は
--chmod非対応。転送後に読めないファイルが0件かを数えて403を防ぐ
更新作業を外部に任せている場合も、「転送したあと、サーバー側で何を確かめていますか」と聞いてみてください。ファイル数や表示の確認まで手順に入っているかで、公開作業の確実さはおおよそ判断できます。