🌊

CUIコマンドを作る。uvとargcompleteを使って

に公開

3行で

  • uv init --package HOGEHOGE 最高
  • argcomplete 最強
  • なお、当方のシェルはfish
    • bash、zshの人も幸せになれるそうだけど未確認

「uvとかargparseとか今更...」という人は こちら

なぜPython?ここまでの道程

C言語から入り、Javaを通ってRoRからRubyに行って...
10数年のブランク [1] を経てWin10のゴニョゴニョからLinuxに戻ってきたのですが、日々のちょっとした作業とか簡略化しようというところからスクリプトを書くわけです。

ここのところ

当初は以下のような棲み分けでした。

  • 引数が1つで足りたり、ちょっと書き換えれば良いものなんかは fish の abbr を使用
  • ちょっと複雑だったり、オプション取ったりするものは bashスクリプト を作成
  • python のライブラリで楽が出来そうなら python で組む

というわけで、当初pythonは消去法で使っていたのです。
最近慣れてきましたが、ブロックを明示しないところとか、オブジェクトメソッドとしてあるべきものが関数だったりがしっくり来なくて。

でも、 getopt だったり、 getopts だったり、logの扱いでとっちらかったり。
少し間が開くと調べ直しなんですよね > bashスクリプト

なので、いいかげん bashスクリプト の雛形でも作ろうかな?というところで出会ってしまったのです。
argcomplete に。

uvについて

もう一つ、 python でコマンドを作るにあたって便利に使わせてもらっているのが uv なんです。

非常に高速 [2] ということで使い始めたのですが、 uv init --package PACKAGE_NAME が非常に強力。 [3]

╰─❯ uv init --package hogehoge
Initialized project `hogehoge` at `/home/foobar/work/hogehoge`
╰─❯ cd hogehoge
╰─❯ tree -a
.
├── .git
... snip ...
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
└── src
    └── hogehoge
        └── __init__.py

╰─❯ uv run hogehoge
Hello from hogehoge!

という感じで一気に雛形が出来上がる。
そして、 uv tool install . で ~/.local/bin へインストールしてくれる。
するとターミナルから hogehoge だけで実行できる。[4]

pyproject.toml の version で管理してくれる。 [5]

src配下を読んでくれるように設定してくれているので、 src/hogehoge/fugafuga.py なんてファイルを作ったとき、 import hogehoge.fugafuga で読み込んでくれる。
まぁ、当たり前といえば当たり前なんですけど、0スタートから手作業でここまで持ってくるのって結構手間じゃないです?

uvのインストール

英語だけどドキュメント充実しているので本家参照のこと。

https://github.com/astral-sh/uv

一応。
以下のコマンドでインストール。[6]

Install uv with our standalone installers:

# On macOS and Linux.
curl -LsSf https://astral.sh/uv/install.sh | sh

基本コマンドについては以下にまとめられているので(ry
ありがとうございます。

https://zenn.dev/thirdlf/articles/11-zenn-uv-tuyotuyo

argcompleteについて

argparse 等の記述からシェルの completions を作ってくれるというものです。
動的に引数の候補を提示したりということもできるそうなんですが、まだそこまでは使ってないです。
が、表面なぞっただけでも便利。

argparse

python標準の引数処理機構ですね。

下のサンプルは実際に使用しているコマンドの一部です。
実際には args_hogehoge() が続きます。
物によってはサブパーサーのサブパーサーが複数存在します。

src/pag_task/main.py
import argcomplete  # 後述
import argparse
... snip ...

from . import __version__
from pag_task.tools import prnpos
... snip ...


def main():
    args = args_parse()
    if hasattr(args, "handler"):
        # handler に設定された src/pag_task/tools/prnpos.py#watch_position() が呼ばれる
        args.handler(args)


def args_parse():
    parser = argparse.ArgumentParser(description="hogehoge")
    parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")

    sub = parser.add_subparsers(dest="cmd", required=True)
    args_utl(sub.add_parser("utl", help="ユーティリティ"))

... snip ...

    argcomplete.autocomplete(parser)    # 後述
    return parser.parse_args()


def args_utl(parser):
    sp = parser.add_subparsers(dest="scmd", required=True, title="subcommands of utl")
    sp_pp = sp.add_parser("print_position", aliases=["p"], help="マウス座標取得 'k' key",
                          description=dedent(prnpos.watch_position.__doc__ or ""),
                          formatter_class=argparse.RawDescriptionHelpFormatter)
    # handler に src/pag_task/tools/prnpos.py#watch_position() を設定
    sp_pp.set_defaults(handler=prnpos.watch_position)
src/pag_task/tools/prnpos.py
import pyautogui
from pynput import keyboard

def watch_position(args=None):
    """\
    マウス座標を取得する
      'k' キーで取得
      'ESC' で終了
    """
... snip ...

これによって、以下のようなヘルプが表示される、と。

╰─❯ pag_task -h
usage: pag_task [-h] [--version] {utl,...} ...

hogehoge

positional arguments:
  {utl,...}
    utl              ユーティリティ
... snip ...

options:
  -h, --help         show this help message and exit
  --version          show programs version number and exit
╰─❯ pag_task utl -h
usage: pag_task utl [-h] {print_position,p} ...

options:
  -h, --help            show this help message and exit

subcommands of utl:
  {print_position,p}
    print_position (p)  マウス座標取得 'k' key
╰─❯ pag_task utl p -h
usage: pag_task utl print_position [-h]

マウス座標を取得する
'k' キーで取得
'ESC' で終了

options:
  -h, --help  show this help message and exit

サブコマンドやオプションが増えてくると、自分で決めたのに大文字小文字を間違えてみたり、「--memoだったっけ?--noteだったっけ?」となるので、ちょくちょく -h を打つようになります。

argcomplete

長かったですが、ここで登場するのが argcomplete なわけです。
下のように要所で <TAB> を打てば候補を表示してくれるのです。

╰─❯ pag_task <TAB>
utl          (ユーティリティ)  
... snip ...
-h                (show this help message and exit)
--help            (show this help message and exit)
--version   (show programs version number and exit)

╰─❯ pag_task utl <TAB>
p               (マウス座標取得 'k' key)  -h      (show this help message and exit)
print_position  (マウス座標取得 'k' key)  --help  (show this help message and exit)

やること

上のサンプルで「後述」としていた部分です。

対象プロジェクトにて

ライブラリ argcomplete を追加。

uv add argcomplete

argparse の処理をしているpyファイルでインポート。

import argcomplete

パーサーの登録処理がすべて完了したところで、
argparse.ArgumentParser() で取得した大本の parser を登録。[7]

argcomplete.autocomplete(parser)

fishシェルにて

bashやzshの人は引数とかが異なってたはずなので調べてみてください。

コマンドのインストール

uv tool install argcomplete

register-python-argcomplete というコマンドが使用可能になるので以下を実行。[8]

register-python-argcomplete --shell fish hogehoge > ~/.config/fish/completions/hogehoge.fish

hogehoge はコマンド名。 pyproject.toml 内、以下の左辺。
逆にここを変更すればコマンド名を変えられるという、ね?すごいよね。 > uv

[project.scripts]
pag_task = "pag_task.main:main"

感想

長々と書いたけど、環境が出来上がってしまえば たった3行でfishのcompletionが実装される ってすごいことだと思う。
インポート、パーサーの登録、書き出しコマンド実行のみ。

fishの場合にはオプションとか毎回コマンドへ確認しに行っているようなのでタイムラグはあるものの、ヘルプを確認するより断然早い。
それぞれの意味をhelpの文言から表示してくれるのも非常にありがたい。
completionファイルの更新忘れとかも関係無いし。

bashやzshは静的ファイルを作成されると読んだような気も。
その場合は毎回更新必要なんだろうね?

脚注
  1. 当時はMacBookを使用。現在は10年近く前のthinkpadで....まぁ困ってはいない ↩︎

  2. 複数同時に処理してくれたり、キャッシュから持ってきて使ってくれる ↩︎

  3. 逆に --package 以外はほとんど使っていないので、どのへんが違うのか聞かれても困るのですが ↩︎

  4. ~/.local/bin がPATHに入っていれば ↩︎

  5. バージョン番号上がってなくても uv tool install . で上書きインストールしてくれる?
    けど、ライブラリを追加・削除したり、大きく変更を入れた時なんかはバージョン上がってないと上手く動かないケースがある。
    深くは追っていない。 ↩︎

  6. termuxは apt install uv で入る。一時期不安定だったけど、今は安定して動いている様子 ↩︎

  7. 上のサンプルでは p としているやつ ↩︎

  8. 使えないようなら PATH に ~/.local/bin が入っているか確認 ↩︎

Discussion