Notepad++のXMLファイルをソートして整形して出力する:2発目

id:eel3:20110225:1298647360 に載せたコードは個人的に「これはいかがなものか」と問い質したくなる代物だったので、全面的に書き直してみることにした。

前回の反省を踏まえて、こんな方針で攻めてみた。

  • 設定XMLファイルをテキストファイルとして扱わず、全部XMLとしてREXMLで操作する。
  • 各要素ごとに自前でタグを出力するようにする。
  • ツリー構造なので当然ながら再帰で。

ちなみに、色々あって昨年購入した『Scheme手習い』を序盤で放り出したままなのにも関わらず、微妙に影響を受けたコードになっている気がする*1

#!/usr/bin/ruby -w -Ks
#
#= Notepad++の関数補完用の設定XMLファイルをキーワード順にソートするツール
#
#author:: eel3 @ TRASH BOX
#date::   2011/02/25
#
#== 動作確認環境
# - ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mswin32] @ Windows XP Pro SP3
#

require 'rexml/document'

# 要素 _elem_ の要素名、属性、属性値をダンプした文字列を返す
def dump(elem)
  str = ''
  str << elem.fully_expanded_name
  elem.attributes.each_attribute do |attr|
    str << " #{attr.to_string.gsub(/'/, '"')}"
  end
  str
end

# 要素 _elem_ の子要素をソートした配列を返す
def sort_childs(elem)
  elem.elements.to_a.sort {|a, b|
    if a.fully_expanded_name != b.fully_expanded_name
      a.fully_expanded_name <=> b.fully_expanded_name
    elsif a.fully_expanded_name == 'KeyWord'
      a.attributes.get_attribute('name').value <=> b.attributes.get_attribute('name').value
    else
      0   # XXX: do not sort order
    end
  }
end

# 要素 _elem_ を含むXMLノードを整形した文字列を返す
def format_xml(elem, recurs_lv = 0)
  indent = "\t" * recurs_lv
  if elem == nil
    ''
  elsif elem.has_elements?
    "#{indent}<#{dump(elem)}>\n"\
    "#{sort_childs(elem).collect {|i| format_xml(i, recurs_lv + 1) }.join}"\
    "#{indent}</#{elem.fully_expanded_name}>\n"
  else
    "#{indent}<#{dump(elem)} />\n"
  end
end

# XML宣言 _xml_decl_ を整形した文字列を返す
def format_decl(xml_decl)
  output = ''
  xml_decl.write output
  output.gsub(/'/, '"') << "\n"
end

# メインルーチン
def main
  doc = REXML::Document.new(readlines.join)
  print format_decl(doc.xml_decl)
  print format_xml(doc.root)
end

main

属性値がシングルクォートで括られているのが少々気に入らないので、ダブルクォートにしている。実は属性の順番が元のXMLと違う点も気になるのだが、これといった解決策は思いついていない。

物凄く細かい話だが、書き方で1点だけ気になっている所がある。メソッド sort_childs の中で配列をソートしている所のブロックをdo ... endにするか{ ... }にするかだ。というのも複数回呼び出されるブロックなのでdo ... endにするべきかと思う反面、ソートした結果をメソッドの戻り値として返すので{ ... }の方が適当な気もするのだ。sortの戻り値を使わないのなら迷わずdo ... endで書くのだが。

*1:特にメソッド format_xml とか。