Macでは動くのにlaunchdではファイルが見つからない|相対パスの落とし穴

Macでは動くのにlaunchdではファイルが見つからない|相対パスの落とし穴 定期実行と通知

手で叩けば動くのに、launchdに任せた途端「そんなファイルはありません」で止まる。うちでも自動化を作り始めた頃に一番混乱したのがこれでした。

結論から書きます。launchdは、スクリプトが置いてあるフォルダからは起動してくれません。だから ./設定.json や 記事一覧.tsv のような「フォルダ名を省いた書き方」(相対パス)は、手で動かすときだけ通って、launchdでは通りません。直し方は3つあり、うちでは使い分けています。この記事はその3つと、いま自分が踏んでいるかを30秒で確かめる方法です。

なぜ手で動かすと通ってしまうのか

ターミナルでスクリプトを動かすとき、たいてい人はそのフォルダに移動してから動かします。

cd ~/scripts/media
./ネタ補充.sh

このとき、スクリプトから見た「いまいる場所」は ~/scripts/media です。だから中で 記事一覧.tsv とだけ書いてあっても、ちゃんと ~/scripts/media/記事一覧.tsv が読めます。

launchdには、この「いまいる場所」がありません。人が cd してから呼ぶわけではないからです。man launchd.plist にはこう書かれています。

WorkingDirectory — ジョブを走らせる前に chdir する先を指定する、任意のキー。

「任意の」というのが要点です。書かなければ、どこから起動されるかはこちらの都合とは無関係に決まります。少なくとも「スクリプトが置いてあるフォルダ」にはなりません。だから相対パスが総崩れになります。

しかも厄介なことに、この失敗はふだんの動作確認をすり抜けます。人がテストするときは必ず cd してから叩くので、テストでは100%通ってしまう。launchdに登録した翌朝、初めて壊れます。

いま自分が踏んでいるか、30秒で確かめる

推測しないで、実際にどこから起動されているかを1回だけ吐かせるのが確実です。次の3行だけのスクリプトを作ります。

cat > ~/scripts/場所しらべ.sh <<'EOF'
#!/bin/zsh
echo "[$(date '+%Y-%m-%d %H:%M')] いまいる場所: $(pwd)" >> "$HOME/場所しらべ.txt"
echo "[$(date '+%Y-%m-%d %H:%M')] 自分の置き場: ${0:A:h}" >> "$HOME/場所しらべ.txt"
EOF
chmod +x ~/scripts/場所しらべ.sh

これを、いま疑っているplistの ProgramArguments に一時的に差し替えて1回動かすか、いちばん手軽なのは、疑っているスクリプトの先頭にこの2行をそのまま貼ることです。翌朝 ~/場所しらべ.txt を開けば、launchdが実際にどこから呼んだかが1行で分かります。

cat ~/場所しらべ.txt

「いまいる場所」が自分の想定と違っていれば、それが原因です。確かめ終わったら2行を消してください。

直し方は3つ。うちの使い分け

① いちばん堅い:$HOME 起点の絶対パスで書く

うちのメディア自動化は、設定を1か所にまとめて、そこで全部フォルダ名から書いています。~/scripts/media/config.sh の中身がこれです。

# 記事HTMLが置いてあるフォルダ
MEDIA_KIJI_DIR="${MEDIA_KIJI_DIR:-$HOME/scripts/media/記事}"

# どのファイルをどのタイトル・スラッグで出すかの対応表
MEDIA_LIST="${MEDIA_LIST:-$HOME/scripts/media/記事一覧.tsv}"

# Python(このMacは /usr/bin/python3 が動かないので venv のものを使う)
MEDIA_PY="${MEDIA_PY:-$HOME/scripts/keiri/.venv/bin/python}"

MEDIA_HOME="$HOME/scripts/media"

そして読み込む側も、読み込む相手の場所まで絶対パスで書きます。

source "$HOME/scripts/media/config.sh"

ここを source ./config.sh と書きたくなりますが、それをやると設定ファイルそのものが読めずに落ちます。いちばん最初の1行が、いちばん相対パスに書き換えたくなる場所です。ここだけは絶対パスにしてください。

② 引っ越しても壊れない:スクリプト自身の場所から測る

フォルダごと移動するかもしれないものは、自分の置き場を自分で調べさせます。zshならこの1行です。

BASE="${0:A:h}"

$0 が「自分自身」、:A が「省略なしのフルパスに直す」、:h が「ファイル名を落としてフォルダだけにする」。つまり「このスクリプトが置いてあるフォルダ」が入ります。1つ上の階層が欲しければ :h をもう1回足して ${0:A:h:h} です。

うちの旅行メディア側のスクリプトはこの書き方で、scripts/ の中から1つ上を基準にしています。

BASE="${0:A:h:h}"
"$PY" "$BASE/scripts/週2回公開.py" "$@"

これなら、フォルダごと別の場所に移してもそのまま動きます。$HOME 起点の絶対パスだと、移した瞬間に全部書き換えになります。「置き場所が変わりうるか」で①と②を選んでください。

③ どうしても相対パスが要るとき:先頭で自分から cd する

中で呼ぶ他のコマンドがカレントフォルダを見る作りだったり、相対パス前提の道具に渡したりするときは、スクリプトのいちばん上で自分から移動してしまいます。うちの自動公開はこれです。

cd "$HOME/scripts/media" || exit 1
source "$HOME/scripts/media/config.sh"

|| exit 1 を必ず付けてください。移動に失敗したまま先に進むと、まったく違うフォルダで処理を続けてしまいます。これは「見つからない」で止まるより、はるかにたちが悪い。

plistの側に WorkingDirectory を書いても同じことができます。

<key>WorkingDirectory</key>
<string>/Users/あなたのユーザー名/scripts/media</string>

うちはこちらを使っていません。理由は1つで、plistに書くと「手で動かしたとき」には効かないからです。手動とlaunchdで前提が変わる作りは、また同じ罠を踏みます。スクリプトの先頭で cd しておけば、どちらから呼んでも同じ場所から始まります。設定は、動かし方が変わっても効くほうに置く。これが3つのうちで一番大事な判断です。

plistの中では ~ が使えない

相対パスと並んでよくやるのがこれです。plistの中の ~/scripts/media/1本作る.sh は展開されません。~ を「ホームフォルダ」に読み替えるのはシェルの仕事で、plistを読むlaunchdはそれをやらないからです。~ という名前のフォルダを探しに行って、無いので落ちます。

うちのplistは、実行するスクリプトもログの出し先も、全部 /Users/ から書いています。

<key>ProgramArguments</key>
<array>
    <string>/bin/zsh</string>
    <string>/Users/あなたのユーザー名/scripts/media/1本作る.sh</string>
</array>
<key>StandardOutPath</key>
<string>/Users/あなたのユーザー名/scripts/media/logs/launchd_generate_out.log</string>

ログの出し先を ~ で書いてしまうと、失敗しても、失敗の理由を書いたログ自体がどこにも出ません。原因調査の入口を自分で塞ぐことになるので、ここは特に気をつけてください。

Pythonに渡すときも、場所を持たせて渡す

シェルからPythonを呼ぶ構成だと、Python側でまた同じ問題が起きます。うちはPythonに場所を推測させず、シェルが決めた絶対パスを環境変数で手渡ししています。

# シェル側(toukou.sh)
source "$HOME/scripts/media/config.sh"
export MEDIA_KIJI_DIR MEDIA_LIST MEDIA_AUTH_FILE
"$MEDIA_PY" "$MEDIA_HOME/wp_post.py" 投稿 "$@"
# Python側(wp_post.py)
KIJI_DIR = os.environ.get("MEDIA_KIJI_DIR", "")
LIST_FILE = os.environ.get("MEDIA_LIST", "")

Pythonのスクリプトを呼ぶときも "$MEDIA_HOME/wp_post.py" とフォルダ付きで書いています。場所を知っているのはシェル1か所だけ。Pythonは渡されたものを使うだけにすると、どこから起動されても同じ結果になります。

「見つからない」にはもう1種類ある

正直に書いておくと、うちが実際に朝の自動化を止めたのは、相対パスではなく権限のほうでした。相対パスで転ばずに済んだのは、最初から上の書き方をしていたからで、運がよかっただけです。

2026年8月27日の朝9時、記事作成の自動化がこのログを残して止まりました。

find: /Users/…/Desktop/新規事業/G_メディア事業/記事/wordpress投稿用: Operation not permitted

パスは1文字も間違っていません。手で叩けば普通に読めます。それでもlaunchdからは読めなかった。macOSがデスクトップ以下を保護していて、launchd経由のプログラムには許可していなかったからです。同じ日の10時49分に記事の置き場を ~/scripts/media/記事 に移して直しました。config.sh の1行を書き換えるだけで済んだのは、置き場所を設定1か所にまとめてあったおかげです。

つまり、症状がまったく同じ「手動では動くのにlaunchdでは見つからない」でも、原因は場所と権限の2種類あります。切り分けは簡単で、

  • ログのメッセージが No such file or directory 系 → 場所の問題。この記事の話
  • Operation not permitted → 権限の問題。パスは合っている

と読んでください。エラー文をそのまま検索する前に、この2語だけ見分ければ、探す方向を間違えずに済みます。

明日やるなら、この順番で

  1. launchdに登録してあるスクリプトを開き、./ で始まる行と、フォルダ名なしのファイル名を全部探す
  2. まず source の行を絶対パスに直す。ここが落ちると他が全部無意味になる
  3. 置き場所が変わらないものは $HOME/…、フォルダごと動かしうるものは BASE="${0:A:h}" に統一する
  4. plistの中に ~ が1つも無いか確かめる。特に StandardOutPath と StandardErrorPath
  5. 直したら、時刻を待たずに1回手で動かして、ログに何も出ないことを確かめる

手で動かして通ったからといって、launchdで通る保証にはなりません。相対パスを全部消し終えて初めて、その2つが一致します。

まとめ

  • launchdはスクリプトの置き場所からは起動しない。相対パスは手動テストだけ通って、本番で落ちる
  • WorkingDirectory は任意のキー。書かなければ場所はこちらの都合では決まらない
  • 直し方は3つ。①$HOME起点の絶対パス ②${0:A:h} で自分の場所を測る ③先頭で cd … || exit 1
  • うちは③をplistではなくスクリプト側に置いている。手動でもlaunchdでも同じ場所から始まるようにするため
  • plistの中の ~ は展開されない。ログの出し先を ~ で書くと、失敗の理由すら残らない
  • 置き場所は設定1か所にまとめる。うちはそのおかげで、置き場の引っ越しが1行の書き換えで済んだ
  • 同じ症状でも No such file or directory は場所、Operation not permitted は権限。先にログの2語を見る

Operation not permitted のほう、つまりパスは合っているのにmacOSの保護で読めないケースは launchdが動かない・終了コード127の原因と対処 に、うちが実際に踏んだ経緯ごとまとめてあります。ログにこの文字列が出ているなら、この記事ではなくそちらを読んでください。

コメント

タイトルとURLをコピーしました