#![forbid(unsafe_code)]
use std::io::{Read as _, Write as _};
use std::process::ExitCode;
const USAGE: &str = "\
oxml — query, validate and inspect XML
USAGE:
oxml <COMMAND> [OPTIONS] [FILE]
COMMANDS:
query <XPATH> Evaluate an XPath expression
validate <XSD> Validate against an XML Schema
stats Summarise the document
check Report whether the document is well-formed
OPTIONS:
-t, --text Print matched nodes' text rather than a summary
-c, --count Print only the number of matches
-h, --help Show this message
-V, --version Show the version
FILE defaults to standard input, so oxml composes with pipes:
curl -s https://example.com/feed.xml | oxml query '//item/title' -t
EXIT STATUS:
0 success, and for `validate` the document conforms
1 the document is invalid, or no nodes matched
2 a usage or I/O error
";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(code) => code,
Err(e) => {
eprintln!("oxml: {e}");
ExitCode::from(2)
}
}
}
fn run(args: &[String]) -> Result<ExitCode, String> {
if args.is_empty() || args.iter().any(|a| a == "-h" || a == "--help") {
print!("{USAGE}");
return Ok(ExitCode::SUCCESS);
}
if args.iter().any(|a| a == "-V" || a == "--version") {
println!("oxml {}", env!("CARGO_PKG_VERSION"));
return Ok(ExitCode::SUCCESS);
}
let text_only = args.iter().any(|a| a == "-t" || a == "--text");
let count_only = args.iter().any(|a| a == "-c" || a == "--count");
let positional: Vec<&String> =
args.iter().filter(|a| !a.starts_with('-')).collect();
let command = positional
.first()
.ok_or_else(|| "no command given; try `oxml --help`".to_owned())?;
match command.as_str() {
"query" => {
let expr = positional
.get(1)
.ok_or_else(|| "query needs an XPath expression".to_owned())?;
let source = read_input(positional.get(2).map(|s| s.as_str()))?;
cmd_query(&source, expr, text_only, count_only)
}
"validate" => {
let xsd_path = positional
.get(1)
.ok_or_else(|| "validate needs a path to an .xsd".to_owned())?;
let source = read_input(positional.get(2).map(|s| s.as_str()))?;
cmd_validate(&source, xsd_path)
}
"stats" => {
let source = read_input(positional.get(1).map(|s| s.as_str()))?;
cmd_stats(&source)
}
"check" => {
let source = read_input(positional.get(1).map(|s| s.as_str()))?;
Ok(cmd_check(&source))
}
other => Err(format!("unknown command `{other}`; try `oxml --help`")),
}
}
fn read_input(path: Option<&str>) -> Result<String, String> {
if let Some(p) = path {
std::fs::read_to_string(p).map_err(|e| format!("cannot read {p}: {e}"))
} else {
let mut buf = String::new();
let _ = std::io::stdin()
.read_to_string(&mut buf)
.map_err(|e| format!("cannot read stdin: {e}"))?;
Ok(buf)
}
}
fn parse_or_report(source: &str) -> Result<oxml::Document, String> {
oxml::parse(source).map_err(|e| {
let (line, col) = e.line_column(source);
format!("{line}:{col}: {e}")
})
}
fn cmd_query(
source: &str,
expr: &str,
text_only: bool,
count_only: bool,
) -> Result<ExitCode, String> {
let doc = parse_or_report(source)?;
let xpath =
oxml::XPath::compile(expr).map_err(|e| format!("bad XPath: {e}"))?;
let value = xpath.evaluate(&doc);
let Some(nodes) = value.nodes() else {
println!("{}", value.to_str(&doc));
return Ok(ExitCode::SUCCESS);
};
if count_only {
println!("{}", nodes.len());
return Ok(if nodes.is_empty() {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
});
}
let stdout = std::io::stdout();
let mut out = stdout.lock();
for &node in nodes {
let line = if text_only {
doc.text(node)
} else {
describe(&doc, node)
};
writeln!(out, "{line}").map_err(|e| e.to_string())?;
}
Ok(if nodes.is_empty() {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
})
}
fn describe(doc: &oxml::Document, node: oxml::NodeId) -> String {
match doc.kind(node) {
Some(oxml::NodeKind::Element { name, .. }) => {
let attrs: Vec<String> = doc
.attributes(node)
.iter()
.map(|a| format!(" {}=\"{}\"", a.name.local, a.value))
.collect();
let text = doc.text(node);
let preview = text.trim();
if preview.is_empty() {
format!("<{}{}/>", name.local, attrs.concat())
} else {
format!(
"<{}{}>{}</{}>",
name.local,
attrs.concat(),
truncate(preview, 60),
name.local
)
}
}
Some(oxml::NodeKind::Attr(a)) => {
format!("{}=\"{}\"", a.name.local, a.value)
}
Some(oxml::NodeKind::Text(t)) => truncate(t.trim(), 80),
Some(oxml::NodeKind::Comment(c)) => {
format!("<!--{}-->", truncate(c.trim(), 60))
}
_ => String::new(),
}
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_owned();
}
let head: String = s.chars().take(max).collect();
format!("{head}…")
}
fn cmd_validate(source: &str, xsd_path: &str) -> Result<ExitCode, String> {
let xsd = std::fs::read_to_string(xsd_path)
.map_err(|e| format!("cannot read {xsd_path}: {e}"))?;
let schema = xmlschema::parse_schema(&xsd)
.map_err(|e| format!("bad schema: {e}"))?;
let doc = parse_or_report(source)?;
let report = xmlschema::validate(&doc, &schema);
if report.is_valid() {
println!("valid");
return Ok(ExitCode::SUCCESS);
}
eprintln!("{} violation(s):", report.violations.len());
for v in &report.violations {
eprintln!(" {} — {}", v.path, v.message);
}
Ok(ExitCode::from(1))
}
fn cmd_stats(source: &str) -> Result<ExitCode, String> {
let doc = parse_or_report(source)?;
let mut elements = 0usize;
let mut attributes = 0usize;
let mut text = 0usize;
let mut comments = 0usize;
let mut depth_max = 0usize;
for id in doc.descendants() {
match doc.kind(id) {
Some(oxml::NodeKind::Element { .. }) => {
elements += 1;
let mut d = 0usize;
let mut cur = Some(id);
while let Some(n) = cur {
cur = doc.parent(n);
d += 1;
}
depth_max = depth_max.max(d);
}
Some(oxml::NodeKind::Attr(_)) => attributes += 1,
Some(oxml::NodeKind::Text(_)) => text += 1,
Some(oxml::NodeKind::Comment(_)) => comments += 1,
_ => {}
}
}
println!("nodes {}", doc.len());
println!("elements {elements}");
println!("attributes {attributes}");
println!("text {text}");
println!("comments {comments}");
println!("max depth {depth_max}");
Ok(ExitCode::SUCCESS)
}
fn cmd_check(source: &str) -> ExitCode {
match oxml::parse(source) {
Ok(doc) => {
println!("well-formed ({} nodes)", doc.len());
ExitCode::SUCCESS
}
Err(e) => {
let (line, col) = e.line_column(source);
eprintln!("not well-formed at {line}:{col}: {e}");
ExitCode::from(1)
}
}
}