LoginSignup
13
10

More than 5 years have passed since last update.

xmlstarlet(1) の力を借りて Bash スクリプト内で XML を操作する

Last updated at Posted at 2016-05-23

前回、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 さんは、改行が入っていても気にとめない)。

ループしつつ参照

こんな感じが、一番よくある使い方かと思われます。

xmlloop
#!/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 が使えるので、スッキリ書けますね。

xmlsearch
#!/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 コマンドを列挙してチマチマと構築せざるをえませんでした。

xmlupdate
#!/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>
13
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10