Documents, Parameters and the Context
A query gets its input in three ways: the context item, which paths
start from; documents and other files that it reads by name; and
external variables, the parameters of the whole query. This section
shows each of them with the sxml command, on both versions of the
draft.
The context item
--context-item=FILE parses a document and makes it the context item,
so that a path such as //t starts from it. A file name of - reads
the document from standard input:
$ sxml xquery --context-item=- --expression='count(//t[@anchor])' < vane.xml
11
A query can say what context item it expects, with declare context value. Then a wrong document is an error at the start, instead of an
empty result later. Here the schema of the draft is given where a draft
was meant:
$ sxml xquery --context-item=vane.xsd --expression='
declare context value as document-node(element(rfc)) external;
count(//t[@anchor])'
expression: err:XPTY0004: The value does not match the required type (XPath 4.0, section 3.4, coercion rules)
A declaration can also give a default, used when no context item is given:
$ sxml xquery --expression='
declare context value as document-node(element(rfc)) external := doc("vane-00.xml");
count(//t[@anchor])'
8
(XQuery 3.1 wrote this declare context item, which still works.)
Reading documents
doc reads an XML document by its URI. With the sxml command, a
relative URI names a file relative to the working directory. A query
can read as many documents as it likes; reading the same URI twice gives
the same document, not a copy:
$ sxml xquery --method=adaptive --expression='doc("vane.xml") is doc("vane.xml")'
true()
doc-available asks whether a document can be read, without raising
an error when it can't:
$ sxml xquery --method=adaptive --expression='
for $file in ("vane.xml", "vane-00.xml", "vane-02.xml")
return if (doc-available($file)) then string(doc($file)/rfc/@docName) else $file || " is missing"'
"draft-example-vane-protocol-01"
"draft-example-vane-protocol-00"
"vane-02.xml is missing"
Data in other formats has functions of its own: json-doc reads JSON
into maps and arrays, unparsed-text and unparsed-text-lines read
text, and csv-doc and html-doc read CSV and HTML.
JSON, CSV and HTML has the details. They mix freely. The
draft's editorial notes name issues, and the issue tracker exports its
issues as JSON:
issues.json:
{
"issue-1": { "title": "Configurable pause duration", "state": "open", "opened": "2026-07-20" },
"issue-2": { "title": "Anchor for the encryption requirement", "state": "open", "opened": "2026-09-02" },
"issue-3": { "title": "Wording of the rollout section", "state": "closed", "opened": "2026-07-08" }
}
$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
let $issues := json-doc("issues.json")
for $note in //cref
let $issue := $issues($note/@anchor)
return `{$note/@anchor} ({$issue?state}, opened {$issue?opened}): {$issue?title}`'
"issue-1 (open, opened 2026-07-20): Configurable pause duration"
"issue-2 (open, opened 2026-09-02): Anchor for the encryption requirement"
A map is a function from keys to values, so $issues($note/@anchor)
looks up the key "issue-1"; ?state then takes the issue's state.
Many documents
XQuery's collection function returns a whole set of documents by one
URI. What the URI names is up to the processor: a database, a list, or
for the sxml command a directory. The project keeps every published
version of the draft in drafts, and collection("drafts") gives the
document of every .xml file in it, sorted by name:
$ sxml xquery --method=adaptive --expression='
for $draft in collection("drafts")
return `{document-uri($draft) => tokenize("/") => foot()}: {$draft/rfc/@docName}, {count($draft//t[@anchor])} requirements`'
"vane-00.xml: draft-example-vane-protocol-00, 8 requirements"
"vane-01.xml: draft-example-vane-protocol-01, 11 requirements"
document-uri tells which file each document came from; it is an
absolute file: URI, so the query keeps only its last part. The
documents are the ones doc reads for the same files, so
doc("drafts/vane-00.xml") is collection("drafts")[1] is true.
uri-collection gives the URIs without reading the files.
Parameters after a ? change what the directory gives, separated by
;. select names the files (* stands for any characters, and the
default is *.xml), so uri-collection can also find files that are
not XML, for the functions that read them; recurse=yes takes the files
of subdirectories too:
$ sxml xquery --method=adaptive --expression='
doc("drafts/vane-00.xml") is collection("drafts")[1],
uri-collection("drafts?select=*-01.xml") ! tokenize(., "/")[last()]'
true()
"vane-01.xml"
A file that is not well-formed stops collection with
err:FODC0002, which names the file and the line; with
on-error=ignore it is left out instead.
--collection=DIR makes a directory the default collection, the one
collection() returns without an argument. The query then does not
name the directory, and the same query reads another one next time:
$ sxml xquery --method=adaptive --collection=drafts --expression='
count(collection()//t[@anchor])'
19
External variables
An external variable is declared in the prolog with external, and
its value comes from outside the query. A type after as says what it
must be, and a value after := is its default. An Internet-Draft is
valid for six months, which the IETF counts as 185 days; this query
says when drafts expire:
expiry.xq:
declare option output:method "text";
declare option output:item-separator " ";
(:~ The drafts to look at: file names, separated by spaces. :)
declare variable $drafts as xs:string external := "vane.xml";
(:~ The day to compute from: give it with --param=today=YYYY-MM-DD. :)
declare variable $today as xs:date external := current-date();
(:~ How long an Internet-Draft is valid (RFC 2026, 2.2). :)
declare variable $days as xs:integer external := 185;
declare variable $months := ("January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December");
for $file in tokenize($drafts)
let $draft := doc($file)/rfc
let $date := $draft/front/date
let $published := xs:date(`{$date/@year}-{format-integer(index-of($months, $date/@month), "00")}-{format-integer($date/@day, "00")}`)
let $expires := $published + xs:dayTimeDuration("P" || $days || "D")
return `{$draft/@docName}: published {$published}, expires {$expires}{
if ($expires lt $today) then ", expired" else ""
}`
--param=NAME=VALUE sets an external variable:
$ sxml xquery --param=today=2026-10-21 expiry.xq
draft-example-vane-protocol-01: published 2026-10-21, expires 2027-04-24
$ sxml xquery --param=today=2027-02-01 --param="drafts=vane-00.xml vane.xml" expiry.xq
draft-example-vane-protocol-00: published 2026-07-06, expires 2027-01-07, expired
draft-example-vane-protocol-01: published 2026-10-21, expires 2027-04-24
$ sxml xquery --param=today=2027-02-01 --param="drafts=vane-00.xml vane.xml" --param=days=90 expiry.xq
draft-example-vane-protocol-00: published 2026-07-06, expires 2026-10-04, expired
draft-example-vane-protocol-01: published 2026-10-21, expires 2027-01-19, expired
The command line has only text, so --param passes the value as an
untyped value, and the declared type converts it: 2027-02-01 became
an xs:date, 90 an xs:integer. Text that isn't a value of the type
is an error:
$ sxml xquery --param=today=1.2.2027 expiry.xq
expiry.xq: err:FORG0001: the value is not in the lexical space or value space of the target type
Without a default, an external variable must be given:
$ sxml xquery --expression='declare variable $today as xs:date external; $today'
expression:1:46: err:XPDY0002: The focus or a variable of the dynamic context is absent (XPath 4.0, section 2.2.2)
A --param for a variable the query doesn't declare is ignored, so one
script can pass the same parameters to several queries.
Some advice for external variables:
- Always declare a type. It documents the parameter and converts the text from the command line.
- Give a default when there is a sensible one.
current-date()is a good default for "today", and a parameter makes the query testable with a fixed date, as above. - Pass a list as one string and split it with
tokenize, as the file names of$draftsabove.