Como analisar XML no Bash?

Idealmente, o que eu gostaria de poder fazer é:

cat xhtmlfile.xhtml | getElementViaXPath --path='/html/head/title' | sed -e 's%(^|$)%%g' > titleOfXHTMLPage.txt 

Isso é apenas uma explicação da resposta de Yuzem , mas eu não senti que muita edição deveria ser feita para outra pessoa, e comentários não permitem formatação, então …

 rdom () { local IFS=\> ; read -d \< EC ;} 

Vamos chamar isso de "read_dom" em vez de "rdom", espaçá-lo um pouco e usar variables ​​mais longas:

 read_dom () { local IFS=\> read -d \< ENTITY CONTENT } 

Ok, isso define uma function chamada read_dom. A primeira linha torna o IFS (o separador de campos de input) local para essa function e altera para>. Isso significa que quando você lê dados em vez de ser dividido automaticamente no espaço, na guia ou nas novas linhas, ele é dividido em '>'. A próxima linha diz para ler a input de stdin e, em vez de parar em uma nova linha, parar quando você vir um caractere '< ' (o -d para sinalizador de delimitador). O que é lido é então dividido usando o IFS e atribuído à variável ENTITY e CONTENT. Então pegue o seguinte:

 value 

A primeira chamada para read_dom obtém uma string vazia (já que '< ' é o primeiro caractere). Isso é dividido pelo IFS em apenas '', já que não há um caractere '>'. Ler então atribui uma string vazia às duas variables. A segunda chamada recebe a string 'tag> value'. Isso é dividido pelo IFS nos dois campos 'tag' e 'value'. Ler então atribui as variables ​​como: ENTITY=tag e CONTENT=value . A terceira chamada recebe a string '/ tag>'. Isso é dividido pelo IFS nos dois campos '/ tag' e ''. Ler então atribui as variables ​​como: ENTITY=/tag e CONTENT= . A quarta chamada retornará um status diferente de zero porque chegamos ao final do arquivo.

Agora seu loop while limpou um pouco para combinar com o acima:

 while read_dom; do if [[ $ENTITY = "title" ]]; then echo $CONTENT exit fi done < xhtmlfile.xhtml > titleOfXHTMLPage.txt 

A primeira linha apenas diz: "enquanto a function read_dom retorna um status zero, faça o seguinte". A segunda linha verifica se a entidade que acabamos de ver é "título". A próxima linha ecoa o conteúdo da tag. As quatro linhas saem. Se não fosse a entidade do título, o loop se repetirá na sexta linha. Nós redirecionamos "xhtmlfile.xhtml" para a input padrão (para a function read_dom ) e redirecionamos a saída padrão para "titleOfXHTMLPage.txt" (o eco do anterior no loop).

Agora, é dado o seguinte (semelhante ao que você obtém da listview de um depósito no S3) para input.xml :

  sth-items false  item-apple-iso@2x.png 2011-07-25T22:23:04.000Z "0032a28286680abee71aed5d059c6a09" 1785 STANDARD   

e o seguinte loop:

 while read_dom; do echo "$ENTITY => $CONTENT" done < input.xml 

Voce deveria pegar:

  => ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/" => Name => sth-items /Name => IsTruncated => false /IsTruncated => Contents => Key => item-apple-iso@2x.png /Key => LastModified => 2011-07-25T22:23:04.000Z /LastModified => ETag => "0032a28286680abee71aed5d059c6a09" /ETag => Size => 1785 /Size => StorageClass => STANDARD /StorageClass => /Contents => 

Então, se escrevêssemos um loop while como o da Yuzem:

 while read_dom; do if [[ $ENTITY = "Key" ]] ; then echo $CONTENT fi done < input.xml 

Nós obteríamos uma listview de todos os arquivos no bucket do S3.

EDIT Se por algum motivo local IFS=\> não funcionar para você e você o definir globalmente, você deve redefini-lo no final da function como:

 read_dom () { ORIGINAL_IFS=$IFS IFS=\> read -d \< ENTITY CONTENT IFS=$ORIGINAL_IFS } 

Caso contrário, qualquer divisão de linha feita posteriormente no script será desordenada.

EDITAR 2 Para dividir os pares nome / valor do atributo, você pode aumentar o read_dom() forma:

 read_dom () { local IFS=\> read -d \< ENTITY CONTENT local ret=$? TAG_NAME=${ENTITY%% *} ATTRIBUTES=${ENTITY#* } return $ret } 

Em seguida, escreva sua function para analisar e obter os dados desejados assim:

 parse_dom () { if [[ $TAG_NAME = "foo" ]] ; then eval local $ATTRIBUTES echo "foo size is: $size" elif [[ $TAG_NAME = "bar" ]] ; then eval local $ATTRIBUTES echo "bar type is: $type" fi } 

Então, enquanto você read_dom chama parse_dom :

 while read_dom; do parse_dom done 

Em seguida, dada a seguinte marcação de exemplo:

  bars content foos content  

Você deve obter esta saída:

 $ cat example.xml | ./bash_xml.sh bar type is: metal foo size is: 1789 

EDIT 3 outro usuário disse que eles estavam tendo problemas com ele no FreeBSD e sugeriu salvar o status de saída de leitura e retornando no final de read_dom como:

 read_dom () { local IFS=\> read -d \< ENTITY CONTENT local RET=$? TAG_NAME=${ENTITY%% *} ATTRIBUTES=${ENTITY#* } return $RET } 

Eu não vejo nenhum motivo para que isso não funcione

Você pode fazer isso muito facilmente usando apenas bash. Você só precisa adicionar esta function:

 rdom () { local IFS=\> ; read -d \< EC ;} 

Agora você pode usar rdom como ler, mas para documentos em HTML. Quando chamado rdom, atribuirá o elemento à variável E e o conteúdo a var C.

Por exemplo, para fazer o que você queria fazer:

 while rdom; do if [[ $E = title ]]; then echo $C exit fi done < xhtmlfile.xhtml > titleOfXHTMLPage.txt 

Ferramentas de linha de comando que podem ser chamadas de scripts de shell incluem:

  • 4xpath – wrapper de linha de comando ao redor do pacote 4Suite do Python
  • XMLStarlet
  • xpath – wrapper de linha de comando em torno da biblioteca XPath do Perl
  • Xidel – funciona com URLs e arquivos. Também trabalha com JSON

Eu também uso xmllint e xsltproc com pequenos scripts de transformação XSL para fazer processamento XML a partir da linha de comando ou em scripts de shell.

Você pode usar o utilitário xpath. É instalado com o pacote Perl XML-XPath.

Uso:

 /usr/bin/xpath [filename] query 

ou XMLStarlet . Para instalá-lo no opensuse use:

 sudo zypper install xmlstarlet 

ou tente cnf xml em outras plataformas.

Isso é suficiente …

 xpath xhtmlfile.xhtml '/html/head/title/text()' > titleOfXHTMLPage.txt 

Confira o XML2 em http://www.ofb.net/~egnor/xml2/ que converte XML em um formato orientado a linhas.

Outra ferramenta de linha de comando é o meu novo Xidel . Ele também suporta XPath 2 e XQuery, ao contrário do já mencionado xpath / xmlstarlet.

O título pode ser lido como:

 xidel xhtmlfile.xhtml -e /html/head/title > titleOfXHTMLPage.txt 

E também tem um recurso interessante para exportar várias variables ​​para bash. Por exemplo

 eval $(xidel xhtmlfile.xhtml -e 'title := //title, imgcount := count(//img)' --output-format bash ) 

define $title para o título e $imgcount para o número de imagens no arquivo, que deve ser tão flexível quanto analisá-lo diretamente no bash.

a partir da resposta do chad, aqui está a solução de trabalho COMPLETA para analisar a UML, com manipulação de comentários, com apenas 2 pequenas funções (mais de 2 bu você pode mixar todas elas). Eu não digo que o único de Chad não funcionou, mas ele tinha muitos problemas com arquivos XML mal formatados: Então você tem que ser um pouco mais complicado para lidar com comentários e espaços perdidos / CR / TAB / etc.

O objective desta resposta é fornecer funções prontas e prontas para uso em qualquer pessoa que precise analisar UML sem ferramentas complexas usando perl, python ou qualquer outra coisa. Quanto a mim, não consigo instalar o cpan, nem módulos perl para o antigo sistema operacional de produção em que estou trabalhando, e o python não está disponível.

Primeiro, uma definição das palavras UML usadas neste post:

  content... 

EDIT: funções atualizadas, com alça de:

  • Websphere xml (atributos xmi e xmlns)
  • deve ter um terminal compatível com 256 colors
  • 24 tons de cinza
  • compatibilidade incluída para o IBM AIX bash 3.2.16 (1)

As funções, primeiro é o xml_read_dom que é chamado recursivamente por xml_read:

 xml_read_dom() { # https://stackoverflow.com/questions/893585/how-to-parse-xml-in-bash local ENTITY IFS=\> if $ITSACOMMENT; then read -d \< COMMENTS COMMENTS="$(rtrim "${COMMENTS}")" return 0 else read -d \< ENTITY CONTENT CR=$? [ "x${ENTITY:0:1}x" == "x/x" ] && return 0 TAG_NAME=${ENTITY%%[[:space:]]*} [ "x${TAG_NAME}x" == "x?xmlx" ] && TAG_NAME=xml TAG_NAME=${TAG_NAME%%:*} ATTRIBUTES=${ENTITY#*[[:space:]]} ATTRIBUTES="${ATTRIBUTES//xmi:/}" ATTRIBUTES="${ATTRIBUTES//xmlns:/}" fi # when comments sticks to !-- : [ "x${TAG_NAME:0:3}x" == "x!--x" ] && COMMENTS="${TAG_NAME:3} ${ATTRIBUTES}" && ITSACOMMENT=true && return 0 # http://tldp.org/LDP/abs/html/string-manipulation.html # INFO: oh wait it doesn't work on IBM AIX bash 3.2.16(1): # [ "x${ATTRIBUTES:(-1):1}x" == "x/x" -o "x${ATTRIBUTES:(-1):1}x" == "x?x" ] && ATTRIBUTES="${ATTRIBUTES:0:(-1)}" [ "x${ATTRIBUTES:${#ATTRIBUTES} -1:1}x" == "x/x" -o "x${ATTRIBUTES:${#ATTRIBUTES} -1:1}x" == "x?x" ] && ATTRIBUTES="${ATTRIBUTES:0:${#ATTRIBUTES} -1}" return $CR } 

e o segundo:

 xml_read() { # https://stackoverflow.com/questions/893585/how-to-parse-xml-in-bash ITSACOMMENT=false local MULTIPLE_ATTR LIGHT FORCE_PRINT XAPPLY XCOMMAND XATTRIBUTE GETCONTENT fileXml tag attributes attribute tag2print TAGPRINTED attribute2print XAPPLIED_COLOR PROSTPROCESS USAGE local TMP LOG LOGG LIGHT=false FORCE_PRINT=false XAPPLY=false MULTIPLE_ATTR=false XAPPLIED_COLOR=g TAGPRINTED=false GETCONTENT=false PROSTPROCESS=cat Debug=${Debug:-false} TMP=/tmp/xml_read.$RANDOM USAGE="${C}${FUNCNAME}${c} [-cdlp] [-x command < -a attribute>]  [tag | \"any\"] [attributes .. | \"content\"] ${nn[2]} -c = NOCOLOR${END} ${nn[2]} -d = Debug${END} ${nn[2]} -l = LIGHT (no \"attribute=\" printed)${END} ${nn[2]} -p = FORCE PRINT (when no attributes given)${END} ${nn[2]} -x = apply a command on an attribute and print the result instead of the former value, in green color${END} ${nn[1]} (no attribute given will load their values into your shell; use '-p' to print them as well)${END}" ! (($#)) && echo2 "$USAGE" && return 99 (( $# < 2 )) && ERROR nbaram 2 0 && return 99 # getopts: while getopts :cdlpx:a: _OPT 2>/dev/null do { case ${_OPT} in c) PROSTPROCESS="${DECOLORIZE}" ;; d) local Debug=true ;; l) LIGHT=true; XAPPLIED_COLOR=END ;; p) FORCE_PRINT=true ;; x) XAPPLY=true; XCOMMAND="${OPTARG}" ;; a) XATTRIBUTE="${OPTARG}" ;; *) _NOARGS="${_NOARGS}${_NOARGS+, }-${OPTARG}" ;; esac } done shift $((OPTIND - 1)) unset _OPT OPTARG OPTIND [ "X${_NOARGS}" != "X" ] && ERROR param "${_NOARGS}" 0 fileXml=$1 tag=$2 (( $# > 2 )) && shift 2 && attributes=$* (( $# > 1 )) && MULTIPLE_ATTR=true [ -d "${fileXml}" -o ! -s "${fileXml}" ] && ERROR empty "${fileXml}" 0 && return 1 $XAPPLY && $MULTIPLE_ATTR && [ -z "${XATTRIBUTE}" ] && ERROR param "-x command " 0 && return 2 # nb attributes == 1 because $MULTIPLE_ATTR is false [ "${attributes}" == "content" ] && GETCONTENT=true while xml_read_dom; do # (( CR != 0 )) && break (( PIPESTATUS[1] != 0 )) && break if $ITSACOMMENT; then # oh wait it doesn't work on IBM AIX bash 3.2.16(1): # if [ "x${COMMENTS:(-2):2}x" == "x--x" ]; then COMMENTS="${COMMENTS:0:(-2)}" && ITSACOMMENT=false # elif [ "x${COMMENTS:(-3):3}x" == "x-->x" ]; then COMMENTS="${COMMENTS:0:(-3)}" && ITSACOMMENT=false if [ "x${COMMENTS:${#COMMENTS} - 2:2}x" == "x--x" ]; then COMMENTS="${COMMENTS:0:${#COMMENTS} - 2}" && ITSACOMMENT=false elif [ "x${COMMENTS:${#COMMENTS} - 3:3}x" == "x-->x" ]; then COMMENTS="${COMMENTS:0:${#COMMENTS} - 3}" && ITSACOMMENT=false fi $Debug && echo2 "${N}${COMMENTS}${END}" elif test "${TAG_NAME}"; then if [ "x${TAG_NAME}x" == "x${tag}x" -o "x${tag}x" == "xanyx" ]; then if $GETCONTENT; then CONTENT="$(trim "${CONTENT}")" test ${CONTENT} && echo "${CONTENT}" else # eval local $ATTRIBUTES => eval test "\"\$${attribute}\"" will be true for matching attributes eval local $ATTRIBUTES $Debug && (echo2 "${m}${TAG_NAME}: ${M}$ATTRIBUTES${END}"; test ${CONTENT} && echo2 "${m}CONTENT=${M}$CONTENT${END}") if test "${attributes}"; then if $MULTIPLE_ATTR; then # we don't print "tag: attr=x ..." for a tag passed as argument: it's usefull only for "any" tags so then we print the matching tags found ! $LIGHT && [ "x${tag}x" == "xanyx" ] && tag2print="${g6}${TAG_NAME}: " for attribute in ${attributes}; do ! $LIGHT && attribute2print="${g10}${attribute}${g6}=${g14}" if eval test "\"\$${attribute}\""; then test "${tag2print}" && ${print} "${tag2print}" TAGPRINTED=true; unset tag2print if [ "$XAPPLY" == "true" -a "${attribute}" == "${XATTRIBUTE}" ]; then eval ${print} "%s%s\ " "\${attribute2print}" "\${${XAPPLIED_COLOR}}\"\$(\$XCOMMAND \$${attribute})\"\${END}" && eval unset ${attribute} else eval ${print} "%s%s\ " "\${attribute2print}" "\"\$${attribute}\"" && eval unset ${attribute} fi fi done # this trick prints a CR only if attributes have been printed durint the loop: $TAGPRINTED && ${print} "\n" && TAGPRINTED=false else if eval test "\"\$${attributes}\""; then if $XAPPLY; then eval echo "\${g}\$(\$XCOMMAND \$${attributes})" && eval unset ${attributes} else eval echo "\$${attributes}" && eval unset ${attributes} fi fi fi else echo eval $ATTRIBUTES >>$TMP fi fi fi fi unset CR TAG_NAME ATTRIBUTES CONTENT COMMENTS done < "${fileXml}" | ${PROSTPROCESS} # http://mywiki.wooledge.org/BashFAQ/024 # INFO: I set variables in a "while loop" that's in a pipeline. Why do they disappear? workaround: if [ -s "$TMP" ]; then $FORCE_PRINT && ! $LIGHT && cat $TMP # $FORCE_PRINT && $LIGHT && perl -pe 's/[[:space:]].*?=/ /g' $TMP $FORCE_PRINT && $LIGHT && sed -r 's/[^\"]*([\"][^\"]*[\"][,]?)[^\"]*/\1 /g' $TMP . $TMP rm -f $TMP fi unset ITSACOMMENT } 

e por último, as funções rtrim, trim e echo2 (to stderr):

 rtrim() { local var=$@ var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters echo -n "$var" } trim() { local var=$@ var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters echo -n "$var" } echo2() { echo -e "$@" 1>&2; } 

Colorização:

Ah, e você precisará de algumas variables ​​dinâmicas de colorização para serem definidas primeiro e exportadas também:

 set -a TERM=xterm-256color case ${UNAME} in AIX|SunOS) M=$(${print} '\033[1;35m') m=$(${print} '\033[0;35m') END=$(${print} '\033[0m') ;; *) m=$(tput setaf 5) M=$(tput setaf 13) # END=$(tput sgr0) # issue on Linux: it can produces ^[(B instead of ^[[0m, more likely when using screenrc END=$(${print} '\033[0m') ;; esac # 24 shades of grey: for i in $(seq 0 23); do eval g$i="$(${print} \"\\033\[38\;5\;$((232 + i))m\")" ; done # another way of having an array of 5 shades of grey: declare -a colorNums=(238 240 243 248 254) for num in 0 1 2 3 4; do nn[$num]=$(${print} "\033[38;5;${colorNums[$num]}m"); NN[$num]=$(${print} "\033[48;5;${colorNums[$num]}m"); done # piped decolorization: DECOLORIZE='eval sed "s,${END}\[[0-9;]*[m|K],,g"' 

Como carregar todas essas coisas:

Ou você sabe como criar funções e carregá-las via FPATH (ksh) ou uma emulação de FPATH (bash)

Se não, basta copiar / colar tudo na linha de comando.

Como funciona:

 xml_read [-cdlp] [-x command < -a attribute>]  [tag | "any"] [attributes .. | "content"] -c = NOCOLOR -d = Debug -l = LIGHT (no \"attribute=\" printed) -p = FORCE PRINT (when no attributes given) -x = apply a command on an attribute and print the result instead of the former value, in green color (no attribute given will load their values into your shell as $ATTRIBUTE=value; use '-p' to print them as well) xml_read server.xml title content # print content between  xml_read server.xml Connector port # print all port values from Connector tags xml_read server.xml any port # print all port values from any tags 

Com o modo de debugging (-d), os comentários e os atributos analisados ​​são impressos em stderr.

Não tenho conhecimento de nenhuma ferramenta de análise XML de shell puro. Então você provavelmente precisará de uma ferramenta escrita em outro idioma.

Meu módulo XML :: Twig Perl vem com uma ferramenta desse tipo: xml_grep , onde você provavelmente escreveria o que quiser como xml_grep -t '/html/head/title' xhtmlfile.xhtml > titleOfXHTMLPage.txt (a opção -t fornece a você resultado como texto em vez de xml)

Bem, você pode usar o utilitário xpath. Eu acho que o XML :: Xpath do perl contém.

Após algumas pesquisas para tradução entre os formatos Linux e Windows dos caminhos de arquivos em arquivos XML, encontrei tutoriais e soluções interessantes sobre:

  • Informações gerais sobre XPaths
  • Amara – coleção de ferramentas Pythonic para XML
  • Desenvolva Python / XML com 4Suite (2 partes)

Embora existam alguns utilitários de console prontos que podem fazer o que você quer, provavelmente levará menos tempo para escrever algumas linhas de código em uma linguagem de programação de propósito geral, como Python, que você pode facilmente estender e adaptar suas necessidades.

Aqui está um script python que usa lxml para análise – ele usa o nome de um arquivo ou uma URL como o primeiro parâmetro, uma expressão XPath como o segundo parâmetro e imprime as strings / nodes correspondentes à expressão especificada.

Exemplo 1

 #!/usr/bin/env python import sys from lxml import etree tree = etree.parse(sys.argv[1]) xpath_expression = sys.argv[2] # a hack allowing to access the # default namespace (if defined) via the 'p:' prefix # Eg given a default namespaces such as 'xmlns="http://maven.apache.org/POM/4.0.0"' # an XPath of '//p:module' will return all the 'module' nodes ns = tree.getroot().nsmap if ns.keys() and None in ns: ns['p'] = ns.pop(None) # end of hack for e in tree.xpath(xpath_expression, namespaces=ns): if isinstance(e, str): print(e) else: print(e.text and e.text.strip() or etree.tostring(e, pretty_print=True)) 

lxml pode ser instalado com o pip install lxml . No Ubuntu você pode usar o sudo apt install python-lxml .

Uso

 python xpath.py myfile.xml "//mynode" 

lxml também aceita um URL como input:

 python xpath.py http://www.feedforall.com/sample.xml "//link" 

Nota : Se o seu XML tem um espaço de nomes padrão sem prefixo (por exemplo, xmlns=http://abc... ) então você tem que usar o prefixo p (fornecido pelo ‘hack’) em suas expressões, por exemplo, //p:module para obter os módulos de um arquivo pom.xml . Caso o prefixo p já esteja mapeado em seu XML, você precisará modificar o script para usar outro prefixo.


Exemplo 2

Um script único que serve ao propósito restrito de extrair nomes de módulos de um arquivo do Apache Maven. Observe como o nome do nó ( module ) é prefixado com o namespace padrão {http://maven.apache.org/POM/4.0.0} :

pom.xml :

 < ?xml version="1.0" encoding="UTF-8"?>   cherries bananas pears   

module_extractor.py :

 from lxml import etree for _, e in etree.iterparse(open("pom.xml"), tag="{http://maven.apache.org/POM/4.0.0}module"): print(e.text) 

O método de Yuzem pode ser melhorado invertendo a ordem dos sinais < e > na function rdom e nas atribuições de variables, de modo que:

 rdom () { local IFS=\> ; read -d \< EC ;} 

torna-se:

 rdom () { local IFS=\< ; read -d \> CE ;} 

Se a análise não for feita desta forma, a última tag no arquivo XML nunca será atingida. Isso pode ser problemático se você pretende produzir outro arquivo XML no final do loop while.

Isso funciona se você estiver querendo atributos XML:

 $ cat alfa.xml  $ sed 's.[^ ]*..;s./>..' alfa.xml > alfa.sh $ . ./alfa.sh $ echo "$stream" H264_400.mp4