hugr-cli 0.27.1

Compiler passes for Quantinuum's HUGR
Documentation
//! Standard command line tools for the HUGR format.
//!
//! This library provides utilities for the HUGR CLI.
//!
//! ## CLI Usage
//!
//! Run `cargo install hugr-cli` to install the CLI tools. This will make the
//! `hugr` executable available in your shell as long as you have [cargo's bin
//! directory](https://doc.rust-lang.org/book/ch14-04-installing-binaries.html)
//! in your path.
//!
//! The top level help can be accessed with:
//! ```sh
//! hugr --help
//! ```
//!
//! Refer to the help for each subcommand for more information, e.g.
//! ```sh
//! hugr validate --help
//! ```

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;

/// CLI arguments.
#[derive(Parser, Debug)]
#[clap(version = crate_version!(), long_about = None)]
#[clap(about = "HUGR CLI tools.")]
#[group(id = "hugr")]
pub struct CliArgs {
    /// The command to be run.
    #[command(subcommand)]
    pub command: CliCommand,
    /// Verbosity.
    #[command(flatten)]
    pub verbose: Verbosity<InfoLevel>,
}

/// The CLI subcommands.
#[derive(Debug, clap::Subcommand)]
#[non_exhaustive]
pub enum CliCommand {
    /// Validate a HUGR package.
    Validate(validate::ValArgs),
    /// Write standard extensions out in serialized form.
    GenExtensions(extensions::ExtArgs),
    /// Write HUGR as mermaid diagrams.
    Mermaid(mermaid::MermaidArgs),
    /// Convert between different HUGR envelope formats.
    Convert(convert::ConvertArgs),
    /// External commands
    #[command(external_subcommand)]
    External(Vec<OsString>),

    /// Describe the contents of a HUGR package.
    ///
    /// If an error occurs during loading partial descriptions are printed.
    /// For example if the first module is loaded and the second fails then
    /// only the first module will be described.
    Describe(describe::DescribeArgs),
}

/// Error type for the CLI.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CliError {
    /// Error reading input.
    #[error("Error reading from path.")]
    InputFile(#[from] std::io::Error),
    /// Error parsing input.
    #[error("Error parsing package.")]
    Parse(#[from] serde_json::Error),
    #[error("Error validating HUGR.")]
    /// Errors produced by the `validate` subcommand.
    Validate(#[from] PackageValidationError),
    /// Pretty error when the user passes a non-envelope file.
    #[error(
        "Input file is not a HUGR envelope. Invalid magic number.\n\nUse `--hugr-json` to read a raw HUGR JSON file instead."
    )]
    NotAnEnvelope,
    /// Invalid format string for conversion.
    #[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}")]
    /// Errors produced by the `validate` subcommand, with a known generator of the HUGR.
    ValidateKnownGenerator {
        #[source]
        /// The inner validation error.
        inner: PackageValidationError,
        /// The generator of the HUGR.
        generator: Box<String>,
    },
    #[error("Error reading envelope.")]
    /// Errors produced when reading an envelope.
    ReadEnvelope(#[from] hugr::envelope::ReadError),
    /// Error produced while loading extension files.
    #[error("Error loading extension file.")]
    LoadExtensionFile(#[from] ExtensionResolutionError),
}

impl CliError {
    /// Returns a validation error, with an optional generator.
    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 {
    /// Run a CLI command with optional input/output overrides.
    /// If overrides are `None`, behaves like the normal CLI.
    /// If overrides are provided, stdin/stdout/files are ignored.
    /// The `gen-extensions` and `external` commands don't support overrides.
    ///
    /// # Arguments
    ///
    /// * `input_override` - Optional reader to use instead of stdin/files
    /// * `output_override` - Optional writer to use instead of stdout/files
    ///
    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 {
    /// Parse CLI arguments from the environment.
    pub fn new() -> Self {
        CliArgs::parse()
    }

    /// Parse CLI arguments from an iterator.
    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)
    }

    /// Entrypoint for cli - process arguments and run commands.
    ///
    /// Process exits on error.
    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);
        }
    }

    /// Run a CLI command with bytes input and capture bytes output.
    ///
    /// This provides a programmatic interface to the CLI.
    /// Unlike `run_cli()`, this method:
    /// - Accepts input instead of reading from stdin/files
    /// - Returns output as a byte vector instead of writing to stdout/files
    ///
    /// # Arguments
    ///
    /// * `input` - The input data as bytes (e.g., a HUGR package)
    ///
    /// # Returns
    ///
    /// Returns `Ok(Vec<u8>)` with the command output, or an error on failure.
    ///
    ///
    /// # Note
    ///
    /// The `gen-extensions` and `external` commands don't support byte I/O
    /// and should use the normal `run_cli()` method instead.
    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.")]
/// Error type for `run_with_io` method.
pub enum RunWithIoError {
    /// Error describing HUGR package.
    Describe {
        #[source]
        /// Error returned from describe command.
        source: anyhow::Error,
        /// Describe command output.
        output: Vec<u8>,
    },
    /// Non-describe command error.
    Other(anyhow::Error),
}

fn run_external(args: Vec<OsString>) -> Result<()> {
    // External subcommand support: invoke `hugr-<subcommand>`
    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(())
}