侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 154 篇文章
  • 累计收到 7 条评论

同一条命令,终端里跑得通,放进定时任务就找不到

2026-9-30 / 0 评论 / 7 阅读

同一条命令,终端里跑得通,放进定时任务就找不到

一行调用 node 的脚本,在终端里跑得好好的,交给调度器之后日志里只有一行 command not found,退出码 127。脚本本身没问题,问题在它被启动时的那个环境。这份记录是本机实测的排查顺序。

先确认 127 是谁返回的

127 是 shell 的约定,表示「命令找不到」;126 表示命令找到了但没有执行权限。日志里是 127,说明脚本文件本身被找到了、被执行了,是它内部调用的那一行没找到。第一步排除了文件路径和权限这类常见怀疑对象,方向转到环境上。

把两边的环境打出来对着看

== 1. 当前终端与干净环境的 PATH 段数 ==
  当前终端:20 段
  /opt/homebrew/bin 在当前终端 PATH 里:在
  /Users/yxh/.hermes/node/bin 在当前终端 PATH 里:在
  env -i /bin/sh 的 PATH:/usr/gnu/bin:/usr/local/bin:/bin:/usr/bin:.
  env -i /bin/sh 的段数:5 段
  env -i zsh 的 PATH:/bin:/usr/bin:/usr/ucb:/usr/local/bin
  /opt/homebrew/bin 在干净环境的 PATH 里:不在

当前终端的 PATH 有 20 段,homebrew 和 node 所在的目录都在里面。换成一个只剩基本变量的环境,PATH 掉到 5 段,两个目录都不在其中。段数从 20 变 5,差的不是一两个目录,是整个用户级的配置层。

找 node:两种环境两种结果

== 2. 两种环境里找 node 的结果 ==
  当前终端:/Users/yxh/.hermes/node/bin/node
  env -i /bin/sh:没找到
  本机 node 实际位置:/Users/yxh/.hermes/node/bin/node
  这个目录在 env -i 的 PATH 里:不在

终端里能定位到 node,干净环境里没找到。这里有个容易混的点:node 装在一个用户目录下,不在 /usr/bin 这类系统目录里,所以最小环境的默认 PATH 天然覆盖不到它。

用绝对路径先验证一次假设

如果原因是 PATH,那把命令换成绝对路径就应该能跑。

== 3. 一个脚本在两种环境里的结果 ==
  当前终端里跑:node 跑起来了(退出码 0)
  干净环境里跑:/tmp/verify-cron-path/hello.sh: line 2: node: command not found(退出码 127)
== 4. 换成绝对路径,同一个脚本在干净环境里也能跑 ==
  绝对路径版本:node 跑起来了(退出码 0)

== 5. 脚本自己补上 PATH 也能跑 ==
  自己补 PATH 的版本:node 跑起来了(退出码 0)

同名的两个脚本,唯一差别是命令那一行的写法,一个 127 一个 0。到这里可以确认:路径查找范围就是根因,脚本逻辑没有问题。

shebang 那一行同样走 PATH

脚本第一行写 #!/usr/bin/env node 的写法很常见,它看起来比绝对路径干净,代价一样:env 也要靠 PATH 去找 node。

== 6. shebang 写 env node,同样受 PATH 影响 ==
  干净环境里执行:env: node: No such file or directory(退出码 127)

env: node: No such file or directory,退出码还是 127。这条和上一条的区别在于:绝对路径把依赖钉死在文件系统上,写 env 是把依赖交给环境,环境不同结果就不同。

启动文件什么时候被读

调度器起进程时不会开一个登录 shell,也不会给交互终端。shell 的启动文件按「登录」和「交互」两个属性分别加载,这个区别决定了脚本能看到什么。

== 7. 启动文件的读取规则:写在哪一层决定谁能看到 ==
  .zshenv 里的变量,非交互 zsh 读到:read_from_zshenv
  .zshrc 里的变量,非交互 zsh 读到:没读到
  两者在交互式 zsh 里:read_from_zshenv read_from_zshrc
  .zshenv 里的变量,/bin/sh 读到:没读到
  export PATH 写在 .zshrc 里,非交互 zsh 的 PATH 段数:20 段

.zshenv 里的变量,非交互的 zsh 能读到;.zshrc 里的读不到,因为那是交互式启动才加载的文件。如果 PATH 是在 .zshrc 里追加的,那么所有非交互的调用都看不到它,包括调度器、编辑器里的任务、git 钩子。写在 .zshenv 里则会影响到每一次调用,这一层又可能让不该有这些变量的场景也拿到它们。两者各有代价,选哪一层取决于这个变量该被谁看见。

落在配置上的三条做法

第一种是脚本自己补 PATH:在文件开头把需要的目录加进去,并在旁边写一行注释说明为什么。这种做法让脚本可以带着走,代价是每个脚本都要重复一遍。

第二种是在调度器的配置里显式声明环境变量。任务和它依赖的环境写在一起,改动集中在配置里,好处是排查时看一处就够。

第三种是把关键命令写成绝对路径。最简单,也最容易被后来的人改回去,所以最好在提交信息或者注释里留一句原因。

判断一个脚本能不能交给调度器,有个成本很低的办法:用一个只剩基本变量的环境跑一次。命令是 env -i /bin/sh 脚本路径,它不会给任何用户级变量,跑得通才算真的不依赖当前终端。

我的取舍是第二种:环境变量归调度器配置管,脚本本身保持和交互式运行一致的写法。这样同一份脚本在两种场景下的行为一致,出了问题只用看一个地方。

    🤞 分享