Fetch multiple child-notes from same node with xmlstarlet in a block

Viewed 358

I have an XML file containing multiple user entries with some user data like name, email and other data. It seems that this could be done using multiple --value-of (-v) arguments like this:

$ xmlstarlet sel -N n="http://www.w3.org/2005/Atom" -t --nl -v "//n:title" -v "//n:email" ~/tests/test-xml.xml

Some user
Some user #2
Some user #3some.user@example.com
some.user2@example.com
some.user3@example.com

But they're not together, it looks like the tool process all <title> elements first and then all <email> ones. I like to have the following format:

Some user
some.user@example.com
Some user #2
some.user2@example.com
...

Found out that I need the xpath function concat for this. Now I get them at least with comma seperated:

$ xmlstarlet sel -N n="http://www.w3.org/2005/Atom" -t -m "//n:entry" -v "concat(current()//n:title, ',', current()//n:email)" ~/tests/test-xml.xml
Some user,some.user@example.comSome user #2,some.user2@example.comSome user #3,some.user3@example.com

That's exactly what I need, but when I set \n as delimiter instead of ,, it will just print \n instead of doing a line break. The same happens for \\n and \r\n. As a workaround, this could be replaced using sed like this: sed 's/,/\n/g'

However, this doesn't solve the problem that there is no new line between some.user@example.comSome user #2:

$ xmlstarlet sel -N n="http://www.w3.org/2005/Atom" -t -m "//n:entry" -v "concat(current()//n:title, ',', current()//n:email)" ~/tests/test-xml.xml | sed 's/,/\n/g'
Some user
some.user@example.comSome user #2
some.user2@example.comSome user #3
some.user3@example.com

How can I realize this? Would prefer a solution without additional sed command if it makes sense and would be possible.

Workaround

The only workaround I found is to nest it in another concat call to add another char, that identifies the placed where another new line is needed and could be replaced with \n too like this:

$ xmlstarlet sel -N n="http://www.w3.org/2005/Atom" -t -m "//n:entry" -v "concat(concat(current()//n:title, ',', current()//n:email), '|', '')" ~/tests/test-xml.xml | sed -E 's/[,|]+/\n/g'
Some user
some.user@example.com
Some user #2
some.user2@example.com
Some user #3
some.user3@example.com

Altough this works, it seems like a nasty workaround to me. Would like to know if there is a more clean way to do this. I guess it's possible with a deeper experience of xmlstarlet and maybe also xpath.

Test XML document

<?xml version="1.0" encoding="UTF-8"?>
<feed
  xmlns="http://www.w3.org/2005/Atom"
  xmlns:app="http://www.w3.org/2007/app"
  xmlns:snx="http://www.ibm.com/xmlns/prod/sn"
  xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
  <opensearch:totalResults
    xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">67
  </opensearch:totalResults>
  <opensearch:startIndex
    xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">1
  </opensearch:startIndex>
  <opensearch:itemsPerPage
    xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">100
  </opensearch:itemsPerPage>

  <entry>
    <title>Some user</title>
    <contributor>
      <email>some.user@example.com</email>
    </contributor>
  </entry>

  <entry>
    <title>Some user #2</title>
    <contributor>
      <email>some.user2@example.com</email>
    </contributor>
  </entry>

  <entry>
    <title>Some user #3</title>
    <contributor>
      <email>some.user3@example.com</email>
    </contributor>
  </entry>

</feed>
2 Answers

The simplest way is to output a newline (--nl) after every entry:

xmlstarlet sel -N n="http://www.w3.org/2005/Atom" -t -m "//n:entry" -v "n:title" --nl -v "n:contributor/n:email" --nl input.xml

but this will output an extra newline at the end of the output:

Some user
some.user@example.com
Some user #2
some.user2@example.com
Some user #3
some.user3@example.com

An alternative is to output a newline before an entry if it's not the first. (Using -i (xsl:if) and -b (break nesting))...

xmlstarlet sel -N n="http://www.w3.org/2005/Atom" -t -m "//n:entry" -i "position() > 1" --nl -b -v "n:title" --nl -v "n:contributor/n:email" input.xml

output:

Some user
some.user@example.com
Some user #2
some.user2@example.com
Some user #3
some.user3@example.com

I'm using xmllint here but the use of xpath and xargs/printf should still be the same:

xmllint --format --xpath "concat('\"',/feed/entry/title,'\" \"',/feed/entry/contributor/email,'\"')" ~/tests/test-xml.xml | xargs printf "%s\n"

With xpath, we format the data by adding quotations and a space between the title and the email. We then pipe through to xargs and printf to print each entry on a separate line.

Related