Linux 基础 08:环境变量与 shell 启动文件——为什么 ssh 跑命令找不到你的 PATH

发布于 2022-04-16 20:30 更新于 2022-04-16 22:10 1315 字 7 min read ... 访问量 手写 每个字都是我自己敲的。

Linux 基础 25:收尾——自查清单与继续深入的方向Linux 基础 24:启动流程与救援——UEFI、GRUB、initramfs 与 emergency modeLinux 基础 23:容器与内核边界——namespace、cgroup 与 docker exec 背后Linux 基础 22:日志与故障排查方法论——从现象到根因的六步Linux 基础 21:定时任务——crontab 的坑与 systemd timerLinux 基础 20:Shell 脚本(下)——trap、日志、幂等与并发锁Linux 基础 19:Shell 脚本(上)——引号、条件与退出码Linux 基础 18:性能观测与 strace——机器到底卡在哪Linux 基础 17:防火墙——iptables、nftables、firewalld 与 ufw 的关系Linux 基础 16:SELinux——当权限都对,服务还是被拒绝Linux 基础 15:权限进阶——sudoers 的通配陷阱、ACL 与 capabilitiesLinux 基础 14:SSH 与远程运维——密钥、权限与三种隧道Linux 基础 13:网络基础——自下而上的排错顺序Linux 基础 12:磁盘与文件系统——从块设备到挂载点Linux 基础 11:systemd——unit、systemctl 与 journalctl 的正确用法Linux 基础 10:包管理——apt 到底做了什么,以及四个高频报错Linux 基础 09:用户与组——passwd、shadow、sudoers 三件套Linux 基础 08:环境变量与 shell 启动文件——为什么 ssh 跑命令找不到你的 PATHLinux 基础 07:进程与信号——TERM 和 KILL 到底差在哪Linux 基础 06:管道与重定向——三条流、一个顺序问题Linux 基础 05:文本处理——把 grep/sort/uniq 变成你的分析工具Linux 基础 04:权限与属主——rwx 在文件和目录上不是一回事Linux 基础 03:ls、cp、mv、rm 的真实语义,以及那个由 shell 展开的 *Linux 基础 02:/ 底下为什么是这些目录,inode 里到底存了什么Linux 基础 01:终端、Shell,和命令是怎么被找到的
变量的作用域与"快照"语义、登录/非登录/交互/非交互四种组合各加载哪些文件、为什么 ssh 远程执行命令拿不到你的 PATH、sudo 的 secure_path 是怎么改变环境的。

这一节要解决什么

“我在终端里明明是 node -v 能用的,ssh host 'node -v' 却说找不到”——这个问题把环境变量、shell 的四种启动方式、以及 sudo 的环境策略串在了一起。搞清这条链,日常那些”本地能跑、远程不行""脚本里能跑、cron 里不行”的怪现象都会消失。

一、变量的三个层次

VAR=local          # 1) shell 变量:只存在于当前 shell
export VAR=global  # 2) 环境变量:会被子进程继承
bash -c 'echo "${VAR:-<empty>}"'      # 3) 子进程读的是“fork 那一刻的快照”

MYVAR=1; export MYVAR; bash -c 'echo $MYVAR'   # 1
MYVAR=2; bash -c 'echo $MYVAR'                 # 1 ← 已启动的子进程不受影响

批注:环境变量是拷贝,不是共享。父进程之后怎么改,已经跑起来的子进程都看不到——这条语义解释了第 01 篇那个 export 实验,也是”脚本里改了 PATH 但命令还是找不到”的根源。

env | sort | head              # 只看环境变量
printenv PATH                  # 取单个变量
set | head                     # 环境变量 + shell 变量 + 函数(最全)
env -i bash -c 'env'           # 干净环境(调试“到底缺了哪个变量”)

二、shell 的四种启动方式,加载不同的文件

启动方式典型场景加载的文件
登录 + 交互ssh 登录、控制台登录、bash -l/etc/profile → ~/.bash_profile / ~/.bash_login / ~/.profile(按顺序取第一个存在的)
非登录 + 交互新开一个终端窗口、直接敲 bash/etc/bash.bashrc(Debian 系)→ ~/.bashrc
非登录 + 非交互ssh host 'cmd'、脚本、bash -c都不加载,只读 $BASH_ENV 指向的文件
登录 + 非交互ssh host 'bash -lc cmd'同”登录 + 交互”那一行

批注:这张表是本节的核心。绝大多数”远程执行找不到命令”的问题,都是因为第 3 行——你的 PATH 扩展写在 ~/.bashrc 里,而它根本不会被加载。

三、真实故障 1:ssh host 'node -v' 找不到 node

原因通常不是没装,而是两件事叠加:

  1. PATH 扩展写在 ~/.bashrc(或 nvm 的初始化块)里;
  2. ~/.bashrc 顶部几乎都有这么一段(Ubuntu 默认就有):
case $- in
  *i*) ;;      # 交互式:继续往下执行
  *) return;;  # 非交互式:直接返回 ← ssh 执行命令时走的就是这里
esac

于是 ssh host 'node -v' 既没加载 .bashrc、也没加载 .profile,PATH 里自然没有 nvm 的目录。

四种解法(按推荐度):

ssh host 'bash -lc "node -v"'                       # 1) 显式让它当登录 shell 跑
ssh host 'source ~/.bashrc; node -v'                # 2) 手动加载(注意输出可能被污染)
# 3) 把 PATH 改成写进 ~/.profile(登录 shell 会读,非交互登录也会读)
# 4) 在 ~/.bashrc 里把“交互式判断”拿掉或改成只给 PATH/别名分块

我自己的做法是把 PATH 与 nvm 初始化放到 ~/.profile,交互式专属的东西(别名、提示符)才留在 ~/.bashrc。

四、真实故障 2:sudo 之后 PATH 变了

echo "$PATH"
sudo sh -c 'echo $PATH'      # Debian 默认会是 secure_path:/usr/local/sbin:...:/bin
sudo env | grep -i path      # policies 里也能看到

原因在 sudoers 的 secure_path:出于安全,sudo 默认重置 PATH,只保留系统目录(env_reset + secure_path)。后果是 /opt/xxx/bin 里的程序用 sudo 就跑不了。

sudo -E whoami                     # 保留当前环境(可用,但要清楚风险:别整段环境都带过去)
sudo visudo                        # 或给具体命令写完整路径的规则

五、把 PATH 写”幂等”

重复加载启动文件时,PATH 容易被叠成一长串。稳妥写法是先判断再加:

case ":$PATH:" in
  *":$HOME/bin:"*) ;;                       # 已经在了,什么都不做
  *) PATH="$HOME/bin:$PATH" ;;
esac
export PATH

批注:~/.bashrc 被 source 多次(每条新终端一次)很常见,幂等都是省下自己排查”PATH 为什么有 8 份 /usr/local/bin”的时间。

六、动手清单

MYVAR=1; bash -c 'echo "${MYVAR:-<empty>}"'      # 1. 未导出:子进程看不到
export MYVAR; bash -c 'echo $MYVAR'              # 2. 导出后可见
echo $-                                          # 3. 当前 shell 是否交互(含 i)
bash -lc 'echo login: $0'; bash -c 'echo plain: $0'   # 4. 两种启动的差别
ssh localhost 'echo "$PATH"' 2>/dev/null || true  # 5. 非交互的非登录 PATH 长什么样
sudo sh -c 'echo "$PATH"'                         # 6. secure_path
env -i bash -c 'env'                              # 7. 干净环境对照

小结

  • 环境变量是子进程启动时的快照,改父进程不影响已运行的子进程;子进程要用就得 export。
  • 登录 shell 读 profile 系,交互非登录读 bashrc 系,非交互(ssh host 'cmd'、脚本、cron)两个都不读。
  • PATH 扩展放 ~/.profile 更通用;~/.bashrc 顶部的交互判断是”远程执行找不到命令”的常见原因。
  • sudo 默认重置环境(secure_path);需要保留用 -E,或走 sudoers 规则。
  • PATH 追加要写成幂等方式。

下一节预告

下一节:用户与组——/etc/passwd 七个字段、/etc/shadow 里 ! 与 * 的含义、主组与附加组、usermod -aG 为什么必须带 -a,以及 su、sudo、sudoers 的关系。

执笔 手写

喜欢的话,留下你的评论吧~

加载中...