Pythonで簡単CLIツール作成。Python Fireを試してみた
こんにちは。サービスグループの武田です。
手元で実行するちょっとしたCLIツールを作成したくなること、ありますよね。いくつか選択肢はありますが、シェルスクリプトで作る人。Node.jsで作る人。Goで作る人。そしてPythonで作る人。今回はPythonでCLIツール作成をする際に便利そうなライブラリ、Python Fireを試してみました。
Python Fireとは
Python FireはPythonで定義した関数やメソッドをCLIで簡単に呼び出せるようにするライブラリです。とても多機能なので今回はすべてを紹介することはできませんが、興味を持たれた方はぜひドキュメントなども見てみてください。
環境
次のような環境で検証しています。
1 2 3 4 5 6 7 8 9 10 | $ sw_vers ProductName: Mac OS X ProductVersion: 10.14.6 BuildVersion: 18G2022 $ python3 -V Python 3.7.0 $ pipenv --version pipenv, version 2018.11.26 |
やってみた
今回は引数に渡したURLをいい感じにエンコード/デコードするツールを作成してみます。使用イメージは次のようになります。
1 2 3 4 5 6 7 8 | $ pipenv run python cli.py encode 'https://ja.wikipedia.org/wiki/日本語' https: //ja .wikipedia.org /wiki/ %E6%97%A5%E6%9C%AC%E8%AA%9E $ pipenv run python cli.py encode --username 'takeda.takashi@example.com' --password '++foo//bar==buz??' 'https://example.com/mypage' https: //takeda %2Etakashi%40example%2Ecom:%2B%2Bfoo%2F%2Fbar%3D%3Dbuz%3F%3F@example.com /mypage $ pipenv run python cli.py decode 'https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E8%AA%9E' https: //ja .wikipedia.org /wiki/ 日本語 |
文字コードの指定など作り込むとたいへんになってしまいますので、渡したURLをシンプルにUTF-8でエンコード/デコードします。また追加機能として、ベーシック認証のユーザー名とパスワードを指定できるオプションを追加してみます。
まずは基本的な動作を確認するために、プロジェクトを作成してメッセージを表示するだけの機能を実装してみましょう。pipenvで環境を作成し、fire
パッケージをインストールします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $ cd /path/to/ $ mkdir example-fire && cd $_ $ mkdir .venv $ pipenv install fire Creating a virtualenv for this project… Pipfile: /path/to/example-fire/Pipfile Using /usr/local/opt/python/bin/python3 .7 (3.7.0) to create virtualenv… ✔ Successfully created virtual environment! Virtualenv location: /path/to/example-fire/ .venv Creating a Pipfile for this project… Installing fire… Adding fire to Pipfile's [packages]… ✔ Installation Succeeded Pipfile.lock not found, creating… Locking [dev-packages] dependencies… Locking [packages] dependencies… ✔ Success! Updated Pipfile.lock (0f41c9)! Installing dependencies from Pipfile.lock (0f41c9)… 🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 3 /3 — 00:00:00 To activate this project's virtualenv, run pipenv shell. Alternatively, run a command inside the virtualenv with pipenv run. |
それでは機能を実装してみます。「Hello」だけを表示するシンプルなもので、文字列を関数の戻り値として返せばOKです。
1 2 3 4 5 6 7 | import fire def hello(): return 'Hello' if __name__ = = '__main__' : fire.Fire(hello) |
実行してみます。
1 2 | $ pipenv run python cli.py Hello |
動きました!一番素朴な実装は関数を定義し、fire.Fire()
に渡すだけです。これだけでCLIとしてその関数を実行できます。
今後の拡張を踏まえクラスにしてみます。hello
関数をCliクラスのメソッドに変更し、fire.Fire
に渡すものもCliクラスオブジェクトに変わっています。
1 2 3 4 5 6 7 8 9 | import fire class Cli( object ): def hello( self ): return 'Hello' if __name__ = = '__main__' : fire.Fire(Cli) |
クラスを渡した場合は、メソッド名をサブコマンドとして指定する書式になります。実行してみます。
1 2 | $ pipenv run python cli.py hello Hello |
めっちゃ簡単ですね!
なんとなくわかってきたところで本題のエンコード/デコードの機能を実装してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import fire import urllib.parse class Cli( object ): def encode( self , url): u = urllib.parse.urlparse(url) path = urllib.parse.quote(u.path) u = u._replace(path = path) query = urllib.parse.urlencode(urllib.parse.parse_qsl(u.query)) u = u._replace(query = query) return u.geturl() def decode( self , url): return urllib.parse.unquote(url) if __name__ = = '__main__' : fire.Fire(Cli) |
実行してみます。
1 2 3 4 5 | $ pipenv run python cli.py encode 'https://ja.wikipedia.org/wiki/日本語' https: //ja .wikipedia.org /wiki/ %E6%97%A5%E6%9C%AC%E8%AA%9E $ pipenv run python cli.py decode 'https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E8%AA%9E' https: //ja .wikipedia.org /wiki/ 日本語 |
いい感じです!CLIでパラメーターが欲しい場合はメソッドに引数を追加するだけで簡単に受け取れます。urllib.parse.quote
でURLエンコードが可能なのですが、単純に引数を渡してしまうとパラメーターの?=xxx
の部分もエスケープされてしまいます。いい感じにするため、一度parseしてからそれぞれの部分をエンコードするようにしました。
それでは最後にベーシック認証のユーザー名とパスワードを指定できるオプションを追加します。オプションも難しいことを考えずに、メソッドに引数を追加するだけです。必須ではないのでデフォルト値を指定し、仮引数名はCLIのオプションで使いたい名前と同じにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import fire import urllib.parse trans = str .maketrans({ '.' : '%2E' }) class Cli( object ): def encode( self , url, username = ' ', password=' '): userinfo = None if username: if password: username = urllib.parse.quote(username, safe = '').translate(trans) password = urllib.parse.quote(password, safe = '').translate(trans) userinfo = f '{username}:{password}' else : raise TypeError( 'Both username and password must be specified.' ) u = urllib.parse.urlparse(url) if userinfo: u = u._replace(netloc = f '{userinfo}@{u.netloc}' ) path = urllib.parse.quote(u.path) u = u._replace(path = path) query = urllib.parse.urlencode(urllib.parse.parse_qsl(u.query)) u = u._replace(query = query) return u.geturl() def decode( self , url): return urllib.parse.unquote(url) if __name__ = = '__main__' : fire.Fire(Cli) |
実行してみます。
1 2 | $ pipenv run python cli.py encode --username 'takeda.takashi@example.com' --password '++foo//bar==buz??' https: //example .com /mypage https: //takeda %2Etakashi%40example%2Ecom:%2B%2Bfoo%2F%2Fbar%3D%3Dbuz%3F%3F@example.com /mypage |
とってもいい感じですね!urllib.parse.quote
は_.-~
の4文字および/
をエスケープしません(/
はsafe
のデフォルト引数で指定されている)。nginxとChromeでしか動作確認をしていませんが、少なくとも./
の2文字はエスケープしないとベーシック認証が正しく動作しませんでした。そのためsafe
引数とtranslate
メソッドでエスケープされるようにしています。
まとめ
CLIツール作成のうえで面倒なのは複数モードやオプションのパースなどですが、そこが省力化できるのは素敵ですね。最初のリリース(v0.1.0)は2017年ということで、当時試してみた方もいると思われますが、今年の7月にv0.2.0がリリースされるなど開発は続けられています。使い勝手がどの程度変わっているのかは知りませんが、一度やってみた方もこれからの方もぜひ試してみてください。