Python
Python3

Pythonでシェルスクリプト①

概要

 そろそろブックマークしている記事も膨れ上がってきたので、備忘録として。
ファイル操作、プログラム実行をする、ということで、シェルスクリプトと表現しました。
Pythonが大好きな僕は、まずsubprocessモジュールを用いることにしました。

環境

macOS High Sierra(10.13.1)
Python 3.6.1

subprocessモジュールとは?

新しいプロセスの開始、それに関してリターンを受け取ることができる。

(引用:17.5. subprocess — サブプロセス管理 Python 3.6.3 ドキュメント

簡潔な結論

cmd_exe.py
import subprocess as sp
sp.call('echo "Hello"', shell=True)

このように、call関数を呼び出してやって、シェルに渡せば実行できるわけです。簡単ですね。
ただ、この状態では、シェルインジェクションが起きるそうです。

What is shell injecton?

古来からUnixに存在するパイプなどの、シェルにある特殊文字を使った攻撃みたいです。
SQLインジェクションの方が馴染みがあると思います。

(参考:Code injection - Wikipedia

なるほど、単純にshell=Trueとすると、危険であることがわかります。
ただし、ちょっとした活用であれば考慮するほどでないかもしれないですね。

組み込み関数を考慮する

シェルを通してコマンドを実行することができました。
ただ、組み込み関数、osモジュール、shutilモジュールを用いた方が、よりPythonらしいでしょう。

Pythonによるファイル操作

Pythonのファイル操作は、Unixのファイル操作をモデルに作られています。
まずはopen関数でファイルを開きます。
fileobj = open(filename, mode)

想像通り、fileobjは、ファイルオブジェクトです。
modeは、
一文字目は、rwxに加え、行末に加えていくaがある。
二文字目は、t(text)または、b(binary)の二種類が存在します。
例)fileobj = open("hoge.txt", "at")

そして、ファイルオブジェクトに対してメソッドで書き込みをする。

  • write()メソッド

引数として、書き込む文字列を渡す。当然ですね。返り値は、書き込んだバイト数です。

  • print()関数で書き込み

print関数は、セパレータと、末尾文字列を追加してしまう整形する関数なので、挙動に注意すれば
以下のようになる。

fileobj = open("file.txt", "wt")
print("Good night world", file=fileobj)
fileobj.close()

close()メソッドは、メモリ領域の開放なので、必須ですね。

チャンク分けして書き込み

膨大なデータ量の時に必須です。これを理解して覚えるのもありだと思います。

chunk.py
fileobj = open("hoge.txt", "wt")
something = "書き込む何かですよ"
size = len(something)  # 書き込む文字列がどれくらいのサイズか
offset = 0
chunk_size = 3  # 小さすぎるが例として
while True:
    if offset > size:  # データが終わっていればbreak
        break
    fileobj.write(something[offset:offset+chunk_size])
    offset += chunk_sizes

ファイルの保護

open()関数に渡す第二引数の、一文字目をwaのいづれでもなく、xにすると、
ファイルを保護をできます。
この場合、例外オブジェクトとして、FileExistsErrorを生成するので、以下のようにも
なります。

try:
    fileobj = open("hoge.txt","xt")
    fileobj.write("fun fun fun")
except FileExistsError:
    print("File already exists!!")

read()メソッドによる読み出し

read()メソッドは引数に、読み出す文字数を指定します。
言わずもがな、メモリの関係上先ほどのチャンクロードの形をとりますね。

chunk_read.py
fileobj = open("hoge.txt", "rt")
chunk_size = 100
content = ""
while True:
    fragment = fileobj.read(chunk_size)
    if not fragment:  # fragmentは最終的に空文字('')が返される
        break
    content += fragment
fileobj.close() 

見せておいて非常に申し訳ありませんが、イテレータを用いたほうが格段に楽でした。

file_itr.py
content = ""
fileobj = open("hoge.txt", "rt")
for line in fileobj:
    content += line
fileobj.close()
print(content)

with文を用いた自動close

Pythonが持つ、コンテキストマネージャを用います。

コンテキストマネージャとは?

enter()呼び出しで構築され、構築された環境は、オブジェクトのプロパティとして保持される。
この環境は、exit()で、破壊される。

あんまりよく分かってないです。Guidoに怒られそうですが、なんとなくそんなことかと推定してます。

Pythonのコンテキストマネージャは何が嬉しいのか? - Qiita

Pythonでコンテキストマネージャを用いる

with_file.py
with open("hoge.txt", "wt") as fileobj:
    fileobj.write("What is your name?")

osモジュールを考慮する

path操作に関しては、接頭辞として、os.pathを用いる。

  • os.path.exists("hoge.txt")

fileが存在するか否かを確認する。returnは、True/False。

  • os.path.isfile(name)
  • os.path.isdir(name)
  • os.path.isabs(name)

上から、ファイルかどうか?ディレクトリかどうか?絶対パスかどうか?

  • os.rename(before, after)

beforeからafterへとリネームする。

  • os.symlink(origin, copy)
  • os.path.islink(copy)

シムリンクを作成、それがどうかを判定する。

  • os.chmod(filename, 0o401)
  • os.chown(filename, uid, gid)

この二つは、このコマンド自体を知らない方には難しいかもしれませんが、
知っているか否かです。

chmodについて
chownについて

  • os.path.abspath(relative_path)
  • os.path.realpath(sym_link)

abspathは、相対パスを絶対パスに変更します。
realpathは、シムリンクの参照元の絶対パスを返します。

  • os.remove(filename)
  • os.mkdir(dirname)
  • os.rmdir(dirname)
  • os.chdir(dirname)

解説するまでもないですね。ただ、次はどうでしょうか。

  • os.listdir(dirname)

この関数は、ディレクトリに入っているファイル名をリストで返します。なにやら便利そうですね。

globモジュールを考慮する。

  • glob.glob(pattern)

patternに使えるのは正規表現ではなく、シェルの構文
例)*はユニバーサルセレクタ(なんでもさす)
そして、returnは、listです。

shutilモジュールを考慮する。

copyってなんで、os.copyじゃないんだ!!
稀代の天才でなければ、一度は思ったことがあると思います。
それは、また、別の機会に、低レベルか、高レベルかを考えるとして今は置いておきましょう。

  • shutil.copy(origin, copy)

もう一つ、空でないディレクトリを再帰的に削除するためには、rmtreeが必要です。

  • shutil.rmtree(dirname)

最後に

盛りだくさんでしたが、今回は、
「やり方」の単語集
的なものを執筆させていただきました。
なんだかわからないけど、動けばいい!場合には、ここまでが理解できれば問題ないと思います。
次回は、より深く、プロセスについて理解して、Pythonで書けるシェルスクリプトを充実させていければいいと思います。

参考

pythonでコマンド実行するには - それマグで!
O'Reilly Japan - 入門 Python 3