おじさん、Pythonでスクリプトを作成したい

# 1. はじめに

プログラムの開発思考がシェルスクリプトからPythonの型へとシフトしていきつつある。
最近ではシェルスクリプトを書いていると「これどう書くんだっけ」と悩み、過去の産物やGoogle先生に頼ることが多くある。
PythonでもOSコマンドの実行ができ、スクリプトの作成ができるようなので、そちらに移行しようと考えた。

# 2. 今回試したいこと

今回 理解したい内容のスコープは以下の3点となる。

- bashのコマンド実行
- 制御文の実装
- 簡単なエラー処理



# 3. スクリプト

### 3-1. ライブラリ
本稿では以下のライブラリを利用する。

- subprocess
- sys
- string
- secret

### 3-2. 最終目標
本稿では乱数を作成し、それをファイル名としてファイルを作成するようなスクリプトを作成する。
ここでは以下のような条件とする。

- 作成ファイル数：5
- ファイル名の文字数：20
- 文字種：英数字と一部の記号

### 3-3. コマンド実行テスト
今回 調べている中で、bashのコマンド実行には"subprocess"のライブラリが推奨されることを知った。
"subprocess"に関して詳細を調べることはしていないが、どうやらコマンドを配列で渡すと実行できるらしい。
いくつか関数を持っているようだが、主要な関数は2つだけのようだ。
今回はそのうち、"run"を使って検証していく。

以下のようなスクリプトを作り、実行した。
カレントディレクトリ内の一覧を表示するだけの簡単なスクリプトである。
すると返り値として"0"が返り、正常に実行できたことがわかる。

```python
import subprocess as sp
sp.run(['ls', '-l'])

---

CompletedProcess(args=['ls', '-l'], returncode=0)
```

ただ"ls -l"の標準出力が見えないと確からしさが明確でないため、標準出力できるようにする。
すると、stdoutに"ls -l"の結果が入ることが確認できた。
よって、"ls -l"自体は正しく動いている事が分かる。

```python
import subprocess as sp
sp.run(['ls', '-l'], stdout=sp.PIPE)

---

CompletedProcess(args=['ls', '-l'], returncode=0, stdout=b'total 32\ndrwxr-xr-x 2 root root    64 Jul 21 07:07 output\n-rw-r--r-- 1 root root 32756 Jul 21 07:27 script1.ipynb\n')
```

余談にはなるが、ここまですると今度は標準エラー出力がほしくなる。
よって、今度は標準エラー出力を結果に入れるようにした。

下記では存在しないオプションである"-J"を使い、標準エラー出力を得られるようにした。
その結果、stderrの項目内に出力が入ることが確認できた。

```python
import subprocess as sp
sp.run(['ls', '-J'], stdout=sp.PIPE, stderr=sp.PIPE)

---
CompletedProcess(args=['ls', '-J'], returncode=2, stdout=b'', stderr=b"ls: invalid option -- 'J'\nTry 'ls --help' for more information.\n")
```

さらに余談になるが、標準エラー出力をstderrでは無く、stdoutに入れることもできるようだ。
いわゆるファイルディスクリプタの指定である。
下記のようなスクリプトを実行すると、確かにstdoutに標準エラー出力が入った。

```python
import subprocess as sp
sp.run(['ls', '-J'], stdout=sp.PIPE, stderr=sp.STDOUT)

---

CompletedProcess(args=['ls', '-J'], returncode=2, stdout=b"ls: invalid option -- 'J'\nTry 'ls --help' for more information.\n")
```

以上のことより、"subprocess"を用いてbashのコマンド実行が可能であることがわかった。

### 3-4. 乱数生成
ここでは「3-2. 最終目標」で述べた条件に従って乱数を生成するための関数を定義していく。
"string"を用いて文字種を定義し、"secrets"を用いてランダム的に文字を選択していく方法になる。
下記の関数を用いて、変数file_listに5つの乱数を格納することができた。

```python
# ライブラリをインポート
import string, secrets

# 乱数格納用のリストを定義
file_list = []

# ファイル名生成用の関数を定義
def random_gen(5, 20):
    # 5ファイル分の乱数を生成
    for _ in range(5):
        # 乱数に使用する文字種を定義する。
        # ここでは大・小文字のアルファベット、数字、記号(()-+*[]{}<>!$%&)としている。
        chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + '()-+*[]{}<>!$%&'
        # ファイル名を格納するための変数を宣言
        name = ''
        # 1ファイル当たり20文字の文字列を作成
        for _ in range(20):
            # 上記で定義した文字種からランダムで1文字を選択し、nameに連結
            name = name + secrets.choice(chars)
        # ファイル名をリストに保存
        file_list.append(name)
    return file_list
```

### 3-5. エラーチェック
「3-3. コマンド実行テスト」において、コマンド実行結果を返り値で判別できることが分かった。
よって、返り値が"0"以外の場合はエラーが発生していると判別し、スクリプトを中断することが可能である。
以下のような関数を定義し、コマンド実行後に返り値チェックを行えるような機能を実装する。

```python
# ライブラリをインポート
import sys

# 返り値チェック用の関数を定義
def err_check(returncode):
    # 返り値が"0"以外の場合は中断
    if returncode != 0:
        sys.exit()
```

### 3-6. ファイル作成スクリプトの実装
以上の内容を踏まえて、ランダム生成した文字列をファイル名としたファイルを作成するスクリプトを実装する。
メインとなる処理は"Files Create"と示した部分である。
ここでは以下のような流れとしている。

- 関数random_gen()をコールして5種類の乱数文字列を作成し、変数FileListに格納
- 変数FileListから乱数文字列を1つずつ呼び出し、変数FileNameに格納
- 実行したいコマンドを変数Commandにて作成
- sp.runで変数Commandの内容を実行し、結果を変数Resultに格納
- 関数err_check()をコールし、返り値をチェック
- 返り値が"0"の場合はスクリプトを続行し、"0"以外の場合は中断

```python
### Libraries Import ###
import subprocess as sp
import string, secrets, sys

### Global Variable ###
FileList = []
FilesNumber = 5
FilesLength = 20
Path = './output/'
# BaseCommand = '/usr/bin/touch -v '
BaseCommand = '/usr/bin/touch '

### Error Check ###
def err_check(Returncode):
    if Returncode != 0:
        sys.exit()
        
### Random Name Generate ###
def random_gen(Number, Length):
    for _ in range(Number):
        Chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + '()-+*[]{}<>!$%&'
        Names = ''
        for _ in range(Length):
            Names = Names + secrets.choice(Chars)
        FileList.append(Names)
    return FileList

### Files Create ###
random_gen(FilesNumber, FilesLength)
for FileName in FileList:
    Command = BaseCommand + Path + FileName
    Result = sp.run(Command.split(), stdout=sp.PIPE, stderr=sp.STDOUT)
    err_check(Result.returncode)
```

始めに正常終了パターンを検証する。
./output内のディレクトリを見ると、確かに5つのファイルが作成できていることが分かる。

```bash
$ ls -l output/
total 0
-rw-r--r--  1 mottai  staff  0  7 21 17:00 Sd1J{L+U+NK)$SaG{e%J
-rw-r--r--  1 mottai  staff  0  7 21 17:00 Sna&97p&zz(XWs><j1W3
-rw-r--r--  1 mottai  staff  0  7 21 17:00 Su$AIX(7Swkd}sMj{3(]
-rw-r--r--  1 mottai  staff  0  7 21 17:00 a-(fWv5HO%J**k2*FvPP
-rw-r--r--  1 mottai  staff  0  7 21 17:00 m1-zI9LhRB7BP(DV2I90
```

次に異常終了パターンを検証する。
touchコマンドに存在しないオプション"-v"を付与した場合の動作になる。
実行した結果、以下のように例外発生として認識され、中断されている様子が見られる。
実際、./output内にはファイルは生成されていなかった。

```python
An exception has occurred, use %tb to see the full traceback.

SystemExit
```

# 4. まとめ

本稿では"subprocess"のライブラリを使用し、シェルスクリプトの代用となるようなプログラムの開発検証を行った。
その結果、一通り実行したいことはできることが確認できた。
標準ライブラリ内でここまでできるのであれば、特に問題なく実装に移せそうではあるが、使っている中で問題が発生したら都度 本稿を更新していく。