Python subprocess 模組使用教學
Last updated on Jul 26, 2024 in Python 程式設計 - 中階 by Amo Chen ‐ 5 min read
有時候開發功能不需要從頭寫到尾,利用他人開發的函式庫(library), API 之外,也可以整合既有的指令工具,不僅可以節省開發時間,也能夠完成需求。
我的碩士論文就是用 Python 作為膠水語言整合各種指令工具所完成的, Python 的 subprocess 模組在其中扮演不可獲缺的角色, subprocess 模組讓人可以執行各種指令,例如常見的 awk, sort, sed, uniq 等指令,並擷取其輸出(stdout / stderr),讓 Python 程式可以讀取,並進一步做其他處理。
本文將介紹 Python subprocess 模組的使用方法,以及應該注意的資安問題。
本文環境
- macOS
- Python 3
subprocess 模組簡介
subprocess 模組提供開發者在 Python 程式中執行外部指令的方法,例如用 subprocess 執行 ping 指令:
>>> import subprocess
>>> subprocess.run(['ping', '127.0.0.1'])
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.076 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.162 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.175 ms
有了 subprocess 模組的幫忙,就可以整合各種外部指令,以迅速完成想要的功能。
subprocess 模組使用方法
subprocess.run()
Python 官方最推薦的 subprocess 使用方法,就是呼叫 subprocess.run() , 該函式提供相當多的參數,不過都是選填(optional),基本上只需要將指令以空格做為分隔,變成 Python list 帶入 subprocess.run()
即可,例如 ls -alh
需要變成 ["ls", "-alh"]
帶入:
>>> import subprocess
>>> subprocess.run(['ls', '-alh'])
total 1248
drwxr-xr-x 14 usr staff 448B Jun 16 14:34 .
drwxr-xr-x 21 usr staff 672B Jun 16 10:09 ..
drwxr-xr-x 15 usr staff 480B Jun 16 14:33 .git
-rw-r--r-- 1 usr staff 19B Jan 6 14:22 .gitignore
CompletedProcess(args=['ls', '-alh'], returncode=0)
上述範例可以看到 subprocess.run()
會輸出執行結果,並回傳 1 個 CompletedProcess 實例(instance),該實例有 args, returncode, stdout, stderr 等屬性可以存取,可以視需求使用。
噢,如果不想自己手動轉換指令成 Python list, 可以用 shlex 模組 幫忙轉換:
>>> import shlex
>>> shlex.split('ls -alh')
['ls', '-alh']
capture_output / text / encoding 參數
預設外部指令的輸出(stdout / stderr)會直接列印出來,而且也不會存到 CompletedProcess 實例(instance)的 stdout 屬性中,例如:
>>> r = subprocess.run(['ls', '-alh'])
total 1248
drwxr-xr-x 14 usr staff 448B Jun 16 14:34 .
drwxr-xr-x 21 usr staff 672B Jun 16 10:09 ..
drwxr-xr-x 15 usr staff 480B Jun 16 14:33 .git
-rw-r--r-- 1 usr staff 19B Jan 6 14:22 .gitignore
>>> print(r.stdout)
None
如果需要處理指令的輸出,則需要將 capture_output
參數設定為 True
, 輸出就會改成存到 CompletedProcess 實例中:
>>> r = subprocess.run(['ls', '-alh'], capture_output=True)
>>> print(r.stdout)
b'...(略)...'
值得注意的是 stdout 屬性預設是存 bytes, 如果想轉為 string 可以設定 text
參數為 True
:
>>> r = subprocess.run(['ls', '-alh'], capture_output=True, text=True)
或者你的指令輸出是其他種編碼的文字,也可以指定 encoding
參數進行解碼(decode):
>>> r = subprocess.run(['ls', '-alh'], capture_output=True, encoding='utf-8')
check 參數
如果是希望在外部指令執行發生錯誤時,讓 Python 也跟著拋出例外(exception)的話,可以設定 check
參數為 True
, 該參數會讓 Python 檢查指令的回傳碼(returncode),只要不是 0, 就會拋出錯誤:
>>> r = subprocess.run(['/bin/sh', '-c', 'exit 1'], check=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/.../subprocess.py", line 516, in run
raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['/bin/sh', '-c', 'exit 1']' returned non-zero exit status 1.
上述範例可以看到 Python 拋出 subprocess.CalledProcessError
例外。
p.s. CompletedProcess 實例(instance)也有 check_returncode()
方法可以呼叫,作用跟 check 參數一樣。
cwd / env 參數
cwd 參數可以改變 subprocess.run()
的當前工作目錄(current working directory, cwd):
>>> subprocess.run(['pwd'], cwd='/Users')
/Users
CompletedProcess(args=['pwd'], returncode=0)
env 參數則是可以設定 subprocess.run()
執行時的環境變數,該參數接受字典型別的值,例如:
>>> subprocess.run(['env'], env={'MY_ENV': 'Hello'})
MY_ENV=Hello
CompletedProcess(args=['env'], returncode=0)
shell 參數
如果想讓 subprocess.run()
直接執行字串形式的指令的話,可以將 shell 參數設定為 True, 這代表指令會交給 shell 執行(預設為 /bin/sh
),如此一來就不再需要將指令換成 list 形式:
>>> subprocess.run('ls -alh', shell=True)
total 1248
drwxr-xr-x 14 usr staff 448B Jun 16 14:34 .
drwxr-xr-x 21 usr staff 672B Jun 16 10:09 ..
drwxr-xr-x 15 usr staff 480B Jun 16 14:33 .git
-rw-r--r-- 1 usr staff 19B Jan 6 14:22 .gitignore
CompletedProcess(args='ls -alh', returncode=0)
不過我們不推薦這種用法,這會導致一種稱為 shell injection 的漏洞產生,下一章節將詳細介紹 shell injection 。
input 參數
如果有些指令需要從 stdin 輸入資料的話,就需要使用 input 參數將資料帶入,例如 grep 指令可以從 stdin 帶入資料,並進行資料篩選:
>>> data = b"""
... a = 1;
... b = 2;
... c = 3;
... """
>>> subprocess.run(["grep", "b ="], input=data)
b = 2;
CompletedProcess(args=['grep', 'b = 2'], returncode=0)
input 預設只接受 bytes 作為輸入,因此上述的 data 是 bytes, 如果想接受字串的話,可以多帶個參數 text=True
:
>>> data = """
... a = 1;
... b = 2;
... c = 3;
... """
>>> subprocess.run(["grep", "b = 2"], input=data, text=True)
b = 2;
CompletedProcess(args=['grep', 'b = 2'], returncode=0)
組合多個指令
有時候,單單一個指令無法完成需求,經常需要用 Pipe 或稱管道 |
組合多個指令完成需求,例如 ls -l | grep .py
是 ls 指令列出所有檔案後,交由 grep 指令濾出含 .py
字串的檔案,這種組合多個指令的方式,就需要分別設定 input 與 stdout 2 個參數進行資料傳遞,也就是前 1 個指令的 stdout 是後 1 個指令的 input 。
以 ls -l | grep *.py
為例,可以寫成:
import subprocess
r1 = subprocess.run(
["ls", "-l"],
capture_output=True,
text=True,
)
r2 = subprocess.run(
["grep", ".py"],
input=r1.stdout,
capture_output=True,
text=True,
)
print(r2.stdout)
詳見如何用 Python 組合指令工具(Command Line Tools)。
subprocess.Popen()
subprocess.run() 的底層其實是 subprocess.Popen() ,如果你需要針對新產生的 process 做更細部的設定,例如 user, group, umask, pipe 的 buffer size 等等,就可以考慮直接使用 subprocess.Popen() , 不過一般來說用 subprocess.run() 就足夠。
Shell Injection 攻擊
當我們使用 subprocess 模組時,要特別注意 shell injection 的問題,例如以下程式,可以讓使用者自行輸入 IP 位址並執行 ping 指令:
import subprocess
s = input('ip = ')
subprocess.run(f'ping {s}', shell=True)
但是使用者只要輸入以下字串,就可以讓 ping 指令結束工作,並且把你的系統中的帳號列出來⋯⋯,這種手法就被稱為 shell injection 或稱 command injection:
-c 1 127.0.0.1; cat /etc/passwd
上述字串執行結果如下:
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.113 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.113/0.113/0.113/0.000 ms
##
# User Database
#
# Note that this file is consulted directly only when the system is running
# in single-user mode. At other times this information is provided by
# Open Directory.
#
# See the opendirectoryd(8) man page for additional information about
# Open Directory.
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
這就是為何使用 shell=True
時要注意資安問題,若設定 shell=False
則不會有問題,這是由於 shell=False
的情況下,指令的執行不是透過 shell, 因此 shell 相關的指令就不會被接受,而是視為其中 1 個參數或值處理,例如下列 Python 程式碼在遇到 shell injection 攻擊時完美地防禦:
import subprocess
s = input('ip = ')
subprocess.run(['ping', s])
上述程式碼執行結果:
ip = -c 1 127.0.0.1; cat /etc/passwd
ping: invalid count of packets to transmit: ` 1 127.0.0.1; cat /etc/passwd'
總結
subprocess 是 1 個強大的工具模組,可以幫助我們在 Python 中執行外部指令,提供更多元的實作彈性與應用。不過該模組仍然需要一些額外的認識,才能正確使用以避免資安問題。
希望本文能夠幫助大家更好地理解和使用 Python subprocess 模組。
Happy coding!
References
subprocess — Subprocess management — Python 3.11.4 documentation