Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Paths, Steps and Predicates

A path expression is a series of steps separated by /. Each step starts from the nodes the previous step found and selects new nodes relative to each of them.

Steps from the top

A path that starts with / starts at the document node. Each step names the elements to go down to:

$ sxml xpath --context-item=vane.xml '/rfc/middle/section/name/string()'
"Introduction"
"Flag States"
"Messages"
"Pause"
"Security Considerations"
"IANA Considerations"

/rfc selects the rfc element, /middle its middle child, /section the sections in it, and /name the name of each section. The final string() turns each name element into its string. The nested sections, such as Terminology, are not there: each step goes down one level only.

// means "at any depth". //section is every section element in the document, wherever it is. A * selects elements of any name:

$ sxml xpath --context-item=vane.xml '//section[@anchor = "rollout"]/* ! name()'
"name"
"t"
"t"
"t"

name() gives the name of a node. The ! operator ("for each") applies it to each child that * found.

Predicates

A predicate in square brackets keeps only the nodes for which it is true. Inside the brackets, the expression is evaluated once for each node, with that node as the context item, so names in it are relative to the node:

$ sxml xpath --context-item=vane.xml '//section[@anchor = "pause"]/t/@anchor/string()'
"req-7"
"req-8"
"req-11"

A predicate can test anything: whether a child exists, what its value is, or what a function says about it:

$ sxml xpath --context-item=vane.xml '//t[xref[starts-with(@target, "RFC")]]/@anchor/string()'
"req-9"
$ sxml xpath --context-item=vane.xml '//section[t[@anchor]] ! @anchor/string()'
"states"
"rollout"
"status"
"pause"

The first finds the requirements that cite an RFC: the paragraphs with an xref whose target starts with RFC. Inside the inner predicate, @target is the target of the xref being tested. The second finds the sections that have requirements of their own.

Positions

A number in a predicate selects by position, counting from 1:

$ sxml xpath --context-item=vane.xml '/rfc/middle/section[2]/name/string()'
"Flag States"

This is where // can surprise you. Which section has the first requirement with a key word?

$ sxml xpath --context-item=vane.xml '//t[bcp14][1]/../name/string()'
"Flag States"
"Rollout"
"Status"
"Pause"
"Security Considerations"

There are five answers because //t[bcp14][1] means "each t with a bcp14 that is the first such t of its parent", and five sections have one. To get the first in the whole document, put the path in parentheses and filter the result:

$ sxml xpath --context-item=vane.xml '(//t[bcp14])[1]/@anchor/string()'
"req-1"

last() is the position of the last item, and position() the position of the current one:

$ sxml xpath --context-item=vane.xml '(//t[bcp14])[last()]/string()'
"Vane messages travel in clear text on the network. Deployments
      SHOULD carry Vane over an encrypted link.
      This needs a requirement anchor of
      its own."
$ sxml xpath --context-item=vane.xml '(//t[@anchor])[position() <= 3]/@anchor/string()'
"req-1"
"req-2"
"req-3"

The last paragraph with a key word is in Security Considerations, and it has no anchor: an editor's note in it says so. We'll come back to it.

Axes

So far every step went down the tree, to children or attributes. A step can go in other directions too. The direction is called an axis, and it is written before the name with ::. Which section is requirement req-5 in?

$ sxml xpath --context-item=vane.xml '//t[@anchor = "req-5"]/ancestor::section/name/string()'
"Messages"
"Rollout"
$ sxml xpath --context-item=vane.xml '//t[@anchor = "req-5"]/ancestor::section[1]/name/string()'
"Rollout"

ancestor::section goes up: to every section that contains the paragraph, Messages and its subsection Rollout. The result of a path is always in document order, but a predicate on a reverse axis such as ancestor counts from the node outwards, so ancestor::section[1] is the nearest one.

The sibling axes go sideways, to the nodes with the same parent, and following and preceding go to everything after or before in the document:

$ sxml xpath --context-item=vane.xml '//t[@anchor = "req-4"]/following-sibling::t/@anchor/string()'
"req-5"
"req-9"
$ sxml xpath --context-item=vane.xml '//cref[@anchor = "issue-2"]/preceding::t[@anchor][1]/@anchor/string()'
"req-11"

The second asks for the last requirement before the editor's note issue-2. These are the axes you can use:

AxisSelects
child::the children (the default: name is child::name)
attribute::the attributes (@anchor is attribute::anchor)
descendant::, descendant-or-self::children, their children, and so on
parent::the parent (.. is parent::node())
ancestor::, ancestor-or-self::the parent, its parent, up to the document node
following-sibling::, preceding-sibling::the nodes after or before, with the same parent
following::, preceding::every node after or before in the document, other than descendants and ancestors
self::the node itself

.. is the most common: from the xrefs to the term "pause", go up to the paragraphs that use it.

$ sxml xpath --context-item=vane.xml '//xref[@target = "term-pause"]/../@anchor/string()'
"req-5"
"req-7"

The draft does not number its sections: the renderer does, by position. Axes can compute those numbers. A section's number is one more than the number of sections before it with the same parent, and a subsection's number puts its parent's number before it:

$ sxml xpath --context-item=vane.xml '//t[@anchor] ! (@anchor || " in " || string-join(ancestor::section ! (count(preceding-sibling::section) + 1), "."))'
"req-1 in 2"
"req-2 in 2"
"req-3 in 2"
"req-4 in 3.1"
"req-5 in 3.1"
"req-9 in 3.1"
"req-6 in 3.2"
"req-10 in 3.2"
"req-7 in 4"
"req-8 in 4"
"req-11 in 4"

For each requirement, ancestor::section finds its sections from the outermost in, count(preceding-sibling::section) + 1 numbers each, and string-join joins the numbers with dots. Insert a section, and every number after it changes, while the anchors stay what they are. That is why the draft refers to sections by anchor.

Kinds of nodes

Names select elements (or attributes, after @). Other tests select other kinds of node: text() selects text nodes, comment() comments, processing-instruction() processing instructions, node() any node, and element() and attribute() elements and attributes of any name. When sxml writes a text node, it writes the text itself, without quotes, because a text node is not a string:

$ sxml xpath --context-item=vane.xml '/rfc/front/title/text()'
The Vane Protocol for Feature Flag Rollouts
$ sxml xpath --context-item=vane.xml 'count(/comment()), count(//processing-instruction())'
1
0

The draft has one comment and no processing instructions. Its editorial notes are not comments but cref elements, so that xml2rfc can show them in a draft and leave them out of the RFC. They are elements in the middle of the text:

$ sxml xpath --context-item=vane.xml '//cref/text()'
Should an owner be able to
      change the duration?
This needs a requirement anchor of
      its own.

Combining paths

| (or union) combines the nodes of two paths, intersect keeps the nodes in both, and except the nodes in the first but not the second. The result is always in document order, the order of the nodes in the file, without duplicates. How many paragraphs with a key word are there outside Security Considerations, and which requirements have an xref?

$ sxml xpath --context-item=vane.xml 'count(//t[bcp14] except //section[@anchor = "security"]//t)'
11
$ sxml xpath --context-item=vane.xml '(//t[@anchor] intersect //t[xref])/@anchor/string()'
"req-1"
"req-4"
"req-5"
"req-9"
"req-7"

The anchors come out as req-9 before req-7 because that is their order in the file. A step can also be any expression in parentheses. This makes it easy to pick several children at once:

$ sxml xpath --context-item=vane.xml '//reference[@anchor = "RFC5234"]/front/(title, author/@surname)/string()'
"Augmented BNF for Syntax Specifications: ABNF"
"Crocker"
"Overell"

The value of a node

Every node has a string value: all the text inside it, joined. For a name element, that's its name. For a paragraph, it's the text of the paragraph and of every element in it, line breaks included. normalize-space collapses the whitespace:

$ sxml xpath --context-item=vane.xml '//t[@anchor = "req-5"]/string()'
"A service MUST NOT roll out a flag
        while it is in a pause
        ()."
$ sxml xpath --context-item=vane.xml 'normalize-space(//t[@anchor = "req-5"])'
"A service MUST NOT roll out a flag while it is in a pause ()."

The key word and the term are in the value, because they are text inside the paragraph. The empty parentheses are a reference to a section, <xref target="pause"/>, which has no text of its own: a renderer writes "Section 4" there, and the string value has nothing. The same goes for editorial notes, which are part of the value too. To get the text of req-8 without its note, take only its own text nodes and its key word:

$ sxml xpath --context-item=vane.xml 'normalize-space(//t[@anchor = "req-8"])'
"A pause SHOULD last five minutes. Should an owner be able to change the duration?"
$ sxml xpath --context-item=vane.xml 'normalize-space(string-join(//t[@anchor = "req-8"]/(text() | bcp14)))'
"A pause SHOULD last five minutes."

When you compare a node with a value, or do arithmetic with it, XPath uses this value. That is why //t[bcp14 = "MUST"] works: the string value of the bcp14 element is compared with the string "MUST". How comparisons work is the subject of the next section.