DEV Community

codemee
codemee

Posted on

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 ) 
Enter fullscreen mode Exit fullscreen mode

如果以如下引數執行:

python test_env.py none 
Enter fullscreen mode Exit fullscreen mode

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

-------------------- 00:AKASH_API_KEY ... 80:ZES_ENABLE_SYSMAN -------------------- 00:AKASH_API_KEY ... 80:ZES_ENABLE_SYSMAN 
Enter fullscreen mode Exit fullscreen mode

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

python test_env.py empty 
Enter fullscreen mode Exit fullscreen mode

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

-------------------- 00:AKASH_API_KEY ... 80:ZES_ENABLE_SYSMAN -------------------- 
Enter fullscreen mode Exit fullscreen mode

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

python test_env.py TEST=hello 
Enter fullscreen mode Exit fullscreen mode

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

80:ZES_ENABLE_SYSMAN -------------------- 00:AKASH_API_KEY ... -------------------- 00:TEST 
Enter fullscreen mode Exit fullscreen mode

shell 參數的影響

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

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

python test_env.py none shell 
Enter fullscreen mode Exit fullscreen mode

在 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 
Enter fullscreen mode Exit fullscreen mode

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

python test_env.py empty shell 
Enter fullscreen mode Exit fullscreen mode

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

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

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

python test_env.py TEST=hello shell 
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

就可以正常執行了:

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

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

不同平台的差異

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

shell=True 時 args 的差異

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

>>> subprocess.run( ... args='dir test_env.py', ... shell=True ... ) 
Enter fullscreen mode Exit fullscreen mode

確認結果正確:

 磁碟區 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) 
Enter fullscreen mode Exit fullscreen mode

改成傳入字串串列:

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

一樣可以正常運作:

 磁碟區 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) >>> 
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

但如果改成字串串列:

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

執行結果就不對了:

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) >>> 
Enter fullscreen mode Exit fullscreen mode

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

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

python test_env.py none shell 
Enter fullscreen mode Exit fullscreen mode

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

-------------------- 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. >>> 
Enter fullscreen mode Exit fullscreen mode

不會結束,這是因為命令解譯程式只把串列中的第一個元素 '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 ) 
Enter fullscreen mode Exit fullscreen mode

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

-------------------- 00:SHELL ... 31:_ -------------------- 00:WARP_HONOR_PS1 ... 31:WSLENV 
Enter fullscreen mode Exit fullscreen mode

env 會影響搜尋可執行檔

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

>>> subprocess.run( ... args='python', ... ) 
Enter fullscreen mode Exit fullscreen mode

可以正確看到 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) 
Enter fullscreen mode Exit fullscreen mode

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

>>> subprocess.run( ... args='python', ... env={} ... ) 
Enter fullscreen mode Exit fullscreen mode

同樣可以看到 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) 
Enter fullscreen mode Exit fullscreen mode

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

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

會看到錯誤訊息:

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

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

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

>>> import subprocess >>> subprocess.run( ... 'python', ... ) 
Enter fullscreen mode Exit fullscreen mode

執行後會看到 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) 
Enter fullscreen mode Exit fullscreen mode

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

>>> subprocess.run( ... 'python', ... env={} ... ) 
Enter fullscreen mode Exit fullscreen mode

執行會出現如下錯誤:

Traceback (most recent call last): File "<stdin>", line 1, in <module> 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' 
Enter fullscreen mode Exit fullscreen mode

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

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

python test_env.py empty 
Enter fullscreen mode Exit fullscreen mode

執行結果如下:

-------------------- 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 <module> 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' 
Enter fullscreen mode Exit fullscreen mode

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

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

結語

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

Top comments (0)