Shell 脚本里 nvm 不识别,node 却能用?原理与最佳实践

1. 直观案例展示

你可能遇到这种情况:

脚本内容:

sh 复制代码
# a.sh
nvm use 18
node -v

不同执行方式对比:

  • 方式一:用 shbash 直接执行

    sh 复制代码
    sh ./a.sh
    # 或
    bash ./a.sh

    输出

    bash 复制代码
    nvm: command not found
  • 方式二:用 source(或 .)加载脚本

    sh 复制代码
    source ./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 这类工具的初始化代码,例如:

    sh 复制代码
    export NVM_DIR="$HOME/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
  • 这样,nvm 命令才能在你每个新开的 shell 里可用。

3. 案例解答

  1. 如果直接用 sh xxx.shbash xxx.sh 来运行脚本,系统会新开一个"干净"的 shell,这个 shell 默认不会 加载你的 /.bashrc、/.zshrc,所以 nvm 这类命令就找不到。

  2. 而如果使用 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 等目录。例如 nodelsgit 等。只要 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 即可,避免重复代码。
相关推荐
weixin-a1530030831642 分钟前
【playwright篇】教程(十七)[html元素知识]
java·前端·html
ai小鬼头1 小时前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
一只叫煤球的猫2 小时前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
vvilkim2 小时前
Electron 自动更新机制详解:实现无缝应用升级
前端·javascript·electron
vvilkim2 小时前
Electron 应用中的内容安全策略 (CSP) 全面指南
前端·javascript·electron
aha-凯心2 小时前
vben 之 axios 封装
前端·javascript·学习
遗憾随她而去.2 小时前
uniapp 中使用路由导航守卫,进行登录鉴权
前端·uni-app
xjt_09013 小时前
浅析Web存储系统
前端
foxhuli2293 小时前
禁止ifrmare标签上的文件,实现自动下载功能,并且隐藏工具栏
前端
青皮桔4 小时前
CSS实现百分比水柱图
前端·css