シェルスクリプトプロジェクトにおける共通関数・定数の管理

# シェルスクリプトプロジェクトってなんやねん
世の中にそんな地獄案件がどれほどあるのか知りませんが、
とりあえず今自分は環境構築・設定ファイル同期用のスクリプト群を１つのリポジトリ（[GitHub](https://github.com/ikngtty/dotfiles)）にまとめてます。
いわゆるdotfilesってやつです。

その中で、共通でよく使う定数やutil関数を括り出したい欲求が出てきました。
例えばこんなやつ↓

```common.sh
#!/bin/sh
set -Ceu

# 定数
login_shell=fish
config_dir_path="$HOME/Config"

# util関数
echo_with_color() {
  case "$1" in
    red)
      color_code='\e[31m'
      ;;
    green)
      color_code='\e[32m'
      ;;
    yellow)
      color_code='\e[33m'
      ;;
    magenta)
      color_code='\e[36m'
      ;;
  esac
  shift

  printf "$color_code"
  for msg in "$@"; do
    printf "$(echo "$msg" | sed \
      -e 's/<b>/\\e\[1m/g'\
      -e 's/<\/b>/\\e[m\'"$color_code"'/g')"
  done
  printf '\e[m'
  echo # 改行用
}

echo_err() {
  echo_with_color red "<b>ERROR</b>: " "$@"
}

echo_log() {
  echo_with_color magenta "LOG: " "$@"
}
```

ところがどっこい、残念ながらシェルスクリプトには変数やら関数やらオブジェクトやらをexport/importするようなモジュール化の仕組みはありません。

# 最初に思いついたやつ
外部スクリプトに対してできることは、

* 実行する
* `.`（ドット）コマンドで読み込む。（Bashでは`source`コマンドとも。）

の2択しかありません。
実行するだけでは変数や関数は結局ロードできないので、ドットコマンドで読み込んでみます。

```main.sh
#!/bin/sh
set -Ceu

# プロジェクトフォルダの絶対パス取得
cd "$(dirname "$0")"  # このスクリプトがあるフォルダ
cd ./                 # ＝プロジェクトフォルダとします。
dir_project=$(pwd)

# 共通部分読み込み
. "$dir_project/common.sh"

# 使用
echo_with_color yellow 'わぁい'"$login_shell"' あかり'"$login_shell"'大好き'
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/140384/f78bb0e7-e71f-fbee-004f-90270aa99872.png)

できました。

## 問題点
名前空間が区切られてないので、`main.sh`と`common.sh`が密結合なんですよね。
互いの変数・関数が流出して良からぬ動きをするかもしれない。

実は上の例ではわざとバグりそうな穴を作ってあります。

```（続き）main.sh
color_code='イェ〜イ　エンジニア見てる〜？　実装失敗してどうだった？'
echo_with_color RED 'あなたは赤い部屋が好きですか？'

echo_with_color green 'グリーン姉さん'
echo $color_code
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/140384/202caddc-7ff1-9fd3-e386-54183987e205.png)
`color_code`変数が双方向に流出しています。
関数スコープが無いせいと言えなくも無いですが、良くない状況なのは確かです。

# 解決策
ドットコマンドがダメなら実行するしか無いですね。

実行可能なスクリプトファイルっていうのは、すなわちコマンドです。
全部コマンドに落とし込んでみましょう。
コマンド同士なら色々流出することはないです。

小さなコマンドの組み合わせで大きなことを実現する。
UNIX哲学の基本でしたね。

```util/project_const.sh
#!/bin/sh
set -Ceu

case "$1" in
  login_shell)
    echo fish
    ;;
  config_dir_path)
    echo "$HOME/Config"
    ;;
  *)
    exit 1
    ;;
esac
```

```util/echo_with_color.sh
#!/bin/sh
set -Ceu

case "$1" in
  red)
    color_code='\e[31m'
    ;;
  green)
    color_code='\e[32m'
    ;;
  yellow)
    color_code='\e[33m'
    ;;
  magenta)
    color_code='\e[36m'
    ;;
esac
shift

printf "$color_code"
for msg in "$@"; do
  printf "$(echo "$msg" | sed \
    -e 's/<b>/\\e\[1m/g'\
    -e 's/<\/b>/\\e[m\'"$color_code"'/g')"
done
printf '\e[m'
echo # 改行用
```

```util/echo_err.sh
#!/bin/sh
set -Ceu

cd "$(dirname "$0")"  # このスクリプトがあるフォルダに移動

./echo_with_color.sh red "<b>ERROR</b>: " "$@"
```

```util/echo_log.sh
#!/bin/sh
set -Ceu

cd "$(dirname "$0")"  # このスクリプトがあるフォルダに移動

./echo_with_color.sh magenta "LOG: " "$@"
```

少々手間ですがこれで割と堅牢になりました。
使ってみます。

```main.sh
#!/bin/sh
set -Ceu

# プロジェクトフォルダの絶対パス取得
cd "$(dirname "$0")"  # このスクリプトがあるフォルダ
cd ./                 # ＝プロジェクトフォルダとします。
dir_project=$(pwd)
cd util
dir_util=$(pwd)

# 使用
login_shell="$($dir_util/project_const.sh login_shell)"
"$dir_util/echo_with_color.sh" yellow 'わぁい'"$login_shell"' あかり'"$login_shell"'大好き'

color_code='イェ〜イ　エンジニア見てる〜？　実装失敗してどうだった？'
"$dir_util/echo_with_color.sh" RED 'あなたは赤い部屋が好きですか？'

"$dir_util/echo_with_color.sh" green 'グリーン姉さん'
echo $color_code
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/140384/9649dbec-5642-5f9b-a73f-80467e89aef9.png)

「`color_code`なんて宣言してねえだろゴルァ！」的なことを言われたので、`main.sh`で仕込んだ`color_code`が流出して悪さすることはなくなってるのが確認できました。

```（修正）main.sh
# RED -> red
"$dir_util/echo_with_color.sh" red 'あなたは赤い部屋が好きですか？'
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/140384/74abd3bd-31a3-a0f8-15f8-df1b74049ea6.png)

もちろん、`util/echo_with_color.sh`から`main.sh`への流出も起きてません。

## より綺麗に
いちいちファイルパスを指定して呼び出すのも長ったらしいです。
最初にまとめてエイリアス関数を定義しちゃいましょう。

```main.sh
#!/bin/sh
set -Ceu

# プロジェクトフォルダの絶対パス取得
cd "$(dirname "$0")"  # このスクリプトがあるフォルダ
cd ./                 # ＝プロジェクトフォルダとします。
dir_project=$(pwd)
cd util
dir_util=$(pwd)

# インポート
## util関数
echo_with_color() {
  $dir_util/echo_with_color.sh "$@"
}
project_const() {
  $dir_util/project_const.sh "$@"
}
## 定数
login_shell="$(project_const login_shell)"

# 使用
echo_with_color yellow 'わぁい'"$login_shell"' あかり'"$login_shell"'大好き'

color_code='イェ〜イ　エンジニア見てる〜？　実装失敗してどうだった？'
echo_with_color red 'あなたは赤い部屋が好きですか？'

echo_with_color green 'グリーン姉さん'
echo $color_code
```

import文ライクな使用感になりました。
依存関係がまとまってていい感じ。

# 感想
マイクロサービスアーキテクチャって感じ（多分違う）。

あ、ここでは省略しましたが、本当はもっと引数の数チェックしたりエラーメッセージちゃんとしたりステータスコード整備したりした方が良さげです。