Python3.5〜でバッチスクリプトを書く

この記事はBashスクリプトをPythonスクリプトで置き換えるためのノウハウをまとめていますが、今後加筆修正を行っていく予定です。

# 文字列の表示
`print`関数を使用します。これは可変長引数で、複数与えるとデフォルトではスペースで区切って出力します。

```bash:Bash
echo Hello, world!
```

↓

```py3:Python3.5
print("Hello,", "world!")
```

標準出力以外に出力したい場合は`file=○○`の形で指定します。例えば標準エラーの場合は次のようになります。

```bash:Bash
echo Error >&2
```

↓

```py3:Python3.5
import sys

print("Error", file=sys.stderr)
```

ファイルに上書き・追記する場合は次のようになります。

```bash:Bash
echo foo > file1.txt
echo bar >> file1.txt
echo hoge >> file2.txt
echo fuga >> file2.txt
```

↓

```py3:Python3.5
with open("file1.txt", "w") as f:
    print("foo", file=f)
    print("bar", file=f)
with open("file2.txt", "a") as f:
    print("hoge", file=f)
    print("fuga", file=f)
```

なお、表示する引数間のスペース、及び行末の改行文字は別の文字に変更できます。例えば、区切り文字をタブ、行末の文字をなしにする場合は次のようになります。

```py3:Python3.5
print("foo", "bar", sep="\t", end="")
```

# オプションの解析
Bashなどでは色々大変なオプションの解析ですが、Pythonでは`argparse`モジュールで簡単に解析できます。基本的には次のように使います。

```py3
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--verbose", "-v", help="Verbose", action="store_true")
parser.add_argument("--quiet", "-q", help="Suppress messages", action="store_true")
parser.add_argument("target", help="target")
parser.add_argument("source", nargs="+", help="source")
args = parser.parse_args()

function(args.option)
```

各オプションには`args.verbose`・`args.quiet`などでアクセスできます。

詳しくは公式の解説を見てください。
https://docs.python.jp/3/library/argparse.html

# コマンドの実行
Python3.5から`subprocess.run`が使えます。過去バージョンとの互換性を考えないならこれを使います。

## ただ実行するだけ

```bash:Bash
g++ sample.cpp -o sample -std=gnu++14 -march=native -O3
```

↓

```py3:Python3.5
import subprocess

subprocess.run(["g++", "sample.cpp", "-o", "sample", "-std=gnu++14", "-march=native", "-O3"])
```

なお、リスト以外でも、iterableならOKです。(タプルなど)

## コマンドが0以外で終了したら例外
`check=True`を指定します。例外は`subprocess.CalledProcessError`です。

```py3:Python3.5
import subprocess

subprocess.run(["git", "push"], check=True)
```

## 標準出力・エラーを捕捉
`stdout=subprocess.PIPE`・`stderr=subprocess.PIPE`を指定します。それぞれ返り値の`stdout`・`stderr`属性で取り出せます。`byte`型なので注意してください。

```py3:Python3
import subprocess

result = subprocess.run(["apt", "list", "--upgradable"], stderr=subprocess.PIPE, stdout=subprocess.PIPE, check=True)
process(result.stdout)
```

## 標準出力・エラーを捨てる
`subprocess.PIPE`の代わりに`subprocess.DEVNULL`を指定します。

## 標準入力を指定
`subprocess.run`の引数に`input=`を加えます。byte型で指定してください。

```py3:Python3.5
import subprocess

assert subprocess.run(["cat"], input=b"aaa\n", stdout=subprocess.PIPE).stdout == b"aaa\n"
```

## リダイレクト
`subprocess.run`の引数`stdin`(`input`ではない)・`stdout`・`stderr`の各引数に`open`関数などで開いたファイルオブジェクトを指定します。入出力がテキストデータである限り`open`の第2引数は`b`なしでも大丈夫ですが、あった方が確実です。

# ディレクトリ関連
追記: `pathlib` を使った方が楽です。

```py3
from pathlib import Path

python_path = Path("/usr/bin/python3")
```

以下では`pathlib`を使わないレガシーな方法を紹介しています。

## カレントディレクトリの取得
`os.getcwd()`で取得できます。または、`pathlib`を使う場合は`Path()`(相対パス)・`Path.cwd()`または`Path().resolve()`(絶対パス)で取得できます。

## ディレクトリの変更
`os.chdir`を使用します。

```bash:Bash
cd directory
```

↓

```py3:Python3.5
import os

os.chdir("directory")
```

## ディレクトリの中身を表示
`os.listdir`を使用します。カレントディレクトリの中身を表示する場合には引数は不要です。

```bash:Bash
foo=$(ls)
```

↓

```py3:Python3.5
import os

foo = os.listdir()
```

特定のディレクトリの中身を表示したい時にはそのパスを与えます。

```bash:Bash
foo=$(ls bar)
```

↓

```py3:Python3.5
import os

foo = os.listdir("bar")
```

`pathlib.Path`を使う場合にはメソッド`iterdir()`を使います。返ってくるのはジェネレータなので、forループを使うなり、`list()`でリスト化するなりしてください。

```py3:Python3.5+pathlib
from pathlib import Path

foo = Path().iterdir() # foo=$(ls)
bar = Path("baz").iterdir() # bar=$(ls baz)

# ls qux
for path in Path("qux").iterdir():
  print(path)
```

パターンマッチングを使用したい場合は、代わりに`glob.glob`、または`glob.iglob`を使用します。前者はリスト、後者はイテレータを返すので、後者は`for`文や`map`・`filter`などに渡すために使うといいでしょう。

```bash:Bash
audio_files=$(ls *.wav)

ls *.tmp | xargs rm -f
```

↓

```py3:Python3.5
import glob
import os

audio_files = glob.glob("*.wav")

for tmp_file in glob.iglob("*.tmp"):
    os.remove(tmp_file)
```

`pathlib.Path`を使う場合はメソッド`glob`を使用します。

```py3:Python3.5+pathlib
from pathlib import Path

for tmp_file in Path().glob("*.tmp"):
    tmp_file.unlink()
```

## ディレクトリの作成
単一のディレクトリを作成する場合、`os.mkdir`を使用します。

```bash:Bash
mkdir test_dir
```

↓

```py3:Python3.5
import os

os.mkdir("test_dir")
```

`pathlib.Path`の場合はメソッド`mkdir`を使用します。

```py3:Python3.5+pathlib
from pathlib import Path

Path("test_dir").mkdir()
```

パーミッションをデフォルト値(777 & umask値)より制限したい場合は、`mode`を使用します。

```bash:Bash
mkdir -m 700 secret_dir
```

↓

```py3:Python3.5
import os

os.mkdir("secret_dir", mode=0o700)
```

なお、この関数でディレクトリを作成しようとしたもののすでに存在した場合には、例外`FileExitsError`が投げられます。また、この引数は`pathlib.Path.mkdir`メソッドでも使用できます。

ディレクトリを再帰的に作成したい場合は`os.makedirs`を使用します。ディレクトリがすでに存在しても構わない場合には、`exist_ok`オプションを使用します。また、`os.mkdir`と同様modeオプションを使用することもできます。

```bash:Bash
mkdir -p /usr/local/my_software
```

↓

```py3:Python3.5
import os

os.makedirs("/usr/local/my_software", exist_ok=True)
```

なお、`exist_ok=True`を指定しない場合にディレクトリがすでに存在した場合には、例外`OSError`が投げられます。pathlibの場合は`mkdir`メソッドの`parents`オプションを使用します。

```py3:Python3.5+pathlib
from pathlib import Path

Path("/usr/local/my_software").mkdir(parents=True, exist_ok=True)
```


## ディレクトリ内部の探索

```bash:Bash
find . -type d -exec chmod 700 {} \;
find . -type f -exec chmod 600 {} \;
```

↓

```py3:Python
import os

for root, dirs, files in os.walk("."):
    for dir in dirs:
        os.chmod(0o700, os.path.join(root,dir))
    for file in files:
        os.chmod(0o600, os.path.join(root, file))
```

引数に`Path`クラスのインスタンスを渡す場合、Python3.6以降ならそのまま渡して構いません。3.5の場合は`str`を利用して一旦文字列化します。

# パーミッション・所有権・UID・GID
## パーミッションの変更
`os.chmod`を使用します。

```bash:Bash
chmod 700 directory
```

↓

```py3:Python3.5
import os

os.chmod("directory", 0o700)
```

`Path`クラスにも同名のメソッドがあります。

```py3:Python3.5+pathlib
from pathlib import Path

Path("directory").chmod(0o700)
```

また、実行ビットを付与する場合など、元のパーミッション+αの形式で指定する場合には、`os.stat`・ビット演算と併用してください。

```bash:Bash
chmod +x script.sh
```

↓

```py3:Python3.5
import os

os.chmod("script.sh", os.stat("script.sh").st_mode | 0o111)
```

Pathクラスの場合は`os.stat`の代わりに`stat`メソッドを使用します。

```py3:Python3.5+pathlib
from pathlib import Path

script_path = Path("script.sh")
script_path.chmod(script_path.stat().st_mode | 0o111)
```

## UID・GIDの取得
現在のユーザのUID・GIDを取得するにはそれぞれ`os.getuid()`・`os.getgid()`を使用します。

```py3:Python3.5
import os, sys

if os.getuid() != 0:
    print("このスクリプトはrootで実行してください", file=sys.stderr)
    exit(1)
```

また、指定された名前のユーザのUID・グループのGIDを取得するにはそれぞれ`pwd.getpwnam`・`grp.getgrnam`を使用します。それぞれの返り値の`pw_uid`・`gr_gid`属性がそれぞれUID・GIDとなります。なお、指定された名前のユーザ・グループがない場合には例外`KeyError`が返ってきますので、適宜捕捉してください。

```py3:Python3.5
import grp
import pwd

assert pwd.getpwnam("root").pw_uid == 0
assert grp.getgrnam("root").gr_gid == 0
```

## 所有権の変更
`shutil.chown`コマンドを使用します。

```bash:Bash
chown root:www /var/www/html
```

↓

```py3:Python3.5
import shutil

shutil.chown("/var/www/html", "root", "www")
```

注: これらの関数はWindowsには実装されていません。また、`pwd`・`grp`モジュールもWindowsからは利用できません。

# ファイル・ディレクトリのコピー・移動・削除
## コピー
ファイルを普通にコピーする場合は`shutil.copy`を使用します。`cp`コマンドと同じ感覚で使用します。

```py3
import shutil

shutil.copy("old.txt", "new.txt")
shutil.copy("old.txt", "directory")
```

メタデータもコピーしたい場合は`shutil.copy2`を使用します。使い方は`copy`と同じです。ファイルオブジェクト同士でコピーしたい場合は`shutil.copyfileobj`を使用します。

```py3
import shutil

with open("old.txt", "rb") as old:
    with open("new.txt", "wb") as new:
        shutil.copyfileobj(old, new)
```

ディレクトリをコピーしたい場合は`shutil.copytree`を使用します。

## 移動

`shutil.move`を使用します。`os.rename`もありますが、ファイル専用です。

## 削除

ファイルを削除するには`os.remove`を使用します。

```bash:Bash
rm file
```

↓

```py3:Python3.5
import os
from pathlib import Path

os.remove("file") # or Path("file").unlink()
```

空のディレクトリを削除する場合には、`os.rmdir`または`Path`クラスのメソッド`unlink`を使用します。空でないディレクトリ全体を再帰的に削除する場合には`shutil.rmtree`を使用します。

```bash:Bash
rm -rf /
```

↓

```py3:Python3.5
import shutil

shutil.rmtree("/")
```

## シンボリックリンクの作成

`os.symlink`または`Path`クラスの`symlink_to`メソッドで作成できます。

```bash:Bash
ln -s foo bar
```

↓

```py3:Python3.5
import os

os.symlink("foo", "bar") # or Path("bar").symlink_to("foo")
```

# ファイルのダウンロード
`requests`パッケージを使用するといいでしょう。
ファイルの内容を取得して後に直接Pythonで解析する場合は次のように書きます。

```py3
import requests

req = requests.get("http://server/page.html")
html = req.text
```

大きなファイルをダウンロードしたい場合は次のように書くとよいでしょう。

```py3
import requests
import posixpath
import os
import shutil

req = requests.get("http://server/largefile.deb", stream=True)

with open(os.path.join("~/ダウンロード",posixpath.basename(req.url)), "wb") as f:
    shutil.copyfileobj(req.raw, f)
```

万が一同パッケージが使えない場合には、`urllib.request`モジュール(https://docs.python.jp/3/library/urllib.request.html)で代用します。

# `test`コマンドの代替
## パスの存在チェック

```bash:Bash
[[ -e "file.txt" ]] && echo "Exist" || echo "Not exist"
```

↓

```py3:Python3.5
import os

print("Exist" if os.path.exists("file.txt") else "Not exist")
```

```py3:Python3.5+pathlib
from pathlib import Path

print("Exist" if Path("file.txt").exists() else "Not exist")
```

## ファイル/ディレクトリのチェック

```bash:Bash
[[ -f "file.txt" ]] && echo "File" || echo "Not file"
[[ -d "directory" ]] && echo "Directory" || echo "Not directory"
```

↓

```py3:Python3.5
import os

print("File" if os.path.isfile("file.txt") else "Not file")
print("Directory" if os.path.isdir("directory") else "Not directory")
```

```py3:Python3.5+pathlib
from pathlib import Path

print("File" if Path("file.txt").is_file() else "Not file")
print("Directory" if Path("directory").is_dir() else "Not directory")
```

## 権限のチェック

現在のユーザに対する権限をチェックする場合には`os.access`を利用します。

```bash:Bash
[[ -r 'file.txt' ]] && echo 'True' || echo 'False'
[[ -r 'file.txt' -a -w 'file.txt' ]] && echo 'True' || echo 'False'
[[ -r 'directory' -a -x 'directory' ]] && echo 'True' || echo 'False'
```

↓

```py3:Python3.5
import os

print(os.access("file.txt", os.R_OK))
print(os.access("file2.txt", os.R_OK | os.W_OK))
print(os.access("directory", os.R_OK | os.X_OK))
```

## 更新日のチェック

更新日のタイムスタンプは`os.stat`または`Path`クラスのメソッド`stat`で取得できる名前付きタプルの`st_mtime`属性で取得できます。


## ファイルサイズのチェック

ファイルサイズは同じく`os.stat`または`Path`クラスのメソッド`stat`の返り値の`st_size`属性で取得できます。ディレクトリの場合、中に入っているファイルの総サイズという訳ではないので注意してください。

# アーカイブの圧縮・解凍
## shutilの利用
解凍には`shutil.unpack_archive`を利用します。各種tarball・zipならばこれが一番楽です。

```bash:Bash
tar xf foo.tar.xz
```
↓

```py3:Python
import shutil

shutil.unpack_archive("foo.tar.xz")
```

なお、この関数の第2引数で解凍先を指定できます。

```bash:Bash
tar xf foo.tar.gz -C bar
```

↓

```py3:Python
import shutil

shutil.unpack_archive("foo.tar.gz", "bar")
```

あるディレクトリを丸ごと圧縮したい場合には`shutil.make_archive`を使用します。例えば`bar`という名前のディレクトリを圧縮して`foo.tar.xz`という名前のアーカイブを作りたい場合は次のように書きます。

```bash:Bash
tar cf foo.tar.xz bar
```

↓

```py3:Python
import shutil

shutil.make_archive("foo", "xztar", base_dir="bar")
```


## gz・bz2・xz
これらのファイルはそれぞれ`gzip`・`bz2`・`lzma`モジュールを利用すると普通のファイルのように読み書きできます。

```py3
import gzip
import bz2
import lzma
import shutil

with gzip.open("file.gz", "rb") as gz:
    with bz2.open("file.bz2", "wb") as bzip2:
        shutil.copyfileobj(gz, bzip2)
    with lzma.open("file.xz", "wb") as xz:
        shutil.copyfileobj(gz, xz)
```

## tar
`tarfile`モジュールを使います。解凍は`extractall()`を使用します。例えば、次のように利用します。

```py3
import tarfile

with tarfile.open("tarfile.tar.xz") as f:
    f.extractall()
```

なお、第1引数にディレクトリを指定すると、そこに展開します。tarファイルのファイル一覧は、`members()`で取得します。(リストが返ってきます)

tarファイルを作成する場合は`add()`を使用しますが、`open()`で指定するファイルモードに注意が必要です。`w`だけだと非圧縮とみなされます。例えばtar.xz形式のアーカイブを作成したい場合は次のように書きます。

```py3
import tarfile

with tarfile.open("archive.tar.xz", "w:xz") as f:
    f.add("directory")
```

このように`w`の後に`:(圧縮形式名)`を追加します。

## zip
`zipfile.ZipFile`を使用します。(http://docs.python.jp/3.6/library/zipfile.html#zipfile-objects)
例えばZIPファイルの中身をカレントディレクトリ内に全て展開したい場合は次のように書きます。

```py3
import zipfile

with zipfile.ZipFile("foo.zip") as ar:
    ar.extractall()
```

zipファイルにはファイル名のエンコーディング問題があるので、OSの`zip`・`unzip`コマンドを`subprocess.run`で呼び出すといいかもしれません。(Windows・Ubuntu日本語Remix)

# 一時ファイル・ディレクトリ
一時ファイル・ディレクトリは`tempfile`モジュールを利用します。
## 一時ファイル
ファイルを外部から使用しない場合は`tempfile.TemporaryFile`を使用します。

```py3
import tempfile

with tempfile.TemporaryFile() as temp_file_handler:
  temp_file_handler.write(some_byte_array)
  # ...
  some_bytes = temp_file_handler.read(length)
  # ...
```

外部から使用する場合は`tempfile.NamedTemporaryFile`を使用します。`name`属性があり、これでファイル名を取得できます。

```py3
import tempfile
import subprocess

with tempfile.NamedTemporaryFile() as temp_file:
  temp_file.write(some_byte_array)
  # ...
  subprocess.run(("external-process", temp_file.name), check=True)
  # ...
```

拡張子・プレフィックスを指定したい場合は`suffix`・`prefix`を使用します。例えば、次のコード

```py3
import tempfile

with tempfile.NamedTemporaryFile(prefix="tempfile-", suffix=".deb") as temp_file:
  print(temp_file.name)
```

は、`/tmp/tempfile-hz1u73cr.deb`のような文字列を出力して終了します。

なお、どちらも`with`文を抜けるとファイルは削除されます。また、ファイルの規定のオープンモードは`w+b`、すなわちバイナリモードです。

## 一時ディレクトリ
一時ディレクトリは`tempfile.TemporaryDirectory`で作成します。通常は次のように`with`文を使用します。

```py3
import tempfile
import os
import subprocess

with tempfile.TemporaryDirectory() as temp_dir_name:
  temp_conf_name = os.path.join(temp_dir_name, "hoge.conf")
  with open(temp_conf_name, "w") as temp_conf:
    print("hoge=hogehoge", file=temp_conf)
  subprocess.run(("sudo", "hoge", temp_conf_name), check=True)
```

この場合、`with`文を抜けると一時ディレクトリ及びその中のファイルはは削除されます。また、この例の場合`temp_dir_name`の型は`str`です。

# 環境変数
`os.environ`という名前の辞書(のようなもの)で取得・設定できます。

```py3:Python3.5
import os

print("256色" if "TERM" in os.environ and "256color" in os.environ["TERM"] else "16色")
```

```py3:Python3.5
import os
import pip

if "CUDA_ROOT" not in os.environ:
    os.environ["CUDA_ROOT"] = "/usr/local/cuda"
pip.main(["install", "-U", "chainer"])
```

文字列の中に環境変数を埋め込みたい場合は`os.path.expandvars`を利用します。

```py3:Python3.5
import os

WORKING_DIR = os.path.join(COMMON_ROOT, os.path.expandvars("work-$USER"))
```

# Pathクラスのメソッドによるパスの文字列操作
## パス連結

```py3:Python3.5+pathlib
from pathlib import Path

# 全て Path("/bin/bash")を返す
Path("/bin","bash")
Path("/bin") / "bash"
Path("/bin") / Path("bash")
Path("/bin").joinpath("bash")
Path("/bin").joinpath(Path("bash"))
```

## パス分割

```py3:Python3.5+pathlib
from pathlib import Path

p = Path("dir/dir2/file.tar.gz")
print(p.name) # file.tar.gz
print(p.parent) # dir/dir2
print([str(dir) for dir in p.parents]) # ['dir/dir2', 'dir', '.']

print(p.stem) # file.tar
print(p.suffix) # .gz
print(p.suffixes) # ['.tar', '.gz']
```

## 親ディレクトリはそのままで名前・拡張子の変更

```py3:Python3.5+pathlib
from pathlib import Path

ico = Path("img/favicon.ico")

gitignore = ico.with_name(".gitignore") # img/.gitignore
png = ico.with_suffix(".png") # img/favicon.png
```

# 文字列操作
## パス操作 (非推奨; Pathクラスの使用を推奨)
各種区切り文字は`os.sep`(ディレクトリ)・`os.extsep`(拡張子)・`os.pathsep`(環境変数PATH)で取得できます。

```py3:Python3.5
import os

print(os.sep, os.extsep, os.pathsep)
# Windows: \ . ;
# それ以外: / . :
```

パスの連結には`os.path.join`を使用します。

```py3:Python3.5
import os

os.makedirs(os.path.join(PROJECT_ROOT, "build"), exist_ok=True)
```

パスをbasename・dirnameに分割するには`os.path.split`・`os.path.basename`・`os.path.dirname`を使用します。

```py3:Python3.5
import os

dirname, basename = os.path.split(FILE_PATH)
assert dirname == os.path.dirname(FILE_PATH)
assert basename == os.path.basename(FILE_PATH)
```

パスの拡張子・拡張子以外の部分を取り出す場合は`os.path.splitext`を使います。

```py3:Python3.5
import os

root, ext = os.path.splitext("/etc/nginx/nginx.conf")
print(root, ext)
# /etc/nginx/nginx .conf
```

ホームディレクトリを表す`~`を利用したパスを取り扱うには`os.path.expanduser`を使用します。

```bash:Bash
echo "Your home directory is: $HOME"
```

↓

```py3:Python3.5
import os

print("Your home directory is:", os.path.expanduser("~"))
```

Windowsとの互換性を意識する場合にはこのようなホームディレクトリを取得する使い方のみをし、`os.path.join`を併用してください。

また、環境変数を含んだパスを扱うには`os.path.expandvars`を使用します。

```py3:Python3.5
import os

os.makedirs(os.path.join(PROJECT_ROOT, "build", os.path.expandvars("python-$PYTHON_VERSION")), exist_ok=True)
```

`pathlib`を使う方法もありますが、現在のところは解説しません。
https://docs.python.jp/3/library/pathlib.html
## その他

他には`sed`・`awk`・`grep`相当のことを書く予定です


# 参考
http://qiita.com/supersaiakujin/items/12451cd2b8315fe7d054