5

I have a big XML file and I get all occurrences between 2 tags:

Here is what I do:

sed -n '/<tag>/,/<\/tag>/p' file.xml

And I need to filter to get only first N occurrences. I've tried with l param but it was not enough :(

So any one knows how to get N matched occurrences from all resultset?

For Example. Here the xml file content :

<?xml version="1.0" encoding="UTF-8"?>
<root>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
</root>

 sed -n '/<tag>/,/<\/tag>/p' file.xml 

returns all elements.

So the goal is to filter to get the First n matched patterns ( Elements are multi-line) If n = 2 then result =:

<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
1
  • 1
    Sample input and an expected output would be better. Commented Jun 3, 2014 at 10:29

6 Answers 6

6

Try:

xmllint --xpath '//tag[position()<=2]' file.xml

Or:

xmlstarlet sel -t -c '//tag[position()<=2]' file.xml

Or:

xmlstarlet sel -t -m '//tag[position()<=2]' -c . -n file.xml

If you wanted to do it with sed only you could do something like:

sed -n '
  1{x;s/^/../;x;}; # initialise counter with two tokens
  /<tag>/,/<\/tag>/ {
    p; /<\/tag>/{
      x;s/.//;/./!q;x; # remove a token and quit if hold space empty
    }
  }' file.xml

That is, use the hold space as a counter of remaining sections to display (using dot characters).

1
  • xmlstarlet with parameter from the command line: xmlstarlet sel -t --var n=3 -c '//tag[position() < $n + 1]' file.xml
    – Kusalananda
    Commented Jul 10, 2021 at 15:54
4

You really should be using a parser for this, but, just so you know, sed -n '/<tag>/,/<\/tag>/p' file.xml gets you all elements because you print them all. That command works by addressing all lines between a line containing <tag> and the next line in input that contains </tag>. Since that makes pretty much all of your lines, just printing them doesn't show up much of a difference. Something like the following might be a little nearer to the mark:

sed -n '\|<tag>|{:n
    \|</tag>|!{N;bn}
    y|\n| |;p
}'

It addresses <tag> lines and checks them for </tag>. If they don't contain the closing string it pulls in another line - and it does so repeatedly until the pattern space contains <tag>.*</tag>[^\n]*$.

Then I just translate all \newline characters in pattern space into spaces.

Here it is again:

sed -n '\|<tag>|{:n;\|</tag>|!{N;bn};y|\n| |;p}' <<\DATA
<?xml version="1.0" encoding="UTF-8"?>
<root>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
</root>
DATA

OUTPUT:

<tag>  <t1>john</t1>  <t2>john</t2>  <t3>john</t3> </tag>
<tag>  <t1>john</t1>  <t2>john</t2>  <t3>john</t3> </tag>
<tag>  <t1>john</t1>  <t2>john</t2>  <t3>john</t3> </tag>
<tag>  <t1>john</t1>  <t2>john</t2>  <t3>john</t3> </tag>

Now you might do:

sed -n '\|<tag>|{:n
    \|</tag>|!{N;bn}
    y|\n| |;p
}' ./file | 
sed 's|> |>\n|g;2q'

...which gets me:

<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
<tag>
 <t1>john</t1>
 <t2>john</t2>
 <t3>john</t3>
</tag>
1
  • 1
    @TVart - while I appreciate the vote of confidence, Stephane's method is, admittedly, faster. It requires only the one sed process and doesn't need to loop through the file twice. I definitely recommend you get the hang of the :label;/address/!{N|H;b label} idea if you're going to be doing multiline addressing, but his method implements a hold space counter along the lines of what you'll find in some of the most advanced info sed examples and quits the file as soon as possible. Best, in my opinion, would be a combination of the two.
    – mikeserv
    Commented Jun 6, 2014 at 14:26
1

I think this is what you want,

sed -n '/<tag>/,/<\/tag>/p' file.xml | head -10

Try the below command to get the first two lines which starts with <tag>,

$ sed -n '/^<tag>/p' file.xml | head -2
<tag><t1>john</t1></tag>
<tag><t1>john</t1></tag>
8
  • unfortunatly this did not do expected behaviour. What is required is to get the first n occurrencies of contents includigs tags, your solution return only the first 10 lines starting at the first occurrence
    – TVart
    Commented Jun 3, 2014 at 11:01
  • please explain your question clearly with an example. Commented Jun 3, 2014 at 11:04
  • <?xml version="1.0" encoding="UTF-8"?> <root> <tag><t1>john</t1></tag> <tag><t1>john</t1></tag> <tag><t1>john</t1></tag> <tag><t1>john</t1></tag> </root> sed -n '/<tag>/,/<\/tag>/p' file.xml returns all <tag> elements. I want to get First n Elements
    – TVart
    Commented Jun 3, 2014 at 11:06
  • post the above comment in your question. Commented Jun 3, 2014 at 11:07
  • @user70355 see my edit. Commented Jun 3, 2014 at 13:06
0

Pipe through head -N. After the first N occurrences, head exits and thus sed will stop.

0
0

As far as I know, sed matches are always greedy i.e. /<tag>/,/<\/tag>/ will match from the first instance of <tag> to the last instance of <\tag> - including any other XML objects between.

If your version of awk supports multi-character record separators, you might be able to do something like

awk -v n=2 'BEGIN{RS="</tag>\n";ORS=RS} NR<=n'

but really a more robust solution would be to use a dedicated XML parser - for example a very minimal implementation using python's minidom

#!/usr/bin/python

from xml.dom import minidom

xmldoc = minidom.parse('file.xml')
taglist = xmldoc.getElementsByTagName('tag')
for i in range(2) :
        print taglist[i].toxml()
3
  • Your command should be awk 'BEGIN{RS="</tag>\n";ORS=RS} NR<=2' c | sed -n '/<tag>/,/<\/tag>/p' so that it would remove the first two lines. Commented Jun 3, 2014 at 15:54
  • Well, if it needs to pass by a script, I already have a solution using XMLReader in php, but the challenge for me was to do it in commande-line tool like sed or awk
    – TVart
    Commented Jun 3, 2014 at 18:40
  • On a line, yes, sed is greedy. But multi-line addresses aren't - -it wouldn't make sense. sed doesn't know what's in the next input line - or even whether it is the last line - until it is read, so it takes the first available match for //,/address2/ that it can.
    – mikeserv
    Commented Jun 6, 2014 at 8:18
0

Well, eventually I'll answer myself to my own question.

The solution I've found works in 2 ( maybe 3 ) Steps :

1 - Getting all Required Element by :

sed -n '/<tag>/,/<\/tag>/p' file.xml > selectedItems.xml

2 - Getting the N-th position of the last item by

POS = grep -n '</tag>' ./selectedItems.xml | head -n [POS] | tail -n 1

3 - Getting First N Required Items :

sed -n 1,[POS]p selectedItems.xml > selectedItems.xml

Of course it is possible to do all steps without splitting, but It wont be such clear.

P.S. To be sure that the position corresponds to the real N-th position in the tree ( when xml file is formed all in-line ) I used to use:

xmllint --format ./myxmlfile.xml

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .