subprocess.run 的眉眉角角

subprocess.run 可以讓我們透過 Python 程式碼執行外部的程式,不過個別參數都會對實際的執行結果有影響,以下我們分別說明。 env 參數會影響傳遞的環境變數內容 env 參數會影響傳遞到子行程中的環境變數,以底下這個簡單的程式為例: import subprocess import os, sys print('-' * 20) for i, name in enumerate(os.environ): print(f'{i:02d}:{name}') if len(sys.argv) > 1: match sys.argv[1]: case 'empty': env = {} case 'none': env = None case _: k, v = sys.argv[1].split('=') env = {k: v} shell = len(sys.argv) > 2 and sys.argv[2] == 'shell' subprocess.run( args=['python', 'test_env.py'], env=env, shell=shell ) 如果以如下引數執行: python test_env.py none 這會傳入 None 給 subprocess.run 的 env 參數,這也是 env 參數的預設值,會將目前的環境變數全數傳遞給子行程,得到如下結果: -------------------- 00:AKASH_API_KEY ... 80:ZES_ENABLE_SYSMAN -------------------- 00:AKASH_API_KEY ... 80:ZES_ENABLE_SYSMAN 父行程和子行程的環境變數一模一樣。如果以如下引數執行程式: python test_env.py empty 由於這會傳入 {} 空的字典給 env 參數,所以子行程的環境變數是空的,什麼都沒有: -------------------- 00:AKASH_API_KEY ... 80:ZES_ENABLE_SYSMAN -------------------- 如果傳入客製的環境變數內容,例如: python test_env.py TEST=hello 就會看到雖然父行程的環境變數都不會傳入子行程,但會傳遞客製的環境變數: 80:ZES_ENABLE_SYSMAN -------------------- 00:AKASH_API_KEY ... -------------------- 00:TEST shell 參數的影響 如果設定 shell 參數為 True,表示要先以子行程啟動系統預設的命令解譯器,由該命令解譯器來執行傳給 args 參數的指令。 以下仍以前一小節的範例說明。我們重複上述的測試,首先是採用 env 預設值 None,但是在命令行加上 shell 引數: python test_env.py none shell 在 Windows 上會依據環境變數 COMSPEC 的設定執行預設解譯器,沒有修改的話就是 cmd,它會額外新增一個 PROMPT 的環境變數,用來制訂輸入提示符號的格式,所以你會看到以子行程比父行程多了一個環境變數: -------------------- 00:AKASH_API_KEY ... 52:PROGRAMW6432 53:PSMODULEPATH ... 80:ZES_ENABLE_SYSMAN -------------------- 00:AKASH_API_KEY ... 52:PROGRAMW6432 53:PROMPT 54:PSMODULEPATH ... 81:ZES_ENABLE_SYSMAN 再來測試傳遞空的環境變數給子行程: python test_env.py empty shell 由於子行程的環境變數是空的,所以命令解譯程式並沒有 PATH 環境變數的資訊,所以它不知道 python 指令該去哪裡找到可執行檔來執行,因而無法執行而顯示錯誤訊息: -------------------- 00:AKASH_API_KEY ... 80:ZES_ENABLE_SYSMAN 'python' 不是內部或外部命令、可執行的程式或批次檔。 同樣的道理,如果傳遞客製的環境變數: python test_env.py TEST=hello shell 也一樣沒有 PATH 環境變數,還是無法執行: -------------------- 00:AKASH_API_KEY ... 80:ZES_ENABLE_SYSMAN 'python' 不是內部或外部命令、可執行的程式或批次檔。 如果傳遞客製的 PATH 環境變數,涵蓋 python 直譯器的路徑: python test_env.py PATH=C:\users\meebo\code\python\test_py3.13\.venv\scripts shell 就可以正常執行了: -------------------- 00:AKASH_API_KEY ... 10:COMSPEC ... 34:PATH 35:PATHEXT ... 80:ZES_ENABLE_SYSMAN -------------------- 00:COMSPEC 01:PATH 02:PATHEXT 03:PROMPT 除了我們傳遞過去的 PATH 以外,其他三個環境變數都是 cmd 會自動建立的環境變數。 不同平台的差異 雖然 Python 已經為不同平台提供了一致的介面,但是實際上不同平台還是存在差異,以下再詳細說明。 shell=True 時 args 的差異 在 Linux 平台上,如果 shell 設為 True,那麼 args 必須傳入字串,內含完整的命令行,不能拆開成指令與引數的串列,舉例來說,以下是 Windows 平台的例子: >>> subprocess.run( ... args='dir test_env.py', ... shell=True ... ) 確認結果正確: 磁碟區 C 中的磁碟是 Book 13 磁碟區序號: 8482-E7D5 C:\Users\meebo\code\python\test_py3.13 的目錄 2025/04/05 下午 04:22 635 test_env.py 1 個檔案 635 位元組 0 個目錄 75,697,201,152 位元組可用 CompletedProcess(args='dir test_env.py', returncode=0) 改成傳入字串串列: >>> subprocess.run( ... args=['dir', 'test_env.py'], ... shell=True ... ) 一樣可以正常運作: 磁碟區 C 中的磁碟是 Book 13 磁碟區序號: 8482-E7D5 C:\Users\meebo\code\python\test_py3.13 的目錄 2025/04/05 下午 04:22 635 test_env.py 1 個檔案 635 位元組 0 個目錄 75,695,497,216 位元組可用 CompletedProcess(args=['dir', 'test_env.py'], returncode=0) >>> 不論是傳入單一字串或是字串串列,都可以正確運作。但如果是在 Linux 平台上: >>> subprocess.run( ... 'ls -l test_env.py', ... shell=True ... ) 可以看到指定檔案的詳細資訊: -rwxrwxrwx 1 meebox meebox 635 Apr 5 16:22 test_env.py CompletedProcess(args='ls -l test_env.py', returncode=0) 但如果改成字串串列: >>> subprocess.run( ... args=['ls', '-l', 'test_env.py'], ... shell=True ... ) 執行結果就不對了: asyncio_openai.py package-lock.json test2.py test_FAISS.py my_faiss_db __pycache__ test_copilot.py test.py package.json spotify_play.py test_env.py CompletedProcess(args=['ls', '-l', 'test_env.py'], returncode=0) >>> 你可以看到傳入整個命令行的字串可以正確運作,但是若拆成字串串列,命令解譯程式只會執行第一個元素,也就是 'ls',結果變成是顯示當前資料夾下的檔案,而不是顯示指定檔案的詳細資訊了。 以剛剛的測試程式為例,如果以下列方式執行: python test_env.py none shell 就會看到程式停在直譯器的輸入提示: -------------------- 00:SHELL ... 30:TERM_PROGRAM 31:_ Python 3.13.2 (main, Mar 17 2025, 21:02:54) [Clang 20.1.0 ] on linux Type "help", "copyright", "credits" or "license" for more information. >>> 不會結束,這是因為命令解譯程式只把串列中的第一個元素 'python' 當成命令行,所以就會進入 REPL 等待使用者輸入程式。在 Windows 上並不會發生同樣的問題。 把程式碼改成以下這樣: import subprocess import os, sys print('-' * 20) for i, name in enumerate(os.environ): print(f'{i:02d}:{name}') if len(sys.argv) > 1: match sys.argv[1]: case 'empty': env = {} case 'none': env = None case _: k, v = sys.argv[1].split

Apr 5, 2025 - 11:24
 0
subprocess.run 的眉眉角角

subprocess.run 可以讓我們透過 Python 程式碼執行外部的程式,不過個別參數都會對實際的執行結果有影響,以下我們分別說明。

env 參數會影響傳遞的環境變數內容

env 參數會影響傳遞到子行程中的環境變數,以底下這個簡單的程式為例:

import subprocess
import os, sys

print('-' * 20)
for i, name in enumerate(os.environ):
    print(f'{i:02d}:{name}')

if len(sys.argv) > 1:
    match sys.argv[1]:
        case 'empty':
            env = {}
        case 'none':
            env = None
        case _:
            k, v = sys.argv[1].split('=')
            env = {k: v}
    shell = len(sys.argv) > 2 and sys.argv[2] == 'shell'
    subprocess.run(
        args=['python', 'test_env.py'],
        env=env,
        shell=shell
    )

如果以如下引數執行:

python test_env.py none

這會傳入 Nonesubprocess.runenv 參數,這也是 env 參數的預設值,會將目前的環境變數全數傳遞給子行程,得到如下結果:

--------------------
00:AKASH_API_KEY
...
80:ZES_ENABLE_SYSMAN
--------------------
00:AKASH_API_KEY
...
80:ZES_ENABLE_SYSMAN

父行程和子行程的環境變數一模一樣。如果以如下引數執行程式:

python test_env.py empty

由於這會傳入 {} 空的字典給 env 參數,所以子行程的環境變數是空的,什麼都沒有:

--------------------
00:AKASH_API_KEY
...
80:ZES_ENABLE_SYSMAN
--------------------

如果傳入客製的環境變數內容,例如:

python test_env.py TEST=hello

就會看到雖然父行程的環境變數都不會傳入子行程,但會傳遞客製的環境變數:

80:ZES_ENABLE_SYSMAN
--------------------
00:AKASH_API_KEY
...
--------------------
00:TEST

shell 參數的影響

如果設定 shell 參數為 True,表示要先以子行程啟動系統預設的命令解譯器,由該命令解譯器來執行傳給 args 參數的指令。

以下仍以前一小節的範例說明。我們重複上述的測試,首先是採用 env 預設值 None,但是在命令行加上 shell 引數:

python test_env.py none shell

在 Windows 上會依據環境變數 COMSPEC 的設定執行預設解譯器,沒有修改的話就是 cmd,它會額外新增一個 PROMPT 的環境變數,用來制訂輸入提示符號的格式,所以你會看到以子行程比父行程多了一個環境變數:

--------------------
00:AKASH_API_KEY
...
52:PROGRAMW6432
53:PSMODULEPATH
...
80:ZES_ENABLE_SYSMAN
--------------------
00:AKASH_API_KEY
...
52:PROGRAMW6432
53:PROMPT
54:PSMODULEPATH
...
81:ZES_ENABLE_SYSMAN

再來測試傳遞空的環境變數給子行程:

python test_env.py empty shell

由於子行程的環境變數是空的,所以命令解譯程式並沒有 PATH 環境變數的資訊,所以它不知道 python 指令該去哪裡找到可執行檔來執行,因而無法執行而顯示錯誤訊息:

--------------------
00:AKASH_API_KEY
...
80:ZES_ENABLE_SYSMAN
'python' 不是內部或外部命令、可執行的程式或批次檔。

同樣的道理,如果傳遞客製的環境變數:

python test_env.py TEST=hello shell

也一樣沒有 PATH 環境變數,還是無法執行:

--------------------
00:AKASH_API_KEY
...
80:ZES_ENABLE_SYSMAN
'python' 不是內部或外部命令、可執行的程式或批次檔。

如果傳遞客製的 PATH 環境變數,涵蓋 python 直譯器的路徑:

python test_env.py PATH=C:\users\meebo\code\python\test_py3.13\.venv\scripts shell

就可以正常執行了:

--------------------
00:AKASH_API_KEY
...
10:COMSPEC
...
34:PATH
35:PATHEXT
...
80:ZES_ENABLE_SYSMAN
--------------------
00:COMSPEC
01:PATH
02:PATHEXT
03:PROMPT

除了我們傳遞過去的 PATH 以外,其他三個環境變數都是 cmd 會自動建立的環境變數。

不同平台的差異

雖然 Python 已經為不同平台提供了一致的介面,但是實際上不同平台還是存在差異,以下再詳細說明。

shell=True 時 args 的差異

在 Linux 平台上,如果 shell 設為 True,那麼 args 必須傳入字串,內含完整的命令行,不能拆開成指令與引數的串列,舉例來說,以下是 Windows 平台的例子:

>>> subprocess.run(
...     args='dir test_env.py',
...     shell=True
... )

確認結果正確:

 磁碟區 C 中的磁碟是 Book 13
 磁碟區序號:  8482-E7D5

 C:\Users\meebo\code\python\test_py3.13 的目錄

2025/04/05  下午 04:22               635 test_env.py
               1 個檔案             635 位元組
               0 個目錄  75,697,201,152 位元組可用
CompletedProcess(args='dir test_env.py', returncode=0)

改成傳入字串串列:

>>> subprocess.run(
...     args=['dir', 'test_env.py'],
...     shell=True
... )

一樣可以正常運作:

 磁碟區 C 中的磁碟是 Book 13
 磁碟區序號:  8482-E7D5

 C:\Users\meebo\code\python\test_py3.13 的目錄

2025/04/05  下午 04:22               635 test_env.py
               1 個檔案             635 位元組
               0 個目錄  75,695,497,216 位元組可用
CompletedProcess(args=['dir', 'test_env.py'], returncode=0)

>>>

不論是傳入單一字串或是字串串列,都可以正確運作。但如果是在 Linux 平台上:

>>> subprocess.run(
...     'ls -l test_env.py',
...     shell=True
... )

可以看到指定檔案的詳細資訊:

-rwxrwxrwx 1 meebox meebox 635 Apr  5 16:22 test_env.py
CompletedProcess(args='ls -l test_env.py', returncode=0)

但如果改成字串串列:

>>> subprocess.run(
...     args=['ls', '-l', 'test_env.py'],
...     shell=True
... )

執行結果就不對了:

asyncio_openai.py  package-lock.json  test2.py         test_FAISS.py
my_faiss_db    __pycache__        test_copilot.py  test.py
package.json       spotify_play.py    test_env.py
CompletedProcess(args=['ls', '-l', 'test_env.py'], returncode=0)

>>>

你可以看到傳入整個命令行的字串可以正確運作,但是若拆成字串串列,命令解譯程式只會執行第一個元素,也就是 'ls',結果變成是顯示當前資料夾下的檔案,而不是顯示指定檔案的詳細資訊了。

以剛剛的測試程式為例,如果以下列方式執行:

python test_env.py none shell

就會看到程式停在直譯器的輸入提示:

--------------------
00:SHELL
...
30:TERM_PROGRAM
31:_
Python 3.13.2 (main, Mar 17 2025, 21:02:54) [Clang 20.1.0 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

不會結束,這是因為命令解譯程式只把串列中的第一個元素 'python' 當成命令行,所以就會進入 REPL 等待使用者輸入程式。在 Windows 上並不會發生同樣的問題。

把程式碼改成以下這樣:

import subprocess
import os, sys

print('-' * 20)
for i, name in enumerate(os.environ):
    print(f'{i:02d}:{name}')

if len(sys.argv) > 1:
    match sys.argv[1]:
        case 'empty':
            env = {}
        case 'none':
            env = None
        case _:
            k, v = sys.argv[1].split('=')
            env = {k: v}
    shell = len(sys.argv) > 2 and sys.argv[2] == 'shell'
    args = 'python test_env.py' if shell else ['python', 'test_env.py'] 
    subprocess.run(
        args=args,
        # args='which python3',
        env=env,
        shell=shell
    )

在 Linux 上就可以正確執行了:

--------------------
00:SHELL
...
31:_
--------------------
00:WARP_HONOR_PS1
...
31:WSLENV

env 會影響搜尋可執行檔

在 Windows 上,subprocess.run 底層是 CreateProcess,會以當前的環境設定找尋 args 中指定的可執行檔,但是在 Linux 上底層是 evecve,是以 env 所設定的環境搜尋可執行檔。以下是 Windows 的例子:

>>> subprocess.run(
...     args='python',
... )

可以正確看到 python REPL 的輸入提示:

Python 3.13.1 (main, Dec 19 2024, 14:38:48) [MSC v.1942 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 
CompletedProcess(args='python', returncode=0)

以上可以確認當前環境可以執行 python。接著傳遞空的環境變數:

>>> subprocess.run(
...     args='python',
...     env={}
... )

同樣可以看到 python REPL 的輸入提示:

Python 3.13.1 (main, Dec 19 2024, 14:38:48) [MSC v.1942 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 
CompletedProcess(args='python', returncode=0)

由於當前的環境沒有改變,仍然可以執行 python。但如果傳入 Trueshell

>>> subprocess.run(
...     args='python',
...     env={},
...     shell=True
... )

會看到錯誤訊息:

'python' 不是內部或外部命令、可執行的程式或批次檔。
CompletedProcess(args='python', returncode=1)

因為是先執行 cmd,這是環境已經變成 env 設定的空環境,所以沒有 PATH 的資訊,找不到 python,因此會看到 cmd 顯示的錯誤訊息。

換成 Linux 環境,首先確認可以執行 python:

>>> import subprocess
>>> subprocess.run(
...     'python',
... )

執行後會看到 python REPL 的輸入提示:

Python 3.13.2 (main, Mar 17 2025, 21:02:54) [Clang 20.1.0 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
CompletedProcess(args='python', returncode=0)

接著一樣傳遞空的環境給 env 參數:

>>> subprocess.run(
...     'python',
...     env={}
... )

執行會出現如下錯誤:

Traceback (most recent call last):
  File "", line 1, in 
  File "/home/meebox/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/subprocess.py", line 556, in run
    with Popen(*popenargs, **kwargs) as process:
         ~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "/home/meebox/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/subprocess.py", line 1038, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                        pass_fds, cwd, env,
                        ^^^^^^^^^^^^^^^^^^^
    ...<5 lines>...
                        gid, gids, uid, umask,
                        ^^^^^^^^^^^^^^^^^^^^^^
                        start_new_session, process_group)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/meebox/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/subprocess.py", line 1974, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'python'

這時候因為是依照 env 參數的設定尋找可執行檔,但是 env 是空的,所以也沒有路徑資訊可參考,因此就無法找到 python 執行檔了。

之前測試的範例,如果拿到 Linux 下,除了 env 使用預設的 None 以外,執行都會出錯,例如:

python test_env.py empty

執行結果如下:

--------------------
00:SHELL
...
30:TERM_PROGRAM
31:_
Traceback (most recent call last):
  File "/mnt/c/Users/meebo/code/python/test_py3.13/test_env.py", line 20, in 
    subprocess.run(
    ~~~~~~~~~~~~~~^
        args=args,
        ^^^^^^^^^^
    ...<2 lines>...
        shell=shell
        ^^^^^^^^^^^
    )
    ^
  File "/home/meebox/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/subprocess.py", line 556, in run
    with Popen(*popenargs, **kwargs) as process:
         ~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "/home/meebox/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/subprocess.py", line 1038, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                        pass_fds, cwd, env,
                        ^^^^^^^^^^^^^^^^^^^
    ...<5 lines>...
                        gid, gids, uid, umask,
                        ^^^^^^^^^^^^^^^^^^^^^^
                        start_new_session, process_group)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/meebox/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/subprocess.py", line 1974, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'python'

這就是因為設定的環境中並沒有 PATH,所以都無法找到 python 可執行檔執行。

因此,使用 subprocess.run 時,最好就是使用完整的路徑或是相對路徑傳入 args 參數,不然就可能會發生找不到可執行檔而無法正常執行的狀況。

結語

以上的展示應該可以清楚說明 envshell 的影響,之後使用時就不會錯亂了。