Contents
Getting started
- Install — Cargo, from source
- Quick Start — a query in one line
The oxml ecosystem
- The oxml ecosystem — seven crates, one version
Reference
- Commands —
query,validate,stats,check - Options
- Exit status — and why it matters in a pipeline
- Reading from standard input
- Migration — from
xmllint,xq,xmlstarlet - Why this approach?
- Capabilities in 0.0.11
- Ecosystem comparison
- Benchmarks
Practical
- Examples — recipes that do real work
- Configuration
- When not to use oxml-cli
- FAQ
- Development
- Security
- Documentation
- Acknowledgements
- License
Install
From source:
The binary is called oxml.
There are no C dependencies and no build toolchain requirements. If
cargo works, this installs.
Quick Start
)
The oxml ecosystem
| Crate | What it is |
|---|---|
oxml |
The library: parser, tree, XPath 1.0 |
xmlschema |
XSD validation |
oxml-cli |
This crate — the command line |
oxml-wasm |
WebAssembly bindings |
oxml-mcp |
Model Context Protocol server |
oxml-lsp |
Language Server Protocol server |
oxml-json |
JSON support for the language server |
All seven ship one version number, moving in steps of 0.0.1. oxml-cli 0.0.11 goes with oxml 0.0.11, and no other combination is supported.
Commands
query <XPATH> [FILE]
Evaluate an XPath 1.0 expression.
<title>Dune</title>
<title>Germinal</title>
By default each match is printed as a summary of the node. -t prints
the text instead, and -c prints only how many matched.
Non-node results — numbers, strings, booleans — are printed directly:
validate <XSD> [FILE]
Validate against an XML Schema.
Exit status 0 means the document conforms, 1 means it does not. The schema comes first because the document is the argument that defaults to standard input.
stats [FILE]
Summarise the document: node counts by kind, and maximum depth.
Useful for finding out what is actually in a file before writing a query against it, and for spotting a document that is deeper or larger than you expected.
check [FILE]
Report whether the document is well-formed.
The position is a line and column, counted in characters, so it points where an editor would.
Options
| Option | Effect |
|---|---|
-t, --text |
Print matched nodes' text rather than a summary |
-c, --count |
Print only the number of matches |
-n, --ns P=URI |
Bind a namespace prefix for query; repeatable |
-h, --help |
Usage |
-V, --version |
Version |
-t and -c apply to query. Both accept the flag anywhere in the
argument list.
Exit status
| Code | Meaning |
|---|---|
| 0 | Success; for validate, the document conforms |
| 1 | The document is invalid, or no nodes matched |
| 2 | A usage or I/O error |
The distinction between 1 and 2 is the point. A shell script that tests
if oxml query ... ; then wants to know "did anything match", and it
must not confuse that with "you passed a path that does not exist". A
tool that returns 1 for both makes set -e a liability.
if ; then
fi
Reading from standard input
FILE defaults to standard input, so oxml composes:
|
|
Migration
From xmllint
xmllint |
oxml |
|---|---|
xmllint --xpath '//title' f.xml |
oxml query '//title' f.xml |
xmllint --noout f.xml |
oxml check f.xml |
xmllint --schema s.xsd f.xml |
oxml validate s.xsd f.xml |
xmllint --format f.xml |
not available |
xmllint --xinclude |
not available |
xmllint exits 0 when --xpath matches nothing and prints an error to
stderr; oxml exits 1, which is usually what a script wants.
From xq / xmlstarlet
xmlstarlet |
oxml |
|---|---|
xmlstarlet sel -t -v '//title' f.xml |
oxml query -t '//title' f.xml |
xmlstarlet val f.xml |
oxml check f.xml |
xmlstarlet val -s s.xsd f.xml |
oxml validate s.xsd f.xml |
xmlstarlet ed … |
not available — oxml does not write |
xmlstarlet's template language is more capable and considerably
larger. oxml query is one expression and one output mode.
Why this approach?
One binary, no dependencies. xmllint means libxml2, which means a
package manager, a shared library and a CVE stream. oxml is a static
binary produced by cargo install.
Exit codes that mean something. See Exit status.
Memory safety. The parser underneath forbids unsafe, and never
fetches external entities — so oxml cannot be made to read
/etc/passwd by a document that asks it to. For a tool routinely
pointed at files from elsewhere, that matters more than throughput.
Predictable output. One match per line, nothing else on stdout. Diagnostics go to stderr. It is meant to be piped.
Capabilities in 0.0.11
- XPath 1.0 queries: ten axes, 25 functions, all four value types
- Text-only and count-only output modes
- XSD validation via
xmlschema - Well-formedness checking with line and column
- Document statistics by node kind, and maximum depth
- Standard input, so it composes with pipes
- Exit codes distinguishing "no match" from "usage error"
- UTF-8, UTF-16 and ISO-8859-1 input
- Namespace prefixes bound on the command line with
-n, --ns
Not yet: formatting or pretty-printing, editing, XInclude, XSLT.
Ecosystem comparison
oxml |
xmllint |
xmlstarlet |
xq |
|
|---|---|---|---|---|
| XPath 1.0 | Yes | Yes | Yes | Yes |
| XSD validation | Yes | Yes | Yes | No |
| Editing | No | No | Yes | No |
| Formatting | No | Yes | Yes | Yes |
| XSLT | No | Yes | Yes | No |
| Single static binary | Yes | No | No | Depends |
| C dependency | None | libxml2 | libxml2 | Varies |
| Fetches external entities | Never | Configurable | Configurable | Varies |
Benchmarks
Per invocation, with process spawn excluded — spawn is the operating
system's business and swamps everything else on a small document.
Argument handling and file I/O add roughly 9% over the bare parse on a
200 KB file; --help costs a fraction of a microsecond.
No absolute throughput figure is published, and that is deliberate: the same benchmark binary in this suite measured 14.7 and 123.1 MB/s on one machine on one day, at a load average above 30. A figure without its conditions is not a measurement, so the benchmark reports paired comparisons instead. See oxml's BENCHMARKS.md.
Examples
Runnable scripts live in examples/, each with the sample
document it operates on. They run in CI, so they cannot rot.
| Example | What it shows |
|---|---|
query-basics.sh |
Every output mode of query |
pipeline.sh |
Standard input, exit codes, composing with other tools |
inspect.sh |
stats and check, including a malformed document |
validate.sh |
XSD validation, valid and invalid |
Configuration
There is none. No config file, no environment variables, no dotfile.
That is a decision rather than an omission: a command whose behaviour
depends on a file somewhere in your home directory behaves differently
in CI than on your laptop, and the difference surfaces as a confusing
failure. Everything oxml does is on the command line.
File and stdin reads stop at 8 MiB. Parsing and XPath compilation use
Limits::strict(); each query uses EvalLimits::default() to bound work,
intermediate nodes and strings. Exceeding a budget is an error, not a partial result.
When not to use oxml-cli
- You need to edit XML.
xmlstarlet eddoes; this does not. - You need to format or pretty-print.
xmllint --formatdoes. - You need XSLT.
xsltproc. - You need XPath 2.0 or 3.1. This is 1.0, so no
matches(), nofor, no sequences. - The document is larger than memory. The whole tree is built.
FAQ
Why is the binary called oxml and the crate oxml-cli?
Because oxml on crates.io is the library. The crate has to be
oxml-cli; the thing you type should be short.
Why does a query that matches nothing exit 1?
So a shell script can test it. if oxml query -c '//error' log.xml
reads naturally and does the right thing. Exit 2 is reserved for "you
made a mistake", which is a different situation and needs a different
response.
Can it read gzipped XML?
Not directly. Pipe it: gunzip -c f.xml.gz | oxml query ….
How do I query a document with namespaces?
Bind the prefix on the command line:
The binding lives with the query, not the document, so the same
command works against a document that spells the prefix differently —
only the URI has to match. -n is repeatable, and later bindings for
the same prefix win.
An unbound prefix is an error, not a silent match on the local part:
$ oxml query -t '//m:item' catalogue.xml
oxml: bad XPath: unbound namespace prefix `m`; bind it with --ns m=URI
An unprefixed name test matches only nodes in no namespace, which
is what XPath 1.0 specifies. If your document declares a default
namespace, //item matches nothing — bind a prefix to that URI.
namespace-uri() still works and needs no binding, which is useful
when you know the URI and not the prefix:
Is the output stable enough to parse?
query -t and query -c are: one result per line, nothing else on
stdout. stats and the default query output are human-facing and may
change.
Does it fetch anything over the network?
No. There is no network code anywhere in the dependency tree, and
external entities are never dereferenced — a document containing
<!ENTITY xxe SYSTEM "file:///etc/passwd"> cannot make it read that
file. See
oxml's SECURITY-MODEL.md.
Why no --format?
The core supports serialization, but this CLI does not expose a format
subcommand. Use the core serializer or the MCP xml_format tool when
formatting is required.
What happens on a document that is not well-formed?
It is rejected, with a line and column. There is no recovery mode. A parser that guesses at what a malformed document meant produces a tree that no two tools agree on.
Can I use it in CI?
That is the intended use.
- run: oxml check config.xml
- run: oxml validate schema.xsd config.xml
Both exit non-zero on failure, which is all a CI step needs.
Does it work on Windows?
Yes. CI builds and tests on Linux, macOS and Windows.
Development
That runs everything CI runs, in the order that fails fastest: format,
clippy, tests, rustdoc, the #![forbid(unsafe_code)] check, the
examples, the 95% coverage floor and an MSRV build. It pins the
toolchain rather than trusting rust-toolchain.toml, because a
RUSTUP_TOOLCHAIN in the environment silently overrides that file and
a lint that exists in one release and not another then makes a green
local run and a red CI one.
The individual steps, if you want them one at a time:
OXML="/target/release/oxml"
CI runs the same set on Linux, macOS and Windows.
Security
Scorecard scans use a digest-pinned upstream image. Results are retained as CI artifacts and repository posture findings in GitHub code scanning, not published to Scorecard's public service. The badge shows workflow status, not a security score; a successful scan can still report findings.
The parser underneath never dereferences external entities, so XXE is
foreclosed structurally rather than by a default. Entity expansion is
bounded. #![forbid(unsafe_code)].
To report a vulnerability, see SECURITY.md. Please do not open a public issue.
Documentation
- doc/ — the decisions, in longer form
- oxml's documentation
- CHANGELOG.md
- CONTRIBUTING.md
Acknowledgements
xmllint— the tool this is measured against, and the reason the exit-code behaviour is what it is.xmlstarlet— for showing how much a command-line XML tool can do.jq— for the standard a pipeline-friendly query tool is held to.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.