【シェルスクリプト】独自のconfファイルを読み込む関数

# はじめに
Unix系のOSの中には色んなサービスがありますが、
そのサービスの設定ファイルの書きっぷりは様々ですよね。
素直に``変数名=値``が記述されているだけであれば、そのままsourceコマンドで読み込めますが、そうではないケースも散見されます。
シェルスクリプトにおいても変更容易性の観点から、スクリプトとは別に用意したconfファイルからパラメータを読み取るといったケースはよくあると思います。
そこで今回は以下のようなセクションを持つconfファイルから定義情報をシェル変数として取得する関数を作成しました。

# confファイル

```text:test.conf
[test1]
param1:Hello
param2:World!

[test2]
param1:goodbye
param2:2023

[test3]
param1 Thank
param2 you
```
- 各パラメータはセクションとしてひとまとめのブロックとして定義されている。
- セクション間は空白行で区切られている
- パラメータは``key-Value``の形で定義されている。
- 区切り文字は制限なし

# 実装構想
Unixシェルにおいて、強力なテキストツールとしてsedとawkがある。
恐らくこれらを利用することになるだろう。
sedとawkの2つのプロブラムは以下のような特徴があると勝手に思っている。

| プログラム | 特徴                             | 
| ---------- | -------------------------------- | 
| sed        | 非構造データのテキスト処理に強い | 
| awk        | 構造データの処理に強い   | 

今回想定しているconfファイルは半構造データになるので、まずsedで構造データに近い形にして、それをawkで処理するのが良いかなと考える。

awkだけ、またはsedだけを使って実装することも可能だろうけれど、2つのツールの強みをそれぞれ活かした方がコードの複雑さを低減できると期待。

では実際に必要となる処理を一つずつ作っていく。

## Sectionの開始行を取得する
まずconfファイルの中で指定したセクションの部分だけを抜き出したい。
sedには開始位置と終了位置を指定してその範囲を抜き出すことができるのでこれを利用したいと思う。
そのためにも、まずは開始位置となるためセクション名を使ったキーワード検索でセクションの先頭行を取得する。

```zsh:セクションの先頭行を取得
#対象のセクション名をシェル変数に格納
section=test3

#セクションの先頭行を取得
startSection=`sed -n "/\[${section}\]/=" <confファイル>`

```

```zsh:結果
$ echo $startSection
9
```

``-n``をつけることで余分な出力を抑止し、処理対象だけを出力する。

## 対象のセクションを抜き出す
先に取得した開始位置を起点に空白行までの間を抜き出す。
```zsh:セクション抽出
#セクションの先頭行から空白行間での間を抜き出す
section=`sed -n $startSection,/^$/p <ファイル名>`
```
```zsh:結果
$ echo $section
[test3]
param1 Thank
param2 you
```

この抽出をするために、セクション間は空白行で区切られる必要がある。

## セクションから変数のリストを作成する
confファイルの中身は``変数名=値``の形になっていないため、awkを使って整形する。
```zsh:変数リストの作成
varList=`echo $section | awk 'NR>1 {print $1"="$2}'`
```
```zsh:結果
$ echo $varList
param1=Thank
param2=you
```

1行目は``[セクション名]``といったセクションの見出しとなっているため、処理の対象外。そのため1行目は無視させる条件を書く。
NR>1は１行目を無視するために記述。
NRはawkの組み込み変数で、現在処理を行っている行番号が格納されている。
これを``>1``としてあげれば、１行目は無視され、2行目から処理の対象とすることができる。

:::note info
NRは"Number of Record"の略らしい
:::

## リストから変数を定義する
varListの中身を１行ずつ逐次処理していく。
先の処理で、変数``varList``には``変数名=値``の形式でリスト化されたテキスト情報が格納されていますから、これをそのままシェル変数として読み込んでいく。
```zsh:変数定義
while IFS= read -r line; do
  eval "$line"
done <<< $varList
```
複数行にわたる情報を含んだ変数のデータをコマンドの入力に渡すため``<<<``を利用する。

IFSはシェルの環境変数でシェル上で何の文字を区切り文字にするかが定義されている。
わかりにくいが、IFSに空文字を設定している。
これにより、readコマンドが行末までを1つのフィールドとして扱うようになる。
今回のケースでは特に記述しなくても動作は変わらないが、意図しない挙動を避けるために念の為記述。

``-r``も意図しないエスケープを避けるために記述。
特にバックスラッシュはWindows環境だとパスの区切り文字ということもあるため、念の為。

:::note info
IFSは"Internal Field Separator"
:::

# 関数の作成
先の処理をうまいこと組み合わせて、関数を作成。

## Conf情報取得関数(Set-Variable)
```zsh:Set-Variable
function Set-Variable() {
	#オプション解析
	while getopts "f:d:s:" opt; do
		case "$opt" in
			f)
				fFlg=true
				fileName="$OPTARG"
				;;
			d)
				dFlg=true
				delimiter="$OPTARG"
				;;
			s)
				sFlg=true
				sectionName="$OPTARG"
				;;
			\?)
				echo "定義外のオプションが指定されています"
				return 1
				;;
		esac
	done
	
	#confファイルチェック
	if [ -z "$fileName" ]
	then
		echo "confファイルが未指定です"
		return 1
	fi
	
	#セクションの先頭行を取得
	startSection=`sed -n "/\[${sectionName}\]/=" $fileName`
	echo "セクション開始行:${startSection}"
	
	#セクションの抽出
	section=`sed -n $startSection,/^$/p $fileName`
	echo "\n$section"

	#変数リスト生成コマンド作成
	if [ $dFlg ]
	then
		#区切り文字の指定有
		cmd="echo \$section | awk -F \"$delimiter\" 'NR>1 {print \$1\"=\"\$2}'"
	else
		#区切り文字の指定無(スペース区切り扱い)
		cmd="echo \$section | awk 'NR>1 {print \$1\"=\"\$2}'"
	fi
	
	varList=`eval $cmd`
	if [ $? -ne 0 ]
	then
		echo "リストの作成に失敗"
		return 1
	fi
	
	echo "\n$varList"
	
	#varListから変数を定義
	while IFS= read -r line; do
  		eval "$line"
			if [ $? -eq 0 ]
			then
				echo "${line}:Succeeded"
			else
				echo "${line}:failed"
			fi
	done <<< $varList
	
}
```
## 作成備忘
- 区切り文字オプションを使用しない場合は、スペースを区切り文字として処理する(awkのデフォルト)
- eval用のコマンドラインを作成する際は、変数の展開タイミングに注意。

### 課題
- オプションで変数をexportさせる
- コメント行を含むconfファイルに対応する

# 環境
OS:macOS 13.3
CPUアーキテクチャ：ARM
シェル:zsh 5.9