blog
開発メモWindowsのPowerShellからssh経由でwp-cliを叩くときにハマった地雷と回避策
本番の WordPress を、管理画面ではなく wp-cli(コマンドラインから WordPress を操作するツール)で動かす運用に切り替えました。すると、Windows(PowerShell)から ssh 越しにレンタルサーバー(Xserver)の wp-cli を叩く——という構成ならではの落とし穴を、いくつも踏みました。日本語タイトルが消える、コマンドが構文エラーで落ちる、画像が二重になる。
厄介なのは、エラーの見た目からは「どの層で壊れているか」が分かりにくいことです。たとえば「タイトルが消える」の犯人は、WordPress ではなく PowerShell でした。同じ作業を Mac のターミナルからやっている人にはまず起きない類の地雷なので、踏んだものと回避策を順に記録します。
(Claude Code を実務で使うなかで踏んだ、本番運用まわりの記録です。同じ Xserver の本番では、子ディレクトリの .htaccess が親の 301 リダイレクトを継承しなかった話でもつまずいています。)
安定させる型:複雑なコマンドはスクリプト化して送る
細かい地雷を一つずつ潰す前に、いちばん効いた対策を先に置きます。wp-cli のコマンドを ssh の引数として直接書くのをやめて、bash スクリプトにして送り込む。これだけで、後述する文字化け・クォート落ちの大半が消えます。
ssh include-xs “wp post update 123 –post_title=’日本語タイトル'”
→ PowerShell がクォートを食う → タイトルが消える/既定の古い php で wp が動かない
# 1. ローカルで run.sh にコマンドを書く(PHPは版を明示)
# 2. BOMと改行コードを掃除してから scp
# 3. 本番で bash run.sh として実行
この「型」を図にすると、こうです。
ここから、この型にたどり着くまでに踏んだ地雷を、一つずつ。
地雷①:既定の php が古すぎて wp-cli が動かない(parse error)
最初の壁がこれでした。ssh でログインして wp post list と打っただけで、WordPress のコアがパースエラーを吐いて止まります。
✗ PHP Parse error: syntax error, unexpected …
原因は、サーバーの既定の php が古い(5.4系)こと。今どきの WordPress コアは、この古い php では構文からして解釈できません。Xserver には php 7.0〜8.5 が /usr/bin/php7.4 のようにバージョンごとのバイナリで同居しているので、新しい方を明示して wp-cli を呼びます。
/usr/bin/php7.4 /usr/bin/wp post list
「レンタルサーバーで wp-cli が動かない」の多くは、たぶんこれです。php -v で既定バージョンを確認し、古ければ /usr/bin/phpX.Y を明示する。以降のコマンドは、すべてこの「php を明示した wp」で叩く前提です。
地雷②:PowerShell が ssh 引数の二重引用符を食う → 日本語タイトルが消える
これがいちばん気づきにくい地雷でした。日本語タイトルを付けて投稿・更新したのに、タイトルが空っぽになる。文字化けですらなく、まるごと消えるのです。
犯人は WordPress ではなく PowerShell でした。PowerShell から ssh "... --post_title='日本語' ..." のように書くと、PowerShell が文字列内のクォートを自分で先に処理してしまい、リモートの bash に届く頃にはクォートが崩れています。$() のようなものを書けば、リモートに行く前にローカルの PowerShell 側で評価までされてしまう。ssh はただの通り道のつもりでも、引数はローカルシェルを一度くぐるのです。
対策が、冒頭の「型」です。コマンドを bash スクリプトに書いて scp で送り、本番で bash run.sh として実行する。こうすればクォートも日本語も、PowerShell に食われずそのまま本番の bash に渡ります。ssh の引数として直に渡すのは、クォートの要らない単純な一行だけにします。
地雷③:scp で送ったスクリプトに BOM と CRLF が混ざる
スクリプト化しても、まだ落とし穴があります。Windows で作ったテキストファイルには、しばしば先頭に BOM(UTF-8 であることを示す見えない印)が付き、改行が CRLF(Windows 式の改行)になっています。これをそのまま本番へ送ると、bash が先頭の BOM を余計な文字として読んだり、行末の \r がコマンドにくっついたりして、静かに壊れます。
なので scp の前(または後)に、BOM と CR を掃除しておきます。Git Bash があれば sed ひとつで済みます。
sed -i ‘1s/^\xEF\xBB\xBF//’ run.sh
# 行末のCR(\r)を削る
sed -i ‘s/\r$//’ run.sh
本文HTMLを wp post update の本文ファイルとして送るときも同じです。文字化け・改行事故の多くは、この BOM/CRLF が原因でした。
地雷④:wp post term set が「Invalid taxonomy」で落ちる
記事にタグをまとめて付けようとしたら、こんなエラーで落ちました。
✗ Error: Invalid taxonomy 171.
数字の 171 はタグの ID のつもりでした。なぜそれが「タクソノミー(分類の種類)」扱いされるのか。答えは構文で、wp post term set は2番目の引数にタクソノミー名(blog_tag など)を必ず取るのです。ここを省くと、最初のタグ ID がタクソノミー名の位置にずれ込んで、「そんな分類は無い」と怒られる。正しくはこうです。
/usr/bin/php7.4 /usr/bin/wp post term set 123 blog_tag 456 789 –by=id
この set は総入れ替えなので、指定しなかった既存タグは外れます。入れ替えの結果、どの記事にも属さなくなった空のタグは、wp term delete blog_tag <id> で掃除しておきます。0件のタグを放置すると、中身のない薄いアーカイブページが残り、SEO 的にもよくありません。
地雷⑤:WP-CLI 2.4 では使えないオプションがある
ネット上の記事どおりのコマンドが、手元の WP-CLI 2.4.0 では通らないことがありました。バージョンが少し古いと、後から追加されたフィールドやオプションが無いためです。踏んだのは次の2つ。
| やりたいこと | 通らない書き方 | 回避 |
|---|---|---|
| 投稿のURLを取る | wp post get <id> --field=url |
--field=guid で代用 |
| タグを名前で付ける | wp post term set … --by=name |
先にIDを引いて --by=id |
タグを名前から付けたいときは、--by=name が無いので、いったん ID に変換してから --by=id で渡します。
wp term list blog_tag –name=”家紋” –field=term_id
wp term create blog_tag “家紋” –porcelain # 未作成ならこちらでIDを得る
地雷⑥:同名画像を import すると衝突する
アイキャッチや本文画像を wp media import で入れ直すとき、同じファイル名のまま再取り込みすると衝突します。WordPress が別名(連番サフィックス付き)で二重に取り込んでしまい、狙った画像を指せなくなる。
入れ替えたいときは、旧い添付を先に消してから取り込みます。
wp post delete <旧attachment_id> –force
wp media import new.jpg –porcelain # → 新しいID
wp post meta update <投稿ID> _thumbnail_id <新attachment_id>
地雷⑦:set -e のバッチが途中で死ぬと、投稿だけ残る
ここまでの手順を1本のスクリプトにまとめ、set -e(どれか1つ失敗したら止まる)で回していたのですが、これが裏目に出ることがあります。「投稿の作成」は成功したあと、「カテゴリ/タグ/アイキャッチの設定」でコケて止まると、記事だけが中途半端に生まれた状態で残ります。
厄介なのは、ここでスクリプトを頭から再実行してはいけないこと。wp post create がもう一度走って、記事も画像も二重になります。復旧は、すでに出来ている投稿の ID を指して、足りないメタデータだけを後付けする。バッチは「何度流しても同じ結果になる(冪等)」か「途中から安全に足せる」設計にしておくと、事故が減ります。
小さな地雷:ssh 越しの echo に丸括弧を書かない
最後に細かいものを一つ。区切りの目印に echo "=== 更新 (after) ===" のように丸括弧を入れて ssh 越しに流したら、リモートの bash が括弧を構文エラーと見なして落ちました。ログの見出しに括弧を使いたくなりますが、[after] のような別記号にしておくのが無難です。
地雷⑧:日本語・丸括弧・全角記号を含む値は、コマンド引数ではなくファイルで渡す
冒頭の「型」(コマンドを bash スクリプト化して送る)で大半の事故は防げますが、値そのものに日本語・丸括弧・全角記号が混じるケースでは、まだ足りないことがあります。PowerShell → ssh → リモート bash という三段のシェルを値が通過する構成では、スクリプトのどこかに関数定義や裸の丸括弧を書いただけで、予期しないトークン '(' 周辺に構文エラーのような形で崩れます。
いちばん堅いのは、値そのものをコマンド引数にしないことです。更新したい内容を PHP ファイルとしてローカルで作成し、scp で送ってから wp eval-file で実行する。こうすると値はシェルの引用符処理を一度も通らず、バイトのまま届きます。
scp f.php include-xs:/tmp/f.php
# 動的なIDが要る場合もシェル変数に閉じ、値自体は外に出さない
ssh include-xs ‘ID=$(wp media import img.jpg –porcelain); /usr/bin/php7.4 /usr/bin/wp eval-file /tmp/f.php $ID’
動的な ID を必要とする処理も、ID=$(...) のようにリモート側のシェル変数に閉じ込め、$args[0] として PHP 側に渡します。ID がシェル境界をローカル→リモートと跨がないぶん、事故る余地が一段減ります。
地雷⑨:複数キーをまとめて持つ option を丸ごと上書きしない
WordPress の wp_options テーブルには、プラグインの設定一式がひとつのシリアライズ配列に詰まっていることがあります。実際、SEO SIMPLE PACK の設定はそういう構造で、70キーの束から2キーだけ書き換えたい場面がありました。ここで値を丸ごと wp option update し直すと、触るつもりのなかった68キーまで消えるリスクがあります。
対処は「外科的更新」です。①wp option get <key> --format=json でまず退避、②PHP側で get_option() して対象キーだけ書き換え、③ update_option()、④直後に読み直して更新前後をバイト比較(===で一致すれば VERIFY_OK を出力)、⑤count()でキー総数が更新前後で変わっていないことまで確認します。「意図したキーだけ変わった」を機械的に検算してから本番を離れるのが安全です。
地雷⑩:md5 が一致しない=本番に実差がある、とは限らない
本番のファイルを取得してローカルの手元ファイルと md5 を比べ、一致しないと「本番側で誰かが変更した」と決めつけたくなります。ですが、SSH案件でプルする前の確認作業のたびに踏むのがこれで、差の正体が改行コードだけということが少なくありません。本番は LF、ローカル(Windows)は CRLF、という組み合わせでよく起きます。
file a b # CRLFの有無を確定させる
diff --strip-trailing-cr の出力が空なら、差は改行コードだけ。この場合は上書きしても中身に実差はなく、わざわざ「本番の変更をローカルへ取り込むコミット」を作る必要はありません。md5 の不一致だけを見て早合点しないのが要点です。
地雷⑪:scp に Windows のフルパスをそのまま渡すと、ドライブレターがホスト名と誤認される
OGP画像などを scp C:\tmp\ogp.png include-xs:/path/ のようにフルパスのまま送ろうとすると、scp が C: のコロンを「ホスト名:パス」の区切りと誤認し、失敗します。対処は、送りたいファイルのディレクトリへ Set-Location してから、相対のファイル名だけを渡すことです。
地雷⑫:ヘッドレスの msedge がスクリーンショットを撮らず NO_IMAGE で終わる
OGP画像を自前でレンダリングしようと msedge --headless --screenshot を叩いても、母艦(Windows)で既に別の Edge ウィンドウが起動中だと、そちらへ処理を委譲してヘッドレス側は即終了し、画像が生成されません。Chrome でも同様の挙動が起きます。
対処は --user-data-dir=<一時ディレクトリ> で専用プロファイルを切ること。既存インスタンスに相乗りしなくなり、確実に撮影できます。GSAP などで描画が遅延するページは --virtual-time-budget=<ms> で仮想時間を進めてから撮ると、アニメーション途中のコマを掴まずに済みます。Playwright や Puppeteer を導入していなくても、システムの Chrome/Edge のヘッドレスだけで 1200×630 の HTML→PNG は十分作れ、日本語フォントも母艦に入っている Yu Gothic 等で足ります。
同じ地雷を踏まないための勘どころ
- ssh 越しに複雑なコマンドを直に渡さない。スクリプトにして送り、向こうで実行する。クォートも日本語も、ローカルシェルに食われずに済む。
- 値に特殊文字が混じるなら、コマンド引数ではなくファイルで渡す。
wp eval-fileならシェルを一度も通さない。 - 文字化け・改行事故を見たら BOM と CRLF を疑う。Windows 発のテキストは、掃除してから本番へ。md5 が違うだけで「実差あり」と決めつけず、まず改行コードの差を疑う。
- 複数キーをまとめて持つ option は丸ごと上書きしない。対象キーだけ書き換えて、更新前後をバイト比較・キー数比較で検算する。
- 本番のコマンドはバージョンの動物園。php は既定が古いことがある。
/usr/bin/phpX.Yを明示。wp-cli 自体も版で使えるオプションが違う。 - バッチは冪等に、失敗時の後始末を先に考える。「投稿だけ生まれて止まる」を想定しておく。再実行より、ID を指した後付けを。
- そして——それらしいエラーの主語を取り違えない。「タイトルが消える」の犯人は WordPress ではなく PowerShell でした。どのレイヤーで壊れているかを疑うのが近道です。
2.4.0、PHP は 5.4〜8.5 がバージョン別のバイナリで同居していて、既定は 5.4。

