本文 AI 產出,尚未審核

Python 自動化與腳本應用:os.systemos.exec* 系列函式


簡介

在日常的自動化腳本、系統管理或是簡易的 CLI 工具開發中,我們常常需要 呼叫外部指令,例如 git statusffmpeg、或是自訂的 shell 腳本。Python 標準函式庫的 os 模組提供了兩大類工具:

  1. os.system – 直接在子殼層 (shell) 中執行一段文字指令,並回傳執行結果的狀態碼。
  2. os.exec* 系列 – 取代目前的 Python 進程,直接執行另一個程式,成功呼叫後 不會返回,相當於「換身」的概念。

掌握這兩類函式不僅能讓腳本更具彈性,也能在需要時提升效能或安全性。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步熟悉 os.systemos.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.execvpos.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.systemos.exec* (使用 *p 變體) 可能被惡意指令利用。 使用 列表形式 (execv, execvp) 或 subprocess.run(..., shell=False),避免字串解析。
返回值誤解 os.system 回傳的是 狀態碼,不是指令輸出。 若需要捕獲輸出,改用 subprocess.check_outputsubprocess.run(..., capture_output=True)
失去控制 exec* 成功後程式不再繼續,若忘記此行為會導致後續清理工作未執行。 在呼叫 exec* 前,先關閉檔案描述符釋放資源,或使用 try/finally 包住前置工作。
平台差異 Windows 沒有 forkexec* 行為仍可使用,但路徑分隔符與指令名稱會不同。 使用 os.nameplatform.system() 判斷平台,或採用跨平台的 subprocess
PATH 搜尋失敗 使用 execvp/execvpe 時,如果目標程式不在 PATH,會拋出 FileNotFoundError 在呼叫前先 shutil.which() 檢查可執行檔是否存在。

推薦的寫程式風格

  1. 盡量使用 subprocess:它提供更完整的錯誤處理與輸出捕獲功能。
  2. 只有在需要「換身」 時才使用 exec*,例如寫 init 程式容器入口腳本、或 系統服務
  3. 避免硬編碼路徑:使用 os.path.abspathpathlib.Path 取得絕對路徑,提升可搬移性。
  4. 資源釋放:在 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 自動化腳本在 效能、可維護性與安全性 上都更上一層樓。祝你在寫腳本的路上 玩得開心、寫得順利! 🚀