Skip to main content

hugr_cli/
lib.rs

1//! Standard command line tools for the HUGR format.
2//!
3//! This library provides utilities for the HUGR CLI.
4//!
5//! ## CLI Usage
6//!
7//! Run `cargo install hugr-cli` to install the CLI tools. This will make the
8//! `hugr` executable available in your shell as long as you have [cargo's bin
9//! directory](https://doc.rust-lang.org/book/ch14-04-installing-binaries.html)
10//! in your path.
11//!
12//! The top level help can be accessed with:
13//! ```sh
14//! hugr --help
15//! ```
16//!
17//! Refer to the help for each subcommand for more information, e.g.
18//! ```sh
19//! hugr validate --help
20//! ```
21
22use std::ffi::OsString;
23
24use anyhow::Result;
25use clap::{Parser, crate_version};
26#[cfg(feature = "tracing")]
27use clap_verbosity_flag::VerbosityFilter;
28use clap_verbosity_flag::{InfoLevel, Verbosity};
29use hugr::extension::resolution::ExtensionResolutionError;
30use hugr::package::PackageValidationError;
31use thiserror::Error;
32#[cfg(feature = "tracing")]
33use tracing::{error, metadata::LevelFilter};
34
35pub mod convert;
36pub mod describe;
37pub mod extensions;
38pub mod hugr_io;
39pub mod mermaid;
40pub mod validate;
41
42/// CLI arguments.
43#[derive(Parser, Debug)]
44#[clap(version = crate_version!(), long_about = None)]
45#[clap(about = "HUGR CLI tools.")]
46#[group(id = "hugr")]
47pub struct CliArgs {
48    /// The command to be run.
49    #[command(subcommand)]
50    pub command: CliCommand,
51    /// Verbosity.
52    #[command(flatten)]
53    pub verbose: Verbosity<InfoLevel>,
54}
55
56/// The CLI subcommands.
57#[derive(Debug, clap::Subcommand)]
58#[non_exhaustive]
59pub enum CliCommand {
60    /// Validate a HUGR package.
61    Validate(validate::ValArgs),
62    /// Write standard extensions out in serialized form.
63    GenExtensions(extensions::ExtArgs),
64    /// Write HUGR as mermaid diagrams.
65    Mermaid(mermaid::MermaidArgs),
66    /// Convert between different HUGR envelope formats.
67    Convert(convert::ConvertArgs),
68    /// External commands
69    #[command(external_subcommand)]
70    External(Vec<OsString>),
71
72    /// Describe the contents of a HUGR package.
73    ///
74    /// If an error occurs during loading partial descriptions are printed.
75    /// For example if the first module is loaded and the second fails then
76    /// only the first module will be described.
77    Describe(describe::DescribeArgs),
78}
79
80/// Error type for the CLI.
81#[derive(Debug, Error)]
82#[non_exhaustive]
83pub enum CliError {
84    /// Error reading input.
85    #[error("Error reading from path.")]
86    InputFile(#[from] std::io::Error),
87    /// Error parsing input.
88    #[error("Error parsing package.")]
89    Parse(#[from] serde_json::Error),
90    #[error("Error validating HUGR.")]
91    /// Errors produced by the `validate` subcommand.
92    Validate(#[from] PackageValidationError),
93    /// Pretty error when the user passes a non-envelope file.
94    #[error(
95        "Input file is not a HUGR envelope. Invalid magic number.\n\nUse `--hugr-json` to read a raw HUGR JSON file instead."
96    )]
97    NotAnEnvelope,
98    /// Invalid format string for conversion.
99    #[error(
100        "Invalid format: '{_0}'. Valid formats are: json, model, model-exts, s-expression, s-expression-exts"
101    )]
102    InvalidFormat(String),
103    #[error("Error validating HUGR generated by {generator}")]
104    /// Errors produced by the `validate` subcommand, with a known generator of the HUGR.
105    ValidateKnownGenerator {
106        #[source]
107        /// The inner validation error.
108        inner: PackageValidationError,
109        /// The generator of the HUGR.
110        generator: Box<String>,
111    },
112    #[error("Error reading envelope.")]
113    /// Errors produced when reading an envelope.
114    ReadEnvelope(#[from] hugr::envelope::ReadError),
115    /// Error produced while loading extension files.
116    #[error("Error loading extension file.")]
117    LoadExtensionFile(#[from] ExtensionResolutionError),
118}
119
120impl CliError {
121    /// Returns a validation error, with an optional generator.
122    pub fn validation(generator: Option<String>, val_err: PackageValidationError) -> Self {
123        if let Some(g) = generator {
124            Self::ValidateKnownGenerator {
125                inner: val_err,
126                generator: Box::new(g.to_string()),
127            }
128        } else {
129            Self::Validate(val_err)
130        }
131    }
132}
133
134impl CliCommand {
135    /// Run a CLI command with optional input/output overrides.
136    /// If overrides are `None`, behaves like the normal CLI.
137    /// If overrides are provided, stdin/stdout/files are ignored.
138    /// The `gen-extensions` and `external` commands don't support overrides.
139    ///
140    /// # Arguments
141    ///
142    /// * `input_override` - Optional reader to use instead of stdin/files
143    /// * `output_override` - Optional writer to use instead of stdout/files
144    ///
145    fn run_with_io<R: std::io::Read, W: std::io::Write>(
146        self,
147        input_override: Option<R>,
148        output_override: Option<W>,
149    ) -> Result<()> {
150        match self {
151            Self::Validate(mut args) => args.run_with_input(input_override),
152            Self::GenExtensions(args) => {
153                if input_override.is_some() || output_override.is_some() {
154                    return Err(anyhow::anyhow!(
155                        "GenExtensions command does not support programmatic I/O overrides"
156                    ));
157                }
158                args.run_dump(&hugr::std_extensions::STD_REG)
159            }
160            Self::Mermaid(mut args) => args.run_print_with_io(input_override, output_override),
161            Self::Convert(mut args) => args.run_convert_with_io(input_override, output_override),
162            Self::Describe(mut args) => args.run_describe_with_io(input_override, output_override),
163            Self::External(args) => {
164                if input_override.is_some() || output_override.is_some() {
165                    return Err(anyhow::anyhow!(
166                        "External commands do not support programmatic I/O overrides"
167                    ));
168                }
169                run_external(args)
170            }
171        }
172    }
173}
174
175impl Default for CliArgs {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181impl CliArgs {
182    /// Parse CLI arguments from the environment.
183    pub fn new() -> Self {
184        CliArgs::parse()
185    }
186
187    /// Parse CLI arguments from an iterator.
188    pub fn new_from_args<I, T>(args: I) -> Self
189    where
190        I: IntoIterator<Item = T>,
191        T: Into<std::ffi::OsString> + Clone,
192    {
193        CliArgs::parse_from(args)
194    }
195
196    /// Entrypoint for cli - process arguments and run commands.
197    ///
198    /// Process exits on error.
199    pub fn run_cli(self) {
200        #[cfg(feature = "tracing")]
201        {
202            let level = match self.verbose.filter() {
203                VerbosityFilter::Off => LevelFilter::OFF,
204                VerbosityFilter::Error => LevelFilter::ERROR,
205                VerbosityFilter::Warn => LevelFilter::WARN,
206                VerbosityFilter::Info => LevelFilter::INFO,
207                VerbosityFilter::Debug => LevelFilter::DEBUG,
208                VerbosityFilter::Trace => LevelFilter::TRACE,
209            };
210            tracing_subscriber::fmt()
211                .with_writer(std::io::stderr)
212                .with_max_level(level)
213                .pretty()
214                .init();
215        }
216
217        let result = self
218            .command
219            .run_with_io(None::<std::io::Stdin>, None::<std::io::Stdout>);
220
221        if let Err(err) = result {
222            #[cfg(feature = "tracing")]
223            error!("{:?}", err);
224            #[cfg(not(feature = "tracing"))]
225            eprintln!("{:?}", err);
226            std::process::exit(1);
227        }
228    }
229
230    /// Run a CLI command with bytes input and capture bytes output.
231    ///
232    /// This provides a programmatic interface to the CLI.
233    /// Unlike `run_cli()`, this method:
234    /// - Accepts input instead of reading from stdin/files
235    /// - Returns output as a byte vector instead of writing to stdout/files
236    ///
237    /// # Arguments
238    ///
239    /// * `input` - The input data as bytes (e.g., a HUGR package)
240    ///
241    /// # Returns
242    ///
243    /// Returns `Ok(Vec<u8>)` with the command output, or an error on failure.
244    ///
245    ///
246    /// # Note
247    ///
248    /// The `gen-extensions` and `external` commands don't support byte I/O
249    /// and should use the normal `run_cli()` method instead.
250    pub fn run_with_io(self, input: impl std::io::Read) -> Result<Vec<u8>, RunWithIoError> {
251        let mut output = Vec::new();
252        let is_describe = matches!(self.command, CliCommand::Describe(_));
253        let res = self.command.run_with_io(Some(input), Some(&mut output));
254        match (res, is_describe) {
255            (Ok(()), _) => Ok(output),
256            (Err(e), true) => Err(RunWithIoError::Describe { source: e, output }),
257            (Err(e), false) => Err(RunWithIoError::Other(e)),
258        }
259    }
260}
261
262#[derive(Debug, Error)]
263#[non_exhaustive]
264#[error("Error running CLI command with IO.")]
265/// Error type for `run_with_io` method.
266pub enum RunWithIoError {
267    /// Error describing HUGR package.
268    Describe {
269        #[source]
270        /// Error returned from describe command.
271        source: anyhow::Error,
272        /// Describe command output.
273        output: Vec<u8>,
274    },
275    /// Non-describe command error.
276    Other(anyhow::Error),
277}
278
279fn run_external(args: Vec<OsString>) -> Result<()> {
280    // External subcommand support: invoke `hugr-<subcommand>`
281    if args.is_empty() {
282        eprintln!("No external subcommand specified.");
283        std::process::exit(1);
284    }
285    let subcmd = args[0].to_string_lossy();
286    let exe = format!("hugr-{subcmd}");
287    let rest: Vec<_> = args[1..]
288        .iter()
289        .map(|s| s.to_string_lossy().to_string())
290        .collect();
291    match std::process::Command::new(&exe).args(&rest).status() {
292        Ok(status) => {
293            if !status.success() {
294                std::process::exit(status.code().unwrap_or(1));
295            }
296        }
297        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
298            eprintln!("error: no such subcommand: '{subcmd}'.\nCould not find '{exe}' in PATH.");
299            std::process::exit(1);
300        }
301        Err(e) => {
302            eprintln!("error: failed to invoke '{exe}': {e}");
303            std::process::exit(1);
304        }
305    }
306
307    Ok(())
308}