Como ordenar elementos XML no local?

2

Estou tentando controlar arquivos de configuração do IntelliJ IDEA. Aqui está uma pequena amostra:

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ChangeListManager">
    <ignored path="tilde.iws" />
    <ignored path=".idea/workspace.xml" />
    <ignored path=".idea/dataSources.local.xml" />
    <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
    <option name="TRACKING_ENABLED" value="true" />
    <option name="SHOW_DIALOG" value="false" />
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
    <option name="LAST_RESOLUTION" value="IGNORE" />
  </component>
  <component name="ToolWindowManager">
    <frame x="1201" y="380" width="958" height="1179" extended-state="0" />
    <editor active="false" />
    <layout>
      <window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
      <window_info id="Palette&#9;" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
    </layout>
  </component>
</project>

Alguns elementos, como /project/component[@name='ToolWindowManager']/layout/window_info , parecem ser salvos em seqüência arbitrária toda vez que o IDE salva a configuração. Todos os elementos do mesmo tipo parecem ter sempre os mesmos atributos na mesma sequência. Considerando que a sequência de elementos é irrelevante para o funcionamento do IDE, seria útil se elementos fossem classificados por nome de elemento e, em seguida, valores de atributo, e atributos e espaços em branco permanecessem no lugar.

Com base em outra resposta , cheguei a isso :

<stylesheet version="1.0" xmlns="http://www.w3.org/1999/XSL/Transform">
    <output method="xml" indent="yes" encoding="UTF-8"/>
    <strip-space elements="*"/>

    <template match="processing-instruction()|@*">
        <copy>
            <apply-templates select="node()|@*"/>
        </copy>
    </template>

    <template match="*">
        <copy>
            <apply-templates select="@*"/>
            <apply-templates>
                <sort select="name()"/>
                <sort select="@*[1]"/>
                <sort select="@*[2]"/>
                <sort select="@*[3]"/>
                <sort select="@*[4]"/>
                <sort select="@*[5]"/>
                <sort select="@*[6]"/>
            </apply-templates>
        </copy>
    </template>
</stylesheet>

Está quase lá, mas com alguns problemas:

  • Ele não classifica por todos os valores de atributo (e @* não funciona)
  • Remove espaço antes do final dos elementos vazios ( <foo /> se torna <foo/> ).
  • Adiciona uma nova linha no EOF (que IMO não é um bug, mas torna o arquivo resultante menos semelhante ao original).
por l0b0 25.04.2016 / 23:49

2 respostas

2

Eu lidaria com isso usando perl e XML::Twig .

perl tem uma função sort , que permite especificar critérios arbitrários para comparar uma sequência de valores. Contanto que sua função retorne positivo, negativo ou zero com base na ordem relativa.

Então é aí que a mágica acontece - especificamos um critério de classificação que:

  • Compara com base no nome do nó (tag)
  • Em seguida, compara com base na existência de atributos
  • Em seguida, compara o valor do atributo.

Ele precisa fazer isso de forma recursiva na sua estrutura para classificar subnós também.

Então:

#!/usr/bin/env perl
use strict;
use warnings;

use XML::Twig;

my $xml = XML::Twig -> new -> parsefile ('sample.xml');

sub compare_elements {
   ## perl sort uses $a and $b to compare. 
   ## in this case, it's nodes we expect;

   #tag is the node name. 
   my $compare_by_tag = $a -> tag cmp $b -> tag;
   #conditional return - this works because cmp returns zero
   #if the values are the same.
   return $compare_by_tag if $compare_by_tag; 

   #bit more complicated - extract all the attributes of both a and b, and then compare them sequentially:
   #This is to handle case where you've got mismatched attributes.
   #this may be irrelevant based on your input. 
   my %all_atts;
   foreach my $key ( keys %{$a->atts}, keys %{$b->atts}) { 
      $all_atts{$key}++;
   }
   #iterate all the attributes we've seen - in either element. 
   foreach my $key_to_compare ( sort keys %all_atts ) {

      #test if this attribute exists. If it doesn't in one, but does in the other, then that gets sorted to the top. 
      my $exists = ($a -> att($key_to_compare) ? 1 : 0) <=> ($b -> att($key_to_compare) ? 1 : 0);
      return $exists if $exists;

      #attribute exists in both - extract value, and compare them alphanumerically. 
      my $comparison =  $a -> att($key_to_compare) cmp $b -> att($key_to_compare);
      return $comparison if $comparison;
   }
   #we have fallen through all our comparisons, we therefore assume the nodes are the same and return zero. 
   return 0;
}

#recursive sort - traverses to the lowest node in the tree first, and then sorts that, before
#working back up. 
sub sort_children {
   my ( $node ) = @_;
   foreach my $child ( $node -> children ) { 
      #sort this child if is has child nodes. 
      if ( $child -> children ) { 
         sort_children ( $child )
      }     
   }  

   #iterate each of the child nodes of this one, sorting based on above criteria
      foreach my $element ( sort { compare_elements } $node -> children ) {

         #cut everything, then append to the end.
         #because we've ordered these, then this will work as a reorder operation. 
         $element -> cut;
         $element -> paste ( last_child => $node );
      }
}

#set off recursive sort. 
sort_children ( $xml -> root );

#set output formatting. indented_a implicitly sorts attributes. 
$xml -> set_pretty_print ( 'indented_a');
$xml -> print;

Qual dado sua entrada, saídas:

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ChangeListManager">
    <ignored path=".idea/dataSources.local.xml" />
    <ignored path=".idea/workspace.xml" />
    <ignored path="tilde.iws" />
    <option
        name="EXCLUDED_CONVERTED_TO_IGNORED"
        value="true"
    />
    <option
        name="HIGHLIGHT_CONFLICTS"
        value="true"
    />
    <option
        name="HIGHLIGHT_NON_ACTIVE_CHANGELIST"
        value="false"
    />
    <option
        name="LAST_RESOLUTION"
        value="IGNORE"
    />
    <option
        name="SHOW_DIALOG"
        value="false"
    />
    <option
        name="TRACKING_ENABLED"
        value="true"
    />
  </component>
  <component name="ToolWindowManager">
    <editor active="false" />
    <frame
        extended-state="0"
        height="1179"
        width="958"
        x="1201"
        y="380"
    />
    <layout>
      <window_info
          active="false"
          anchor="bottom"
          auto_hide="false"
          content_ui="tabs"
          id="TODO"
          internal_type="DOCKED"
          order="6"
          show_stripe_button="true"
          sideWeight="0.5"
          side_tool="false"
          type="DOCKED"
          visible="false"
          weight="0.33"
      />
      <window_info
          active="false"
          anchor="left"
          auto_hide="false"
          content_ui="tabs"
          id="Palette&#x09;"
          internal_type="DOCKED"
          order="2"
          show_stripe_button="true"
          sideWeight="0.5"
          side_tool="false"
          type="DOCKED"
          visible="false"
          weight="0.33"
      />
    </layout>
  </component>
</project>

Não importa em qual ordem os vários nós filhos caiam.

Eu pessoalmente gosto de indented_a porque envolve os atributos em novas linhas, e acho que isso é mais claro. Mas indented formato de saída poderia fazer o mesmo truque.

    
por 12.01.2018 / 12:35
1

Não tenho certeza dos detalhes de uma classificação xml canônica e, se ela corresponder ao que você descreveu, recomendaria usar xmllint para fazer uma classificação xml canônica antes de salvar o arquivo no controle de origem. Se você é consistente com isso, então seu controle de versão deve ser muito limpo e útil. Você pode refazer as coisas abaixo para estar em um script e se você estiver usando o git, você pode configurar um githook para chutar o script para você.

$ xmllint --c14n originalConfig.xml > sortedConfig.xml
$ mv sortedConfig.xml originalConfig.xml

O acima deve funcionar para você se você estiver usando linux ou mac. Você pode ter que instalar algo como o cygwin, se você estiver usando o windows.

    
por 02.12.2016 / 18:28

Tags