nikkie-ftnextの日記

イベントレポートや読書メモを発信

パッケージを pip install して生えるコマンドの実体は、シバンでPythonを指定したテキストファイルでした

はじめに

書類に、なりたい... nikkieです。

真珠星ちゃんの「養子縁組って……くらいの衝撃の事実を、Pythonで知ってしまいました。

目次

pip installするとコマンドが生える

必ずではないですが、pip installするとコマンドが使えるようになります。

  • pip install pytest -> pytest
  • pip install ruff -> ruff
  • pip install sphinx -> sphinx-buildsphinx-quickstart

このコマンドの実体に迫ります。
動作環境は以下です。

仮想環境を作って、自作ライブラリ kojo-fan-art1 をインストールします。

% python3.12 -m venv .venv --upgrade-deps
% source .venv/bin/activate
(.venv) % python -m pip install kojo-fan-art

kojo-fan-artをインストールすると、kojo-dayコマンドが使えるようになります。

(.venv) % kojo-day kokoro
{"kokoro": "Thursday"}

コマンドの実体

仮想環境を出てもこのコマンドを実行できます。
仮想環境から出ると仮想環境にPATHが通らなくなるので、仮想環境下にあるコマンドの実体を指定します。

(.venv) % deactivate
% .venv/bin/kojo-day kokoro
{"kokoro": "Thursday"}

この.venv/bin/kojo-dayは、ファイルです。

 % ls -l .venv/bin/kojo-day
-rwxr-xr-x  1 (ユーザ名)  (グループ名)  271 12 22 20:31 .venv/bin/kojo-day

パーミッションは755ですね。
実行権限(x)が付いています。

ファイルということは中身を見てみると(cat .venv/bin/kojo-day

#!/.../.venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from the_solitary_castle_in_the_mirror.__main__ import main
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

テキストファイルでした(バイナリではなかった)。
Pythonなので、読める... 読めるぞ!

シバン

1行目のシバン(shebang)がポイントだと思います。

#!/.../.venv/bin/python

シバンまたはシェバン (英: shebang) とはUNIXスクリプトの #! から始まる1行目のこと。起動してスクリプトを読み込むインタプリタを指定する。

起動してスクリプトを読み込むインタプリタ仮想環境のPythonに指定しています!
Python処理系がこのスクリプトを実行するわけです。

過去に書いた「pytestpython -m pytestの何が違うのか」という記事を思い出しました(記事中はunkoコマンドで検証)

  • python -m unkosys.pathの先頭にカレントディレクトリを追加する
  • unkoコマンドはsys.pathの先頭に.venv/binを追加している

後者のunkoコマンドでのsys.pathですが、シバンで仮想環境のPython.venv/bin/python)を指定しているためかもしれません2

落穂拾い

正規表現は何?

sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])

sys.argv[0]を加工しているコード。
末尾に以下のパターンがあれば、それを空文字列に置き換えて削除しています

  • -script.pyw
  • または、.exe

pipx install

pipx installで生えるコマンドでも同様でした。

% pipx --version
1.7.1
% pipx install kojo-fan-art
% which kojo-day
/.../.local/bin/kojo-day
% ls -l $(which kojo-day)
lrwxr-xr-x  1 (ユーザ名)  (グループ名)  58 12 22 21:03 /.../.local/bin/kojo-day -> /.../.local/pipx/venvs/kojo-fan-art/bin/kojo-day
% head -n1 /.../.local/pipx/venvs/kojo-fan-art/bin/kojo-day
#!/.../.local/pipx/venvs/kojo-fan-art/bin/python

pipx runuvx3も一時的ですが仮想環境にインストールしているので、同様の仕組みと思われます。

終わりに

Pythonパッケージをインストールして使えるようになるコマンドがなぜ動くのか、その理由が分かりました。
インストールしたPython処理系をシバンで指定しているのですね。
つまりコマンドを動かすとき、裏でPythonを動かしているわけです(だから「遅い」という指摘になり、コマンドのRust実装(酸化)が進むのか)

コマンドを生やすにはpyproject.toml[project.scripts]を指定している4わけですが、この指定から今回見たファイルが作られる仕組みは
https://packaging.python.org/ja/latest/specifications/entry-points/#entry-points

pip (または console_scripts を認識する他のインストーラ) が配布物をインストールする際に、各エントリポイントのコマンドラインラッパを生成します。

ということのようです(こちらの方にも潜ってみたいな5