脚本手动跑正常放进 crontab 就 command not found:一次环境变量加载顺序的复盘

一个调 aws 命令的备份脚本,手动 bash backup.sh 跑得好好的文件传上去了,写进 crontab 每天凌晨跑日志里却报 aws command not found,同一个脚本同一台机器一个标点没改。排查梳理:我手动跑时 PATH 含 /usr/local/bin 找得到 aws,让 cron 打印它的 PATH 只有 /usr/bin:/bin 没有 /usr/local/bin 所以找不到 aws;一个 shell 启动有四种身份由两个维度交叉而成交互式还是非交互式有没有人在终端敲、登录还是非登录是不是会话起点,SSH 登录是交互加登录 cron 跑脚本是非交互加非登录身份不同启动读的配置文件就不同;bash 启动读哪些文件严格由身份决定交互加登录读 /etc/profile 和 ~/.bash_profile 后者通常再 source .bashrc,交互加非登录只读 ~/.bashrc,非交互 shell 脚本和 cron 和 systemd 这三个文件一个都不读只看默认空的 BASH_ENV;把 PATH 或环境变量只设在 .bashrc 里交互使用一切正常会误以为全系统都有,但 cron systemd ssh 远程命令这些非交互 shell 根本不读 .bashrc 你设的东西在那里等于不存在;cron 跑任务不是模拟你登录一遍而是直接 fork 一个非交互非登录 shell 环境极简基本只有 HOME USER SHELL 和很短的 PATH,两大翻车点 PATH 太短命令找不到、依赖的环境变量是空的;治本是让脚本自带环境不靠运气命令用绝对路径或脚本内 export PATH、要用的环境变量在脚本里显式 export、真需要完整登录环境就显式 source、cron 里用 bash -l 当登录 shell 跑、systemd 服务用 Environment 或 EnvironmentFile。正确做法是脚本手动跑能 cron 跑不能先 dump 两边 env 做 diff,以及一套环境变量加载排查纪律。

2022 年,一次"我亲手敲一遍跑得好好的脚本,一放进 crontabcommand not found"的事故,把我对"环境"这两个字的理解,从头到尾翻新了一遍。那是一个数据备份脚本,里面调了 aws 命令把文件传到 S3。我在服务器上手动跑 bash backup.sh,一路顺畅,文件稳稳传上去了。我很满意,把它写进 crontab,设成每天凌晨跑。第二天我一看日志——脚本是跑了,但里面赫然一行 aws: command not found,备份一个字节都没传。我懵了:这脚本我【刚刚】才亲眼看着它成功跑完,一个标点都没改,我只是把它从"手动敲"换成了"cron 来敲",它怎么就找不到 aws 了?aws 这个命令,明明就在那台服务器上,装得好好的,我手动 which aws 立刻就能查到它。同一个命令、同一台机器、同一个脚本文件,我跑它能成,cron 跑它就说"没这个命令"。难道 cron 跑脚本的时候,它面对的根本【不是同一台服务器】?还是说——它面对的是同一台服务器,只是它看这台服务器的【那双眼睛】,和我的不一样?这件事逼着我把交互式 shell 与非交互式 shell、登录 shell 与非登录 shell、.bashrc.bash_profile 各自的加载时机,还有"我以为的环境"和"脚本实际拿到的环境"的天壤之别,彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个调用 aws 命令的备份脚本 backup.sh
事故现象:
- ★ 手动 bash backup.sh —— 跑得好好的,文件传上去了
- ★ 写进 crontab 每天凌晨跑 —— 日志里 aws: command not found
- 同一个脚本,同一台机器,一个标点没改

现场排查:
# 1. ★ 我手动跑,aws 找得到吗
$ which aws
/usr/local/bin/aws                     # ★ 手动跑:找得到

# 2. ★ 我手动跑时的 PATH 是什么
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
#                ^^^^^^^^^^^^^^ ★ 含 /usr/local/bin

# 3. ★ 让 cron 也打印一次它的 PATH(临时在 crontab 加一行)
* * * * * echo "cron PATH=$PATH" >> /tmp/cronenv.log
$ cat /tmp/cronenv.log
cron PATH=/usr/bin:/bin                # ★★ cron 的 PATH 只有这俩!

# 4. ★ 真相对比
# 我手动跑的 PATH:  ...:/usr/local/bin:...   -> 找得到 aws
# cron 跑的 PATH:   /usr/bin:/bin            -> 没有 /usr/local/bin
#                                           -> 找不到 aws

# 5. ★ aws 装在哪
$ ls -l /usr/local/bin/aws
-rwxr-xr-x ... /usr/local/bin/aws      # ★ 它就在 /usr/local/bin

根因(后来想清楚的):
1. ★ 我手动登录服务器时,是一个"交互式登录 shell"。
   它启动时,读了 /etc/profile、~/.bash_profile、
   ~/.bashrc 一连串文件 —— 这些文件里,把
   /usr/local/bin 加进了 PATH。
2. ★ cron 执行脚本时,起的是一个【极简的、非交互、
   非登录】的 shell。它【几乎什么启动文件都不读】。
3. ★ 所以 cron 那个 shell 的 PATH,是一个内置的、
   贫瘠的默认值:/usr/bin:/bin。它从没读过那些
   把 /usr/local/bin 加进来的文件。
4. ★ 脚本里写 aws,shell 在 PATH 里挨个目录找
   aws 这个可执行文件 —— cron 的 PATH 里没有
   /usr/local/bin -> 找不到 -> command not found。
5. 真相:不是脚本错了,是脚本两次运行,拿到的
   是两份【完全不同的环境】。我手动跑给它的环境
   很"富",cron 给它的环境很"穷"。
不是命令没装,是 cron 那个 shell,根本没读过
那些"让命令被找到"的配置文件。

修复 1:先分清——shell 的四种"身份"

# === ★ 一切的根源:shell 启动时,有四种不同的"身份" ===

# === ★ 维度一:交互式 vs 非交互式 ===
# ★ 交互式(interactive):有人坐在终端前,一条一条
#   敲命令、看回显 —— 比如你 SSH 登录后的那个窗口。
# ★ 非交互式(non-interactive):没人盯着,shell
#   只是【闷头执行一个脚本文件】,执行完就退 ——
#   比如 bash backup.sh、cron 跑脚本。

# === ★ 维度二:登录 vs 非登录 ===
# ★ 登录 shell(login):它是你这次"会话的起点"。
#   比如你 SSH 连上服务器,系统给你开的第一个 shell。
# ★ 非登录 shell(non-login):在一个已有会话里
#   再开的 shell。比如你在终端里又敲了一下 bash。

# === ★ 两个维度一交叉,就是四种身份 ===
# ① 交互式 + 登录   —— ★ 你 SSH 登录服务器的那个窗口
# ② 交互式 + 非登录 —— 你在窗口里再敲 bash 开的子 shell
# ③ 非交互 + 非登录 —— ★★ bash script.sh、cron 跑脚本
# ④ 非交互 + 登录   —— bash --login script.sh(少见)

# === ★ 为什么要分这么细?因为【每种身份,读的启动
#   文件不一样】===
# ★ 这就是本文事故的全部秘密:我手动登录,是身份①;
#   cron 跑脚本,是身份③。这两种身份,启动时读的
#   配置文件【几乎没有交集】—— 所以它俩的环境,
#   天差地别。

# === ★ 当场判断"我现在这个 shell 是什么身份" ===
$ echo $-                            # 输出里有 i = 交互式
# himBHs  -> 含 i,交互式
$ shopt -q login_shell && echo 登录 || echo 非登录
# ★ 这两条,能当场告诉你这个 shell 的身份

# === 认知 ===
# ★ 一个 shell 启动时有四种"身份",由两个维度交叉
#   而成:交互式 / 非交互式(有没有人在终端敲)、
#   登录 / 非登录(是不是会话的起点)。你 SSH 登录
#   是"交互式+登录",而 bash 跑脚本、cron 跑任务是
#   "非交互+非登录"。身份不同,启动时读的配置文件
#   就不同 —— 这是脚本"手动跑能、cron 跑不能"的根。

修复 2:bash 启动到底读哪些文件——一张必须记死的表

# === ★ 把"哪种身份读哪些文件",彻底钉清楚 ===

# === ★ 情况①:交互式 + 登录 shell(你 SSH 登录)===
# ★ 它按顺序读:
#   1. /etc/profile          —— 全局,系统级
#   2. ~/.bash_profile       —— 你的,用户级(找到就停)
#      (没有则找 ~/.bash_login,再没有找 ~/.profile)
# ★ 关键:~/.bash_profile 里,通常【会有一行】手动
#   去 source ~/.bashrc:
$ cat ~/.bash_profile
# ... if [ -f ~/.bashrc ]; then . ~/.bashrc; fi   # ★ 这一行
# ★ 正因为这一行,登录 shell 才"顺带"也读了 .bashrc。

# === ★ 情况②:交互式 + 非登录 shell(已登录后再敲 bash)===
# ★ 它【不读】 /etc/profile,【不读】 ~/.bash_profile。
# ★ 它只读:
#   ~/.bashrc(以及 .bashrc 里 source 的 /etc/bashrc)
# ★ 这就是为什么"别名、自定义函数"都写在 .bashrc ——
#   因为每开一个交互 shell 都要用,而 .bashrc 是
#   交互 shell 必读的。

# === ★ 情况③:非交互式 shell(★ bash script.sh、cron)===
# ★ ★ 它【既不读 profile,也不读 bash_profile,
#   也不读 .bashrc】!几乎什么都不读!
# ★ 它唯一会看的,是环境变量 BASH_ENV 指向的文件
#   —— 而这个变量,默认【是空的】。
# ★ 所以一个非交互 shell,启动时拿到的环境,基本就是
#   它从【父进程继承】来的那一份,加上 bash 内置的
#   极贫瘠默认值。它不会自己去"补课"读任何配置。

# === ★ 一张表,记死它 ===
# 身份                  /etc/profile  .bash_profile  .bashrc
# 交互+登录(SSH登录)      ✓读           ✓读          ✓(被前者source)
# 交互+非登录(再敲bash)    ✗            ✗             ✓读
# ★ 非交互(脚本/cron)      ✗            ✗             ✗ 全不读!

# === ★ 验证:非交互 shell 真的不读 .bashrc ===
$ echo 'echo BASHRC-被读到了' >> ~/.bashrc   # 在 .bashrc 末尾埋一句
$ bash -c 'echo hello'               # ★ 起个非交互 shell
hello                                # ★ 只有 hello,没有"BASHRC-被读到了"
# ★ 证实:非交互 shell 启动时,根本没碰 .bashrc。
$ # (验证完把刚才那行从 .bashrc 删掉)

# === 认知 ===
# ★ bash 启动读哪些文件,严格由身份决定:交互+登录
#   读 /etc/profile 和 ~/.bash_profile(后者通常再
#   source .bashrc);交互+非登录只读 ~/.bashrc;
#   ★ 非交互 shell(脚本、cron)profile / bash_profile
#   / .bashrc 【一个都不读】,只看 BASH_ENV(默认空)。
#   所以脚本拿到的环境,基本只是从父进程继承的那份。

修复 3:为什么"环境变量设在 .bashrc 里"是个常见的坑

# === ★ 顺着上一节,把一个极普遍的错,讲透 ===

# === ★ 无数人(包括当年的我)的习惯 ===
# ★ 想加个环境变量 / 改 PATH,就顺手往 ~/.bashrc 里写:
$ cat ~/.bashrc
# export PATH=$PATH:/usr/local/bin     # ★ 很多人这么加
# export AWS_PROFILE=prod              # ★ 自定义环境变量也塞这

# === ★ 这为什么"平时看着没问题" ===
# ★ 因为你每天的操作 —— SSH 登录、再开终端 —— 用的
#   都是【交互式 shell】。交互式 shell 要么直接读
#   .bashrc,要么通过 .bash_profile source 到它。
# ★ 所以你交互着用,.bashrc 里的东西【永远在】。你
#   会自然地以为"我设的环境变量,全系统都有"。

# === ★ 但它在这些场景【全军覆没】 ===
# ★ 只要是【非交互 shell】,.bashrc 根本不被读 ——
#  - cron 跑的脚本                  -> 读不到
#  - systemd 启动的服务             -> 读不到
#  - ssh user@host 'command' 这种   -> 读不到
#  - 很多 IDE / 工具内部起的 shell  -> 读不到
# ★ 你设在 .bashrc 里的 PATH、AWS_PROFILE,在这些
#   场景里【就像从没存在过】。本文事故,正是如此。

# === ★ 那"系统级、所有人、所有登录"的变量,该放哪 ===
# ★ 放 /etc/profile.d/ 下一个 .sh 文件(登录 shell 读):
$ cat /etc/profile.d/mypath.sh
export PATH=$PATH:/usr/local/bin
# ★ 但注意:这仍只对【登录 shell】生效,cron 照样
#   读不到 —— profile 系列,非交互 shell 也不读。

# === ★ 一个关键区分:export 了,才会"传给子进程" ===
$ MYVAR=hello                        # 没 export:只在当前 shell
$ export MYVAR=hello                 # ★ export:子进程才继承得到
# ★ 环境变量靠"父传子"继承。脚本是 shell 的子进程,
#   只有 export 过的变量,才会被脚本继承到。

# === ★ 结论:配置文件,不是"系统设置",是"某种
#   身份的 shell 的开场白" ===
# ★ .bashrc 不是"系统的环境变量表",它只是"交互
#   shell 每次开场时念的一段稿子"。把全局配置寄托
#   在一段"只有特定身份才会念的稿子"上,本身就错了。

# === 认知 ===
# ★ 把环境变量 / PATH 设在 ~/.bashrc 里,平时交互
#   使用一切正常,会让你误以为"全系统都有";但
#   cron、systemd、ssh 远程命令这些【非交互 shell】
#   根本不读 .bashrc,你设的东西在那里等于不存在。
#   .bashrc 不是系统环境表,只是交互 shell 的开场白
#   —— 别把全局配置,寄托在一段不是人人都念的稿子上。

修复 4:cron 的环境到底有多"穷"

# === ★ 单独把 cron 的环境,拎出来看个明白 ===

# === ★ cron 跑任务时,给的是一个"极简环境" ===
# ★ cron 不是"模拟你登录一遍再跑"。它是个独立的
#   守护进程,到点了,直接 fork 一个【非交互、非登录】
#   的 /bin/sh 来执行你那行命令。
# ★ 这个 shell 的环境,极度贫瘠,大致只有:
#  - HOME      —— 任务属主的家目录
#  - LOGNAME / USER  —— 任务属主
#  - PATH      —— ★ 通常只有 /usr/bin:/bin
#  - SHELL     —— /bin/sh
# ★ 你在 .bashrc / .bash_profile / profile 里设的
#   一切,cron 【全都没有】。

# === ★ 亲眼看一次 cron 的真实环境 ===
# 在 crontab 里临时加这一行,把环境 dump 出来:
* * * * * env > /tmp/cron-real-env.txt 2>&1
# 等一分钟,然后看:
$ cat /tmp/cron-real-env.txt
# HOME=/root
# LOGNAME=root
# PATH=/usr/bin:/bin                  # ★★ 就这么点
# SHELL=/bin/sh
# ★ 对比你手动 env 看到的几十行 —— cron 这个,穷得
#   揪心。这就是你脚本在 cron 里拿到的全部家当。

# === ★ cron 的两个最高频翻车点 ===
# ★ 翻车点一:PATH 太短,命令找不到。
#   /usr/local/bin、/opt/xxx/bin 里的命令,cron 一律
#   command not found。—— 本文事故。
# ★ 翻车点二:依赖某个环境变量的脚本,变量是空的。
#   脚本里用 $JAVA_HOME、$AWS_PROFILE、$LANG …… ——
#   cron 环境里没有,变量展开成空,行为全乱。

# === ★ crontab 文件里,可以直接写环境变量 ===
# ★ crontab 文件顶部,可以写 KEY=VALUE 行,它们会
#   注入到这个 crontab 所有任务的环境里:
$ crontab -e
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin
AWS_PROFILE=prod
LANG=en_US.UTF-8
0 3 * * * /root/backup.sh             # ★ 这行任务,就有上面的环境了
# ★ 注意:这里只能写 KEY=VALUE,【不能写 export】,
#   也不能用 $VAR 引用别的变量(不是完整 shell 语法)。

# === 认知 ===
# ★ cron 跑任务不是"模拟你登录一遍",它直接 fork 一个
#   非交互非登录 shell,环境极简:基本只有 HOME、
#   USER、SHELL 和一个很短的 PATH(/usr/bin:/bin)。
#   你在任何启动文件里设的东西它都没有。两大翻车点:
#   PATH 太短命令找不到、依赖的环境变量是空的。可在
#   crontab 文件顶部直接写 KEY=VALUE 行注入环境。

修复 5:正确的做法——让脚本自带环境,不靠"运气"

# === ★ 治本:脚本不该假设环境,要自己把环境备齐 ===

# === ★ 错误心态:"我登录时环境是对的,脚本应该也对" ===
# ★ 这就是本文我栽的根。脚本是给【各种身份的 shell】
#   去跑的 —— cron、systemd、CI、别人手动跑…… 你
#   无法保证每个调用者的环境都和你登录时一样"富"。
# ★ 正确心态:★ 一个健壮的脚本,【不依赖外部环境】,
#   它自己负责把它要用的环境,在脚本里备齐。

# === ★ 做法一:命令用绝对路径,或脚本内显式设 PATH ===
# 方式 A:脚本开头,显式把 PATH 设全:
#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin
aws s3 cp ... # 这下不管谁来跑,PATH 都是全的
# 方式 B:关键命令直接写绝对路径,最稳:
/usr/local/bin/aws s3 cp ...
# ★ 用 command -v 先查出绝对路径,再写进脚本:
$ command -v aws
/usr/local/bin/aws

# === ★ 做法二:脚本要的环境变量,在脚本里显式给 ===
#!/bin/bash
export AWS_PROFILE=prod
export LANG=en_US.UTF-8
# ★ 别假设调用者会传给你 —— 自己 export 一份。

# === ★ 做法三:若脚本确实需要"完整登录环境",显式加载 ===
# ★ 有时脚本就是依赖一大套登录环境(如 nvm、conda)。
#   那就在脚本开头,【显式 source】那些文件:
#!/bin/bash
source /etc/profile
source ~/.bashrc                     # 显式补上,不靠 shell 自动读
# ★ 注意:.bashrc 开头常有"非交互就直接 return"的
#   判断,显式 source 时可能要留意这一点。

# === ★ 做法四:cron 里,索性"模拟一次登录"再跑 ===
# ★ 让 cron 用一个【登录 shell】来跑脚本,它就会去
#   读 profile / bash_profile 那一套:
0 3 * * * bash -l /root/backup.sh     # ★ -l = 当登录 shell 跑
# 或:
0 3 * * * bash -lc 'source ~/.bash_profile; /root/backup.sh'

# === ★ 做法五:systemd 服务,用 Environment / EnvironmentFile ===
# systemd 服务同理不读那些文件,要在 unit 里显式给:
$ systemctl edit myapp
[Service]
Environment=PATH=/usr/local/bin:/usr/bin:/bin
Environment=AWS_PROFILE=prod
EnvironmentFile=/etc/myapp/env        # 或从文件读

# === 认知 ===
# ★ 治本是让脚本【自带环境,不靠运气】:命令用绝对
#   路径或脚本内 export PATH;要用的环境变量在脚本里
#   显式 export;真需要完整登录环境就显式 source
#   profile/.bashrc;cron 里可用 bash -l 当登录 shell
#   跑;systemd 服务用 Environment / EnvironmentFile。
#   一个健壮的脚本,不假设调用者给它什么环境。

修复 6:环境变量加载排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ 脚本"手动跑能、cron/systemd 跑不能",第一怀疑就是环境差异 ===

# === 2. ★ 先 dump 两边的环境对比,别猜 ===
* * * * * env > /tmp/cronenv.txt 2>&1   # cron 的环境
$ env > /tmp/myenv.txt                    # 我手动的环境
$ diff /tmp/myenv.txt /tmp/cronenv.txt    # ★ 差在哪一目了然

# === 3. ★ 非交互 shell(脚本/cron/systemd)不读 profile/.bashrc ===

# === 4. ★ 别把 PATH / 环境变量只设在 .bashrc,非交互场景全读不到 ===

# === 5. 命令找不到,先比 PATH:echo $PATH 对照 command -v 命令 ===

# === 6. ★ 脚本里命令尽量用绝对路径,或脚本开头显式 export PATH ===

# === 7. 脚本依赖的环境变量,在脚本里自己 export,别假设调用者给 ===

# === 8. cron 要完整环境,用 bash -l 跑;systemd 用 Environment= ===

# === 9. crontab 文件顶部可写 KEY=VALUE 注入环境(不能写 export)===

# === 10. 排查"脚本换个地方跑就挂"的步骤链 ===
$ which 出问题的命令                  # ① 命令到底在哪
$ echo $- ; shopt -q login_shell      # ② 我现在 shell 啥身份
* * * * * env > /tmp/e.txt 2>&1       # ③ dump 出问题环境
$ diff 手动env cron-env               # ④ 对比,差异即根因
# 八成是 PATH 短了,或某个变量空了。脚本里显式补齐。

命令速查

需求                        命令
=============================================================
看当前所有环境变量          env
看 PATH                     echo $PATH
查命令的绝对路径            command -v 命令 / which 命令
看当前 shell 是否交互式     echo $-(含 i 即交互式)
看当前 shell 是否登录       shopt -q login_shell && echo yes
dump cron 的真实环境        crontab 加一行 env > /tmp/e.txt
对比两份环境                diff 文件1 文件2
让 cron 用登录 shell 跑     bash -l /path/script.sh
编辑 crontab                crontab -e
看 bash 启动读了哪些文件    bash -lx -c true 2>&1 | grep source

口诀:非交互 shell 不读 profile 和 bashrc 脚本拿到的环境很穷
      命令找不到先 diff 两边 env,脚本里显式 export PATH 自带环境

避坑清单

  1. 脚本手动跑正常、换 cron 或 systemd 跑就报错,第一怀疑永远是两边环境不一样而不是脚本错了
  2. shell 启动有四种身份:交互登录、交互非登录、非交互非登录、非交互登录,身份不同读的启动文件不同
  3. 交互式登录 shell 读 /etc/profile 和 ~/.bash_profile,后者通常再 source ~/.bashrc
  4. 交互式非登录 shell 只读 ~/.bashrc,这就是别名和函数都写在 .bashrc 的原因
  5. 非交互式 shell(脚本、cron、systemd)profile 和 bash_profile 和 .bashrc 一个都不读
  6. 把 PATH 或环境变量只设在 .bashrc 里,交互使用一切正常,但非交互场景全部读不到
  7. cron 跑任务给的是极简环境,PATH 通常只有 /usr/bin:/bin,你设的东西它全没有
  8. 排查命令找不到先 dump 两边的 env 做 diff,差异即根因,八成是 PATH 短了或某变量空了
  9. 健壮的脚本不假设外部环境,自己在脚本里 export PATH 和所需变量,或命令用绝对路径
  10. cron 要完整环境用 bash -l 当登录 shell 跑,systemd 服务用 Environment= 或 EnvironmentFile= 显式给

总结

这次"aws 命令时有时无"的事故,纠正了我一个关于"环境"的、藏得极深的错觉。在我过去的脑子里,一台服务器的"环境",是它身上一个【固定的、唯一的、属于这台机器本身的】属性。我登录上去,echo $PATH,看到一长串目录;which aws,看到 /usr/local/bin/aws——在那一刻,我心里认定的是:"这台服务器的 PATH 就是这样,这台服务器上 aws 就在这里。"这是【这台机器的事实】,跟谁来用、怎么用,没有关系。所以当 cron 说"找不到 aws"时,我的第一反应是荒谬的困惑:这怎么可能?aws 明明【就在这台机器上】,我刚刚还亲眼看见的。我下意识地觉得,cron 一定是"出 bug 了",因为它否认了一个我认定的、机器层面的客观事实。可现场用一个冷冰冰的 diff,把我这个错觉劈成了两半:我手动跑的环境,和 cron 跑的环境,并排放在一起,一个几十行,一个四五行,判若云泥。它们跑在【同一台机器】上,却拿着【两份完全不同的环境】。我这才被逼着去面对一件我从没正视过的事:"环境",从来就【不是机器的属性】。机器上躺着的,只是一堆【配置文件】——/etc/profile.bash_profile.bashrc——它们只是一些"稿子",静静地躺在那里,本身【什么也不是】。真正的"环境",是某一个 shell 进程,在它【启动的那一刻】,根据它自己的【身份】,挑了其中某几张稿子来念,临时在自己内存里【构建】出来的东西。环境不属于机器,它属于【那一个具体的 shell 进程】,而且是那个进程【为自己、在出生时】攒起来的私有家当。我登录时那个 shell,身份是"交互式登录",它念了一长串稿子,给自己攒了一份很"富"的环境;cron 起的那个 shell,身份是"非交互非登录",它一张稿子都没念,只揣着从父进程继承的、贫瘠的一点点就上了路。我那句"这台机器的 PATH 就是这样",根本是个不成立的句子——这台机器【没有】一个 PATH,只有"我那个 shell 的 PATH""cron 那个 shell 的 PATH",它们各自不同,且都只属于各自。复盘到根上我才看清,我错的不是某个配置,而是我脑子里那个"环境是机器的、是全局的、是唯一的"的模型。这台机器更像一个剧院,每一个 shell 进程都是一个上台的演员,而那些配置文件,是后台一叠【可选的台词本】。一个演员上台时念了哪几本,他这场戏就有哪些台词;另一个演员一本没念,他上台就是哑的。台词不属于"剧院",属于"每个演员各自念过的那些本子"。我冲着剧院喊"这出戏明明有台词",毫无意义——因为台词从不在剧院层面存在,它只在每个演员【各自的开嗓】里存在。这次最大的收获,是我学会了在写任何一个脚本时,先停下来问一句:这个脚本,将来会被【什么身份的 shell】来念?是我这种念全了台词本的交互登录 shell,还是 cron 那种一本没念的哑巴 shell?既然我无法预知、也无法控制每一个未来的调用者会带着多"富"或多"穷"的环境来,那我唯一能做的,就是让脚本【不依赖任何外部环境】——它要用的 PATH,自己在开头 export 全;它要用的变量,自己显式赋值;它要的命令,写绝对路径。一个真正健壮的脚本,不该是一个"假设观众都听得懂"的演员,而该是一个【自带字幕、自带道具、把要用的一切都揣在自己兜里】的演员——这样,不管是谁、在什么身份、什么时刻把它请上台,它都能把这出戏,一字不差地演完。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

Too many open files 改了 ulimit 还是报:一次文件描述符上限的复盘

2026-5-21 1:03:51

Linux教程

磁盘还有空间却报 No space left on device:一次 inode 耗尽的复盘

2026-5-21 1:11:42

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索