閑古錐：awk で CSV ファイルのデータを「少し加工して」テンプレートに埋め込んで行毎に出力する（応用編）(Indirect function call)

## 初めに
基本編でCSVファイルのデータを読み込み、それをテンプレートに埋め込んで行毎に出力する処理をかきました。
単純に埋め込むのではなく、たとえば、文字列にクオートをつけるとか、前後の空白を除くなど、少し加工して出力したいことも多々あります。また、この「少し加工」することは多くのバリエーションがあると思います。
今回は、応答編として、「少し加工」する機能を「アドイン」のように追加できるようにしたいと思います。
「アドイン」を実現するために、gawk4.0以降で実装されている「Indirect function call」を使います。

## データファイル
データはカンマ区切りで、1行目にカラム名称を記載する。
2行目以降はデータ。
カラム名に、「|」で処理するアドイン名を記載する。処理後にさらに追加のアドインを記載すると、処理を多段にすることができる。

```data.txt
id,name|TR|Q,birthday|SQLDate,ans|A|Q
1,  test1,2000/1/1,0
2,test2  ,2001/3/1,1
2,test3  ,2001/4/1,2
```

## テンプレート
ひな型をtemplate.txtに格納しておく。
ひな型で、データと置き換える箇所は、{カラム名}という書式にする。

```template.txt
insert into users(id,name,birthday,ans)
 values({id},{name},{birthday},{ans});
```

## awkスクリプト
アドインの関数を定義したアドインファイル(conv_addin.awk.txt)と本体(conv.awk.txt)の２つのファイルに分ける。

## アドインファイル(conv_addin.awk.txt)
アドインの関数定義を行い、その関数名とアドイン名をregist_func()内で紐づける。
具体的な紐づけは連想配列FUNCで行い、キーがアドイン名で、値が関数名とする。

| アドイン名 | 関数名 | 機能 |
|:-|:-|:-|
| TR| Trim |引数の前後の空白を除去する|
| Q | Quote |引数の前後にシングルクオートを付加する|
| SQLDate | SQLDate  |引数をSQLの日付に変換する|
| A | Ans |引数の数値を文字列に変換する|

```awk:conv_addin.awk.txt
# アドイン登録
##  アドインとして使いたい関数をここで登録する
##   FUNC[A]=B
##     Aは、アドイン名。データの1行目のカラムに記載する
##     Bは、アドイン名に対応する関数名。
function regist_func() {
  FUNC["TR"]="Trim";
  FUNC["Q"]="Quote";
  FUNC["SQLDate"]="SQLDate";
  FUNC["A"]="Ans";
}

# アドイン関数 Trim
##  引数の前後の空白を除去する
function Trim(v) {
  sub("^[ \t]*","",v);
  sub("[ \t]*$","",v);
  return v;
}
## アドイン関数 Quote
##  引数の前後にシングルクオートを付加する
function Quote(v) {
  return "'"v"'";
}

## アドイン関数 SQLDate
##  引数をSQLの日付に変換する
function SQLDate(v) {
  return "toDate('yyyy/mm/dd','"v"')";
}

## アドイン関数 Ans
##  引数の数値を文字列に変換する
function Ans(v) {
  if(v==0) {
    return "はい";
  } else if(v==1) {
    return "いいえ";
  } else {
    return "その他";
  }
}
```

## 本体(conv.awk.txt)

BEGINブロックからアドインを登録するために、アドインファイル内に定義されたregist_func()を呼び出す。
1行目はカラム定義行で、カラム名とアドイン名を連想配列COLに格納する。
2行目以降はデータ行で、カラム位置から該当するカラム名とアドイン名を取り出し、アドイン名からアドイン関数を取得し、Indirect function call機能を使ってアドイン関数を呼び出す。アドイン関数は引数で渡されたカラムの値に「加工処理」を行い、その結果をテンプレートに埋め込む。

```awk:conv.awk.txt
# 本体

## グローバル変数
##  FUNC 連想配列 アドイン名と関数名を保存
##  COL  連想配列 データファイルのカラム名とアドイン名(複数)を保存 
##          [カラム番号][1]:カラム名、[カラム番号][2～n]:アドイン名
##  MAXNF 1行目のカラム数
##  template テンプレートの内容

BEGIN{
  ## アドインを登録する
  regist_func(); ## アドインファイル内に定義がある
}

## データファイルの1行目のカラム名とアドイン名をCOLに保存する
NR==1{
  for(i=1;i<=NF;i++) {
      ## カラム名(para[1])と、アドイン名(para[2～n])に分ける
      n= split($i,para,"|");
      COL[i][1]=para[1]; ## カラム名
      v = $i;
      for(j=2;j<=n;j++) {
        f = FUNC[para[j]]; ## アドイン関数名
        if(f=="") {
          print para[j]" の定義がありません" > "/dev/stderr";
          exit;
        }
        COL[i][j]=para[j]; ## アドイン名
      }
  }
  MAXNF=NF
}

## データファイルの2行目以降のデータ処理
NR>1{
  t=template ## テンプレートはコマンドラインで予め変数に格納しておく
  if(MAXNF<=NF) {
    for(i=1;i<=MAXNF;i++) {
      v = $i; ## カラムの値
      n = length(COL[i]);
      for(j=2;j<=n;j++) {
        f = FUNC[COL[i][j]]; ## アドイン名→アドイン関数名
        v = @f(v); ## アドインファイル内に定義があるアドイン関数を呼び出す
      }
      ## テンプレートを置き換える
      gsub("{"COL[i][1]"}",v,t);
    }
    ## テンプレート置き換え後の値を出力する
    print t;
  }
}
```

## 実行と結果
アドインの関数を定義したアドインファイル(conv_addin.awk.txt)と本体(conv.awk.txt)の２つのファイルを引数で渡す

| 引数 | 意味 |
|:-|:-|
| -F, | カンマ区切りであることを指定する |
| -v template="$(cat template.txt)" | テンプレートを変数templateに格納する |
| -f conv.awk.txt | 本体awkスクリプトファイルを読み込む |
| -f conv_addin.awk.txt | アドインawkスクリプトファイルを読み込む |

```bash:コマンドライン
$ awk -F, -v template="$(cat template.txt)" -f conv.awk.txt -f conv_addin.awk.txt data.txt
insert into users(id,name,birthday,ans)
 values(1,'test1',toDate('yyyy/mm/dd','2000/1/1'),'はい');
insert into users(id,name,birthday,ans)
 values(2,'test2',toDate('yyyy/mm/dd','2001/3/1'),'いいえ');
insert into users(id,name,birthday,ans)
 values(2,'test3',toDate('yyyy/mm/dd','2001/4/1'),'その他');

```

## 最後に

関数の間接呼び出し(Indirect function call)ができると、機能の追加を比較的簡単に行うことができることがお判りいただけたと思います。
呼び出しが連想配列を経由して行われるので、アドインと本体との関係は疎結合であり、アドイン関数は呼び出される際のインターフェースが同じであることが重要であることを理解していただけたらと思いますし、アドイン関数呼び出しのインターフェースを工夫することでさらに汎用性のある処理を記述できるようになると思います。

awkはワンライナーが身上の言語ではありますが、ちょっとした「ソフトウエアツール」を作るのに便利な言語でもあります。自分なりの「ツール」を多く持っていることで問題解決の幅を広げることができるかもしれません。単純ではあるが応用の利く「ツール」を手元に置いて、生産性を上げてもらえればと思います。