nvm 在 bash 中有效,但在 Python 脚本中执行时无效

nvm works in bash, but not when executed in Python script

安装nvm后一直正常工作,直到我需要用nvm远程连接服务器python。没有报错,也没有切换node版本成功,于是在服务器上写了个脚本测试一下:

test.py:

import os
res = os.popen("nvm use v14.2.0")
# res = os.popen("~/nvm/nvm.sh use v14.2.0")
print(res)
print(res.read())

输出:

$ python3 test.py
<os._wrap_close object at 0x7f00acdc4ac8>
/bin/sh: nvm: command not found

nvm安装路径为~/nvm~/nvm/nvm.sh

如果我直接在服务器上执行nvm -v,输出是0.39.1。当我直接执行 nvm 时,它工作正常。为什么从 Python 脚本执行时它不起作用?

在nvm安装目录下执行sh ./nvm.sh没有任何输出。

-rwxrwxr-x 1 asd asd 139220 Mar 11 15:57 nvm.sh

$ cd ~/nvm
$ sh ./nvm.sh
$

我的.bashrc文件:

# .bashrc

# Source global definitions
if [ -f /etc/bashrc ]; then
        . /etc/bashrc
fi

# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=

# User specific aliases and functions
source ~/nvm/nvm.sh

核心问题是nvm可以在命令行执行,但是找不到nvm可执行文件所在路径。或者 nvm.sh 是可执行文件,但当我执行此文件时没有任何反应。

注意:当我输入 which nvm 它输出:

/usr/bin/which: no nvm in (/home/pyer/nvm/versions/node/v14.2.0/bin:/usr/local/nodejs/bin:/usr/local/bin :/usr/bin:/usr/local/sbin:/usr/sbin:/usr/lib/golang/bin:/usr/local/apache-maven-3.8.4/bin)

更新

经过上次测试,代码执行不会再报错或阻塞,但又发现了新的问题。代码好像没有执行successfully.I重写了一个测试脚本

test.py:

import subprocess

def test():
    # p = subprocess.Popen(['bash', '-c', '-i', 'nvm use v14.2.0'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, executable='/usr/bin/bash')
    p = subprocess.Popen(['bash', '-c', '. ~/nvm/nvm.sh; nvm use v14.2.0'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, executable='/usr/bin/bash')
    out, err = p.communicate()
    print(out.decode('utf-8'))

    # p = subprocess.Popen(['bash', '-c', '-i', 'nvm list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, executable='/usr/bin/bash')
    p = subprocess.Popen(['bash', '-c', '. ~/nvm/nvm.sh; nvm list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, executable='/usr/bin/bash')
    out, err = p.communicate()
    print(out.decode('utf-8'))

test()

输出:

$ python3 test.py 
Now using node v14.2.0 (npm v6.14.4)

         v8.9.0
->     v10.24.1
       v12.10.0
        v14.2.0
        v17.5.0
         system
default -> v14.2.0
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v17.5.0) (default)
stable -> 17.5 (-> v17.5.0) (default)
lts/* -> lts/gallium (-> N/A)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1
lts/erbium -> v12.22.10 (-> N/A)
lts/fermium -> v14.19.0 (-> N/A)
lts/gallium -> v16.14.1 (-> N/A)

显示正在使用v14.2.0,但实际上是默认的v10.24.1。我发现 nvm 与其他命令有点不同。在我的机器上,其他命令都有可执行文件。但是nvm加载的是.bashrc/.zshrc,所以我有两个猜测。 1是因为python调用的bash没有加载.bashrc,所以执行python时的nvm和命令行执行的nvm不一样,可能作用域是不同的? 2是因为python的子进程调用了一些脚本,这些脚本会在'safety'的沙箱中执行,比如nvm?

错误很明显:系统找不到nvm命令。这可能是因为子进程中的搜索路径与 shell.

中的搜索路径不同

documentation 对此提出以下建议:

Warning: For maximum reliability, use a fully-qualified path for the executable. To search for an unqualified name on PATH, use shutil.which(). On all platforms, passing sys.executable is the recommended way to launch the current Python interpreter again, and use the -m command-line format to launch an installed module.

Resolving the path of executable (or the first item of args) is platform dependent. (...)

您也可以只更改命令以包含完整路径。所以像:

os.popen("/usr/bin/nvm use v14.2.0")

要找出正确的路径,请在 shell 中键入 which nvm。这应该会打印出您的 nvm 可执行文件的完整路径。

更新

虽然上述一般适用于所有应用程序,但问题是为什么在您的路径中找不到 nvm 可执行文件。 documentation explains that the ~/nvm/nvm.sh script should be called first to "load nvm". I originally suspected that this would just set the PATH variable so the nvm executable can be found, but looking at nvm.sh,看起来nvm命令其实不是一个可执行文件而是一个shell函数。这就是为什么在尝试从 Python 脚本执行它时找不到它的原因。

根据this answer,你应该可以运行命令如下:

subprocess.Popen(['bash', '-c', '. ~/nvm/nvm.sh; nvm'])

更新 2

请注意,nvm 命令似乎旨在用于 command-line。该脚本所做的其中一件事是更改环境变量,例如您的 PATH。例如,参见 here。这就是 select 不同版本的方式 nvm use v14.2.0.

如果您按照自己的方式从 Python 调用它,则它没有任何效果。它将更改其当前环境中的 PATH 变量,然后关闭。您对 subprocess.Popen 的第二次调用会创建一个新环境,在该环境中将再次使用默认版本。不过,以下命令应该会按预期工作,因为两个 nvm 命令现在将在同一个 shell 进程中执行:

subprocess.Popen(['bash', '-c', '. ~/nvm/nvm.sh; nvm use v14.2.0; nvm list'],
                 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                 executable='/usr/bin/bash')

但是,您真的应该考虑一下您要在这里实现的目标。为什么要从 Python 脚本调用 nvm?如果您只是为了设置正确的版本而执行它,您不妨将该逻辑包含在您的 Python 脚本中。如果您只是调用一些 nvm 命令,则可以在 bash 脚本中轻松完成。然而,如果你真的想这样做,你可以这样做,但你必须记住每次调用 subprocess.Popen() 都会产生一个新的环境,类似于启动一个新的远程会话到该服务器。