python 呼叫外部指令

下面這篇文章寫的滿好的,我最後是使用下面的方法來開啟外部指令:

process = subprocess.Popen(args, executable=None, shell=False)

在 Ubuntu 下,以 GNOME calculator(叫出計算機) 做簡單的測試:

import subprocess
process = subprocess.Popen('gcalctool')

說明:在 path 上的執行檔是 ‘gcalctool’


如果你使用 subprocess.Popen 遇到回傳錯誤訊息:

no such file or directory

代表,使用方式錯誤。

You should pass the arguments as a list (recommended):

subprocess.Popen(["wc", "-l", "sorted_list.dat"], stdout=subprocess.PIPE)

Otherwise, you need to pass shell=True if you want to use the whole "wc -l sorted_list.dat" string as a command (not recommended, can be a security hazard).

subprocess.Popen("wc -l sorted_list.dat", shell=True, stdout=subprocess.PIPE)

整理一下 Popen 幾個跟 child process 互動的方法:

  • Popen.poll() – 檢查 child process 是否還在執行。注意這個方法並不會回傳 True/False,而是用 None 來表示程式還在執行,如果程式已經結束則會傳回 return code。
  • Popen.wait() – 等 child process 結束之後,才傳回 return code,並將控制權交回 Python。
  • Popen.communicate(input=None) – 主要用來取回 stdout 跟 stderr 的輸出。

詳全文:
https://imsardine.wordpress.com/tech/shell-scripting-in-python/

Shell Scripting in Python

All you can do with a shell script is make it worse. But since this is Python, you can make it better.

Neopythonic: Overheard
— Guido van Rossum

#!/usr/bin/env python 1 import subprocess, sys cmd = ‘ls -l’ retcode = subprocess.call(cmd, shell=True) 2 if retcode != 0: sys.exit(retcode)
1Python Shebang Line。
2subprocess.call() 的回傳值,直接就是 cmd 執行過後的 exit status

執行外部程式,或用 Python 來寫 Shell Scripts

Python 2.4 新增了 subprocess 這個 module,可以用來執行外部程式(也就是 spawn 一個 subprocess 或 child process),它的出現是為了取代舊有的 module 或 function,包括 os.system()、os.spawn*()、os.popen*()、popen2.* 與 commands.*。

最常用的就是 subprocess.call(),它可以帶參數執行外部程式,等外部程式結束之後再回傳 return code,可以用來判斷執行結果是成功還是失敗:

subprocess.call(args, executable=None, shell=False)

其中 args 可以傳入 sequence of strings 或單一個 string。注意 subprocess的某些行為在 Windows 與 Unix 下會有差異:

Unix

>>> subprocess.call(‘echolo World’) 1 Traceback (most recent call last): File “<stdin>”, line 1, in <module> File “/usr/lib/python2.6/subprocess.py”, line 470, in call return Popen(*popenargs, **kwargs).wait() File “/usr/lib/python2.6/subprocess.py”, line 623, in __init__ errread, errwrite) File “/usr/lib/python2.6/subprocess.py”, line 1141, in _execute_child raise child_exception OSError: [Errno 2] No such file or directory 2 >>> subprocess.call([‘echo’, ‘Hello World’]) 3 Hello World 4 0 5 >>> >>> subprocess.call(‘echolo World’, shell=True) 6 Hello World 0 >>> subprocess.call([‘echo’, ‘Hello World’], shell=True) 7 0 >>> subprocess.call([‘echo Hello World’, ‘shell arguments’], shell=True) Hello World 0
1shell=False 時,單一個 string 的 args 會被解讀成 executable(就這個例子而言,echo Hello World 這個執行檔並不存在);這個用法只適用於沒有 argument 的情況。
2要執行的程式不存在時,會丟出 OSError。
3shell=False 時,把 executable 跟 argument 拆開,就可以避開上述的問題。
4外部程式的輸出會直接寫到 standard output。
5這是外部程式的 return code,可用來判斷執行結果是成功還是失敗。
6shell=True 時,executable 預設是 /bin/sh(因此 args 本身或第一個 item 就不會被視為 executable),單一個 string 的 args 會被視為 command string 整個丟給 shell 執行。
7shell=True 時,只有第一個 item 會被視為 command string 丟給 shell 執行,但剩下的 item 則當做 shell 的 argument。就這個例子而言,echo 沒有接任何 argument(’Hello World’ 是給 shell 用的),因此 standard output 沒有輸出任何東西。
Important什麼時候要用 shell=True?又使用 shell=True 有什麼要注意的地方?   從下面的執行結果來看,使用 shell=False(預設) 或 shell=True 並沒有什麼差別: >>> subprocess.call([‘echo’, ‘Hello World’]) # shell=False Hello World 0 >>> >>> subprocess.call(‘echolo World’, shell=True) Hello World 0 但像 fg 這類 built-in command,就一定得透過 shell 來執行不可: >>> subprocess.call(‘fgraceback (most recent call last): File “<stdin>”, line 1, in <module> File “/usr/lib/python2.6/subprocess.py”, line 470, in call return Popen(*popenargs, **kwargs).wait() File “/usr/lib/python2.6/subprocess.py”, line 623, in __init__ errread, errwrite) File “/usr/lib/python2.6/subprocess.py”, line 1141, in _execute_child raise child_exception OSError: [Errno 2] No such file or directory >>> subprocess.call(‘fghell=True) fg: 1: No current job 2 上面提到 shell=True 時,args 本身或第一個 item 會被視為 command string 整個丟給 shell 執行。也就是說 args 的寫法要跟平常在 shell prompt 裡輸入指令時一樣,同樣要考慮 quoting 或 backslash escaping 的問題,command substitution、shell parameter expansion 等也都會作用… 例如: >>> subprocess.call([‘echo’, ‘`whoami`’]) `whoami` 1 0 >>> subprocess.call(‘echooami`’, shell=True) jeremy 0 >>> subprocess.call(‘echoGNAME’, shell=True) jeremy 0 1 顯然 shell=False 時,command substitution 並不會作用。 因此透過 shell=True 可以很快地將現有的 shell script 轉成 Python 的版本。不過如果不需要 shell 提供 substitution 或 expansion 的支援的話,採用 shell=False 反而可以省去 quoting 或 backslash escaping 的麻煩。例如: >>> subprocess.call(“echofile”, shell=True) /bin/sh: Syntax error: Unterminated quoted string 2 >>> subprocess.call(“echom file\””, shell=True) I’m file 0 >>> subprocess.call([‘echo’, “I’m file”]) I’m file 0

Windows

>>> subprocess.call(‘echolo World’) 1 Hello World 0 >>> subprocess.call(‘echolo World’, shell=True) 2 Hello World 0 >>> subprocess.call([‘echo’, ‘Hello World’]) Hello World 0 >>> subprocess.call([‘echo’, ‘Hello World’], shell=True) 3 “Hello World” 4 0 >>> subprocess.call(‘echoMSPEC%’) %COMSPEC% 0 >>> subprocess.call(‘echoMSPEC%’, shell=True) 5 C:\Windows\system32\cmd.exe 0 >>> subprocess.call([‘echo’, ‘%COMSPEC%’], shell=True) 6 C:\Windows\system32\cmd.exe 0 >>> subprocess.call(r’c:\Programes\Java\jre7\bin\java.exe -version’) 7 java version “1.7.0” Java(TM) SE Runtime Environment (build 1.7.0-b147) Java HotSpot(TM) 64-Bit Server VM (build 21.0-b17, mixed mode) 0 >>> subprocess.call(r’c:\Programes\Java\jre7\bin\java.exe -version’, shell=True) ‘c:\Program’ is not recognized as an internal or external command, operable program or batch file. 1 >>> subprocess.call(r'”c:\Programva\jre7\bin\java.exe” -version’, shell=True) 8 java version “1.7.0” Java(TM) SE Runtime Environment (build 1.7.0-b147) Java HotSpot(TM) 64-Bit Server VM (build 21.0-b17, mixed mode) 0 >>> subprocess.call(‘del9 Traceback (most recent call last): File “<stdin>”, line 1, in <module> File “C:\Python27\lib\subprocess.py”, line 493, in call return Popen(*popenargs, **kwargs).wait() File “C:\Python27\lib\subprocess.py”, line 679, in __init__ errread, errwrite) File “C:\Python27\lib\subprocess.py”, line 893, in _execute_child startupinfo) WindowsError: [Error 2] The system cannot find the file specified >>> subprocess.call(‘delhell=True) The syntax of the command is incorrect. 1
1shell=False 時,並不會像在 Unix 一樣,將單一個 string 的 args 解讀成 executable。
2shell=True 時,executable 預設是環境變數 COMSPEC 的內容,例如 “C:\Windows\system32\cmd.exe”。
3看起來 args 的型式(單一個 string 或 sequence of strings)或 shell 的值,都對執行結果沒有影響?
4為什麼 Hello World 兩側多了雙引號?
5shell=True 時,environment variable 才會被展開(不論 args 的型式為何)。
6簡單歸納一下,subprocess.call() 的行為並不會受 args 的型式影響,但 shell=True 與 shell=False 則有差異。
7讓人比較不解的是,為什麼這個例子可以執行,反倒是加了 shell=True 之後才出狀況?
8像平常在 DOS prompt 輸入的一樣,在含有空白字元的路徑兩側加上雙引號即可。
9跟在 Unix 一樣,像 del、dir 這類 built-in command,還是得透過 shell 來執行才行。

從上面的測試結果來看,subprocess.call() 的行為在 Unix 與 Windows 下存在著一些差異。不過下面兩種用法在不同 OS 下的行為是比較一致的:

  • subprocess.call(command_string, shell=True) – 其中 command_string 的寫法要跟平常在 shell prompt 裡輸入指令時一樣,同樣要考慮 quoting 或 backslash escaping 的問題。
  • subprocess.call(sequence_of_strings, shell=False) – 將個別的的 argument 拆開,不用考慮 quoting 或 backslash escaping 等問題。
Tip除了 subprocess.call() 之外,另外還有 check_call() 跟 check_output()。   除了 check_output 會回傳 output 之外,用法相同跟 call() 一樣。只不過會自動檢查 return code,如果是非零值的話,就會直接丟出 CalledProcessError,這個時候 return code 還是可以透過 returncode 這個 attribute 來取回。例如:(但是要執行的程式不存在時,還是會丟出 OSError) >>> subprocess.call(‘rmm: missing operand Try `rm –help’ for more information. 1 >>> try: … subprocess.check_call(‘rm’) … except Exception as e: … print ‘error code: %i’ % e.returncode … rm: missing operand 1 Try `rm –help’ for more information. error code: 1 >>> output = subprocess.check_output(‘uname -a’, shell=True) >>> type(output), output (<type ‘str’>, ‘Linux jeremy-desktop 2.6.38-13-generic #52-Ubuntu SMP Tue Nov 8 16:48:07 UTC 2011 i686 i686 i386 GNU/Linux\n’) 1 發生錯誤時,外部程式的輸出還是會直接寫到 standard output。 2 傳回的 byte string 包含外部程式的輸出。 用 Python 來寫 shell script 時,check_call() 跟 check_output() 是簡化 error handling 的好工具(類似 shell script 裡 set -o errexit 的效果),省去要一直檢查 return code 的麻煩。不過要注意 subprocess.check_output() 是 Python 2.7 才有的功能。

非同步(asynchronously)執行外部程式

上面的 subprocess.call() 雖然好用,不過控制權必須等外部程式結束之後才會回到 Python 程式。如果要非同步執行外部程式,或想在執行期與 child process 溝通,就必須直接使用 subprocess.call() 底層的 subprocess.Pope 才行。

事實上,subprocess.call() 的參數與 Popen 的 constructor 完全一樣,只是 subprocess.call() 內部在取得 Popen 的 instance 之後(可以將它視為與 child process 溝通的介面),會呼叫 Popen.wait() 等待外部程式執行結束,然後傳回 return code 而已。

process = subprocess.Popen(args, executable=None, shell=False)

在 Ubuntu 下,以 GNOME calculator 做簡單的測試:

>>> import subprocess >>> process = subprocess.Popen(‘gcalctool1 >>> process.wait() 2 0 >>> process = subprocess.Popen(‘gcalctool>> process.poll() 3 >>> process.poll() 4 0
1叫出計算機。
2程式會卡在這裡,直到手動將計算機關掉為止,才會有 return code 傳回來。
3重新開啟計算機,呼叫 process.poll() 傳回 None,表示還在執行。
4將計算機關掉後,再呼叫 process.poll() 一次,傳回 return code。

整理一下 Popen 幾個跟 child process 互動的方法:

  • Popen.poll() – 檢查 child process 是否還在執行。注意這個方法並不會回傳 True/False,而是用 None 來表示程式還在執行,如果程式已經結束則會傳回 return code。
  • Popen.wait() – 等 child process 結束之後,才傳回 return code,並將控制權交回 Python。
  • Popen.communicate(input=None) – 主要用來取回 stdout 跟 stderr 的輸出。
>>> import subprocess >>> from subprocess import PIPE >>> process = subprocess.Popen(‘uname, shell=True) >>> Linux jeremy-laptop 2.6.35-30-generic #61-Ubuntu SMP Tue Oct 11 17:52:57 UTC 2011 x86_64 GNU/Linux 1 >>> process.communicate() (None, None) >>> process = subprocess.Popen(‘uname, shell=True, stdout=PIPE, stderr=PIPE) >>> process.communicate() (‘Linux jeremy-laptop 2.6.35-30-generic #61-Ubuntu SMP Tue Oct 11 17:52:57 UTC 2011 x86_64 GNU/Linux\n’, ”) >>> process.returncode 0
1必須搭配 stdout=PIPE 跟 stderr=PIPE 使用(轉向到 Python 內部),否則會直接輸出到 stdout 或 stderr。
Note參考資料   asynchronous – How can I run an external command asynchronously from Python? – Stack Overflow (2009-03-11) PEP 3145 — Asynchronous I/O For subprocess.Popen

常見問題

.msi 不是可執行檔?

.msi 不能直接用 subprocess 執行:

>>> import subprocess >>> subprocess.Popen(r’C:\tmp\installer.msiraceback (most recent call last): File “<stdin>”, line 1, in <module> File “C:\Python27\lib\subprocess.py”, line 679, in __init__ errread, errwrite) File “C:\Python27\lib\subprocess.py”, line 893, in _execute_child startupinfo) WindowsError: [Error 193] %1 is not a valid Win32 application

問題就出在 .msi 本身不是一個可執行檔,必須要透過 msiexec 執行:

msiexec /i installer.msi
>>> import subprocess >>> subprocess.Popen([‘msiexec’, ‘/i’, r’C:\tmp\installer.msi’])

檔案持續被 child process 鎖住

>>> import subprocess, sys >>> f = open(r’c:\tmp\test.txt’, ‘w’) >>> subprocess.Popen(‘notepad.exesubprocess.Popen object at 0x00AAC790> >>> sys.exit(0)

Python 程式結束時,c:\tmp\test.txt 繼續被 notepad.exe 鎖住,等 notepad.exe 關掉之後,這個 lock 才會解除。

Shell_Scripting_in_Python.files/subprocess_lock.png

要避免這個問題,可以搭配 close_fds=True 來使用。

subprocess.Popen('notepad.exelose_fds=True)

關於 close_fds,官方文件是這麼說的:

on Windows, if close_fds is true then no handles will be inherited by the child process. Note that on Windows, you cannot set close_fds to true and also redirect the standard handles by setting stdin, stdout or stderr.

雖然看不太懂它在寫什麼,但大概是單純執行外部程式,而不把它當做是一個 child process 吧?


參考資料


相關文章:

python 呼叫外部指令
https://stackoverflow.max-everyday.com/2017/09/python-subprocess/

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *