use std::ffi::OsString;
use anyhow::Result;
use clap::{Parser, crate_version};
#[cfg(feature = "tracing")]
use clap_verbosity_flag::VerbosityFilter;
use clap_verbosity_flag::{InfoLevel, Verbosity};
use hugr::extension::resolution::ExtensionResolutionError;
use hugr::package::PackageValidationError;
use thiserror::Error;
#[cfg(feature = "tracing")]
use tracing::{error, metadata::LevelFilter};
pub mod convert;
pub mod describe;
pub mod extensions;
pub mod hugr_io;
pub mod mermaid;
pub mod validate;
#[derive(Parser, Debug)]
#[clap(version = crate_version!(), long_about = None)]
#[clap(about = "HUGR CLI tools.")]
#[group(id = "hugr")]
pub struct CliArgs {
#[command(subcommand)]
pub command: CliCommand,
#[command(flatten)]
pub verbose: Verbosity<InfoLevel>,
}
#[derive(Debug, clap::Subcommand)]
#[non_exhaustive]
pub enum CliCommand {
Validate(validate::ValArgs),
GenExtensions(extensions::ExtArgs),
Mermaid(mermaid::MermaidArgs),
Convert(convert::ConvertArgs),
#[command(external_subcommand)]
External(Vec<OsString>),
Describe(describe::DescribeArgs),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CliError {
#[error("Error reading from path.")]
InputFile(#[from] std::io::Error),
#[error("Error parsing package.")]
Parse(#[from] serde_json::Error),
#[error("Error validating HUGR.")]
Validate(#[from] PackageValidationError),
#[error(
"Input file is not a HUGR envelope. Invalid magic number.\n\nUse `--hugr-json` to read a raw HUGR JSON file instead."
)]
NotAnEnvelope,
#[error(
"Invalid format: '{_0}'. Valid formats are: json, model, model-exts, s-expression, s-expression-exts"
)]
InvalidFormat(String),
#[error("Error validating HUGR generated by {generator}")]
ValidateKnownGenerator {
#[source]
inner: PackageValidationError,
generator: Box<String>,
},
#[error("Error reading envelope.")]
ReadEnvelope(#[from] hugr::envelope::ReadError),
#[error("Error loading extension file.")]
LoadExtensionFile(#[from] ExtensionResolutionError),
}
impl CliError {
pub fn validation(generator: Option<String>, val_err: PackageValidationError) -> Self {
if let Some(g) = generator {
Self::ValidateKnownGenerator {
inner: val_err,
generator: Box::new(g.to_string()),
}
} else {
Self::Validate(val_err)
}
}
}
impl CliCommand {
fn run_with_io<R: std::io::Read, W: std::io::Write>(
self,
input_override: Option<R>,
output_override: Option<W>,
) -> Result<()> {
match self {
Self::Validate(mut args) => args.run_with_input(input_override),
Self::GenExtensions(args) => {
if input_override.is_some() || output_override.is_some() {
return Err(anyhow::anyhow!(
"GenExtensions command does not support programmatic I/O overrides"
));
}
args.run_dump(&hugr::std_extensions::STD_REG)
}
Self::Mermaid(mut args) => args.run_print_with_io(input_override, output_override),
Self::Convert(mut args) => args.run_convert_with_io(input_override, output_override),
Self::Describe(mut args) => args.run_describe_with_io(input_override, output_override),
Self::External(args) => {
if input_override.is_some() || output_override.is_some() {
return Err(anyhow::anyhow!(
"External commands do not support programmatic I/O overrides"
));
}
run_external(args)
}
}
}
}
impl Default for CliArgs {
fn default() -> Self {
Self::new()
}
}
impl CliArgs {
pub fn new() -> Self {
CliArgs::parse()
}
pub fn new_from_args<I, T>(args: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
CliArgs::parse_from(args)
}
pub fn run_cli(self) {
#[cfg(feature = "tracing")]
{
let level = match self.verbose.filter() {
VerbosityFilter::Off => LevelFilter::OFF,
VerbosityFilter::Error => LevelFilter::ERROR,
VerbosityFilter::Warn => LevelFilter::WARN,
VerbosityFilter::Info => LevelFilter::INFO,
VerbosityFilter::Debug => LevelFilter::DEBUG,
VerbosityFilter::Trace => LevelFilter::TRACE,
};
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_max_level(level)
.pretty()
.init();
}
let result = self
.command
.run_with_io(None::<std::io::Stdin>, None::<std::io::Stdout>);
if let Err(err) = result {
#[cfg(feature = "tracing")]
error!("{:?}", err);
#[cfg(not(feature = "tracing"))]
eprintln!("{:?}", err);
std::process::exit(1);
}
}
pub fn run_with_io(self, input: impl std::io::Read) -> Result<Vec<u8>, RunWithIoError> {
let mut output = Vec::new();
let is_describe = matches!(self.command, CliCommand::Describe(_));
let res = self.command.run_with_io(Some(input), Some(&mut output));
match (res, is_describe) {
(Ok(()), _) => Ok(output),
(Err(e), true) => Err(RunWithIoError::Describe { source: e, output }),
(Err(e), false) => Err(RunWithIoError::Other(e)),
}
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
#[error("Error running CLI command with IO.")]
pub enum RunWithIoError {
Describe {
#[source]
source: anyhow::Error,
output: Vec<u8>,
},
Other(anyhow::Error),
}
fn run_external(args: Vec<OsString>) -> Result<()> {
if args.is_empty() {
eprintln!("No external subcommand specified.");
std::process::exit(1);
}
let subcmd = args[0].to_string_lossy();
let exe = format!("hugr-{subcmd}");
let rest: Vec<_> = args[1..]
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect();
match std::process::Command::new(&exe).args(&rest).status() {
Ok(status) => {
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("error: no such subcommand: '{subcmd}'.\nCould not find '{exe}' in PATH.");
std::process::exit(1);
}
Err(e) => {
eprintln!("error: failed to invoke '{exe}': {e}");
std::process::exit(1);
}
}
Ok(())
}