你在一个能连续执行命令的终端里干活,前面 cd 进了某个目录,觉得「已经切过去了」,后面几条命令就默认在当前目录下跑。结果它们跑在了别的地方,行为全错。你回头看,那条 cd 明明成功了。
现象
表现有好几种,都很迷惑:
- 「上一条
cd好像卡住了」——它没生效,或者生效的位置和你想的不一样; - 后续命令跑在错误的目录里,读到了不该读的文件、写到了不该写的地方;
- 同样几条命令,单独跑都对,连起来跑就错。
最迷惑的是:你没法用直觉判断哪些「现场」被保留了。
根因
关键在于,一个命令执行环境里,有两类状态,它们的行为不一样:
- 工作目录(当前所在目录):在多次调用之间会保留。你上一条命令里做的
cd,会改变下一条命令的起点。 - shell 状态(变量、函数、
export的环境变量、别名等):在多次调用之间不会保留。每条命令都在一个新的、干净的 shell 进程里执行。
这个「半保留」的设计是最坑的地方:
- 你以为「每条命令都是全新的,互不影响」——但目录是共享的;
- 你以为「前面的
cd和变量都能接着用」——但变量是不保留的。
于是会出现这样的误判:你在 A 脚本里 cd /path/to/project 并 export VERSION=1.2,下一条命令里 $VERSION 是空的(变量没了),但当前目录确实在 /path/to/project(目录保留了)。一个保留、一个不保留,非常反直觉。
解决
对付这种「部分保留」的环境,方法很简单但很有效:
尽量全程使用绝对路径,不依赖上一条命令的 cd。
不要写:
cd /path/to/project
python build.py
改成在同一条命令内完成目录切换,或者直接用绝对路径:
(cd /path/to/project && python build.py)
这里用 (...) 把 cd 和后续命令包成一个子 shell,让 cd 的作用范围限制在括号内,不会泄漏到下一条命令。这是「我只想在这一步切个目录」的标准写法。
或者更彻底一点,用工具的「指定工作目录」参数:
python /path/to/project/build.py
make -C /path/to/project
git -C /path/to/project status
-C 这类参数让工具自己去指定目录,不需要你 cd。
需要固定目录时,在每条命令内部显式指定。 不要指望「上一条命令切过去的目录还在」,虽然它确实还在——但一旦某条命令里无意中又 cd 了一次,后面的就全乱了,而且很难发现。
延伸与预防
一句话原则:
把每条命令都当成独立进程来写。
这不是保守,而是消除一类不确定性。你可能记得「工作目录会保留」,但团队的其他人、以及未来接手这段流程的人不一定记得。依赖这种隐式状态的脚本,是可移植性最差的那种。
具体建议:
- 不要在自动化脚本里假设当前目录。 脚本开头显式
cd到自己需要的目录(用绝对路径),或者全程用绝对路径访问文件。 - 写脚本时,路径从「脚本自身的位置」推导,而不是从「当前目录」推导。 比如 bash 里用
${BASH_SOURCE[0]}定位脚本所在目录,Python 里用__file__。这样不管从哪里调用,行为都一致。 - 不要把关键配置放在
export的变量里跨命令传递——因为根本传不过去。要么每次显式设置,要么写进配置文件。
这条坑是一种典型的「隐式状态」问题:环境里有一部分东西被悄悄记住了,而另一部分没有。凡是遇到「单独跑对、连起来跑错」,或者「我这明明做过了、怎么没生效」,都该怀疑是不是踩到了这种「一半保留、一半不保留」的隐式状态。