Search code examples
xmlstarlet

xmlstarlet add element with namespace and attributes


I'm trying to add a node with a namespace and an attribute to an xml, but it fails if I try to do it as multiple commands in one execution of xmlstarlet:

<?xml version="1.0"?>
<levela xmlns:xi="http://www.w3.org/2001/XInclude">
  <levelb>
  </levelb>
</levela>

xmlstarlet ed -L -s /levela/levelb -t elem -n xi:input -i //xi:input -t attr -n "href" -v "aHref" file.xml

I'm trying to get:

<?xml version="1.0"?>
<levela xmlns:xi="http://www.w3.org/2001/XInclude">
  <levelb>
     <xi:input href="aHref"/>
  </levelb>
</levela>

But the attribute isn't added. So I get:

<?xml version="1.0"?>
<levela xmlns:xi="http://www.w3.org/2001/XInclude">
  <levelb>
     <xi:input/>
  </levelb>
</levela>

It works if I run it as two executions like this:

xmlstarlet ed -L -s /levela/levelb -t elem -n xi:input file.xml

xmlstarlet ed -L -i //xi:input -t attr -n "href" -v "aHref" file.xml

It also works if I add a tag without a namespace e.g:

xmlstarlet ed -L -s /levela/levelb -t elem -n levelc -i //levelc -t attr -n "href" -v "aHref" file.xml

<?xml version="1.0"?>
<levela xmlns:xi="http://www.w3.org/2001/XInclude">
  <levelb>
     <levelc href="aHref"/>
  </levelb>
</levela>

What am I doing wrong? Why doesn't it work with the namespace?


Solution

  • This will do it:

    xmlstarlet edit \
      -s '/levela/levelb' -t elem -n 'xi:input' \
      -s '$prev' -t attr -n 'href' -v 'aHref' \
    file.xml
    

    xmlstarlet edit code can use the convenience $prev (aka $xstar:prev) variable to refer to the node created by the most recent -i (--insert), -a (--append), or -s (--subnode) option. Examples of $prev are given in doc/xmlstarlet.txt and the source code's examples/ed-backref*.

    Attributes can be added using -i, -a, or -s.

    What am I doing wrong? Why doesn't it work with the namespace?

    Update 2022-04-15
    The -i '//xi:input' … syntax you use is perfectly logical. As your own 2 alternative commands suggest it's the namespace xi that triggers the omission and there's a hint in the edInsert function in the source code's src/xml_edit.c where it says NULL /* TODO: NS */. When you've worked with xmlstarlet for some time you come to accept its limitations (or not); in this case the $prev back reference is useful. I wouldn't expect that TODO to go away anytime soon.
    (end update)

    Well, I think xmlstarlet edit looks upon node naming as a user responsibility, as the following example suggests,

    printf '<v/>' |
    xmlstarlet edit --omit-decl \
      -s '*' -t elem -n 'undeclared:qname' -v 'x' \
      -s '*' -t elem -n '!--' -v ' wotsinaname ' \
      -s '$prev' -t attr -n ' "" ' -v '' \
      -s '*' -t elem -n ' <&> ' -v 'harrumph!' 
    

    the output of which is clearly not XML:

    <v>
      <undeclared:qname>x</undeclared:qname>
      <!--  "" =""> wotsinaname </!-->
      < <&> >harrumph!</ <&> >
    </v>
    

    If you want to indent the new element, for example:

    xmlstarlet edit \
      -s '/levela/levelb' -t elem -n 'xi:input' \
      --var newnd '$prev' \
      -s '$prev' -t attr -n 'href' -v 'aHref' \
      -a '$newnd' -t text -n ignored -v '' \
      -u '$prev' -x '(//text())[1][normalize-space()=""]' \
    file.xml
    

    The -x XPath expression grabs the first text node provided it contains nothing but whitespace, i.e. the first child node of levela. The --var name xpath option to define an xmlstarlet edit variable is mentioned in doc/xmlstarlet.txt but not in the user's guide.

    I used xmlstarlet version 1.6.1.