How to insert a new element under another with xmlstarlet?

24,754

Solution 1

Use -s (or --subnode) instead of -i. Regarding the bonus, you can't insert an element with an attribute directly but since every edit operation is performed in sequence, to insert an element and then add an attribute:

> xml ed -s /config -t elem -n sub -v "" -i /config/sub -t attr -n class -v com.foo test.xml
<?xml version="1.0" encoding="UTF-8"?>
<config>
<sub class="com.foo"></sub></config>

Solution 2

I had a similar problem: I had a Tomcat configuration file (server.xml), and had to insert a <Resource> tag with pre-defined attributes into the <GlobalNamingResources> section.

Here is how it looked before:

<GlobalNamingResources>
    <!-- Editable user database that can also be used
         by UserDatabaseRealm to authenticate users
    -->
    <Resource name="UserDatabase"
              auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>

Here is what I wanted to achieve:

<GlobalNamingResources>
    <!-- Editable user database that can also be used
         by UserDatabaseRealm to authenticate users
    -->
    <Resource name="UserDatabase"
              auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
    <Resource name="jdbc/templateassets"
              auth="Container"
              type="javax.sql.DataSource"
              driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://DBHOST:DBPORT/DBNAME?createDatabaseIfNotExist=false&amp;useUnicode=true&amp;characterEncoding=utf-8"
              username="DBUSER"
              password="DBPASS"
              maxActive="150"
              maxIdle="10"
              initialSize="10"
              validationQuery="SELECT 1"
              testOnBorrow="true" />
</GlobalNamingResources>

Here is how I did it (snippet from a shell script):

if [ -n "$(xmlstarlet sel -T -t -v "/Server/GlobalNamingResources/Resource[@name='jdbc/templateassets']/@name" server.xml)" ]; then
  echo "Resource jdbc/templateassets already defined in server.xml"
else
  echo "Adding resource jdbc/templateassets to <GlobalNamingResources> in server.xml"
  xmlstarlet ed -P -S -L -s /Server/GlobalNamingResources -t elem -n ResourceTMP -v "" \
    -i //ResourceTMP -t attr -n "name" -v "jdbc/templateassets" \
    -i //ResourceTMP -t attr -n "auth" -v "Container" \
    -i //ResourceTMP -t attr -n "type" -v "javax.sql.DataSource" \
    -i //ResourceTMP -t attr -n "driverClassName" -v "com.mysql.jdbc.Driver" \
    -i //ResourceTMP -t attr -n "url" -v "jdbc:mysql://DBHOST:DBPORT/DBNAME?createDatabaseIfNotExist=false&useUnicode=true&characterEncoding=utf-8" \
    -i //ResourceTMP -t attr -n "username" -v "DBUSER" \
    -i //ResourceTMP -t attr -n "password" -v "DBPASS" \
    -i //ResourceTMP -t attr -n "maxActive" -v "150" \
    -i //ResourceTMP -t attr -n "maxIdle" -v "10" \
    -i //ResourceTMP -t attr -n "initialSize" -v "10" \
    -i //ResourceTMP -t attr -n "validationQuery" -v "SELECT 1" \
    -i //ResourceTMP -t attr -n "testOnBorrow" -v "true" \
    -r //ResourceTMP -v Resource \
    server.xml
fi

The trick is to temporarily give a unique name to the new element, so that it can be found later with an XPATH expression. After all attributes have been added, the name is changed back to Resource (with -r).

The meaning of the other xmlstarlet options:

-P (or --pf)        - preserve original formatting
-S (or --ps)        - preserve non-significant spaces
-L (or --inplace)   - edit file inplace

Solution 3

From version 1.4.0 of XMLStarlet (dated 2012-08-26), you can use $prev (or $xstar:prev) as the argument to -i, -a, and -s to refer to the last nodeset inserted. See the examples in the XMLStarlet source code in the files doc/xmlstarlet.txt, examples/ed-backref1, examples/ed-backref2, and examples/ed-backref-delete. You no longer need to use the trick of inserting the element with a temporary element name and then renaming it at the end. The example examples/ed-backref2 is particularly helpful in showing how to define a variable to use to refer to a (the) previously-created note so that you don't need to do tricks such as $prev/.. to "navigate" out of a node.

Solution 4

As mentioned by @npoostavs the correct answer is to use 'subnode'. To improve the answer with the new '$prev' you can do the following:

xml ed --inplace \
       --subnode /config --type elem --name "sub" \
       --var new_node '$prev' \
       --insert '$new_node' --type attr --name "class" --value "com.foo" \ 
       test.xml

With the following explanation:

--inplace    Change the file "test.xml" directly
--subnode    Add a new node called "class" below "/config"
--var        Assign the newly created node to the variable new_node 
             Use single quotes to prevent bash replacing the variable
--insert     Insert attribute and value to the newly created node

Solution 5

The example did not work until I wrapped <GlobalNamingResources> into a <Server> element.

Share:
24,754
simpatico
Author by

simpatico

Author of dp4j.jar, which lets you test access private methods in Java without writing any reflection API code. The necessary reflection code is injected by dp4j at compile-time. So you only write: @Test public void aTest(){ PrivateConstructor pc = new PrivateConstructor("Hello!"); Instead of: import java.lang.reflect.*; @Test public void aTest() throws IllegalAccessException, NoSuchMethodException , InvocationTargetException, InstantiationException { Constructor pcInit = PrivateConstructor.class.getDeclaredConstructor(String.class); pcInit.setAccessible(true); PrivateConstructor pc = (PrivateConstructor) pcInit.newInstance("Hello!"); Check it out at www.dp4j.com

Updated on February 02, 2021

Comments

  • simpatico
    simpatico over 3 years
    $ vim test.xml
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <config>
    </config>
    $ xmlstarlet ed -i "/config" -t elem -n "sub" -v "" test.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <sub></sub>
    <config>
    </config>
    

    But I wanted sub to be a child of config. How should I change the xpath parameter of -i?

    BONUS: Is it possible to insert the child directly with an attribute and even have it set to a value? Something like:

    $ xmlstarlet ed -i "/config" -t elem -n "sub" -v ""  -a attr -n "class" -v "com.foo" test.xml
    
  • Bernhard Hofmann
    Bernhard Hofmann about 12 years
    That last edit action (rename) is a game changer. Thank you so much for that!
  • marsbard
    marsbard over 10 years
    +1 on the rename, nice way to make sure you are getting just your resource for editing and then rename at the end, thanks.
  • Warren Rox
    Warren Rox almost 10 years
    The problem with this approach is that any other XML elements with the same name acquire the attribute as well which is likely undesired. A way around this is to add the element with a temporary name that you can XPath to uniquely and add the attribute and subsequently remain the element when you're done.
  • Frederick Nord
    Frederick Nord almost 5 years
    cool, can you cook up a nicer version of the answer with the (currently) highest votes? stackoverflow.com/a/9172796/2015768