これができれば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() { :; }
- mod1.sh
- pkg1/
func mod1.func pkg1.mod1.func
さらに階層が深いとき。
- packages/
- pkg1/
- sub1/
- sub11/
- mod.sh
func() { :; }
- mod.sh
- sub11/
- sub1/
- pkg1/
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通りある。
- 自分自身
$BASH_IMPORT_DIR
- 指定した絶対パス
このとき、識別子が完全に省略され、モジュール名しかないと、関数名プレフィクスが同一になってしまう。
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
の場合と重複しづらくなった。だが、まだ重複する可能性は残っている。
self
やsystem
といった名前のディレクトリやファイル名がある
さらに、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
- String/
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
- String/
ルールは以下の通り。
これによって期待どおりに実装できる。
mod.sh
という名前がわかりにくいことがある。そこでもうひとつルールを追加する。
これによって、以下のような使い分けができる。
- パッケージ名と実行スクリプト名を一元管理したいなら
- 実行スクリプト名を
mod.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とか別言語でやったほうが早い。
対象環境
- Raspbierry pi 4 Model B
- Raspbian buster 10.0 2019-09-26 ※
- bash 5.0.3(1)-release
$ uname -a Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux