可搬的なpythonツールのためのスケルトン作成ユーティリティ

# 動機

以前「[比較的小規模で可搬的なPython toolのスケルトン](https://qiita.com/Nanigashi_Uji/items/1f06060917b11244ff1d)」というのを作成した。

`python`スクリプトの実行時には**あらかじめ作成しておいた対応する名前のラッパースクリプトを叩くだけでよい**という利点があり、`virtualenv`,`venv`のように実行時にもシェルで`source`したり、`activate`,`deactivate`するのとは違う操作感が気に入って多用していた。

ただ多用しているうちに`python`スクリプトを書く段階での準備手順、(スケルトンディレクトリをコピー --> ファイル名変更 --> READMEファイルのcontents欄の修正 --> テストのためのpipでのモジュールインストール ... )が、案外めんどくさく感じられるようになってきた。この辺をさらに楽にするためにスクリプトを修正することにした。

# 実装方針 (お急ぎの方は次節へ)

- なによりも可搬性を最重要視し、1つのスクリプトファイルにする。その分、可読性(=メンテナンス性)が犠牲になってしまうのは甘受。
- 「[比較的小規模で可搬的なPython toolのスケルトン](https://qiita.com/Nanigashi_Uji/items/1f06060917b11244ff1d)」をベースにし、想定するディレクトリ構成を生成できる機能をもたせる。
- スクリプト自身は、作成したディレクトリ構成の下にコピーされ、以後コピーされたスクリプトは`python`スクリプトを実行するラッパースクリプトとして働く。
- Pythonスクリプトのテンプレートと、適切な名前でラッパースクリプトへのシンボリックリンクを作成する。
- それっぽい`Readme.md`ファイルも準備する。
- 必要に応じて、pipを用いて`python`モジュールを想定ディレクトリ構成の下にローカルにインストールする。
- python2とpython3を使い分けられるようにするが、基本python3をデフォルトにする。

# ファイル置き場

- [github.com: nanigashi-uji/runpyscr](https://github.com/nanigashi-uji/runpyscr)
- [gitlab.com: nanigashi_uji/runpyscr](https://gitlab.com/nanigashi_uji/runpyscr)

# 依存関係 (Requirements)

- Python
- grep
- sed
- awk
- (optional) GNU realpath (On the macOS, it is assumed that it is installed as `grealpath` like `macports` and so on.)

Tested at macOS Big Sur (11.7.2) 

# 使用法

実行スクリプトは、3つの動作モードがある。

## 1. 初期設定モード (init-mode) 

ファイルの実体(`runpyscr`)を、第一引数に`--manage`オプションをつけて呼び、サブコマンドとして`init`を指定する。最も単純な実行例としては、下記のように`/somewhere/pyworktool`以下に`bin`
,`lib`のディレクトリを作る。`bin`の下に自分自身をコピーを作成し、`lib`以下にコマンドラインで指定した名前(例では`pywork`)を元に`python`スクリプトテンプレート(`pywork3.py`)を置き、それに対応して`bin`以下にシンボリックリンク(`pywork3`)を設置する。`-3`は明示的にPhython3を想定するオプション。

```shell:実行例(init-mode)
% ./runpyscr --manage -p /somewhere/pyworktool -3 init pywork
% (cd /somewhere/ ; find pyworktool -ls )
 drwxr-xr-x   ...... pyworktool
 drwxr-xr-x   ...... pyworktool/bin
 -rwxr-xr-x   ...... pyworktool/bin/runpyscr
 lrwxr-xr-x   ...... pyworktool/bin/mng_pyenv -> runpyscr
 lrwxr-xr-x   ...... pyworktool/bin/mng_pyenv2 -> runpyscr
 lrwxr-xr-x   ...... pyworktool/bin/mng_pyenv3 -> runpyscr
 lrwxr-xr-x   ...... pyworktool/bin/pywork -> runpyscr
 lrwxr-xr-x   ...... pyworktool/bin/pywork3 -> runpyscr
 drwxr-xr-x   ...... pyworktool/lib
 drwxr-xr-x   ...... pyworktool/lib/python
 drwxr-xr-x   ...... pyworktool/lib/python/site-packages
 -rw-r--r--   ...... pyworktool/lib/python/pywork3.py
 lrwxr-xr-x   ...... pyworktool/lib/python/pywork.py -> pywork3.py
```

初期設定モードで、`-r`オプションを追加するとそれっぽい`README.md`を、`-g`をつけるとそれっぽい`.gitignore`とかGit関連のファイルも設置する。

## 2. 管理モード(manage-mode)

初期モードを実行すると、`mng_pyenv{,2,3}`というシンボリックリンクができている。このシンボリックでスクリプトが実行されると、`runpyscr --manage`と実行されたときとおなじ動作になる。(`mng_pyenv2`は`runpyscr --manage -2`,`mng_pyenv3`は`runpyscr --manage -3`と同等。)
このモードで、サブコマンド`install`とともに必要なpython module名を指定すると、`pip`によりpython moduleがローカルにインストールされる。

```shell:実行例(manage-mode)
% /somewhere/pyworktool/bin/mng_pyenv3 install pytz tzlocal
Collecting install
  Downloading install-1.3.5-py3-none-any.whl (3.2 kB)
Collecting pytz
  Using cached pytz-2022.7.1-py2.py3-none-any.whl (499 kB)
Collecting tzlocal
  Using cached tzlocal-4.2-py3-none-any.whl (19 kB)
.....
Installing collected packages: install, pytz, tzdata, backports.zoneinfo, pytz-deprecation-shim, tzlocal
Successfully installed backports.zoneinfo-0.2.1 install-1.3.5 pytz-2022.7.1 pytz-deprecation-shim-0.1.0.post0 tzdata-2022.7 tzlocal-4.2

% ls  /somewhere/pyworktool/lib/python/site-packages/3.8/
backports                           install                  pytz                     pytz_deprecation_shim                        tzdata                   tzlocal
backports.zoneinfo-0.2.1.dist-info  install-1.3.5.dist-info  pytz-2022.7.1.dist-info  pytz_deprecation_shim-0.1.0.post0.dist-info  tzdata-2022.7.dist-info  tzlocal-4.2.dist-info
```

## 3. 実行モード(run-mode)

スクリプトが`mng_pyenv{,2,3}`以外の名前のシンボリックリンクから実行された場合には、`lib/python/シンボリックリンク名.py`というファイルに環境変数`PYTHONPATH`を適切に設定して実行する。また、`runpyscr`の第一引数が`--manage`以外の場合、`lib/python/{第一引数}.py`というファイルか、`{第一引数}`というファイルを探して、第二引数以降のコマンドライン引数そのまま渡してを　スクリプトファイルを実行する。下記の実行例では3つとも同じスクリプトが動作する。

```shell:実行例(run-mode)
% ~/tmp/pyworktool/bin/pywork3 Japan
Hello, Japan!
Python : 3.8.9 (/Applications/Xcode.app/Contents/Developer/usr/bin/python3)
....

% /somewhere/pyworktool/bin/runpyscr pywork3 Japan
Hello, Japan!
Python : 3.8.9 (/Applications/Xcode.app/Contents/Developer/usr/bin/python3)
....

% /somewhere/pyworktool/bin/runpyscr  /somewhere/pyworktool/lib/python/pywork3.py  Japan
Hello, Japan!
Python : 3.8.9 (/Applications/Xcode.app/Contents/Developer/usr/bin/python3)
....
```
# 追加機能

## 1.初期モードでのやりたいことを一辺に

初期モードでは、`-m`オプションとともにmodule名を指定すると、`pip`によるモジュールインストールも一気に行える。(複数指定可能)。また、`-r`オプションとともに`-t`オプションで作成する`REAME.md`ファイルのタイトル部分も指定できる。また、作成するpythonのテンプレートも複数指定できる。
```shell:実行例(フルオプション)
% ./runpyscr --manage -p /somewhere/pyworktool -g -r -t 'Worktool by Python' -3 -m pytz -m tzlocal init prepwork mainwork
Collecting pytz
  Using cached pytz-2022.7.1-py2.py3-none-any.whl (499 kB)
Collecting tzlocal
  Using cached tzlocal-4.2-py3-none-any.whl (19 kB)
......
Installing collected packages: pytz, tzdata, backports.zoneinfo, pytz-deprecation-shim, tzlocal
Successfully installed backports.zoneinfo-0.2.1 pytz-2022.7.1 pytz-deprecation-shim-0.1.0.post0 tzdata-2022.7 tzlocal-4.2

% ( cd /somewhere ; find pyworktool -ls ) 
...... pyworktool
...... pyworktool/README.md
...... pyworktool/.gitignore
...... pyworktool/bin
...... pyworktool/bin/runpyscr
...... pyworktool/bin/mng_pyenv -> runpyscr
...... pyworktool/bin/mng_pyenv2 -> runpyscr
...... pyworktool/bin/mng_pyenv3 -> runpyscr
...... pyworktool/bin/prepwork -> runpyscr
...... pyworktool/bin/prepwork3 -> runpyscr
...... pyworktool/bin/mainwork -> runpyscr
...... pyworktool/bin/mainwork3 -> runpyscr
...... pyworktool/lib
...... pyworktool/lib/python
...... pyworktool/lib/python/mainwork.py -> mainwork3.py
...... pyworktool/lib/python/mainwork3.py
...... pyworktool/lib/python/prepwork.py -> prepwork3.py
...... pyworktool/lib/python/prepwork3.py
...... pyworktool/lib/python/site-packages
...... pyworktool/lib/python/site-packages/.gitkeep
...... pyworktool/lib/python/site-packages/3.8
...... pyworktool/lib/python/site-packages/3.8/tzlocal-4.2.dist-info
......
```
また、`README.md`の中身もコマンドライン引数で指定したタイトルから始まってそれっぽくなっているはず。

```shell
% cat /somewhere/pyworktool/README.md 
#
# Worktool by Python
#

Skeleton for small portable tools by python script

- Contents:

  1.  README.md:                         This file
  2.  bin/mng_pyenv:                     Symblic link to 'runpyscr' for installing Python modules by pip locally.
  2a. bin/mng_pyenv2:                    Same as above using pip2 as default
  2b. bin/mng_pyenv3:                    Same as above using pip3 as default

  3.  bin/runpyscr:                      Wrapper bash script to invoke Python script. (Entity)

  4.  lib/python/site-packages:          Directory where python modules are stored

  5.  lib/python/prepwork.py:            Example Python script that use modules
  5a. lib/python/prepwork3.py:           Same as above using python3 as default

  6.  bin/prepwork:                      Symbolic link to 'runpyscr' to invoke prepwork.py.
  6a. bin/prepwork3:                     Same as above using python3 as default

  7.  lib/python/mainwork.py:            Example Python script that use modules
  7a. lib/python/mainwork3.py:           Same as above using python3 as default

  8.  bin/mainwork:                      Symbolic link to 'runpyscr' to invoke mainwork.py.
  8a. bin/mainwork3:                     Same as above using python3 as default

  9.  .gitignore:                        Git-related file
  10. lib/python/site-packages/.gitkeep: Git-related file to keep modules directory in repository.
```
## 2. テンプレートの追加

管理モードでサブコマンド`add`を使うと、スクリプトのテンプレートを追加することができる。
```shell:実行例(pythonスクリプトテンプレートの追加)
% /somewhere/pyworktool/bin/mng_pyenv -r add postwork
% ( cd /somewhere ; find pyworktool -ls ) ...... 
...... pyworktool/bin/postwork3 -> runpyscr
...... pyworktool/bin/postwork -> runpyscr
...... 
...... pyworktool/lib/python/postwork3.py
...... pyworktool/lib/python/postwork.py -> postwork3.py
...... 
```
新しいpythonスクリプトのテンプレートとその実行のための`runpyscr`へのシンボリックリンクが作成される。それに加えて、下記のように`README.md`にも記述が追記される。

```shell:
% cat /somewhere/pyworktool/README.md
......
  11.  lib/python/postwork.py:            Example Python script that use modules
  11a. lib/python/postwork3.py:           Same as above using python3 as default

  12.  bin/postwork:                      Symbolic link to 'runpyscr' to invoke postwork.py.
  12a. bin/postwork3:                     Same as above using python3 as default
......
```

# 応用

## Pythonテンプレートの内容の変更すれば...

`runpyscr`は、自分自身の
```########## __py_template_start__ ##########```
と書かれた行と
```########## __py_template_end__ ##########```
と書かれた行で挟まれた部分(`#`の数は1個以上あればいくつでもよい)を`sed`で抽出し、
```## __py_shebang_pattern__ ##```
の行を、コマンドライン引数、もしくは環境変数`PYTHON`、もしくは環境変数`PATH`の中で見つけた`python`に応じてシェバンに置き換えて作成している。なので、**`runpyscr`のこの部分を書き換えることで、作成されるpythonテンプレートの内容を変えることができる**。

## Python script の持ち歩き／配布

さらに別の使い方として、**ある環境で作成したpythonスクリプトを他の環境に持っていって使ったり、他の人に使ってもらう際にファイルを配布するために使うことができる**。前述の通り、このスクリプトはファイルの実体が直接実行されるか、シンボリックで別名で実行されるかで動作を変えているが、**`runpyscr`というファイル名そのものは動作モードの判定に使っていない**ので、適宜別名に変えてもよい。たとえば、`hostA`でよく使っているpythonスクリプト`tool.py`を他の環境`HostB`に持っていくときに、

```shell:pythonスクリプトの配布に利用する例。
UserA@HostA % cp -ai 'runpyscr' '/somewhere/tool-dist'
UserA@HostA % emacs /somewhere/tool-dist # py_templete の部分をtool.pyの内容で書き換える。
UserA@HostA % scp /somewhere/tool-dist UserB@HostB:/tmp/tool-dist

UserB@HostB % /tmp/tool-dist --manage -p /destination/util -g -r -t 'Tool by UserA@HostA' -3 -m pytz -m tzlocal -m other-python-module init tool
UserB@HostB % /destination/util/bin/tool # 実行可能
```
といった方法が使える。`HostB`で`pip`でモジュールをインストールする権限がない場合にとくに有用。頑張れば、`tool-dist`の該当部分を`tool.py`を置き換えるのも、`sed`で自動化(スクリプト化)することもできそう。

Pythonのテンプレート部分だけでなく、`.gitignore`や`README.md`も`runpyscr`のテンプレート部を書き換えればカスタマイズできる。ただし、sedでいくつかキーワード置換している点には注意が必要かもしれない。

### 複数のPythonスクリプトの持ち歩き／配布

Pythonスクリプトのテンプレートには名前をつけることも可能である。

もし、`init`サブコマンドや`add`コマンドの引数で与えられたスクリプト名が`{scriptname}`だった時に、もし自分自身のファイルに
`########## __{scriptname}_template_start__ ##########`
と書かれた行と
`########## __{scriptname}_template_end__ ##########`
で挟まれた行(`#`の数は1個以上あればいくつでもよい)がある場合には、この間の行がPythonスクリプトのテンプレートとして使われる。

この機能により複数のpythonスクリプトをセットで配布することが可能になる。

```shell:複数のpythonスクリプトの配布に利用する例。
UserA@HostA % cp -ai 'runpyscr' '/somewhere/tool-dist'
UserA@HostA % emacs /somewhere/tool-dist # py_templeteの末尾に下記を追記する。
UserA@HostA % cat /somewhere/tool-dist
....
########## __scriptA_template_start__ ##########
## __py_shebang_pattern__ ##
# -*- coding: utf-8 -*-

... scriptAの内容

########## __scriptA_template_end__ ##########
########## __scriptB_template_start__ ##########
## __py_shebang_pattern__ ##
# -*- coding: utf-8 -*-

... ScriptBの内容

########## __scriptB_template_end__ ##########UserA@HostA % scp /somewhere/tool-dist UserB@HostB:/tmp/tool-dist

UserB@HostB % /tmp/tool-dist --manage -p /destination/util -g -r -t 'Tool by UserA@HostA' -3 -m pytz -m tzlocal init scriptA scriptB
UserB@HostB % /destination/util/bin/scriptA # 実行可能
UserB@HostB % /destination/util/bin/scriptB # 実行可能
```

# おまけ （コマンドラインヘルプ)
```shell:管理モードで-hオプションで表示。
% ./runpyscr --manage -h
[Usage]        % mng_pyenv{,2,3}   [options] sub-command [arguments]
               % runpyscr --manage [options] sub-command [arguments]
[sub-commands] 
               % runpyscr --manage [options] init [scriptnames] ... setup directory tree,
                                                                        and prepare templates if arguments are given.
               % runpyscr --manage [options] add script_names   ... prepare python script templates.
               % runpyscr --manage install  module_names        ... Install python module with pip locally.
               % runpyscr --manage download module_names        ... Download python module with pip locally.
               % runpyscr --manage clean                        ... Delete local python module for corresponding python version.
               % runpyscr --manage distclean/cleanall/allclean  ... Delete local python module for all python version.
               % runpyscr --manage info                         ... Show information.
[Options]
               -h     : Show this message
               -P arg : specify python command / path
               -v     : Show verbose messages
               -q     : Supress verbose messages (default)
               -n     : Dry-run mode.
               -2     : Prioritize Python2 than Python3
               -3     : Prioritize Python3 than Python2
[Options for sub-command: setup/init ]
               -p     : prefix of the directory tree. (Default: Grandparent directory if the name of parent directory of 
                        runpyscr is bin, otherwise current working directory.)
               -M     : moving this script body into instead of copying
               -g     : setup files for git
               -G     : do not setup files for git (default)
               -r     : setup/update README.md
               -R     : do not setup/update README.md (default)
               -k     : keep backup file of README.md when it is updated.
               -K     : do not keep backup file of README.md when it is updated. (default)
               -t arg : Project title
[Options for sub-command: install/download ]
               -i arg : specify pip command
```