linhas para colunas com awk

1

Eu tenho que seguir a saída da amostra:

<HARDWARE>
    <NAME>WIN1</NAME>
    <OS>Windows 7</OS>
    <IP>1.2.3.4</IP>
    <DOMAIN>contoso.com</DOMAIN>
</HARDWARE>
<HARDWARE>
    <NAME>WIN2</NAME>
    <OS>Windows 8</OS>
    <IP>10.20.30.40</IP>
    <DOMAIN>contoso.com</DOMAIN>
</HARDWARE>

Qual é a melhor maneira de analisá-lo para que pareça:

WIN1    Windows 7    1.2.3.4     contoso.com
WIN2    Windows 8    10.20.30.40 contoso.com

Procurando uma solução para usar ferramentas padrão como awk, sed etc

    
por autogun 26.08.2015 / 20:03

5 respostas

0

Por favor, não use awk sed etc. Eles não podem manipular XML corretamente. XML faz um monte de coisas como ter espaço em branco, feeds de linha, tags unários, etc., o que significa que expressões regulares não são muito robustas - elas quebram, seguindo uma alteração perfeitamente válida para XML.

A maneira de manipular XML é com um analisador. xmlstarlet é um comumente usado no Linux. Porque eu ainda não vi isso sugerido, eu usaria Perl. Por exemplo:

#!/usr/bin/perl

use strict;
use warnings;

use XML::Twig;

my $twig = XML::Twig -> parsefile ('your_xml_file.xml'); 
foreach my $HW ( $twig -> findnodes ( '//HARDWARE' ) ) {
    print join ( "\t", map { $_ -> text } $HW -> children ),"\n";
}
  • Analise o XML
  • iterar os elementos HARDWARE .
  • Extraia o text das crianças
  • imprima isso.

Você pode estender um pouco para permitir que você lide com, por exemplo conjuntos de campos / pedidos diferentes:

#!/usr/bin/perl

use strict;
use warnings;

use XML::Twig;

my @fields_to_show = qw ( OS NAME ); 

my $twig = XML::Twig -> parsefile ( 'your_filename.xml' ); 
foreach my $HW ( $twig -> findnodes ( '//HARDWARE' ) ) {
    my %fields =  map { $_ -> tag => $_ -> text } $HW -> children;
    print join ("\t", @fields{@fields_to_show}),"\n"; 
}

Ele gera um hash (array associativo) chamado %fields que se parece com (para cada elemento):

$VAR1 = {
          'OS' => 'Windows 7',
          'NAME' => 'WIN1',
          'DOMAIN' => 'contoso.com',
          'IP' => '1.2.3.4'
        };

Em seguida, usamos @fields_to_show para especificar qual exibir e em qual ordem.

Então, isso imprimirá:

Windows 7   WIN1
Windows 8   WIN2

NB: Eu também tenho que 'consertar' seu XML, porque sem uma única tag root é inválido. Outras respostas mencionaram isso. O XML spec é bastante rigoroso - quebrado XML deve ser rejeitado. Então, é realmente uma má forma de "consertar" XML e, normalmente, eu sugiro que alguém o gere na cabeça com uma cópia da especificação XML.

    
por 28.08.2015 / 14:45
6

Com uma pequena modificação no seu XML, envolva todo o seu XML em um pai <DATA> tag 1 , ou outro de sua escolha, chamado arquivo data.xml :

<DATA>
<HARDWARE>
    <NAME>WIN1</NAME>
    <OS>Windows 7</OS>
    <IP>1.2.3.4</IP>
    <DOMAIN>contoso.com</DOMAIN>
</HARDWARE>
<HARDWARE>
    <NAME>WIN2</NAME>
    <OS>Windows 8</OS>
    <IP>10.20.30.40</IP>
    <DOMAIN>contoso.com</DOMAIN>
</HARDWARE>
</DATA>

Usando xmlstarlet + column

 xmlstarlet sel -T -t -m /DATA/HARDWARE -v "concat(NAME,' ',OS,' ',IP,' ',DOMAIN)" -n data.xml | column -t 

dá:

WIN1  Windows  7  1.2.3.4      contoso.com
WIN2  Windows  8  10.20.30.40  contoso.com

Editar:

Com base na grande pegadinha do Peter.O nos comentários e na sua resposta abaixo , vamos enviar um canal delimitado 2 saída para column -ts$'|' , então algo como:

xmlstarlet sel --indent-tab -T -t -m /DATA/HARDWARE -v "concat(NAME,'|',OS,'|',IP,'|',DOMAIN)" -n data.xml | column -ts$'|'

Agora, os campos são bem alinhados, mesmo se tiverem espaços:

WIN1              Windows 7  1.2.3.4 release 5  contoso.com
Really long OS X  Windows 8  10.20.30.40        contoso.com

1. Ou use { echo '<DATA>'; cat file_name; echo '</DATA>'; } | xmlstarlet ... como Peter.O observa no comentário abaixo

2. Usando espaço como o delimitador não alinha as colunas corretamente

    
por 26.08.2015 / 20:47
2

Com o seu exemplo e o GNU sed:

sed -n 's/<[^>]*>//g;s/^ *//g;/./p' file | paste -d ";" - - - - | column -t -s ";"

Saída:

WIN1  Windows 7  1.2.3.4      contoso.com
WIN2  Windows 8  10.20.30.40  contoso.com

Suponho que o seu arquivo não contenha ; . Se você precisar de um CSV, remova | column -t -s ";" .

    
por 26.08.2015 / 20:21
1

O script awk a seguir (mais column para tabulação de saída) fará com que qualquer sequência de posicionamento das sub -tags, e qualquer separação de espaço em branco das tags - ie. ele manipulará o formato de entrada de amostra do OP, assim como o exemplo a seguir, que tem nenhuma espaço em branco e sub-tags de ordem diferente :

    <HARDWARE><OS>Windows 7</OS><IP>1.2.3.4</IP><DOMAIN>contoso.com</DOMAIN><NAME>WIN1</NAME></HARDWARE><HARDWARE><NAME>WIN2</NAME><OS>Windows 8</OS><DOMAIN>contoso.com</DOMAIN><IP>10.20.30.40</IP></HARDWARE>  
awk 'BEGIN{ RS="[[:space:]]*</?HARDWARE>[[:space:]]*"
            FS="[[:space:]]*<|</[^<>/]+>[[:space:]]*"
            tn=split( "NAME OS IP DOMAIN", tag_order, " " ) 
     } 
     $0 { delete tag
          for( i=1;i<=NF;i++ ) if($i) { n=index($i,">"); tag[substr($i,1,n-1)]=substr($i,n+1)  } 
          for( i=1;i<=tn;i++ ) printf "%s\t", tag[tag_order[i]]; print ""
     }' file | column -ts$'\t'

saída:

WIN1  Windows 7  1.2.3.4      contoso.com
WIN2  Windows 8  10.20.30.40  contoso.com
    
por 27.08.2015 / 01:04
0

com awk - defina arbitrariamente cada coluna com 15 caracteres, alinhados à esquerda e preenchidos com espaços:

awk ' BEGIN { FS = "<[A-Za-z/]+>" } { if ( NR % 6 == 0 ) { printf"\n" } else if ( $2 != "" ) { printf"%-15s", $2 } }' file

Ou como nas outras respostas em combinação com column

awk ' BEGIN { FS = "<[A-Za-z/]+>" } { if ( NR % 6 == 0 ) { printf"\n" } else if ( $2 != "" ) { printf"%s ", $2 } }' file | column -t
    
por 26.08.2015 / 21:43