LoginSignup
7
8

More than 1 year has passed since last update.

つよいシェルスクリプトとしてのPython

Posted at

はじめに

最近CI/CD用に書いていたシェルスクリプトをちまちまとPythonに移植しています。なんでかと言うと単純にシェルスクリプトで扱うにはいささか複雑な処理やデータが増えてきたからですね。
じゃあなんでPythonなの?って話なんですが

  • 標準ライブラリだけでわりと色々できる
  • OSの機能にアクセスしやすい
  • 型を扱えつつ雑にもできる
  • 開発環境の構築が楽
  • アレルギー持ちが少ない

この辺りが理由です。あとAmazon LinuxやGitHub-hosted runnerにプリインストールなのもベネ。
ただし全てのシェルスクリプトをPythonに移植することが正しいかと言われるとそれは場合によるとしか言いようがないのでここでは事の是非については触れません。
この記事はシェルでよく書くアレをPythonではこう書きますよという言わば互換表のようなものだと思ってください。

環境構築

本筋ではありませんが冒頭で環境構築について触れたので軽くご紹介しておきます。
とりあえずPythonをスクリプトとして書き始めたい場合に必要な工程は3つです。

  1. pyenv経由でPythonをインストール
  2. VS Codeに拡張機能を入れる
  3. VS Codeの設定を書く

Pythonインストール

pyenvはPythonのバージョンマネージャーです。特にこだわりがなければ開発環境にはpyenv経由でPythonをインストールすることをおすすめします。

pyenvインストール

windows
Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"

参考: https://github.com/pyenv-win/pyenv-win#quick-start

windows以外
curl https://pyenv.run | bash

参考: https://github.com/pyenv/pyenv#automatic-installer

Pythonインストール

pyenv instlal 3.11.0

pyenv install -lでインストール可能なバージョンの一覧が表示されるのでお好みのものをインストールしてください。

VS Codeの拡張機能

インストール

基本機能(型検査含む)

リンター

フォーマッター

フォーマッターその2

設定

setting.jsonに以下の設定を入れてください。

line-lengthはお好みで
{
  "[python]": {
    "editor.defaultFormatter": "ms-python.black-formatter",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.organizeImports": true
    },
    "editor.formatOnType": true
  },
  "flake8.args": ["--max-line-length=120"],
  "black-formatter.args": ["--line-length=120"],
  "isort.args": ["--profile", "black"],
}

これでどのディレクトリで書いても大体のことはできる/自動でやってくれるようになりました。

記法あれこれ

ディレクトリ/ファイル操作

Pythonにおけるディレクトリやファイルの操作は基本的にpathlib.Pathを介して行います。

from pathlib import Path

# 相対パスでも絶対パスでも
r = Path("piyo.txt")
a = Path("/home/hoge/fuga/piyo.txt")

# 相互変換も可能
r.absolute()
>>> PosixPath('/home/hoge/fuga/piyo.txt')

# カレントディレクトリ
Path.cwd()
>>> PosixPath('/home/hoge/fuga')
Path()
>>> PosixPath('.')

# ホームディレクトリ
Path.home()
>>> PosixPath('/home/hoge')

# スクリプト自身のパス
Path(__file__)
>>> PosixPath('/home/hoge/fuga/piyo.py')

移動/検索

from pathlib import Path

cwd = Path()

# 移動
child = cwd / "hoge"
child
>>> PosixPath('./hoge')
child.parent
>>> PosixPath('.')
child.joinpath("fuga", "piyo")
>>> PosixPath('./hoge/fuga/piyo')

# 検索
[*cwd.glob("*.py")]
>>> [PosixPath('./fuga.py'), PosixPath('./hoge.py')]
# サブディレクトリ内も含めて検索
[*cwd.glob("**/*.py")]
>>> [PosixPath('./fuga.py'), PosixPath('./hoge.py'), PosixPath('./piyo/piyo.py')]
# ls
[*cwd.glob("*")]
>>> [PosixPath('./fuga.py'), PosixPath('./hoge.py'), PosixPath('./piyo')]

属性

from pathlib import Path

hoge = Path("hoge.txt")

# ステータス(ls -lとかstatで見るようなやつ)
hoge.stat()
>>> os.stat_result(st_mode=33206, st_ino=20829148276990953, st_dev=105550640, st_nlink=1, st_uid=0, st_gid=0, st_size=3039, st_atime=1680457867, st_mtime=1679661130, st_ctime=1680457867)
# 表示されるもの以外も見れるので詳しくは↓
# https://docs.python.org/ja/3/library/os.html#os.stat_result

# モード(パーミッション)変更
hoge.chmod(0o664)  # プレフィックス"0o"のついた数値は8進数と見なされます
oct(hoge.stat().st_mode)
>>> 0o100664

# 存在確認
hoge.exists()
>>> True

# 名前系
hoge.name
>>> hoge.txt
hoge.suffix
>>> .txt
hoge.stem
>>> hoge

# is_hoge()
hoge.is_dir()
>>> False
hoge.is_file()
>>> True
# 他にも色々

globと属性の取得を組み合わせてlsにオプションを指定したような感じにすることもできます。

[*Path().glob("*")]
>>> [PosixPath('./fuga.py'), PosixPath('./hoge.py'), PosixPath('./piyo')]

[p for p in Path().glob("*") if p.is_dir()]
>>> [PosixPath('./piyo')]

読み書き

from pathlib import Path

hoge = Path("hoge.txt")
hoge.write_text("hoge!!")
hoge.read_text()
>>> 'hoge!!'

なおテキストのエンコード/デコードにはシステムロケールが適用されるのでWindows環境で開発する場合などはencoding="utf-8"を忘れないようにしましょう1

windowsの場合
from pathlib import Path

hoge = Path("hoge.txt")
hoge.write_text("ほげ!!", encoding="utf-8")
hoge.read_text(encoding="utf-8")
>>> 'ほげ!!'

移動/リネーム

from pathlib import Path

hoge = Path("hoge/hoge.txt")

# 移動
hoge.replace(f"fuga/{hoge.name}")

# リネーム
hoge.replace(hoge.with_name("piyo.txt"))

移動/リネーム後のパスに既にディレクトリ/ファイルが存在する場合は権限があれば上書き、権限がなければ怒られます。

リンク

from pathlib import Path

# シンボリックリンク
Path("./fuga").symlink_to("./hoge/fuga")
# "./hoge/fuga"へのリンクが"./fuga"に作られる

# ハードリンク
Path("./piyo").hardlink_to("./hoge/piyo")

作成/削除

from pathlib import Path

# ディレクトリ
d = Path("hoge/fuga")
d.mkdir(parents=True, exist_ok=True)  # 途中のディレクトリがない場合全部作る、既に存在する場合もエラーにならない
d.rmdir()

# ファイル
f = Path("piyo.txt")
f.touch()  # mkdir()はexist_okがデフォルトFalseだがtouch()はTrue
f.unlink()

コピー及びディレクトリの削除はshutilモジュールを使う

上記Path.rmdir()には3.11現在オプションが存在しません。そのため削除したくとも空のディレクトリにしか使えないというなんともな性能をしています。俺がやりたいのはrm -rfなんだよ!という方は併せてshutilモジュールをご利用ください。
またPathにはコピー系のメソッドが存在しないので(なんで?)こちらもshutilで解決します。

from pathlib import Path
import shutil

# ディレクトリコピー
hoge = Path("./hoge")
hoge_ = shutil.copytree(hoge, hoge.with_name("hoge_"))
hoge_
>>> PosixPath('./hoge_')

# ディレクトリ削除
shutil.rmtree(hoge_)

# ファイルコピー
fuga = Path("./fuga.txt")
fuga_ = shutil.copy2(fuga, fuga.with_stem("fuga_"))
fuga_
>>> PosixPath('./fuga_.txt')

ちなみにshutilの各メソッドに渡すsrc, dst引数にはパスを表す文字列でもPathオブジェクトでもどちらを使用してもいいのですが渡したものと同じ型のオブジェクトが返ってくるため個人的にはPathを使った方がおすすめです。

一時ディレクトリ/ファイル

使用モジュール: tempfile

from pathlib import Path
import tempfile

# 一時ディレクトリ
with tempfile.TemporaryDirectory() as tempdir_name:
    tempdir = Path(tempdir_name)
    ...

# 一時ファイル
with tempfile.TemporaryFile() as fp:
    fp.write(b'hoge!')
    ...

TemporaryDirectory()は作成された一時ディレクトリの名前を返しますがTemporaryFile()から返ってくるものはopenされたfile-like objectそのものなので注意が必要です。

環境変数

使用モジュール: os

import os

# 取得
hoge = os.environ["HOGE"]

# 更新
os.environ["HOGE"] = "hoge!"

# 削除
del os.environ["HOGE"]

getenv, putenv, unsetenvに関しては

Assignments to items in os.environ are automatically translated into corresponding calls to putenv(); however, calls to putenv() don't update os.environ, so it is actually preferable to assign to items of os.environ. This also applies to getenv() and getenvb(), which respectively use os.environ and os.environb in their implementations.

となんだかよく分からないことを言っているので使わない方がよさそうです。

正規表現

使用モジュール: re

import re

# 基本の使い方
commit_msg_ptn = re.compile(r"^(?P<prefix>[a-z]+): (?P<description>.*) #(?P<issue>[0-9]+)$")
if m := commit_msg_ptn.search("update: hoge to fuga #0000"):
    m["prefix"]
    m["description"]
    m["issue"]
>>> 'update'
>>> 'hoge to fuga'
>>> '0000'

# 置換
re.sub(r"\s", "", "hoge fuga piyo\n")
>>> 'hogefugapiyo'

# マッチした部分文字列を参照して置換
re.sub(r"(?P<desc>[a-z]+)(?P<num>[0-9]+)", r"\g<desc>_\g<num>", "hoge2.png")
>>> 'hoge_2.png'

参考: 正規表現のシンタックス

HTTP通信/JSON

使用モジュール: urllib, json

@hoto17296 さんのこの記事が全てです。

日時

使用モジュール: datetime

from datetime import datetime, date, timedelta

# 取得
datetime.fromisoformat("2020-01-01T01:01:01+09:00")
>>> 2020-01-01 01:01:01+09:00
datetime.fromtimestamp(1680546010)
>>> 2023-04-04 03:20:10
datetime.today()
>>> 2023-04-04 03:26:35.380565
date.today()
>>> 2023-04-04

# 出力
today = datetime.today()
today.strftime("%Y年%m月%d日")
>>> '2023年04月04日'
today.timestamp()
>>> 1680547503.322209

# 計算
today + timedelta(days=1)
>>> 2023-04-05 03:45:03.322209

アーカイブ

使用モジュール: shutil
デフォルトでzip、tar、gztar、bztar、xztarが扱えます
地味にとても助かる。

import shutil
from pathlib import Path

Path.cwd()
>>> PosixPath('/home/hoge')

# "./fuga/fuga"ディレクトリの中身を"./fuga.tar.gz"として圧縮
fuga_tar = Path(shutil.make_archive("./fuga", "gztar", "./fuga/fuga"))
fuga_tar
>>> PosixPath('/home/hoge/fuga.tar.gz')

# "fuga.tar.gz"を"./piyo"ディレクトリの中に解凍
piyo = Path("./piyo")
shutil.unpack_archive(fuga_tar, piyo)

他のプログラムを動かす

使用モジュール: subprocess

from subprocess import run, Popen, PIPE

# 基本の使い方
cpl_proc = run(["git", "log", "-1"], capture_output=True, text=True)  # Windowsはencoding="utf-8"も必要
cpl_proc.returncode
>>> 0
print(cpl_proc.stdout)
>>> commit aaaaaaaaa...
>>> Author: nicco_mirai <hoge@hoge.com>
>>> Date:   Sun Apr 2 15:35:29 2023 +0900
>>> 
>>> hoge!
>>>
cpl_proc.stderr
>>> 

# 通常のコマンドも(OS依存)
cpl_proc = run(["ps", "aux"], capture_output=True, text=True)
print(cpl_proc.stdout)
>>> USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
>>> ec2-user  258807  0.1  1.0 230460 10168 pts/0    S+   01:47   0:00 python3
>>> ec2-user  258809  0.0  0.2 232520  2712 pts/0    R+   01:48   0:00 ps aux

# バックグラウンドで実行
proc = Popen(["npx", "nuxt", "build"], stdout=PIPE, text=True, cwd="nuxt_project_dir")
do_something()
proc.wait()  # 終了するまで待機

終了

使用モジュール: sys

from sys import exit

# 正常終了
exit()
# 異常終了
exit("エラーメッセージ")  # 終了コードは1になる

おわりに

一通り私が使うものは紹介しきれたと思います。
が、ここで紹介したものはPythonの標準ライブラリ全体から見れば1/100にも満たない程度ですので気になる方はぜひ公式ドキュメントにも目を通してみてください。
Python 標準ライブラリ
今後また使ってみて便利だったものがあれば随時更新していきます。

  1. UTF-8 Modeという手もあります。

7
8
3

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
7
8