前回、jq(1) を用いて JSON フォーマットでやったのの XML 版です。 ∥ jq(1) の力を借りて Bash スクリプト内で JSON を操作する - Qiita
要は、XML はブロック指向にデータをシリアライズするので、それを Bash の変数に格納して、Bash スクリプトでも複雑な構造化データを扱えるようにしよう、という趣旨です。
入手性の高さでは xmllint に分があるのですが、xmllint だと「編集(更新)」「削除」等の機能がないので、かわりに xmlstarlet を使います。パッケージなり Homebrew なりで入れてください。 ∥ XMLStarlet Command Line XML Toolkit: News
XML リテラル
XML リテラルは、スクリプト中で以下のように書こうと思います。一旦 xmlstarlet(1) を通せば、とりあえず XML が “well-formed” であることは保証されます。
members=$(xmlstarlet sel -B -t -c '.' <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<members>
<member id="123">
<name>Alice ALICE</name>
<phone>123-456</phone>
</member>
<member id="456">
<name>Bob BOB</name>
<phone>456-789</phone>
</member>
<member id="789">
<name>Charlie CHARLIE</name>
<phone>789-000</phone>
</member>
</members>
EOF
)
オプションの -t
(--template
) と -c
(--copy-of
) はお馴染みですが、ここでのミソは -B
(--noblanks
) で、出力からスペースを取り去り、minify して変数に格納します。私は、どちらかというとロングな形式のオプション派なのですが、ここではさすがにタイプ量が増えて冗長にすぎるので、ショートの形式を使います。
結果は、<members><member id="123"><name>Alice ALICE</name><phone>123-456</phone></member><member id="456"><name>Bob BOB</name><phone>456-789</phone></member><member id="789"><name>Charlie CHARLIE</name><phone>789-000</phone></member></members>
のような一行のデータになります。
value に改行が入っていると一行になりませんが、まあいいです(Bash さんは、改行が入っていても気にとめない)。
ループしつつ参照
こんな感じが、一番よくある使い方かと思われます。
#!/bin/bash
# -*- coding: utf-8 -*-
set -o nounset -o errexit -o pipefail
members=$(xmlstarlet sel -B -t -c '.' <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<members>
<member id="123">
<name>Alice ALICE</name>
<phone>123-456</phone>
</member>
<member id="456">
<name>Bob BOB</name>
<phone>456-789</phone>
</member>
<member id="789">
<name>Charlie CHARLIE</name>
<phone>789-000</phone>
</member>
</members>
EOF
)
first() { true; }
while read member
do
! first && echo "--------"
echo ID: "$(xmlstarlet sel -t -m 'member' -v '@id' <<<"$member")"
echo Name: "$(xmlstarlet sel -t -m 'member/name' -v '.' <<<"$member")"
echo Phone: "$(xmlstarlet sel -t -m 'member/phone' -v '.' <<<"$member")"
first() { false; }
done < <(xmlstarlet sel -t -m members/member -c . --nl <<<"$members")
実行結果:
$ ./xmlloop
ID: 123
Name: Alice ALICE
Phone: 123-456
--------
ID: 456
Name: Bob BOB
Phone: 456-789
--------
ID: 789
Name: Charlie CHARLIE
Phone: 789-000
検索
次は、検索です。xpath が使えるので、スッキリ書けますね。
#!/bin/bash
# -*- coding: utf-8 -*-
set -o nounset -o errexit -o pipefail
members=$(xmlstarlet sel -B -t -c '.' <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<members>
<member id="123">
<name>Alice ALICE</name>
<phone>123-456</phone>
</member>
<member id="456">
<name>Bob BOB</name>
<phone>456-789</phone>
</member>
<member id="789">
<name>Charlie CHARLIE</name>
<phone>789-000</phone>
</member>
</members>
EOF
)
searchFirstMatchedAndPut() {
local id=$1
local member=$(xmlstarlet sel -t \
-m "members/member[@id=$id]" -c . -n <<<"$members" | head -n 1 )
if test -n "$member"
then
echo ID: "$(xmlstarlet sel -t -m 'member' -v '@id' <<<"$member")"
echo Name: "$(xmlstarlet sel -t -m 'member/name' -v '.' <<<"$member")"
echo Phone: "$(xmlstarlet sel -t -m 'member/phone' -v '.' <<<"$member")"
else
echo ID: $id not found.
fi
}
first() { true; }
for id in 456 999
do
first || echo "--------"
searchFirstMatchedAndPut $id
first() { false; }
done
実行結果:
$ ./xmlsearch
ID: 456
Name: Bob BOB
Phone: 456-789
--------
ID: 999 not found.
更新と追加
更新は、xpath が使えるのでスッキリ書けますね。
追加に関しては、jq(1) で JSON 断片を記述して突っ込んだように、XML 断片をテキストで書いて突っ込む方式が採れなさそうですので、ed
コマンドを列挙してチマチマと構築せざるをえませんでした。
#!/bin/bash
# -*- coding: utf-8 -*-
set -o nounset -o errexit -o pipefail
members=$(xmlstarlet sel -B -t -c '.' <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<members>
<member id="123">
<name>Alice ALICE</name>
<phone>123-456</phone>
</member>
<member id="456">
<name>Bob BOB</name>
<phone>456-789</phone>
</member>
<member id="789">
<name>Charlie CHARLIE</name>
<phone>789-000</phone>
</member>
</members>
EOF
)
members=$(xmlstarlet ed -P -O \
-s "/members" -t elem -n "member" -v ""\
-s '/members/member[last()]' -type attr -n id -v "999" \
-s '/members/member[last()]' -type elem -n name -v "David DAVID" \
-s '/members/member[last()]' -type elem -n phone -v "999-999" \
<<<"$members" )
members=$(xmlstarlet ed -P -O \
-u "members/member[@id=456]/phone/." -v "XXX-XXX" <<<"$members" )
members=$(xmlstarlet ed -P -O \
-u "members/member[@id=789]/phone/." -v "YYY-YYY" <<<"$members" )
xmlstarlet fo <<<"$members"
実行結果:
$ ./xmlupdate
<?xml version="1.0"?>
<members>
<member id="123">
<name>Alice ALICE</name>
<phone>123-456</phone>
</member>
<member id="456">
<name>Bob BOB</name>
<phone>XXX-XXX</phone>
</member>
<member id="789">
<name>Charlie CHARLIE</name>
<phone>YYY-YYY</phone>
</member>
<member id="999">
<name>David DAVID</name>
<phone>999-999</phone>
</member>
</members>