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

Functions and Operators

XPath comes with a large library of functions, defined in a companion specification, XPath and XQuery Functions and Operators 4.0. This section shows the ones you'll use every day, and the expressions that make XPath feel like a small programming language: conditions, variables and loops.

Strings

The draft's name, its docName, says which version it is: draft-example-vane-protocol-01 is version 01. The string functions take it apart:

$ sxml xpath --context-item=vane.xml 'contains(/rfc/@docName, "vane"), starts-with(/rfc/@docName, "draft-"), substring(/rfc/@docName, 15, 4)'
true()
true()
"vane"
$ sxml xpath --context-item=vane.xml 'substring-after(/rfc/@docName, "draft-"), tokenize(/rfc/@docName, "-")[last()]'
"example-vane-protocol-01"
"01"

substring counts characters from 1. tokenize splits a string at a separator, here -, and [last()] keeps the last part: the version.

Paragraphs in XML are full of line breaks and indentation. normalize-space turns every run of whitespace into one space and removes it at both ends, which is what you want before you compare or print a paragraph:

$ sxml xpath --context-item=vane.xml 'normalize-space(//t[@anchor = "req-3"])'
"A rolling-out flag SHOULD return to the state ""stable"" ten minutes after its rollout step began."

The quotes from the paragraph are doubled: that is how a string literal in XPath writes a quote, and sxml writes strings so that you could paste them back into an expression.

string-join joins a sequence with a separator, and || joins two strings. A string template, written between backticks, puts the values of expressions in curly braces into a string:

$ sxml xpath --context-item=vane.xml 'string-join(//section[@anchor = "pause"]/t/@anchor, ", ")'
"req-7, req-8, req-11"
$ sxml xpath --context-item=vane.xml '/rfc/middle/section ! `{name}: {count(.//t[@anchor])} requirements`'
"Introduction: 0 requirements"
"Flag States: 3 requirements"
"Messages: 5 requirements"
"Pause: 3 requirements"
"Security Considerations: 0 requirements"
"IANA Considerations: 0 requirements"

Messages has five because .//t also finds the requirements of its two subsections. Other string functions include upper-case, lower-case, string-length, substring-before, translate and char.

Regular expressions

matches, replace and tokenize take regular expressions, in the syntax of XML Schema with the usual extensions (\d, \s, \w, \b for a word boundary, groups, and $1 in replacements):

$ sxml xpath --context-item=vane.xml 'replace(/rfc/@docName, "-(\d+)$", "-02")'
"draft-example-vane-protocol-02"
$ sxml xpath 'tokenize("feature flag, progressive delivery", ",\s*")'
"feature flag"
"progressive delivery"
$ sxml xpath --context-item=vane.xml '//author ! tokenize(@fullname)[last()]'
"Byron"
"Mathison"
"Bradner"
"Crocker"
"Overell"
"Leiba"
"M'Raihi"
"Machani"
"Pei"
"Rydell"

The first gives the name of the next version: -(\d+)$ matches a dash and the digits at the end. With one argument, tokenize splits at whitespace.

Regular expressions find a classic mistake in drafts: a key word that is written in capitals but not marked up. BCP 14 says the words have their special meaning only "when, and only when, they appear in all capitals", so a tool that lists requirements by their bcp14 elements misses a MUST that is plain text. \b matches at the edges of words, so \b(MUST|SHOULD|MAY)\b finds the words but not "MUSTER". Look for them in the text nodes that are not inside a bcp14:

$ sxml xpath 'matches("Deployments SHOULD carry Vane over an encrypted link.", "\b(MUST|SHOULD|MAY)\b"), matches("A flag should roll out.", "\b(MUST|SHOULD|MAY)\b")'
true()
false()
$ sxml xpath --context-item=vane.xml '//text()[not(parent::bcp14)][matches(., "\b(MUST|SHOULD|MAY)\b")] ! normalize-space()'
"The key words ""MUST"", ""MUST NOT"", ""REQUIRED"", ""SHALL"", ""SHALL NOT"", ""SHOULD"", ""SHOULD NOT"", ""RECOMMENDED"", ""NOT RECOMMENDED"", ""MAY"", and ""OPTIONAL"" in this document are to be interpreted as described in BCP 14"

The one match is the paragraph of Terminology that lists the key words, where they are meant as words, not as requirements. Leave that section out, and nothing is left: the draft marks up every key word it uses.

$ sxml xpath --context-item=vane.xml 'count(//text()[not(parent::bcp14)][not(ancestor::section[@anchor = "terminology"])][matches(., "\b(MUST|SHOULD|MAY)\b")])'
0

Numbers

$ sxml xpath --context-item=vane.xml 'count(//t[@anchor]), sum((1, 2, 3)), avg((1, 2))'
11
6
1.5
$ sxml xpath 'abs(-3), floor(2.7), ceiling(2.1), round(2.5), round-half-to-even(2.5)'
3
2
3
3
2
$ sxml xpath 'format-number(5 div 11, "0%"), format-number(5 div 11, "0.0%"), format-number(1234.5, "#,##0.00"), format-integer(3, "w"), format-integer(11, "I")'
"45%"
"45.5%"
"1,234.50"
"three"
"XI"

format-number writes a number with a picture string; with % in the picture it multiplies by a hundred first. format-integer writes integers as words, roman numerals or letters. "After three failed checks" in req-7 is the number 3 in words. Working with Text shows both in other languages.

Sequences

Many functions work on whole sequences:

$ sxml xpath --context-item=vane.xml 'distinct-values(//bcp14)'
"MUST"
"SHOULD"
"MUST NOT"
"MAY"
"SHOULD NOT"
$ sxml xpath --context-item=vane.xml 'index-of(//t[@anchor]/@anchor ! string(), "req-9")'
6
$ sxml xpath --context-item=vane.xml 'sort(//dt/string())'
"Flag"
"Pause"
"Percentage"
"SDK"
$ sxml xpath --context-item=vane.xml 'head(//t[@anchor]/@anchor ! string()), tail(//section[@anchor = "pause"]/t/@anchor ! string())'
"req-1"
"req-8"
"req-11"
$ sxml xpath 'reverse(1 to 4)'
4
3
2
1

The draft uses five of BCP 14's key words. index-of gives the positions of a value in a sequence: req-9 is the sixth requirement in the document, although it was the ninth to be written. head is the first item and tail all the others. 1 to 4 is the sequence of integers from 1 to 4.

empty and exists test whether a sequence has items, and otherwise gives a fallback when the left side is empty:

$ sxml xpath --context-item=vane.xml 'empty(//t[bcp14 = "SHALL"]), exists(//cref)'
true()
true()
$ sxml xpath --context-item=vane.xml '//t[bcp14 = "SHALL"]/@anchor otherwise "no SHALL"'
"no SHALL"

The arrow operator

Function calls nest inside out, which can be hard to read. The arrow operator => passes the value on its left as the first argument of the function on its right, so you can write a pipeline from left to right:

$ sxml xpath --context-item=vane.xml '/rfc/@docName => tokenize("-") => reverse() => head()'
"01"

=> passes the whole sequence. =!> passes each item separately, as ! does:

$ sxml xpath --context-item=vane.xml '//dt =!> upper-case()'
"FLAG"
"SDK"
"PERCENTAGE"
"PAUSE"

Conditions

if chooses between two values. A section with requirements of its own is normative; the others only explain:

$ sxml xpath --context-item=vane.xml '//section ! `{@anchor}: {if (t[@anchor]) then "normative" else "informative"}`'
"intro: informative"
"terminology: informative"
"states: normative"
"messages: informative"
"rollout: normative"
"status: normative"
"pause: normative"
"security: informative"
"iana: informative"

some and every test a condition over a sequence. Is there a paragraph with a key word but no anchor, and does every requirement have a key word?

$ sxml xpath --context-item=vane.xml 'some $t in //t[bcp14] satisfies empty($t/@anchor), every $t in //t[@anchor] satisfies $t/bcp14'
true()
true()

The first is the paragraph in Security Considerations again: a requirement that other documents cannot refer to, because it has no anchor.

Variables and loops

let binds a name to a value, and for evaluates an expression once for each item of a sequence. Variable names start with $. Here is the share of each key word among the requirements:

$ sxml xpath --context-item=vane.xml 'let $requirements := //t[@anchor] for $keyword in distinct-values($requirements/bcp14) return `{$keyword}: {format-number(count($requirements[bcp14 = $keyword]) div count($requirements), "0%")}`'
"MUST: 45%"
"SHOULD: 18%"
"MUST NOT: 9%"
"MAY: 18%"
"SHOULD NOT: 9%"

A variable can also come from outside. --param binds one from the command line, as an untyped value:

$ sxml xpath --context-item=vane.xml --param=keyword=SHOULD '//t[bcp14 = $keyword]/@anchor/string()'
"req-3"
"req-8"

XQuery builds on for and let with a much richer form, the FLWOR expression, in Writing Programs with XQuery.

Namespaces of functions

Every function has a namespace. The standard functions are in the fn namespace, which is the default, so count is fn:count. Functions on maps and arrays are in map and array, mathematical functions in math, and the constructor functions of types in xs:

$ sxml xpath --context-item=vane.xml 'fn:count(//t[@anchor]), xs:integer("11"), math:pow(2, 10), math:sqrt(2)'
11
11
1.024e3
1.4142135623730951e0

A name the processor doesn't know is a static error, raised before anything runs:

$ sxml xpath 'square-root(2)'
expression:1:1: err:XPST0017: no function with this name and arity is known