1. 直观案例展示
你可能遇到这种情况:
脚本内容:
sh
# a.sh
nvm use 18
node -v
不同执行方式对比:
-
方式一:用
sh
或bash
直接执行shsh ./a.sh # 或 bash ./a.sh
输出
bashnvm: command not found
-
方式二:用
source
(或.
)加载脚本shsource ./a.sh # 或 . ./a.sh
输出
v18.0.0
小结:
同样的脚本,直接执行时 nvm 找不到,用 source/点号加载却正常。为什么?往下看。
2. ~/.bashrc 和 ~/.zshrc 是什么?有什么作用?
~/.bashrc 和 ~/.zshrc 是 shell 的用户定制配置文件:
-
它们位于你的 home 目录(即
~
,比如 /home/yourname 或 /Users/yourname)。 -
每次新开一个终端窗口、或新启动交互式 shell(比如你输入 bash、zsh),这些配置文件都会被自动读取执行。
-
里面可以写 别名(alias) 、环境变量,以及像 nvm、conda 这类工具的初始化代码,例如:
shexport NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
-
这样,nvm 命令才能在你每个新开的 shell 里可用。
3. 案例解答
-
如果直接用
sh xxx.sh
或bash xxx.sh
来运行脚本,系统会新开一个"干净"的 shell,这个 shell 默认不会 加载你的/.bashrc、/.zshrc,所以 nvm 这类命令就找不到。 -
而如果使用
source xxx.sh
或. xxx.sh
来运行脚本,脚本会在当前 shell 下逐行执行,不会新开子 shell。如果你的当前 shell 已经通过~/.bashrc
或~/.zshrc
等配置文件加载过 nvm(即 nvm function 已被声明),那么 nvm 命令就能在脚本里被顺利识别和使用。总结一句话概括:
source xxx.sh
/. xxx.sh
:不新建 shell,当前 shell 有什么环境就能识别用什么(如 nvm function)。
4. 原理简析:二进制命令、shell function 和 alias
很多人容易混淆这些概念,关键区别如下:
- 二进制命令 (binary executable):
真实的可执行文件,一般放在/usr/bin
、/usr/local/bin
等目录。例如node
、ls
、git
等。只要 PATH 包含它们的目录,任何 shell 和脚本都能直接调用。 - shell function(shell 函数) :
用 shell 语言(bash/zsh)编写的内部函数,比如 nvm、conda、pyenv。只有在当前 shell "声明"(source 对应脚本)之后,这个命令才在本次会话里生效。例如source ~/.nvm/nvm.sh
,声明了 nvm 为 shell function,当前 shell 就能用。 - alias(别名) :
某个命令的快捷方式,比如alias ll='ls -lh'
,只是在当前 shell 会话或者被配置文件(如 .bashrc/.zshrc)自动声明后有效。
简要总结:
- 缺省(默认)所有 shell 都认识二进制命令(如 node),但并不天然认识 shell function 或 alias。
- nvm、conda、pyenv 等"命令",本质其实是 function/alias,在你 source 了对应配置文件后才"出生"于当前 shell,否则就会报不认识。
- 脚本用 sh/bash 直接运行,相当于新 shell,没读取你的 bashrc/zshrc,"这些命令还没出生",所以 nvm 用不了。
5. 如何让 shell 脚本中的 nvm 命令生效?
你有两种常见方案可选:
方案一:脚本开头主动 source 初始化
在你的脚本开头加上如下内容(以 nvm 为例):
sh
# 只是告诉 shell,"nvm 的安装目录在哪里",类似于设好一把钥匙。
export NVM_DIR="$HOME/.nvm"
# [ -s "$NVM_DIR/nvm.sh" ] 检查 nvm.sh 是否存在且非空。
# . "$NVM_DIR/nvm.sh" 这就是"source 初始化"------让 nvm.sh 里的函数和配置在这个 shell 里生效。
# . xxx 是 source xxx 的简写。
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 18
node -v
方案二:用 source 的方式运行脚本
sh
source ./a.sh
# 或者
. ./a.sh
这样会在当前 shell 加载并执行,nvm function/alias 都能用。
6. 总结与认知提升
- 配置文件(如 ~/.bashrc、~/.zshrc)只影响"交互式 shell"或你 source 加载的 shell。
- sh xxx.sh/bash xxx.sh 是开新 shell,不会自动加载你的用户配置。
- nvm、conda、pyenv 在脚本中能不能用,取决于脚本是否跑了初始化代码(source 脚本/配置信息)。
- 二进制命令(如 node、ls)只要 PATH 在,随时随地都能直接调用。
- 写涉及 nvm/conda/pyenv 的脚本时,记得先 source 对应的初始化脚本,否则就可能遇到"command not found"。
7.补充:关于 npm run 和 shell 环境隔离
需要注意,每次执行 npm run <script>
时,npm 都会新启动一个干净的 shell 环境 。
这意味着:
npm run
执行的每个 script,本质等价于重新打开一个终端运行命令,环境变量、PATH 以外的 shell function、alias(比如 nvm、conda)不会自动继承,必须在脚本内重新声明/source。- 即使脚本之间存在嵌套调用,凡是 npm run 调用,都是"层层新壳",彼此互不影响。
所以在 npm 的 script 里使用需要 shell function(如 nvm/conda/pyenv)等命令时,务必在每个 script 开头主动 source 对应的初始化脚本,否则依然会遇到 command not found 等问题。
【案例一】npm run 脚本直接用 nvm 会报错
假设你的 package.json 里有如下内容:
json
{
"scripts": {
"test-nvm": "nvm use 18 && node -v"
}
}
直接运行:
sh
npm run test-nvm
会报错:
makefile
sh: 1: nvm: not found
原因分析上文已述:npm run 会新开 shell,这个 shell 不懂 nvm function。
【案例二】正确做法:nvm 初始化
可以在 script 里补全 source nvm 的语句:
json
{
"scripts": {
"test-nvm": "export NVM_DIR=\"$HOME/.nvm\" && \
[ -s \"$NVM_DIR/nvm.sh\" ] && . \"$NVM_DIR/nvm.sh\" && \
nvm use 18 && node -v"
}
}
此时执行:
sh
npm run test-nvm
就可以正常输出 node 版本(比如 v18.0.0)。
【案例三】嵌套 npm run,环境依然隔离
json
{
"scripts": {
"foo": "echo BAR=$BAR",
"bar": "export BAR=hello && npm run foo"
}
}
此时执行:
sh
npm run bar
输出:
makefile
BAR=
原因 :当你执行 npm run bar
时,bar
脚本里的 export BAR=hello
仅作用于当前 shell 环境。 但 npm run foo
会再次新开一个"干净"的 shell 环境去执行 foo
,这个新 shell 并不会继承 bar
里 export 的变量(以及 alias、函数等),所以 echo BAR=$BAR
输出的还是空。
【经验总结】
- npm run 脚本里涉及 shell function/alias 的命令(如 nvm、conda),一定要在每个脚本开头显式 source 初始化,而不能只依赖 shell 配置文件(.bashrc、.zshrc)。
- 只设置 PATH 有助于系统命令(二进制)识别,但对 function/alias 没帮助。
- 这一点同样适用于 cron、CI/CD 等"非交互式 shell"场景。
【一键复用代码片段】
在前端项目等 package.json 里,推荐这样写:
json
{
"scripts": {
"with-nvm": "export NVM_DIR=\"$HOME/.nvm\" && \
[ -s \"$NVM_DIR/nvm.sh\" ] && . \"$NVM_DIR/nvm.sh\" && nvm use 18 && node yourscript.js"
}
}
【推荐最佳实践】
- 在 shell 脚本里,涉及 nvm、conda、pyenv 等 function/alias 命令,务必写明初始化过程。
- 在 package.json 里编写 npm script 时,原则同上。
- 如需全局生效,可考虑将必要的初始化导出成公共 sh 文件,每个 script 开头 source 即可,避免重复代码。