Amir Gonnen
Amir Gonnen

Reputation: 3727

Retrieving all attribute names of an XML element with xpath on xmlstarlet

I can see how I can retrieve all attribute values:
xml sel -t -v "//element/@*"

but I want to get all attribute names.
I can get the n'th name by, for example xml sel -t -v "name(//x:mem/@*[3])" which returns the 3rd attribute name.
but xml sel -t -v "name(//x:mem/@*)" does not work (returns the 1st attribute name only)...

Is there a way to get all attribute names?

Upvotes: 3

Views: 7480

Answers (2)

Mathias Müller
Mathias Müller

Reputation: 22617

Use -t and -m to define a template match and then apply another XPath expression with -v.

$ xml sel -T -t -m "//mem/@*" -v "name()" -n input.xml

when applied to this input XML:

<root>
    <mem yes1="1" yes2="2"/>
    <other no="1" no2="2"/>
</root>

will print:

yes1
yes2

That's a "short line on the shell", but it's completely unintelligible. Therefore, I would still prefer kjhughes' XSLT solution. Do not sacrifice understandable code in favour of brevity.

You could write a stylesheet that takes an input parameter from the command line, so that you do not have to change the XSLT code if you'd like to retrieve the attribute names of a different element.


As suggested by @npostavs, internally, xmlstarlet makes use of XSLT anyway. You can inspect the XSLT that is generated by replacing -T with -C:

$ xml sel -C -t -m "//mem/@*" -v "name()" -n app.xml

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt="http://exslt.org/common" version="1.0" extension-element-prefixes="exslt">
  <xsl:output omit-xml-declaration="yes" indent="no"/>
  <xsl:template match="/">
    <xsl:for-each select="//mem/@*">
      <xsl:call-template name="value-of-template">
        <xsl:with-param name="select" select="name()"/>
      </xsl:call-template>
      <xsl:value-of select="'&#10;'"/>
    </xsl:for-each>
  </xsl:template>
  <xsl:template name="value-of-template">
    <xsl:param name="select"/>
    <xsl:value-of select="$select"/>
    <xsl:for-each select="exslt:node-set($select)[position()&gt;1]">
      <xsl:value-of select="'&#10;'"/>
      <xsl:value-of select="."/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

There are many more options to explore, see the xmlstarlet documentation.

Upvotes: 4

kjhughes
kjhughes

Reputation: 111511

This xmlstarlet command:

xml tr attnames.xsl in.xml

Using this XSLT transform, named attnames.xsl:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:template match="@*">
    <xsl:value-of select="name(.)"/>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
  <xsl:template match="*">
    <xsl:apply-templates select="@*"/>
    <xsl:apply-templates select="*"/>
  </xsl:template>
</xsl:stylesheet>

And this XML file, named in.xml:

<root att1="one">
  <a att2="two"/>
  <b att3="three">
    <c att4="four"/>
  </b>
</root>

Will produce a list of all attributes found in in.xml:

att1
att2
att3
att4

To select all attributes from the b element only, modify the XSLT like this:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:template match="@*">
    <xsl:value-of select="name(.)"/>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
  <xsl:template match="b">
    <xsl:apply-templates select="@*"/>
    <xsl:apply-templates select="*"/>
  </xsl:template>
  <xsl:template match="text()"/>
</xsl:stylesheet>

Upvotes: 3

Related Questions