LoginSignup
13
12

More than 1 year has passed since last update.

Python3 Subprocess.Popen()とcommunicate()について

Last updated at Posted at 2021-06-02

1. はじめに

今回はsubprocess.Popen()subprocess.Popen().communicate()の基本的な動作について記載する.

2. 動作環境

  • macOS Big Sur 11.4
  • Python 3.7.9

3. ソースコードと動作結果

3.1. コマンドの実行

ファイルのコピーコマンドを実行する.

test.py
import subprocess

src = "/Users/ユーザ名/Desktop/a/test.txt"
dst = "/Users/ユーザ名/Desktop/b"

cmd = ["cp", src, dst]
subprocess.Popen(cmd)

または

test.py
import subprocess

src = "/Users/ユーザ名/Desktop/a/test.txt"
dst = "/Users/ユーザ名/Desktop/b"

cmd = "cp " + src + " " + dst
subprocess.Popen(cmd, shell=True)

後者の場合,ターミナルに入力するコマンド文字列がそのまま入るが,コマンドやオプション間に入れる半角スペースを忘れがちなので注意する.

また,shell=True とすることでデメリットがある.
スペースやクォートなどの特殊文字(ターミナルでエスケープ文字が必要な文字)は,ターミナルで入力するときと同様にエスケープ処理する必要がある.

ちなみに,Windowsの場合は shell=Trueにしていないとエラーになってしまうため,常にこれを設定する必要がある(前者の方法は使えない).
Windowsでの場合は shell=True を設定してかつコマンドをリストまたは文字列で渡せば実行できるようだが,macの場合は shell=True を設定してかつコマンドをリストで渡すとエラーが起こることもあることを確認した(cpコマンド).

下記はそのときのエラー.

test.py
import subprocess

src = "/Users/ユーザ名/Desktop/a/test.txt"
dst = "/Users/ユーザ名/Desktop/b/"

cmd = ["cp", src, dst]
subprocess.Popen(cmd, shell=True)


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 "/Users/ユーザ名/Desktop/test.py"
usage: cp [-R [-H | -L | -P]] [-fi | -n] [-apvXc] source_file target_file
       cp [-R [-H | -L | -P]] [-fi | -n] [-apvXc] source_file ... target_directory

"""

3.2. コマンドの出力の取得

フォルダの中身表示コマンドを実行する.

test.py
import subprocess

dir = "/Users/ユーザ名"

cmd = ["ls", dir]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
result = proc.communicate()


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 "/Users/ユーザ名/Desktop/test.py"
(b'Applications\nDesktop\nDocuments\nDownloads\nLibrary\nMovies\nMusic\nPictures\nPublic\n', None)

"""

stdout=subprocess.PIPE を設定することで,communicate() を通じて出力の取得が可能.

communicate() の戻り値は2つ.
1番目に出力,2番目に None
1番目の出力がバイナリのため,デコードする.
2番目は本来はエラーが返ってくるが,エラーにパイプを通していないので None が返ってくる.

test.py
import subprocess

dir = "/Users/ユーザ名"

cmd = ["ls", dir]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
result = proc.communicate()[0].decode("utf-8")

print(result)


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 "/Users/ユーザ名/Desktop/test.py"
Applications
Desktop
Documents
Downloads
Library
Movies
Music
Pictures
Public

"""

ちなみに,stdoutを設定していない場合,なぜかコマンドの結果(正常終了の出力)がターミナルに表示されてしまう(環境にもよるかもしれないが,VSCodeでは表示された).
邪魔なので,stdoutは常に設定した方がいいかもしれない.

3.3. コマンド結果がエラーだった場合の,エラーの取得

ls のスペルをわざと lsa と間違えてコマンドを実行する.
lsa コマンドは無いので,command not found といった内容の出力を期待値とする.

test.py

import subprocess

dir = "/Users/ユーザ名"

cmd = ["lsa", dir]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = proc.communicate()

print(result)


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 "/Users/ユーザ名/Desktop/test.py"
Traceback (most recent call last):
  File "/Users/ユーザ名/Desktop/test.py", line 7, in <module>
    subprocess.Popen(cmd)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 800, in __init__
    restore_signals, start_new_session)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 1551, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'cpa': 'cpa'

"""

出力の取得と同様,stderr=subprocess.PIPE を設定することで,communicate() を通じてエラーの出力が可能となるはず・・・.

コマンドのファイルが無いという FileNotFoundError が発生した.
ファイルとして指定しているようなので,shell オプションでコマンドとして指定する.

test.py
import subprocess

dir = "/Users/ユーザ名"

cmd = "lsa " + dir
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
result = proc.communicate()

print(result)


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 "/Users/ユーザ名/Desktop/test.py"
(b'', b'/bin/sh: lsa: command not found\n')

"""

communicate() の戻り値について,
1番目に b"",2番目にエラー.
2番目の出力がバイナリのため,デコードする.

test.py
import subprocess

dir = "/Users/ユーザ名"

cmd = "lsa " + dir
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
result = proc.communicate()[1].decode("utf-8")

print(result)


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 "/Users/ユーザ名/Desktop/test.py"
/bin/sh: lsa: command not found


"""

ちなみに,stderr=subprocess.PIPE を設定した状態で,正常にコマンド実行すると次のようになる.

test.py
import subprocess

dir = "/Users/ユーザ名"

cmd = "ls " + dir
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
result = proc.communicate()

print(result)


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 "/Users/ユーザ名/Desktop/test.py"
(b'Applications\nDesktop\nDocuments\nDownloads\nLibrary\nMovies\nMusic\nPictures\nPublic\n', b'')

"""

communicate() の戻り値について,
1番目に 出力,2番目に b""
stderrを指定したことで,Noneではなくバイナリ文字列が返ってくる.

ちなみに,stderrを設定していない場合,なぜかコマンドの結果(エラー)がターミナルに表示されてしまう(環境にもよるかもしれないが,VSCodeでは表示された).
邪魔なので,stderrは常に設定した方がいいかもしれない.

3.4. コマンドのエラーを出力側から取得

3.3 と同様,ls のスペルをわざと lsa と間違えてコマンドを実行する.
lsa コマンドは無いので,command not found といった内容の出力を期待値とする.
ただし,out, err = communicate()out にエラーを入れる.

test.py
import subprocess

dir = "/Users/ユーザ名"

cmd = "lsa " + dir
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
result = proc.communicate()

print(result)


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 /Users/ユーザ名/Desktop/test.py
(b'/bin/sh: lsa: command not found\n', None)

"""

stderr=subprocess.STDOUT を指定したことで,
1番目にエラー,2番目に None が返ってくる.

3.5. コマンドの結果が出力かエラーかを判断するフラグ

subprocess.Popen() の戻り値からコマンドが正常に終了したかどうかを示すフラグがある.
それが returncode

test.py
import subprocess

cmd = "ls"

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
print(proc.returncode)


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 /Users/ユーザ名/Desktop/test.py
None

"""

subprocess --- サブプロセス管理 Popen.returncode

None はまだその子プロセスが終了していないことを示します。

終わるまで待てば良い?
subprocess.Popen().wait() というメソッドがあった.これには timeoutのオプションもあった.
しかし,communicate() にも timeout のオプションがあった.
stdoutPIPE を通している(出力を取得することを想定している)ので,今回は communicate()timeout で待つ.
(ちなみに,待つだけなら subprocess.Popen().wait() 使っても同じ.wait() の戻り値は subprocess.Popen().returncode と同じ.)

test.py
import subprocess

cmd = "ls"

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)

try:
    print(proc.returncode)
    result = proc.communicate(timeout=5)
    print(proc.returncode)
# タイムアウトが発生したら kill() によって終了する
except subprocess.TimeoutExpired:
    proc.kill()


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 /Users/ユーザ名/Desktop/test.py
None
0

"""

subprocess --- サブプロセス管理 Popen.returncode

バージョン 3.8 で変更: Popen can use os.posix_spawn() in some cases for better performance. On Windows Subsystem for Linux and QEMU User Emulation, Popen constructor using os.posix_spawn() no longer raise an exception on errors like missing program, but the child process fails with a non-zero returncode.

Google翻訳

バージョン 3.8 で変更: Popen は、パフォーマンスを向上させるために、場合によっては os.posix_spawn() を使用できます。 Linux 用 Windows サブシステムおよび QEMU ユーザー エミュレーションでは、os.posix_spawn() を使用する Popen コンストラクターは、プログラムが見つからないなどのエラーで例外を発生させなくなりましたが、子プロセスはゼロ以外の戻りコードで失敗します。

「0以外の戻り値で失敗」とあるので、0で正常終了と考えて良い.
communicate(timeout=5) 直後の returncode0 と表示されたので,正常終了したといえるだろう.

ちなみに,lslsa とした(存在しないコマンドを実行した)ときの returncode127 だった.

また,存在するコマンドでエラーを発生させた場合は次のようになった.

test.py
import subprocess

src = "/Users/ユーザ名/Desktop/a/test.txt"
dst = "/Users/ユーザ名/Desktop/b/"    # 存在しないフォルダを指定

# フォルダ名 b の直後にスラッシュを入れないと b というファイルとしてコピーされてしまう

cmd = "cp " + src + " " + dst
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)

try:
    print(proc.returncode)
    result = proc.communicate(timeout=5)
    print(proc.returncode)
# タイムアウトが発生したら kill() によって終了する
except subprocess.TimeoutExpired:
    proc.kill()


""" 動作結果

コンピュータ名:~ ユーザ名$ /usr/local/bin/python3 /Users/ユーザ名/Desktop/test.py
None
cp: directory /Users/ユーザ名/Desktop/b does not exist
1

"""

4. おわりに

深くは記載しない.

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12