pythonからterminalを操作する (subprocess)

subprocessを使ってpythonからterminalを操作する方法を調べたのでまとめます。

# subprocessとは
pythonからterminalを操作する為のpackageです。terminalが操作できるという事はシェルスクリプトでやるような事が一通り出来るわけです。便利すぎてヤバイです。

長年開発されてきているのでcall, check_call, check_outputなど色んなモジュールがあるようですが、普通に実行するときはsubprocess.run()を使い、非同期で実行するときはsubprocess.Popen()を使うよう推奨しているようです。Popenは並列処理用のモジュールなので、子プロセスの終了を待ったりkillしたり出来るようになっています。

公式ドキュメント
https://docs.python.org/ja/3.6/library/subprocess.html#using-the-subprocess-module

今回はすぐ終わる処理しかやらないのでsubprocess.run()を使います。

# 試行環境
Windows 10
python 3.6

Ubuntu 18.04
python 3.6

# terminalコマンドを実行する

```terminal:Linux_terminal
ls
```

```terminal:Windows_コマンドプロンプト
dir
```
上記のようなコマンドはsubprocess.run()で以下のように実行できます。

```python:Linuxの場合
import subprocess
subprocess.run('ls')
```

```python:Windowsの場合
import subprocess
subprocess.run('dir', shell=True)
```
terminalに表示が出るコマンドなら、表示を文字列の返り値として受け取れます

```python:Linuxの場合
import subprocess
result = subprocess.run('ls')
print(result)
```

```python:Windowsの場合
import subprocess
result = subprocess.run('dir', shell=True)
print(result)
```

# terminalコマンドに引数を与えて実行する
引数を与える場合、terminalではスペース区切りでしたがsubprocessではlistにして渡します。

```python:python
result = subprocess.run(['ls', '-l'])
print(result)
```
複数引数がある場合はlistが長くなっていくだけです。

```python:python
result = subprocess.run(['ls', '-l', '-a'])
print(result)
```
ちなみに1個だけにして実行することも出来ます。引数があったりなかったりするときはこちらの書き方の方がやりやすそう。

```python:python
result = subprocess.run(['ls'])
print(result)
```

# シェルスクリプト(バッチファイル)を実行する
terminalコマンドはなんでも実行できるので、当然シェルスクリプトも実行できますし引数も渡せます。以下では引数２つで実行してます。

```python:Linuxの場合
result = subprocess.run(['hoge.sh', 'value1', 'value2'])
print(result)
```

```python:Winの場合
result = subprocess.run(['hoge.bat', 'value1', 'value2'])
print(result)
```

# pythonコードを実行する
terminalコマンドが実行できるという事は、pythonインストール済み環境ならpythonも実行できます。以下ではhoge.pyに引数を2つ渡して実行しています。

```python:python
subprocess.run(['python', 'hoge.py', 'value1', 'value2'])
```

# conda環境を指定してpythonコードを実行する
pythonインタプリタは実行ファイルなので、PATHを指定してpythonを実行すればいま動作してる環境とは別のconda環境のpythonも使えます。例えばWindows環境でE:/Anaconda3/にインストールしたconda環境のtfはE:/Anaconda3/envs/tf/python.exeになるので、以下の書き方で実行できます。

```python:Windowsの場合
subprocess.run(['E:/Anaconda3/envs/tf/python.exe', 'hoge.py'])
```

```python:Linuxの場合(追記予定)
```
なお、pythonインタプリタがどこにいるのか調べるのはterminalでconda activateしておいてwhere/whichすれば簡単です。

```terminal:Windowsの場合
conda activate tf
where tf
```

```terminal:Linuxの場合
conda activate tf
which tf
```

# gpuメモリ残量を確認する
nvidia-smiが使える環境なら受け取った文字列をparseしてGPU使用量も取得できるかな、と思っていたらもうやっている人がいました。
https://qiita.com/tomotaka_ito/items/1da001c98b46ecf28ec7

```python:python
import subprocess
import json

DEFAULT_ATTRIBUTES = (
    'index',
    'uuid',
    'name',
    'timestamp',
    'memory.total',
    'memory.free',
    'memory.used',
    'utilization.gpu',
    'utilization.memory'
)

def get_gpu_info(nvidia_smi_path='nvidia-smi', keys=DEFAULT_ATTRIBUTES, no_units=True):
    nu_opt = '' if not no_units else ',nounits'
    cmd = '%s --query-gpu=%s --format=csv,noheader%s' % (nvidia_smi_path, ','.join(keys), nu_opt)
    output = subprocess.check_output(cmd, shell=True)
    lines = output.decode().split('\n')
    lines = [ line.strip() for line in lines if line.strip() != '' ]
    return [ { k: v for k, v in zip(keys, line.split(', ')) } for line in lines ]

if __name__ == "__main__":
    import pprint
    pprint.pprint(get_gpu_info())
```


# 新しくterminalを開いてコマンドを実行する
Linuxだとgnome-terminalコマンドで、Windowsだとstartコマンドを使うと新たにterminalまたはコマンドプロンプトを開くことができます。

```terminal:Linuxの場合
gnome-terminal ls -l
```
```terminal:Windowsの場合
start dir /w
```
上記のコマンドは以下のように書けるのでpythonから実行することが出来ます。

```python:Linuxの場合
subprocess.run(['gnome-terminal', 'ls', '-l'])
```

```python:Windowsの場合
subprocess.run(['start', 'dir', '/w'], shell=True)
```
長時間かかるプロセスならこちらの方がモニターしやすいかもしれません。

# shell=Trueについて
shell=Trueにするとシェルを使って実行します。シェルの埋め込みコマンド(Windowsのdirとかstartとか)はシェルからしか呼べないのでshell=Trueにして使います。でもシェルで実行するという事は何でもできてしまうという事なので脆弱性の原因になってしまいます。ファイルを実行するような場合はシェルで実行する必要がないのでshell=Falseにしておきます。デフォルトもshell=Falseですから省略すればOKです。

詳しくはこちら
https://docs.python.org/ja/3.6/library/subprocess.html#security-considerations

また、shell=Trueにすると引数の与え方が変わります。Linuxの場合、shell=Falseでは引数をlistで与えますが、shell=Trueだとスペース区切りの文字列で引数を与えるように設定されます。うっかりlistで与えると2番目以降が無視されちゃって意図しない動作をしてしまったりしますので注意が必要です。何故かエラーも出してくれないので気付くことができません。Windowsの場合、shell=Trueにするとスペース区切りが使えるようになるのは同じなんですが、shell=Trueでもlistを正常に受け取れちゃいます。stdioの挙動も違ってくるようなので色々注意が必要です。

公式ドキュメントに記述があります
https://docs.python.org/ja/3.6/library/subprocess.html#popen-constructor

# 結論
- subprocessマジで便利
- 脆弱性には気を付けよう
- レッツトライ

# 修正履歴
20190903: shell=Trueが不要なところについたり要るところに付いてなかったりしてたのを修正しました。shell=Trueについての説明を追記しました。