sed/shellでiniファイルを読む

# sedで.iniファイルを読む

## 動機

まっさらな macOS (BSD) / Linux の初期設定を行うスクリプトを書くことにした。「いまどき大抵のディストリビューションにも perl / python くらいプリインストースされてるだろう」というツッコミはあるとは思うが、やはりプロセス処理の記述の手軽さからShell script(bash)にすることにした。とはいえ、いろいろな設定ファイルをshell文法で書くのは可読性に欠けるので、設定ファイルは[.iniファイル](https://ja.wikipedia.org/wiki/INI%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB)もどきで記述することにし、それをsedで処理してshellに読みこむ形式することにした。.iniファイルのsedでのパースに関しては、googleで検索すると色々例が出てくるが、それらを参考にちょっともう可読性のよい.iniファイル(もどき)を処理できる仕様にすることにした。

## .iniファイルもどきの書式

  - `[section]`でセクションを設定する。`[`の直後と`]`の直前の空白文字(space/tab)の並びはセクション名の一部とはみなさないものとする。(セクション名とその前後のカッコの間に空白文字の並びがあってもよい仕様とする。) ただし、セクション名に改行文字は含まないものとする。
  - `parameter=value`で、パラメータ変数とその値を設定する。`=`の前後に空白があってもいいことにする。パラメータ名には`=`が含まれていないものとする。パラメータ名にシェル変数に使えない文字が含まれている場合には、出力するシェル変数名前としてはその文字を`_`に置き換えたものとする。
  - 行末に`\`がある場合は次の行を継続行として扱う。行頭に空白/タブが4個以上ある場合は、前の行からの継続行として扱う。
  - `#`,`;`から行末はコメントとして無視する。
  - `[`と']'の両方を含まない行、または行頭以外の位置に`=`を含まない行は無視。

## シェルスクリプト内で.iniファイルの処理でやりたいこと

  - セクションの一覧を表示。
  - パラメータをシェル変数として読み込む。ただし、バリエーションとして、
     1. ある特定のセクションのパラメータをシェル変数とする。場合によってシェル関数内のlocal変数、または環境変数として設定する。
     2. 全部またはいくつかのセクションのパラメータをシェル変数とする。シェル変数名は`'セクション名に基づいた接頭辞'+'パラメータ名'`とする。この場合もlocal変数または環境変数として設定する。セクション名に基づいた接頭辞は、セクション名の文字列のうちシェル変数に使えない文字を`_`に置き換えて末尾に`_`をつけて生成する。

## 想定する処理系

 - bash
 - BSD sed (GNU sedの便利な拡張仕様は使えないが、まっさらなmacOSで動かしたいので。) ただし、`-E`オプションを使って拡張正規表現は使う。(1つ以上にマッチを示すメタキャラクタ'+'がつかえないと`sed`文が長大になってしまうので。) 

継続行、コメントの処理に関してはより汎用的な使い道がありそうなので、[別途記述](https://qiita.com/Nanigashi_Uji/items/870f720e294e69f63e71)した。そこに記載した`sample5.sed`にコードを追加することで上記を実装する。

## セクション一覧の表示

これは比較的簡単である。セクション名の書式に該当しない行は処理せず。セクション名の書式に該当する行の'[',']'と不要な部分を取り除いて出力すればよい。この際**不用意に改行文字を消してしまわないこと**に留意する必要があり、[別途記述](https://qiita.com/Nanigashi_Uji/items/870f720e294e69f63e71)にも書いた通り改行以外にマッチする表現`([^\\[:space:]]|[[:blank:]])`を活用する。

```sed:list_section.sed
:begin
$!N
s/[#;]([^[:space:]]|[[:blank:]])*([^\\[:space:]]|[[:blank:]])(\n)/\3/
s/[#;]([^[:space:]]|[[:blank:]])*(\\)(\n)/\2\3/
$s/[#;]([^[:space:]]|[[:blank:]])*$//
/(\\\n|\n[[:blank:]]{4})/ {
  s/[[:blank:]]*(\\\n|\n[[:blank:]]{4})[[:blank:]]*/ /
  t begin
}
/^[[:blank:]]*\n/ D
/\n[[:blank:]]*$/ {
  s/\n[[:blank:]]*$//
  t begin
}
/^\[([^[:space:]]|[[:blank:]])*\]/! D
s/\[[[:blank:]]*//
s/[[:blank:]]*\]([^[:space:]]|[[:blank:]])*//
P
D
```
たとえば、サンプルの.iniファイルもどきで試してみると、

```ini:sample.ini
# -*- mode: conf-mode ; -*- \
#;
#; sample .ini file
#;
#;

[tako]
param_a=(1 # 2 3 \
4 5 ### 6 7 
    8 9 # 10
    )

a=b # kani \
# kani \


[kani]
param_a=1
param_b=2

[uni]
param_a=3
param_b=4

[wani]
param_a=5
param_b=6

[hebi]
param_a=9
param_b=10

```

```tcsh:実行例
% sed -nE -f list_section.sed sample.ini
tako
kani
uni
wani
hebi
```
## 特定のセクションだけ抽出する。

下記の例では、上記のサンプルから特定のセクション(`wani`)の内容のうち、適切なパラメータ定義の書式にしたがった行のみ抽出します。セクション名の形式の行を見つけたら、セクション名をホールドスペースに収納しておき、それ以外の場合にはホールドスペースの内容が所望のセクション名と一致しない場合には次の行の処理に移ります。一致した場合にはパラメータ書式の定義にマッチしているかを確認して空白を除いたうけ、この例ではシェル関数のlocal変数となるようにテキストを追加し、またシェル変数定義をまとめられるように行末に`;`をつけている. だいぶ長くなってきたのでコメント行をつけたが、制御構造としては各処理ごとに必要な処理をしてその先の処理が必要なければ先頭に戻っているだけなので、比較的追いやすいはず。

```sed:pickup_section1.sed
:begin
$!N
# Erase comment strings
s/[#;]([^[:space:]]|[[:blank:]])*([^\\[:space:]]|[[:blank:]])(\n)/\3/
s/[#;]([^[:space:]]|[[:blank:]])*(\\)(\n)/\2\3/
$s/[#;]([^[:space:]]|[[:blank:]])*$//
# Concatenate continuation lines
/(\\\n|\n[[:blank:]]{4})/ {
  s/[[:blank:]]*(\\\n|\n[[:blank:]]{4})[[:blank:]]*/ /
  t begin
}
# Erase blank lines
/^[[:blank:]]*\n/ D
/\n[[:blank:]]*$/ {
  s/\n[[:blank:]]*$//
  t begin
}
# Check section headline and store section name to holdspace
/^([^[:space:]]|[[:blank:]])*\[([^[:space:]]|[[:blank:]])*\]/ {
h
x
s/^([^[:space:]]|[[:blank:]])*\[(([^[:space:]]|[[:blank:]])*)\].*$/\2/g
s/^[[:blank:]]*//g
s/[[:blank:]]$//g
x
D
}
# Skip line if current section is not interested one
x
/^wani$/! { 
  x
  D
}
x
# Print if it is proper parameter definition 
/^(([^[:space:]=]|[[:blank:]])*)=(([^[:space:]]|[[:blank:]])*)/ {
  s/^[[:blank:]]*/local /
  s/[[:blank:]]*=[[:blank:]]*/=/
  s/(([^[:space:]]|[[:blank:]])*)[[:blank:]]*(\n)/\1;\3/
  P
}
D
```

## シェル変数名を修飾するには？

やりたいことの節で挙げた通り、シェル変数にセクション名から生成した接頭辞をつけたり、.iniファイルのparameter名がシェル変数に使えない文字列含んでいた場合には適宜変換することを行いたい。安直な方法としてはsedを何回も呼んで処理する方法もあるが、できれば**sedの1プロセス数で処理できた方が美しい気がする**。ここで大きな制約となるのはsedにはホールドスペースが1つかない点である。高級スクリプト言語では、テキストセグメントをいくつもの変数に分けて格納して各々を処理したのち組み合わせればよい。一方sedの場合には、ホールドスペースを改行で区切り文字としてスタックとして複数のテキストを保存して使う方法が定番のようだ。

たとえば下記の例では、ホールドスペースを先頭行から
1. セクション名
2. セクション名を整形した接頭辞
3. パターンスペースの一行目のバックアップ
4. パターンスペースの二行目のバックアップ
5. .iniファイルのパラメータ名から整形したシェル変数名

というふうに使い方を決め、また処理行の遷移するときは**ホールドスペースの行数は一定に保ち(下記例では2行)、パターンスペースを適宜復元しておく**ということが必要である。ホールドスペースとパターンスペースをやりとりするコマンドは`g``G`,`h``H`しかないので、似たような処理の繰り返しが発生したりするので、**はたしてほんとうに1sedプロセスで行うのが美しいのか**という疑問は否めない。

ともかく下記は、上記の`sample.ini`ファイルから、`wani`と`uni`セクションを抽出して、セクション名を追加したシェル変数の定義文を出力する例である。全体の制御構造としてはシンプルのままで、またコメントをつけたので、たとえば別のセクションを抽出するにはどこを書き換えればよいか等は察してもらえると期待。

```sed:pickup_section2.sed
# Initialine the hold space: (Single empty line at the beginning)
1 { H ; x ; 
  # Change the expression for the defalut section name and/or variable prefix, here.
  s/^([^[:space:]]|[[:blank:]])*(\n)([^[:space:]]|[[:blank:]])*$/global\2global_/g
  x
}
:begin
$!N
# Erase comment strings
s/[#;]([^[:space:]]|[[:blank:]])*([^\\[:space:]]|[[:blank:]])(\n)/\3/
s/[#;]([^[:space:]]|[[:blank:]])*(\\)(\n)/\2\3/
$s/[#;]([^[:space:]]|[[:blank:]])*$//
# Concatenate continuation lines
/(\\\n|\n[[:blank:]]{4})/ {
  s/[[:blank:]]*(\\\n|\n[[:blank:]]{4})[[:blank:]]*/ / ; t begin
}
# Erase blank lines
/^[[:blank:]]*\n/ D
/\n[[:blank:]]*$/ {
  s/\n[[:blank:]]*$// ; t begin
}
# Check section headline and store section name to holdspace
/^([^[:space:]]|[[:blank:]])*\[([^[:space:]]|[[:blank:]])*\]/ {
  # Remove blackets and extra spaces at first line
  s/^([^[:space:]]|[[:blank:]])*\[(([^[:space:]]|[[:blank:]])*)\](([^[:space:]]|[[:blank:]])*)(\n)/\2\6/g
  s/^[[:blank:]]*//g; s/[[:blank:]]*(\n)/\1/g
  # Store the section name to the hold space, and format stored one for shell variable for the hold space
  h
  x
  s/(\n)([^[:space:]]|[[:blank:]])*$//
  s/([^[:alnum:]_]|$)/_/g
  x
  # Append the section name to the hold space.
  H
  # Remove unused line of the hold space and rearrange the remaining lines.
  x
  s/(([^[:space:]]|[[:blank:]])*)(\n)(([^[:space:]]|[[:blank:]])*)(\n)(([^[:space:]]|[[:blank:]])*)$/\4\3\1/
  x
  D
}
# Skip line if current section is not interested one
x
/^(wani|uni)(\n)/! { x ; D ; }
x
# Print if it is proper parameter definition 
/^(([^[:space:]=]|[[:blank:]])*)=(([^[:space:]]|[[:blank:]])*)/ {
  # Store current patern space text at the end of the hold space
  H

  # Build shell script variable name and store it at the end of the hold space
  s/(([^[:space:]=]|[[:blank:]])*)=.*$/\1/g
  s/^[[:blank:]]*//
  s/[[:blank:]]*$//
  s/[^[:alnum:]_]/_/g
  # If further rename of the variable name is necessary, put here.

  # Store variable name at the end of the hold space
  H

  # Build parameter variable value and keep it at pattern space
  # At first, Resore the current line text from hold space
  g
  # Remove unused lines.
  s/^(([^[:space:]]|[[:blank:]])*\n){2}//
  s/(\n([^[:space:]]|[[:blank:]])*){2}$//
  # Remove the text other than the parameter value
  s/^([^[:space:]=]|[[:blank:]])*=//g
  # If further formatting of the value is necessary, put here.

  # Append hold space stored date into pattern space
  G
  # Remove unused lines from the pattern space
  s/^(([^[:space:]]|[[:blank:]])*\n)(([^[:space:]]|[[:blank:]])*\n)(([^[:space:]]|[[:blank:]])*\n)(([^[:space:]]|[[:blank:]])*\n)(([^[:space:]]|[[:blank:]])*\n)/\1\5\9/
  # Rearrance the order of line in the pattern space, it is nessacery because only \1 ...\9 is avaiable
  s/^(([^[:space:]]|[[:blank:]])*\n)(([^[:space:]]|[[:blank:]])*\n)(([^[:space:]]|[[:blank:]])*)(\n)(([^[:space:]]|[[:blank:]])*)$/\1\3\8\7\5/

  # Format the output in the first line of the pattern space, and 
  # Restore the next line at the second line of the pattern space
  s/^(([^[:space:]]|[[:blank:]])*)(\n)(([^[:space:]]|[[:blank:]])*)(\n)(([^[:space:]]|[[:blank:]])*)(\n([^[:space:]]|[[:blank:]])*)$/local \4\7=\1;\9/

  # Clean up hold space
  x
  s/(\n([^[:space:]]|[[:blank:]])*){3}$//
  x
  P
}
D
```

```tcsh:実行例
% sed -n -E -f pickup_section2.sed sample.ini
local uni_param_a=3;
local uni_param_b=4;
local wani_param_a=5;
local wani_param_b=6;
```

## シェルスクリプト化

抽出するセクションを変えたり、出力形式(sh/csh,シェル変数/ローカル変数/環境変数,変数名の接頭辞のON/OFF)を切り替えたりするたびにsedファイルを書き換えるのは面倒なので、コマンドラインオプションによってsedのコマンドを生成するシェルスクリプトを用意した。[別記事](https://qiita.com/Nanigashi_Uji/items/1f447416f54cb510cb84)にあるテンプレートを活用して生成した。上記までののsedに対して、.iniファイルのパラメータ変数名(シェル変数名)で出力を選択できるようにもしてみた。(が、section名と変数名が複数指定された場合には組み合わせが一意でないのであまり使えない機能なのかも。)

### ファイル置場

  - https://github.com/nanigashi-uji/parse_ini_sh
  - https://gitlab.com/nanigashi_uji/parse_ini_sh

### 使い方

```tcsh:parse_ini.sh&nbsp;--help
[Usage] % parse_ini.sh -list     file [files ...]
        % parse_ini.sh [options] file [files ...]

[Options]
    -l,--list                       : List sections 
    -S,--sec-select       name      : Section name to select
    -T,--sec-select-regex expr      : Section reg. expr. to select
    -V,--variable-select name       : variable name to select
    -W,--variable-select-regex expr : variable reg. expr. to select
    -L,--local                      : Definition as local variables (B-sh)
    -e,--env                        : Definition as enviromnental variables
    -q,--quot                       : Definition by quoting with double/single-quotation.
    -c,--csh,--tcsh                 : Output for csh statement (default: B-sh)
    -b,--bsh,--bash                 : Output for csh statement (default)
    -s,--sec-prefix                 : add prefix: 'sectionname_' to variable names. 
    -v,--verbose                    : Verbose messages 
    -h,--help                       : Show Help (this message)
```

### 実行例

```tcsh:実行例
% parse_ini.sh --list sample.ini
tako
kani
uni
wani
hebi

% parse_ini.sh -S kani -L sample.ini
local param_a=1;
local param_b=2;

% parse_ini.sh -S kani -L -s sample.ini
local kani_param_a=1;
local kani_param_b=2;

% parse_ini.sh -S kani -L -e -c sample.ini
setenv param_a 1;
setenv param_b 2;

% parse_ini.sh -S kani -L -e -c -q sample.ini
setenv param_a "1";
setenv param_b "2";
```

## 参考資料
  - [sedでiniファイルをパースする方法とその解説](https://qiita.com/little-forest/items/cacb4900741d0f8d550e)
  - [Bashの配列でjoinやsplitする](https://qiita.com/kawaz/items/b82da76ac93b32ddc364)
  - "sed & awk プログラミング" (Dale Dougherty著 福崎俊博訳 ISBN-10: 4756100910)