シェルスクリプトをPythonで書き直した
概要
作業を楽にするために個人的にシェルスクリプトを書きました。
これを他のメンバーにも共有し、チーム全体の作業効率をアップや改善しようと考えています。
しかし、シェルスクリプトに触れたがあるメンバーが少なかったため、全員が触れたことのあるPythonで書き換えることにしました。(何かの試験問題の導入文みたい)
どんなシェルスクリプトだったのか
シェルスクリプトは、こちらの記事のテンプレートを参考に書きました。
処理内容は大まかに5つです。
- テンプレファイルをコピーし、ファイル内容を一部置換
- ファイル名を間違えたとき用に一括置換
- コンパイルで作られた一時ファイルを削除
- TeXファイルをエディタで開く
- 出力PDFファイルをPDFビュワーで開く
ただし、具体的な処理は本筋ではないので、折りたたみの中に書きました。↓
シェルスクリプトはだいたいこんな感じ
#!/usr/bin/env bash
set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
usage() {
cat <<EOF
Usage:
bash $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-ot] [-op] [-r aftername] dirname
# これは何?
hoge用のシェルスクリプトです。
dirnameに作業ディレクトリ名を入力してください。
ディレクトリ構成は以下です。
-------------------------------
./
├── cli.sh
└── template
├── A.tex
├── QandA.tex
└── Q.tex
-------------------------------
# 使い方
"bash cli.sh hoge"とコマンドラインに入力することで、
カレントディレクトリにhogeという作業ディレクトリが作成されます。
構成は以下です。
-------------------------------
hoge
├── hoge_Q.tex ... 問題texファイル
├── hoge_A.tex ... 解答texファイル
└── hoge.tex ... 問題と解答を1つにするtexファイル
-------------------------------
また、"bash cli.sh fuga/hoge"とコマンドラインに入力することで、
カレントディレクトリにfugaというディレクトリが作成され、
fugaの中にhogeという作業ディレクトリが作成されます。
構成は以下です。
-------------------------------
hoge
├── hoge_Q.tex ... texファイル1
├── hoge_A.tex ... texファイル2
└── hoge.tex ... 上記2つを1つにするtexファイル
-------------------------------
# オプション一覧
-h, --help ヘルプを表示
-v, --verbose デバッグ用なので気にしないで
-ot, --opentex エディタでTeXファイルを開く
-op, --openpdf 出力PDFファイルを開く
-c, --clear コンパイルで作られた一時ファイルを削除
-r, --rename 間違えたとき用に一括置換
EOF
exit
}
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
}
setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
}
msg() {
echo >&2 -e "${1-}"
}
die() {
local msg=$1
local code=${2-1}
msg "$msg"
exit "$code"
}
parse_params() {
opentex=0
openpdf=0
rename=''
clearfiles=0
while :; do
case "${1-}" in
-h | --help) usage ;;
-v | --verbose) set -x ;;
--no-color) NO_COLOR=1 ;;
-ot | --opentex) opentex=1 ;;
-op | --openpdf) openpdf=1 ;;
-c | --clear) clearfiles=1 ;;
-r | --rename)
rename="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
[[ ${#args[@]} -eq 0 ]] && die "作業ディレクトリを指定してください。"
return 0
}
parse_params "$@"
setup_colors
# ----------------------------------------------------------------
dirname=${args[0]}
filename=${dirname##*/}
if [[ -n $rename ]]; then
if [[ ! -d $dirname ]]; then
die "「$dirname」が存在しません。"
fi
echo -n "$dirnameから$renameへ一括置換をしますか? (y/n) >"
read do_rename
if [[ $do_rename = 'y' ]]; then
sed -i s/$dirname/$rename/g $dirname/$filename.tex
mkdir $rename
mv $dirname/$filename.tex $rename/$rename.tex
mv $dirname/${filename}_Q.tex $rename/${rename}_Q.tex
mv $dirname/${filename}_A.tex $rename/${rename}_A.tex
rmdir $dirname
echo "$dirnameから$renameへ一括置換しました"
else
echo "一括置換は行われませんでした"
fi
exit 0
fi
if [[ ! -d $dirname ]]; then
mkdir $dirname
cp template/QandA.tex $dirname/$filename.tex
cp template/Q.tex $dirname/${filename}_Q.tex
cp template/A.tex $dirname/${filename}_A.tex
sed -i s/DIRNAME/$filename/g $dirname/$filename.tex
fi
pdf=$dirname/$filename.pdf
if [[ $openpdf = 1 ]]; then
if [[ -f $pdf ]]; then
/mnt/c/Program\ Files/SumatraPDF/SumatraPDF.exe $pdf &
else
/mnt/c/Program\ Files/SumatraPDF/SumatraPDF.exe &
fi
fi
if [[ $clearfiles = 1 ]]; then
rm $dirname/*.aux $dirname/*.dvi $dirname/*.fdb_latexmk $dirname/*.fls $dirname/*.log $dirname/*.synctex.gz
fi
if [[ $opentex = 1 ]]; then
cd $dirname
vim *.tex
fi
僕の環境がUbuntu(WSL2)であるため、テキストエディタやPDFのビュワーの部分は各自で書き換えてもらうことにします。それ以外の部分=ファイル操作については、WindowsでもUbuntuでも動くように改善していきます。また、Pythonでは標準ライブラリのみ使います。
いざ書き換え
引数とヘルプ
シェルスクリプトでは、以下のように while
で引数を処理していました。
opentex=0
openpdf=0
rename=''
clearfiles=0
while :; do
case "${1-}" in
-h | --help) usage ;;
-v | --verbose) set -x ;;
--no-color) NO_COLOR=1 ;;
-ot | --opentex) opentex=1 ;;
-op | --openpdf) openpdf=1 ;;
-c | --clear) clearfiles=1 ;;
-r | --rename)
rename="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
[[ ${#args[@]} -eq 0 ]] && die "作業ディレクトリを指定してください。"
また、ヘルプの内容は、usage
という関数内に書いています。
usage() {
cat <<EOF
Usage:
bash $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-ot] [-op] [-r aftername] dirname
# これは何?
省略
# 詳しい使い方
省略
# オプション一覧
-h, --help ヘルプを表示
省略
EOF
exit
}
これをPythonで書き換えるには、argparse
モジュールを使います。
import argparse
p = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""
# これは何?
省略
# 詳しい使い方
省略
""",
)
# 位置引数
p.add_argument("dirname", help="作業ディレクトリ名")
# オプション引数
group = p.add_mutually_exclusive_group()
group.add_argument("-ot", "--opentex", help="エディタでTeXファイルを開く", action="store_true")
group.add_argument("-op", "--openpdf", help="出力PDFファイルを開く", action="store_true")
group.add_argument("-c", "--clear", help="コンパイルで作られた一時ファイルを削除", action="store_true")
group.add_argument("-r", "--rename", help="間違えたとき用に一括置換")
args = p.parse_args()
ArgumentParser
オブジェクトを作成し、add_argument
メソッドで引数を追加していきます。引数の値は、parse_args
の返り値から、args.dirname
, args.opentex
などのように得ることができます。
自分で --help
オプションを追加しなくても、add_argument
メソッドでhelp
に指定することで、usageやオプションがイイ感じに表示されます。
usage: cli.py [-h] [-ot | -op | -c | -r RENAME] dirname
positional arguments:
dirname 作業ディレクトリ名
optional arguments:
-h, --help show this help message and exit
-ot, --opentex エディタでTeXファイルを開く
-op, --openpdf 出力PDFファイルを開く
-c, --clear コンパイルで作られた一時ファイルを削除
-r RENAME, --rename RENAME
間違えたとき用に一括置換
今回はさらに、description
を指定しました。description
で指定したものは、ヘルプのusageとpositional argumentsの間に追加されます。
description
での改行はそのまま維持したいため、formatter_class
にはRawDescriptionHelpFormatter
を指定しました。
add_argument
メソッドでは、頭に-
を付けたらオプション引数、付けなければ位置引数になります。ここでaction="store_true"
にすることで、その引数が指定されればTrue、指定されなければFalseを得ることができます。
ArgumentParser
のオブジェクトでadd_mutually_exclusive_group
メソッドを使うことで、グループ内の引数は2つ以上同時に使うことができなくなります。usageにもそれが反映されていますね。
あとは、
if args.opentex:
pass
のように条件分岐で処理を書いていくだけです。
ファイル操作
UnixでもWindowsでも処理適切にパスを作成するためにos
モジュールを使います。
忙しい人のために(自分が後で見返しやすいように)、よく使いそうなものを羅列します。
os.sep
:パスの区切り文字の取得
os.path.join
:パスの結合
os.path.split
:パスの末尾とそれ以前を分割
os.path.exists
:パスが実在しているかどうか
os.rename
:リネーム
os.mkdir
:ディレクトリ作成
os.remove
:削除
覚えやすいですね~。
ファイルのコピーは、shutil.copyfile(src, dist)
のようにshutil
モジュールを使いました(これだけは頭の中にぱっと出てこなかったです)。
エディタ等の起動
テキストエディタやPDFビュワーを起動するために、subprocess
モジュールを使いました。
import subprocess as sb
if args.opentex:
sb.run("cd {} && vim *.tex".format(dirname), shell=True)
本来は第一引数にはコマンドと引数のリストを指定しますが、そのまま文字列で書きたかったためshell=True
を指定しました。出力を変えるときは、stdout
やstderr
で指定できます。
完成品
そんなわけで完成品はこちら
import argparse
import os
import re
import sys
import shutil
import subprocess as sb
p = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""
# これは何?
dirnameに作業ディレクトリ名を入力してください。
ディレクトリ構成は以下です。
-------------------------------
./
├── cli.py
└── template
├── A.tex
├── QandA.tex
└── Q.tex
-------------------------------
# 使い方
## 主な使い方
"python cli.py hoge"とコマンドラインに入力することで、
カレントディレクトリにhogeという作業ディレクトリが作成されます。
構成は以下です。
-------------------------------
hoge
├── hoge_Q.tex ... 問題texファイル
├── hoge_A.tex ... 解答texファイル
└── hoge.tex ... 問題と解答を1つにするtexファイル
-------------------------------
また、"python cli.py fuga/hoge"とコマンドラインに入力することで、
カレントディレクトリにfugaというディレクトリが作成され、
fugaの中にhogeという作業ディレクトリが作成されます。
構成は以下です。
-------------------------------
hoge
├── hoge_Q.tex ... 問題texファイル
├── hoge_A.tex ... 解答texファイル
└── hoge.tex ... 問題と解答を1つにするtexファイル
-------------------------------
以下省略
""",
)
# 位置引数
p.add_argument("dirname", help="作業ディレクトリ名")
# オプション引数
group = p.add_mutually_exclusive_group()
group.add_argument("-ot", "--opentex", help="エディタでTeXファイルを開く", action="store_true")
group.add_argument("-op", "--openpdf", help="出力PDFファイルを開く", action="store_true")
group.add_argument("-c", "--clear", help="コンパイルで作られた一時ファイルを削除", action="store_true")
group.add_argument("-r", "--rename", help="間違えたとき用に一括置換")
args = p.parse_args()
# hoge でも hoge/fuga でも hoge/ でも hoge/fuga/ でも対応 windowsでも対応
dirname = re.sub(repr(os.sep + "$")[1:-1], "", args.dirname)
filename = os.path.split(dirname)[-1]
dir_file_name = os.path.join(dirname, filename)
# 一括置換
if args.rename:
re_dirname = re.sub(repr(os.sep + "$")[1:-1], "", args.rename)
re_filename = os.path.split(re_dirname)[-1]
# ファイルチェック
if not os.path.exists(dirname):
print("「{}」が存在しません".format(dirname), file=sys.stderr)
sys.exit(1)
if os.path.exists(re_dirname):
print("「{}」は既に存在しています".format(re_dirname), file=sys.stderr)
sys.exit(1)
do_rename = input("{}から{}へ一括置換をしますか? (y/n) >".format(dirname, re_dirname))
if do_rename == "y":
# ファイル内の文字列を置換
with open(dir_file_name + ".tex") as f:
lines = f.read()
lines = lines.replace(filename, re_filename)
with open(dir_file_name + ".tex", mode="w") as f:
f.write(lines)
# ファイル名のリネーム
os.rename(dirname, re_dirname)
for qa in ["", "_Q", "_A"]:
os.rename(
os.path.join(re_dirname, filename + qa + ".tex"),
os.path.join(re_dirname, re_filename + qa + ".tex"),
)
print("{}から{}へ一括置換しました".format(dirname, re_dirname))
sys.exit(0)
else:
print("一括置換は行われませんでした")
sys.exit(0)
# テンプレートのコピー
if not os.path.exists(dirname):
os.mkdir(dirname)
# テンプレ文字列を置換
with open(os.path.join("template", "QandA.tex")) as f:
lines = f.read()
lines = lines.replace("DIRNAME", filename)
# テンプレをコピー
with open(dir_file_name + ".tex", mode="w") as f:
f.write(lines)
shutil.copyfile(os.path.join("template", "Q.tex"), dir_file_name + "_Q.tex")
shutil.copyfile(os.path.join("template", "A.tex"), dir_file_name + "_A.tex")
if args.openpdf:
pdf = os.path.join(dirname, filename + ".pdf")
if os.path.exists(pdf):
sb.run(
"/mnt/c/Program\ Files/SumatraPDF/SumatraPDF.exe {} &".format(pdf),
stdout=sb.DEVNULL,
stderr=sb.DEVNULL,
shell=True,
)
else:
sb.run(
"/mnt/c/Program\ Files/SumatraPDF/SumatraPDF.exe & ",
shell=True,
)
if args.clear:
for rf in [".aux", ".dvi", ".fdb_latexmk", ".fls", ".log", ".synctex.gz"]:
os.remove(dir_file_name + rf)
if args.opentex:
sb.run("cd {} && vim *.tex".format(dirname), shell=True)
僕がPythonのほうが慣れているのもあって、書きやすいな~と思いました。引数の解析とヘルプが一緒に書け、usageやオプションの表示も勝手にしてくれるので楽ですね。
argparse
にはチュートリアルもあります。
Discussion