-->

How to search for a XML node and add or delete it

2019-06-01 10:08发布

问题:

I try to create a shell script that searches in an XML file for an attribute and create an element with the given attribute if this doesn't exist or delete the element if the attribute exists.

Here is the XML File:

<configuration name="distributor.conf" description="Distributor Configuration">
  <lists>       
    <list name="CRproductionLoadshare">
      <node name="fs100" weight="2"/>
      <node name="fs101" weight="2"/>     
    </list>
    <list name="AnyOtherGroup">
      <node name="fs100" weight="2"/>          
    </list>
  </lists>
</configuration>

And this is my Shellscript so far:

fs_name=fs
cnt=102
xmlstarlet ed \
  --var fs "'$fs_name$cnt'" \
  -a '//list' -t elem -n node -v "$fs_name$cnt" \
  -i '//node' -t attr -n name -v "$fs_name$cnt" \
  -i '//node' -t attr -n weight -v 2 \
  -d '//node[.=$fs]/text()' <distributor.conf.xml

The expected Output is

<configuration name="distributor.conf" description="Distributor Configuration">
  <lists>       
    <list name="CRproductionLoadshare">
      <node name="fs100" weight="2"/>
      <node name="fs101" weight="2"/>  
      <node name="fs102" weight="2"/>    
    </list>
    <list name="AnyOtherGroup">
      <node name="fs100" weight="2"/>          
    </list>
  </lists>
</configuration>

But my script work like this:

<?xml version="1.0"?>
<configuration name="distributor.conf" description="Distributor Configuration">
  <lists>
    <list name="CRproductionLoadshare">
      <node name="fs100" weight="2" name="fs102" weight="2"/>
      <node name="fs101" weight="2" name="fs102" weight="2"/>
    </list>
    <list name="AnyOtherGroup">
      <node name="fs100" weight="2" name="fs102" weight="2"/>          
    </list>
    <node name="fs102" weight="2"/>
  </lists>
</configuration>

How to change the shell script to reach the goal. At first, I want to add the node name="fs102" in case of that this node didn't exist.

回答1:

The most difficult task at hand here is to build the XPath that selects the correct node.

Step 1: find the XPath you need

example 1: Select the node named list who has an attribute @name="CRproductionLoadshare" and has a child named node with attribute @name="fs100".

So you can search for the parent of that particular node named node.

$ xmlstarlet sel -t                                                            \
        -m '//node[@name="fs100"]/parent::list[@name="CRproductionLoadshare"]' \
        -c . -n foo.xml
<list name="CRproductionLoadshare">
      <node name="fs100" weight="2"/>
      <node name="fs101" weight="2"/>     
</list>

or a bit easier :

$ xmlstarlet sel -t                                                        \ 
       -m '//list[@name="CRproductionLoadshare" and node[@name="fs100"]]'  \
       -c . -n foo.xml

example 2: Select the node named list who has an attribute @name="CRproductionLoadshare" and does not have a child named node with attribute @name="fs102".

Here we can use the XPath not-function

$ xmlstarlet sel -t                                                        \ 
       -m '//list[@name="CRproductionLoadshare" and not(node[@name="fs102"])]'  \
       -c . -n foo.xml
<list name="CRproductionLoadshare">
      <node name="fs100" weight="2"/>
      <node name="fs101" weight="2"/>     
</list>

Step 2: edit your XML-file with the XPath you just found

A: Just add the node if it is not there

So, since you now know the correct XPath to select the node, you can edit the XML-file accordingly by first inserting a subnode -s and then updating its values and attributes with -i

$ xpath1='//list[@name="CRproductionLoadshare" and not(node[@name="fs102"])]'
$ xpath2='//list[@name="CRproductionLoadshare" and not(node[@name="fs102" and @weight="2"])]/node[last()]'
$ xmlstarlet ed -s ${xpath1} -t elem -n "node"   -v ""      \
                -i ${xpath2} -t attr -n "name"   -v "fs102" \
                -i ${xpath2} -t attr -n "weight" -v "2"     \
                foo.xml

which outputs

<configuration name="distributor.conf" description="Distributor Configuration">
  <lists>
    <list name="CRproductionLoadshare">
      <node name="fs100" weight="2"/>
      <node name="fs101" weight="2"/>
      <node name="fs102" weight="2"/>
    </list>
    <list name="AnyOtherGroup">
      <node name="fs100" weight="2"/>
    </list>
  </lists>
</configuration>

B: Toggle the node

Toggling can be done by adding a fake attribute and then remove the node with that attribute:

$ xpath0='//list[@name="CRproductionLoadshare"]/node[@name="fs102"]'
$ xpath1='//list[@name="CRproductionLoadshare" and not(node[@name="fs102" and @delete="1"])]'
$ xpath2='//list[@name="CRproductionLoadshare" and not(node[@name="fs102" and @delete="1"])]/node[last()]'
$ xpath3='//list[@name="CRproductionLoadshare"]/node[@name="fs102" and @delete="1"]'
$ xmlstarlet ed -i ${xpath0} -t attr -n "delete" -v "1"     \
                -s ${xpath1} -t elem -n "node"   -v ""      \
                -i ${xpath2} -t attr -n "name"   -v "fs102" \
                -i ${xpath2} -t attr -n "weight" -v "2"     \
                -d ${xpath3}                                \
                foo.xml


回答2:

search in a XML file for a attribute and create it if this doesn't exist

fs_name="fs"
cnt=102

node_exists=$(xmlstarlet sel -t --var fs="'${fs_name}$cnt'" -v 'boolean(//list[@name="CRproductionLoadshare"]/node[@name=$fs])' distributor.conf.xml)
[ "$node_exists" = "false" ] && xmlstarlet ed -O -s '//list[@name="CRproductionLoadshare"]' \
-t elem -n node -i '//list[@name="CRproductionLoadshare"]/node[last()]' \
-t attr -n name -v "${fs_name}$cnt" \
-i '//list[@name="CRproductionLoadshare"]/node[last()]' -t attr -n weight -v 2 distributor.conf.xml

The output:

<configuration name="distributor.conf" description="Distributor Configuration">
  <lists>
    <list name="CRproductionLoadshare">
      <node name="fs100" weight="2"/>
      <node name="fs101" weight="2"/>
      <node name="fs102" weight="2"/>
    </list>
    <list name="AnyOtherGroup">
      <node name="fs100" weight="2"/>
    </list>
  </lists>
</configuration>

Scheme:

  • node_exists is assigned with boolean value indicating the needed node existence
  • [ "$node_exists" = "false" ] && xmlstarlet ed ... - the 2nd xmlstarlet edit command will be only executed if the node_exists is not equal to false


回答3:

The shell script that works for toggle the node looks like this:

fs_name=fs
cnt=102
inputfile=distributor.conf.xml

if [ -n "$(xmlstarlet sel -T -t -v "//list[@name='CRproductionLoadshare']/node[@name='$fs_name$cnt']/@name" $inputfile)" ]; then
  echo "$fs_name$cnt already defined in $inputfile"
  xmlstarlet ed -L -d "//list[@name='CRproductionLoadshare']/node[@name='$fs_name$cnt']" $inputfile
else
  echo "adding $fs_name$cnt to $inputfile"
  xmlstarlet ed -L -s  "//list[@name='CRproductionLoadshare']" -t elem -n TempNode -v "" \
    -i //TempNode -t attr -n "name" -v "$fs_name$cnt" \
    -i //TempNode -t attr -n "weight" -v "2" \
    -r //TempNode -v node \
    $inputfile
fi

Each time I run that script the inputfile toggle the node (add/delete) only in the list element with name CRproductionLoadshare.