use clap::Parser;
use env_logger::{Builder, Env};
use log::LevelFilter;
use std::collections::HashMap;
use std::fs;
use std::io::{self, Read, Write};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "xacro-rs")]
#[command(about = "XML preprocessor for xacro files to generate URDF", long_about = None)]
#[command(version)]
struct Args {
input: Option<PathBuf>,
#[arg(short = 'o', long = "output", value_name = "FILE")]
output: Option<PathBuf>,
#[arg(long = "deps")]
deps: bool,
#[arg(long = "inorder", short = 'i')]
inorder: bool,
#[arg(short = 'q', conflicts_with = "verbose")]
quiet: bool,
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
verbose: u8,
#[arg(
long = "verbosity",
value_name = "LEVEL",
conflicts_with = "verbose",
conflicts_with = "quiet"
)]
verbosity_level: Option<u8>,
#[arg(long = "compat", value_name = "MODES", num_args = 0..=1, default_missing_value = "all")]
compat: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
extra_args: Vec<String>,
}
impl Args {
fn get_verbosity(&self) -> u8 {
if self.quiet {
0
} else if let Some(level) = self.verbosity_level {
level.min(4)
} else {
1 + self.verbose.min(3)
}
}
fn parse_mappings(&self) -> HashMap<String, String> {
self.extra_args
.iter()
.filter_map(|arg| {
if let Some((key, value)) = arg.split_once(":=") {
if key.is_empty() {
log::warn!("Ignoring mapping with empty key: '{}'", arg);
None
} else {
Some((key.to_string(), value.to_string()))
}
} else {
if arg.contains('=') {
log::warn!("Use ':=' instead of '=' for mappings: '{}'", arg);
} else {
log::warn!(
"Ignoring unrecognized argument (expected key:=value format): '{}'",
arg
);
}
None
}
})
.collect()
}
}
fn init_logging(verbosity: u8) {
let mut builder = Builder::from_env(Env::default());
let level = match verbosity {
0 => LevelFilter::Warn, 1 => LevelFilter::Warn,
2 => LevelFilter::Info,
3 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
builder.filter_level(level);
builder.filter_module("xacro_rs", LevelFilter::Info);
let _ = builder.try_init();
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let verbosity = args.get_verbosity();
init_logging(verbosity);
if args.inorder {
log::warn!("in-order processing became default in ROS Melodic. You can drop the option.");
}
let mappings = args.parse_mappings();
let compat_mode = args
.compat
.as_deref()
.map(|s| s.parse())
.transpose()?
.unwrap_or_default();
let mut builder = xacro_rs::XacroProcessor::builder()
.with_args(mappings)
.with_compat_mode(compat_mode)
.with_extension(Box::new(xacro_rs::extensions::FindExtension::new()))
.with_extension(Box::new(xacro_rs::extensions::OptEnvExtension::new()));
#[cfg(feature = "yaml")]
{
builder = builder.with_ros_yaml_units();
}
let processor = builder.build();
let use_stdin = match &args.input {
None => true,
Some(path) => path.as_os_str() == "-",
};
if args.deps {
if use_stdin {
anyhow::bail!("--deps flag is not supported when reading from stdin");
}
let (_, includes) = processor
.run_with_deps(
args.input
.as_ref()
.expect("input must be Some when not using stdin"),
)
.map_err(|e| anyhow::anyhow!("Failed to process xacro file: {}", e))?;
let deps_str = includes
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(" ");
print!("{}", deps_str);
return Ok(());
}
let result = if use_stdin {
let mut content = String::new();
io::stdin()
.read_to_string(&mut content)
.map_err(|e| anyhow::anyhow!("Failed to read from stdin: {}", e))?;
processor
.run_from_string(&content)
.map_err(|e| anyhow::anyhow!("Failed to process xacro from stdin: {}", e))?
} else {
processor
.run(
args.input
.as_ref()
.expect("input must be Some when not using stdin"),
)
.map_err(|e| anyhow::anyhow!("Failed to process xacro file: {}", e))?
};
if let Some(output_path) = &args.output {
fs::write(output_path, result)?;
} else {
io::stdout().write_all(result.as_bytes())?;
}
Ok(())
}