やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Bashインポート要件

 これができればOK。超大変そう。

前回

. import lib.sh

 最低限のインポートができた。それはいいが、以下のような問題がある。

 今回はこれをできるだけ解消する要件を考えてみる。

要件

  • パス指定
  • 名前(重複、リネーム)

パス

 どのシェルスクリプトファイルをどこから.(source)するか。決定する方法は以下。

import /tmp/lib.sh # 絶対パス
import ./lib.sh    # 呼出元からの相対パス
import lib.sh      # $BASH_IMPORT_DIRからの相対パス

 たとえば以下のような条件だったとする。

BASH_IMPORT_DIR=/home/pi/bash/packages
  • /some/
    • lib.sh
  • /home/pi/bash/packages/
    • lib.sh
  • /tmp/work/
    • lib.sh
    • main.sh

 各importコマンド時の参照パスはコメントの通り。

main.sh

import /some/lib.sh # /some/lib.sh
import ./lib.sh     # /tmp/work/lib.sh
import lib.sh       # /home/pi/bash/packages

循環参照

 注意すべきは循環参照。無限ループしてしまう。やってしまったらどうにもならない。

 以下のように互いに参照し合うと循環参照。無限ループに陥る。

lib1.sh

. import ./lib2.sh

lib2.sh

. import ./lib1.sh

名前

 スクリプトファイル内のグローバル空間にある関数や変数の名前。

 importしたとき、プレフィクスにファイルやディレクトリ名などをつけることで名前重複を避ける。

重複

 名前重複すると、後者が優先される。これは変数でも同じと思われる。

a.sh

F() { echo 'F1'; }
F() { echo 'F2'; }
F
$ a.sh
F2

 つまり名前重複すると前者が参照できなくなってしまう。この問題を回避するためリネームする。

リネーム

 名前重複を避けるための対策。ディレクトリ名、ファイル名などを関数名の前にプレフィクスとして付与する。以下のように。

lib1.sh

F() { :; }

lib2.sh

F() { :; }

main.sh

. import ./lib1.sh ./lib2.sh
lib1.F
lib2.F

要件を満たすimportコマンド体系を考える

パス

. import lib.sh          # root=`$BASH_IMPORT_DIR`
. import ./lib.sh         # root=`import`呼出元ファイルが存在するディレクトリ
. import ./sub/lib.sh     # root=`import`呼出元ファイルが存在するディレクトリ
. import ../tests/test.sh   # root=`import`呼出元ファイルが存在するディレクトリ
. import /tmp/lib.sh      # root=`/tmp`

 importするルートディレクトリパスは、パスの先頭文字によって変動する。

パスの先頭文字 importするルートディレクトリパス
/ 絶対パスWindows? 知らんな)
./, ../ 呼出元ファイルが存在するディレクト
$BASH_IMPORT_DIR

名前

 importした関数などのオブジェクト名をどうするか。名前空間のプレフィクスを付与したいときもあるはず。パターンを網羅してみる。

  • packages/
    • pkg1/
      • mod1.sh
        • func() { :; }
func
mod1.func
pkg1.mod1.func

 さらに階層が深いとき。

  • packages/
    • pkg1/
      • sub1/
        • sub11/
          • mod.sh
            • func() { :; }
func
mod.func
sub11.mod.func
sub1.sub11.mod.func
pkg1.sub1.sub11.mod.func

 そもそもパッケージ構造とはまったく別の名前を付けたいときがあるかもしれない。重複せず短縮するにはリネーム以外の手段がないときもあるだろう。

 上記を考慮しつつ、ひとつずつ考えていく。

リネーム

 以下のようにインポートしたとする。

. import lib.sh          # root=`$BASH_IMPORT_DIR`

 以下のうち、どの名前にすべきか。これを選択できるようにしたい。

lib.func
func

 第2引数で付与するプレフィクスの階層数を指定する。(ただし複数ファイルインポート時と区別がつかなくなる)

. import lib.sh -0
func
. import lib.sh -1
lib.func

 デフォルトではフルネームとする。

. import lib.sh
lib.func

 階層が深いときの例。

. import pkg1/sub1/sub11/lib.sh
pkg1.sub1.sub11.lib.func
. import pkg1/sub1/sub11/lib.sh -0
func
. import pkg1/sub1/sub11/lib.sh -1
lib.func
. import pkg1/sub1/sub11/lib.sh -2
sub11.lib.func
. import pkg1/sub1/sub11/lib.sh -3
sub1.sub11.lib.func
. import pkg1/sub1/sub11/lib.sh -4
pkg1.sub1.sub11.lib.func

 プレフィクスを付与する。

. import ./lib.sh in self
self.lib.func
. import ./lib.sh in A.B.C
A.B.C.lib.func

 モジュール名まですべて削除しつつ、別のプレフィクスを付与する。

. import ./lib.sh -0 in A.B.C
A.B.C.func

 階層が深くても同様。

. import pkg1/sub1/sub11/lib.sh -0 in A.B.C
A.B.C.func

 まったく別名にする。これはimport ... -0 in ...と同義である。

. import pkg1/sub1/sub11/lib.sh as L
L.func

 ところで、importによっては名前が重複することがある。たとえば、ルートが異なりモジュール名が同一のファイルlib.shをインポートしたとき。関数名はすべて同一のfuncになってしまう。

. import lib.sh          # root=`$BASH_IMPORT_DIR`
. import ./lib.sh         # root=`import`呼出元ファイルが存在するディレクトリ
. import ./sub/lib.sh     # root=`import`呼出元ファイルが存在するディレクトリ
. import ../tests/lib.sh  # root=`import`呼出元ファイルが存在するディレクトリ
. import /tmp/lib.sh      # root=`/tmp`(先頭が`/`なら絶対パス)

 import時のルートは以下の3通りある。

 このとき、識別子が完全に省略され、モジュール名しかないと、関数名プレフィクスが同一になってしまう。

    lib.func # import lib.sh
    lib.func # import ./lib.sh
tmp.lib.func # import /tmp/lib.sh

 つまりルートが以下2つの相対パスのとき、プレフィクスが同一になってしまう。

  • 自分自身
  • $BASH_IMPORT_DIR

 PythonでもこれによってシステムAPIと同一ファイル名を名付けてしまい、システムAPIをインポートできないバグが生じてしまう。これをどうにか区別したい。

 たとえばルートによって固有のプレフィクスを付与するとか。

ルート プレフィクス
自分自身 self
$BASH_IMPORT_DIR system
. import lib.sh          # root=`$BASH_IMPORT_DIR`
system.lib.func
. import ./lib.sh            # root=`import`呼出元ファイルが存在するディレクトリ
self.lib.func

 ただ、冗長になりすぎる。重複したときに通知してくれるだけのほうが助かる。そもそも重複チェックの実装はできるのか? 要調査。

プレフィクスself

 そこで自パッケージ内のときはselfという特殊プレフィクスを持たせてみる。

. import ./lib.sh
self.lib.func
. import lib.sh
lib.func
. import /tmp/lib.sh
tmp.lib.func

 これで$BASH_IMPORT_DIRの場合と重複しづらくなった。だが、まだ重複する可能性は残っている。

  • selfsystemといった名前のディレクトリやファイル名がある

 さらに、selfはどこをルートにすべきかが不明瞭である。

  • selfは自パッケージのルートディレクトリをルートとすべきである
  • selfは自ファイルが存在するディレクトリをルートとすべきである

 以下の場合を考えてみる。

  • pkg1/
    • main.sh
    • sub1/
      • sub.sh
      • sub11/
        • lib.sh

 main.shのときは問題ない。

main.sh

. import ./sub1/sub11/lib.sh
self.sub1.sub11.lib.func

 subは問題になる。

sub.sh

. import ./sub11/lib.sh
self.sub11.lib.func      # root=自ファイル存在ディレクトリ
self.sub1.sub11.lib.func # root=自パッケージのルート

 subの場合、どちらをrootにすべきか不明瞭である。

 仮に「自パッケージのルート」だとしたとき、sub1をどうやって取得する? これが問題。言い換えると、import呼出元ファイルからみて、どこがこのパッケージのルートか?

 importには自分自身をルートとしているためsub1がない。親をたどるとしても、どこまで辿るか。「README.mdがあればルートである」のようなルールが必要か。

 仮にそれでいいとしても、パッケージの中にサブパッケージがあった場合はどうなるのか。これは解決困難か。サブパッケージがルートとなるが、それでいいのか。「パッケージのルート」という意味では問題ないはずだが、最上位パッケージをルートにすべきという解釈もある。それを制御するにはルートを指定する機構が必要。すごく面倒。selfプレフィクスを付与するのは難しいか?

 いっそパッケージのルートでなく、自分自身をルートとしてselfを付与してもいいかも?

 むしろプレフィクス付与はすべてユーザ自身に都度ゆだねてもいいかも? 必ずしも重複するとは限らないのだから。

. import lib.sh in sys             # sys.lib.func
. import ./lib.sh in mod            # mod.lib.func
. import ./sub/lib.sh in mod        # mod.sub.lib.func
. import ../tests/lib.sh in mod     # mod.test.lib.func
. import /tmp/lib.sh              # tmp.lib.func

 でもルートごとのプレフィクスを指定したいだけなら別の手段を用意してもよさそう。

BASH_IMPORT_PREFIX_ABS=sys
BASH_IMPORT_PREFIX_REL=mod
. import lib.sh          # sys.lib.func
. import ./lib.sh         # mod.lib.func
. import ./sub/lib.sh     # mod.sub.lib.func
. import ../tests/lib.sh  # mod.test.lib.func
. import /tmp/lib.sh      # tmp.lib.func

パッケージの構造

 importする対象は*.shファイルである。ただし内部で使用するだけの非公開スクリプトファイルを持ちたい場合がある。このときはパッケージというディレクトリ構造を持たせる。

  • $HOME/bash/packages/
    • String/
      • String.sh
      • String.partial.sh
BASH_IMPORT_DIR=$HOME/bash/packages/
BASH_IMPORT_PREFIX_ABS=sys
BASH_IMPORT_PREFIX_REL=mod
. import String/String.sh

 こんなインポートは嫌だ。

 以下のようにしたい。

. import String
String.Func

 何が嫌か? 

  • importが冗長(同じ名前を連続で記述する)
  • ディレクトリ名とファイル名がDRYに書けない(名前変更時が面倒)

 以下のようにしてみてはどうか? rust言語のcrateにおけるmod.rsやhtmlのindex.htmlと類似。

  • $HOME/bash/packages/
    • String/
      • mod.sh
      • mod.partial.sh

 ルールは以下の通り。

  • パッケージの実行スクリプトファイル名は必ずmod.shである
  • それをインポートするときは一つ上のディレクトリ名を指定する

 これによって期待どおりに実装できる。

 mod.shという名前がわかりにくいことがある。そこでもうひとつルールを追加する。

  • パッケージの実行スクリプトファイル名は以下のうちのいずれかである
    • mod.sh
    • パッケージ名.sh
  • それをインポートするときは一つ上のディレクトリ名を指定する

 これによって、以下のような使い分けができる。

  • パッケージ名と実行スクリプト名を一元管理したいなら
  • 可読性を向上させたいなら
    • 実行スクリプト名をパッケージ名と同じにする(名前の二重管理が必要。実行ファイル名喪失可能性あり)

 ただし、パッケージ名と同じスクリプト名を、実行スクリプトでなくサブスクリプトにしたいときがあるかもしれない。そんなややこしい事態は避けるべき。だが、名前を変更したときに偶然たまたまそうなってしまうことはありうる。よって実行スクリプトファイル名はmod.sh固定にしたほうが安全。追加したルールもないほうが安全か。

問題

 内部パッケージ作成と管理が面倒。いちいちsub_name/mod.shの2つを作らねばならない。さらにファイル単独だと名前が失われてしまう。コピペするときはディレクトリも必要になってしまう。

パッケージ化・単一ファイル化

 パッケージを単一ファイル化できないか? パッケージ化と単一化を相互変換できるようにしたい。

形態 特徴 使いどころ
パッケージ化 コードのメンテがしやすい 1ファイルあたりのコード量が少ないため問題箇所特定しやすい
単一ファイル化 ファイル移行時に環境構築しやすい メールなどを介して渡す時ファイル1つで済む

 どちらも一長一短。どちらでも読書できるようにしたい。

 例。

  • pkg1/
    • mod.sh
    • sub/
      • lib.sh

pkg1/mod.sh

. import sub/lib.sh
echo 'Called main.'
sub.lib.func

pkg1/sub/lib.sh

func() { echo 'Called sub/lib.sh func()'; }

 これを単一ファイル化すると以下。

pkg1.sh

sub.lib.func() { echo 'Called sub/lib.sh func()'; }
echo 'Called main.'
sub.lib.func

 この単一ファイルをパッケージ化すると先述のディレクトリやファイルとなる。

所感

  • 本当にここまで実装する必要ある?
  • 技術的に確認すべきことが多数ある

 当然だが、ふつうにPythonとか別言語でやったほうが早い。

対象環境

$ uname -a
Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux