I have just learned about xmlstarlet, but unfortunately I have a really hard time with XML, so I hope I'll get some help with this ...
Say, I have this XML file, test.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<objects>
<g id="layer3" inkscape:label="hello">
<circle id="circ2" inkscape:label="there"/>
<rect id="rect2" inkscape:label="world"/>
</g>
<g id="layer4">
<circle id="circ3" inkscape:label="more"/>
</g>
</objects>
So what I want to do is: for each node where inkscape:label
attribute exists, copy value of the inkscape:label
attribute to the id
attribute; so the expected output from the above would be:
<?xml version="1.0" encoding="UTF-8"?>
<objects>
<g id="hello" inkscape:label="hello">
<circle id="there" inkscape:label="there"/>
<rect id="world" inkscape:label="world"/>
</g>
<g id="layer4">
<circle id="more" inkscape:label="more"/>
</g>
</objects>
How can I do this with xmlstarlet?
Apparently I can replace all id attributes with a fixed value by using expression string("TEST")
like this:
$ xmlstarlet edit -N inkscape="http://www.inkscape.org/namespaces/inkscape" --update '//*/@id' --expr 'string("TEST")'
test.xml
test.xml:3.40: Namespace prefix inkscape for label on g is not defined
<g id="layer3" inkscape:label="hello">
^
test.xml:4.46: Namespace prefix inkscape for label on circle is not defined
<circle id="circ2" inkscape:label="there"/>
^
test.xml:5.44: Namespace prefix inkscape for label on rect is not defined
<rect id="rect2" inkscape:label="world"/>
^
test.xml:8.45: Namespace prefix inkscape for label on circle is not defined
<circle id="circ3" inkscape:label="more"/>
^
<?xml version="1.0" encoding="UTF-8"?>
<objects>
<g id="TEST" inkscape:label="hello">
<circle id="TEST" inkscape:label="there"/>
<rect id="TEST" inkscape:label="world"/>
</g>
<g id="TEST">
<circle id="TEST" inkscape:label="more"/>
</g>
</objects>
... and I can "reinsert" the value of the attribute id with expression string(../@id)
like this (so I basically get same output as input):
$ xmlstarlet edit -N inkscape="http://www.inkscape.org/namespaces/inkscape" --update '//*/@id' --expr 'string(../@id)' test.xml
test.xml:3.40: Namespace prefix inkscape for label on g is not defined
<g id="layer3" inkscape:label="hello">
^
test.xml:4.46: Namespace prefix inkscape for label on circle is not defined
<circle id="circ2" inkscape:label="there"/>
^
test.xml:5.44: Namespace prefix inkscape for label on rect is not defined
<rect id="rect2" inkscape:label="world"/>
^
test.xml:8.45: Namespace prefix inkscape for label on circle is not defined
<circle id="circ3" inkscape:label="more"/>
^
<?xml version="1.0" encoding="UTF-8"?>
<objects>
<g id="layer3" inkscape:label="hello">
<circle id="circ2" inkscape:label="there"/>
<rect id="rect2" inkscape:label="world"/>
</g>
<g id="layer4">
<circle id="circ3" inkscape:label="more"/>
</g>
</objects>
... but I cannot use the same trick (expression string(../@inkscape:label)
- or string(../@*[local-name()='label'])
as per How does local-name find attributes in an xml node?) to read from attribute inkscape:label
- and I cannot really tell whether that is because of the "Namespace prefix" .. "not defined" message:
$ xmlstarlet edit -N inkscape="http://www.inkscape.org/namespaces/inkscape" --update '//*/@id' --expr 'string(../@inkscape:label)' test.xml
test.xml:3.40: Namespace prefix inkscape for label on g is not defined
<g id="layer3" inkscape:label="hello">
^
test.xml:4.46: Namespace prefix inkscape for label on circle is not defined
<circle id="circ2" inkscape:label="there"/>
^
test.xml:5.44: Namespace prefix inkscape for label on rect is not defined
<rect id="rect2" inkscape:label="world"/>
^
test.xml:8.45: Namespace prefix inkscape for label on circle is not defined
<circle id="circ3" inkscape:label="more"/>
^
<?xml version="1.0" encoding="UTF-8"?>
<objects>
<g id="" inkscape:label="hello">
<circle id="" inkscape:label="there"/>
<rect id="" inkscape:label="world"/>
</g>
<g id="">
<circle id="" inkscape:label="more"/>
</g>
</objects>
And via get attribute value using xmlstarlet or xmllint ; I can confirm I can target the id attribute with:
xmlstarlet select -N inkscape="http://www.inkscape.org/namespaces/inkscape" --template --value-of '//*/@id' test.xml
... but the corresponding command for the inkscape:label returns nothing:
xmlstarlet select -N inkscape="http://www.inkscape.org/namespaces/inkscape" --template --value-of '//*/@inkscape:label' test.xml
It's probably that namespace thing, but I don't understand how can I ignore the namespace, and just relate to the attribute names in the document as they are ...
EDIT: finally solved the issue here with Python 3:
#!/usr/bin/env python3
# https://stackoverflow.com/questions/30097949/elementtree-findall-to-recursively-select-all-child-elements
# https://stackoverflow.com/questions/13372604/python-elementtree-parsing-unbound-prefix-error
# https://stackoverflow.com/questions/2352840/parsing-broken-xml-with-lxml-etree-iterparse
# https://stackoverflow.com/questions/28813876/how-do-i-get-pythons-elementtree-to-pretty-print-to-an-xml-file
import sys
import lxml
import lxml.etree
import xml.etree.ElementTree as ET
def proc_node(node):
target_label = 'inkscape:label' # file without namespace, like `test.xml` here
#target_label = '{http://www.inkscape.org/namespaces/inkscape}label' # file with namespace (like proper Inkscape .svg)
if target_label in node.attrib:
node.attrib['id'] = node.attrib[target_label]
for childel in node.getchildren():
proc_node(childel)
parser1 = lxml.etree.XMLParser(encoding="utf-8", recover=True)
tree1 = ET.parse('test.xml', parser1)
ET.indent(tree1, space=" ", level=0)
proc_node(tree1.getroot())
print(lxml.etree.tostring(tree1.getroot(), xml_declaration=True, pretty_print=True, encoding='UTF-8').decode('utf-8'))
... if I call this xmlproc.py
, then the result is:
$ python3 xmlproc.py
<?xml version='1.0' encoding='UTF-8'?>
<objects>
<g id="hello" inkscape:label="hello">
<circle id="there" inkscape:label="there"/>
<rect id="world" inkscape:label="world"/>
</g>
<g id="layer4">
<circle id="more" inkscape:label="more"/>
</g>
</objects>
... which is exactly what I wanted.
So to specify in the spirit of how the question is postulated - how do I achieve this with xmlstarlet?