シェルスクリプトの設定ファイルをyamlで書く

背景

今までシェルスクリプトの設定ファイルは変数の定義を別ファイルに外出しして、sourceで読んでいました。↓みたいな感じ。

SRC='/path/to/src/directory/'
DEST='/path/to/dest/directory/'
#!/bin/bash
source variables.conf
rsync -av ${SRC} ${DEST}

多分かなり一般的な方法だと思うんだけど、これだと設定に自由度出そうと思うと設定ファイルの書き方が難しくなるし、設定ファイルから任意のコマンドが実行できちゃうしと色々気に入らない点がありました。 最近jqというコマンドを覚えて、設定をjsonで渡してjqで処理したら設定の自由度も出るし内部でも扱いやすいなと思っていたんですが、jsonは設定ファイルに向いてないのでどうしたものかと。色々探していたらpythonワンライナーyaml -> jsonの変換ができることが判明したので試してみました。

jqに関しては以下のサイトがわかりやすいです。

yaml -> jsonの変換は以下のサイトにあったコードを丸パクリしました。

Converting YAML to JSON with Python: <block end> found - Stack Overflow

前提

以下のパッケージがインストールされている必要があります。好きにパッケージを入れられる環境ばかりだと良いんですが。。

yamlで書いた設定ファイルを読んで処理を流す

- name: backup1
  src: /path/to/src/dir/
  dest: user@host:/path/to/dest/dir/

- name: backup2
  src: /another/src/dir/
  dest: user@host:/another/dest/dir/

上記のようなyamlをもとにrsyncする処理は以下のように書けます。

#!/bin/bash
# yamlで書かれた設定ファイルをもとにjsonファイルを作成
python -c 'import sys, yaml, json; json.dump(yaml.load(sys.stdin), sys.stdout, indent=2)' < config.yml > config.json

# jsonを読み込んで変数に格納
config=$(jq -Mc '.' config.json)

# for文で順次バックアップ処理を実行
num_backup=$(echo "${config}" | jq '. | length')
for ((i=0; i < ${num_backup}; i++)); do
  # パラメータを取り出して変数に格納
  name=$(echo "${config}" | jq -r ".[$i].name | select(. != null)")
  src=$(echo "${config}" | jq -r ".[$i].src | select(. != null)")
  dest=$(echo "${config}" | jq -r ".[$i].dest | select(. != null)")

  # パラメータのチェック(ここでは値が入っている確認のみ)
  if [ -z "${name}" ] || [ -z "${src}" ] || [ -z "${dest}" ]; then
    continue
  fi

  echo "starting ${name}"
  rsync rsync -av --copy-unsafe-links ${src} ${dest}
done

とりあえず以上でyamlの設定ファイルを読んで処理が回せます。。

解説

上記のyamljsonにすると以下のような感じです。

[
  {
    "dest": "user@host:/path/to/dest/dir/",
    "src": "/path/to/src/dir/",
    "name": "backup1"
  },
  {
    "dest": "user@host:/another/dest/dir/",
    "src": "/another/src/dir/",
    "name": "backup2"
  }
]

連想配列の配列ですね。プログラム的に扱いやすいデータが人に優しく書けることが分かると思います。以下でjsonにしたあとの処理を解説します。

jqを雑に説明

jqコマンドでは、keyを指定して値を取り出します。keyは階層構造になっていて、.がルートで以降.で区切りながらkeyを記述していく感じになっています。また、配列に対しては[<index>]でアクセスできます。 例として、以下のようなjsonを考えます。

{"manager" : {"name" : "太郎", "tel" : "####"}, "members" : [{"name" : "次郎"}, {"name" : "三郎"}]}
  • managerのnameが欲しいときには識別子は.manager.nameとなります。
  • 2人目のmemberのnameが欲しいときには.members[1].nameです。
echo '{"manager" : {"name" : "太郎", "tel" : "####"}, "members" : [{"name" : "次郎"}, {"name" : "三郎"}]}' | jq '<識別子>'

とすると確認できます。

スクリプトの中のjqの使い方

以上を踏まえて、上のシェルスクリプトの例を見ていきます。

  1. jq -Mc '.' config.jsonとすると、jsonがコンパクトに一行になって標準出力に出てくるので、これを変数に格納します。複数行の文字列ってシェルだとなんか扱いにくいので。
  2. jqのコマンドの中で、オブジェクトをパイプで繋いでstream的な処理が書けます。. | lengthでは取り出した配列をlengthに渡して、配列の長さを取ってきています。
  3. .[$i].name | select(. != null)では、.で出てくる配列から$i番目の連想配列を取り出し、さらにnameを取ったあとで、それをselectに渡してnullの場合をはじいています。nameが空だと、nullっていう文字列が返ってきちゃうので。

まとめ

シェルスクリプトを書くときに設定ファイルの作りや内部でのパラメータの管理がいまいちやりにくくてストレスなので、yamlでお手軽に設定ファイルを書きつつjqで設定を参照するようにすると捗るんじゃないかというお話でした。

今のところjqみたいに便利にyamlを読む方法がなくてjsonに変換しないといけないのが難点です。何かいいやり方知っている人がいたら教えてもらえるとうれしいです。