本文 AI 產出,尚未審核
Python 自動化與腳本應用:os.system 與 os.exec* 系列函式
簡介
在日常的自動化腳本、系統管理或是簡易的 CLI 工具開發中,我們常常需要 呼叫外部指令,例如 git status、ffmpeg、或是自訂的 shell 腳本。Python 標準函式庫的 os 模組提供了兩大類工具:
os.system– 直接在子殼層 (shell) 中執行一段文字指令,並回傳執行結果的狀態碼。os.exec*系列 – 取代目前的 Python 進程,直接執行另一個程式,成功呼叫後 不會返回,相當於「換身」的概念。
掌握這兩類函式不僅能讓腳本更具彈性,也能在需要時提升效能或安全性。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步熟悉 os.system 與 os.exec* 的使用方式。
核心概念
1. os.system:最簡單的外部指令呼叫
| 特點 | 說明 |
|---|---|
| 回傳值 | 只回傳 整數型別的狀態碼 (0 代表成功)。 |
| 執行環境 | 由系統預設的 shell (/bin/sh on Unix, cmd.exe on Windows) 執行。 |
| 同步 | 呼叫後會 阻塞,直到指令結束才繼續執行 Python 程式。 |
| 安全性 | 直接拼接字串可能導致 shell injection,需謹慎使用。 |
範例 1:列出目錄內容
import os
# 執行 `ls -l` (Linux/macOS) 或 `dir` (Windows) 並印出結果
status = os.system('ls -l')
print(f'指令結束,返回狀態碼: {status}')
註解:
os.system只會把指令的輸出直接寫到終端機,若想捕獲輸出需要使用subprocess。
範例 2:呼叫外部腳本並傳遞參數
import os
script_path = '/path/to/backup.sh'
backup_dir = '/mnt/backup'
# 注意使用字串插值時要避免未經處理的使用者輸入
cmd = f'bash {script_path} "{backup_dir}"'
os.system(cmd)
範例 3:在 Windows 執行 ping 並取得返回值
import os, platform
if platform.system() == 'Windows':
ret = os.system('ping -n 1 8.8.8.8')
print('Ping 結果狀態碼:', ret)
2. os.exec* 系列:取代當前進程
os.exec* 系列共 八 種變體,主要差異在於 參數傳遞方式、是否使用 shell、以及 環境變數 的處理方式。以下列出最常用的幾個:
| 函式 | 參數形式 | 是否使用 shell | 是否可自訂環境 |
|---|---|---|---|
os.execv(path, args) |
args 為 list |
否 | 否 |
os.execve(path, args, env) |
args 為 list |
否 | 可自訂 |
os.execvp(file, args) |
args 為 list |
否,會在 PATH 中搜尋 |
否 |
os.execvpe(file, args, env) |
args 為 list |
否,會在 PATH 中搜尋 |
可自訂 |
os.execl(path, arg0, arg1, ...) |
位置參數 | 否 | 否 |
os.execlp(file, arg0, arg1, ...) |
位置參數,會在 PATH 中搜尋 |
否 | 否 |
os.execle(path, arg0, arg1, ..., env) |
位置參數 + env | 否 | 可自訂 |
os.execvp、os.execvpe 為最常見的「搜尋 PATH」版。 |
為何使用 exec*?
- 效能:不必產生子進程,直接以新程式取代當前 Python 直譯器。
- 資源:不會留下多餘的子進程或檔案描述符。
- 安全性:避免 shell 解析,減少 injection 風險。
注意:成功呼叫
exec*後,Python 程式的後續程式碼不會再執行,因為進程已被新程式取代。
範例 4:使用 os.execv 執行外部程式
import os
# 假設有一個可執行檔 `my_tool`,需要傳入兩個參數
program = '/usr/local/bin/my_tool'
args = [program, '--input', 'data.txt', '--verbose']
# 執行後 Python 程式會被取代,以下的 print 不會被執行
os.execv(program, args)
print('這行不會被印出')
範例 5:使用 os.execvp 讓系統自行搜尋 PATH
import os
# 直接呼叫系統已安裝的 `python3`,等同於在終端執行 `python3 -V`
os.execvp('python3', ['python3', '-V'])
# 若找不到 python3,會拋出 FileNotFoundError
範例 6:自訂環境變數後執行指令 (os.execve)
import os
program = '/usr/bin/env'
args = [program, 'python3', '-c', 'import os, sys; print(os.getenv("GREETING"))']
# 自訂環境變數
new_env = os.environ.copy()
new_env['GREETING'] = '哈囉,世界!'
os.execve(program, args, new_env)
範例 7:使用 os.execle 以位置參數方式傳遞環境
import os
program = '/usr/bin/printenv'
# 只想印出自訂的變數
os.execle(program, program, 'MY_VAR', env={'MY_VAR': 'Python Rocks!'})
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| Shell Injection | 直接把使用者輸入拼接到 os.system 或 os.exec* (使用 *p 變體) 可能被惡意指令利用。 |
使用 列表形式 (execv, execvp) 或 subprocess.run(..., shell=False),避免字串解析。 |
| 返回值誤解 | os.system 回傳的是 狀態碼,不是指令輸出。 |
若需要捕獲輸出,改用 subprocess.check_output 或 subprocess.run(..., capture_output=True)。 |
| 失去控制 | exec* 成功後程式不再繼續,若忘記此行為會導致後續清理工作未執行。 |
在呼叫 exec* 前,先關閉檔案描述符、釋放資源,或使用 try/finally 包住前置工作。 |
| 平台差異 | Windows 沒有 fork,exec* 行為仍可使用,但路徑分隔符與指令名稱會不同。 |
使用 os.name、platform.system() 判斷平台,或採用跨平台的 subprocess。 |
| PATH 搜尋失敗 | 使用 execvp/execvpe 時,如果目標程式不在 PATH,會拋出 FileNotFoundError。 |
在呼叫前先 shutil.which() 檢查可執行檔是否存在。 |
推薦的寫程式風格
- 盡量使用
subprocess:它提供更完整的錯誤處理與輸出捕獲功能。 - 只有在需要「換身」 時才使用
exec*,例如寫 init 程式、容器入口腳本、或 系統服務。 - 避免硬編碼路徑:使用
os.path.abspath、pathlib.Path取得絕對路徑,提升可搬移性。 - 資源釋放:在
exec*前close()所有開啟的檔案或網路連線,防止資源泄漏。
實際應用場景
1. 容器入口腳本(Docker)
在 Dockerfile 中常見的 ENTRYPOINT ["python", "app.py"] 其實就是在容器啟動時執行 exec,取代容器的 PID 1。若自行編寫入口腳本,可使用:
#!/usr/bin/env python3
import os, sys
# 前置檢查 (例如檢查環境變數)
if not os.getenv('CONFIG_PATH'):
sys.exit('缺少 CONFIG_PATH 環境變數')
# 透過 exec 直接啟動真正的服務程式
os.execv('/usr/local/bin/my_service', ['/usr/local/bin/my_service', '--config', os.getenv('CONFIG_PATH')])
2. 系統管理工具
寫一個 自動備份 的 CLI,先檢查磁碟空間,若足夠則直接 exec rsync,避免產生多餘的 Python 子程序:
import os, shutil, sys
def enough_space(dest, needed_gb):
total, used, free = shutil.disk_usage(dest)
return free >= needed_gb * 1024**3
backup_src = '/home/user/data'
backup_dst = '/mnt/backup'
if not enough_space(backup_dst, 10):
sys.exit('備份磁碟空間不足')
# 用 exec 直接跑 rsync,讓 rsync 成為唯一執行中的程式
os.execvp('rsync', ['rsync', '-avz', backup_src, backup_dst])
3. 測試框架的「重啟」功能
有時候測試腳本需要在特定條件下 重新啟動自己(例如載入新的環境變數),可利用 os.execv:
import os, sys
def need_restart():
return '--reload' not in sys.argv
if need_restart():
print('即將重新啟動腳本...')
os.execv(sys.executable, [sys.executable] + sys.argv + ['--reload'])
4. 交叉平台的 CLI 包裝器
使用 os.execvp 可以讓同一段程式碼在 Linux 與 Windows 上都呼叫本機已安裝的工具:
import os, platform
def run_git_status():
if platform.system() == 'Windows':
cmd = 'git.exe'
else:
cmd = 'git'
os.execvp(cmd, [cmd, 'status'])
run_git_status()
總結
os.system是 最簡單 的外部指令執行方式,適合一次性、快速的測試或不需要捕獲輸出的情境。os.exec*系列則提供 取代當前進程 的能力,常用於容器入口腳本、系統服務或需要「換身」的情境。- 使用時必須注意 安全性(避免 shell injection)、平台差異、以及 資源釋放 的問題。
- 在大多數日常腳本中,
subprocess仍是更彈性且安全的選擇;而exec*只在特定需求(如效能、PID 1 需求)時才是最佳方案。
掌握這兩種工具的特性與限制,能讓你的 Python 自動化腳本在 效能、可維護性與安全性 上都更上一層樓。祝你在寫腳本的路上 玩得開心、寫得順利! 🚀