1use 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#[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 #[command(subcommand)]
50 pub command: CliCommand,
51 #[command(flatten)]
53 pub verbose: Verbosity<InfoLevel>,
54}
55
56#[derive(Debug, clap::Subcommand)]
58#[non_exhaustive]
59pub enum CliCommand {
60 Validate(validate::ValArgs),
62 GenExtensions(extensions::ExtArgs),
64 Mermaid(mermaid::MermaidArgs),
66 Convert(convert::ConvertArgs),
68 #[command(external_subcommand)]
70 External(Vec<OsString>),
71
72 Describe(describe::DescribeArgs),
78}
79
80#[derive(Debug, Error)]
82#[non_exhaustive]
83pub enum CliError {
84 #[error("Error reading from path.")]
86 InputFile(#[from] std::io::Error),
87 #[error("Error parsing package.")]
89 Parse(#[from] serde_json::Error),
90 #[error("Error validating HUGR.")]
91 Validate(#[from] PackageValidationError),
93 #[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 #[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 ValidateKnownGenerator {
106 #[source]
107 inner: PackageValidationError,
109 generator: Box<String>,
111 },
112 #[error("Error reading envelope.")]
113 ReadEnvelope(#[from] hugr::envelope::ReadError),
115 #[error("Error loading extension file.")]
117 LoadExtensionFile(#[from] ExtensionResolutionError),
118}
119
120impl CliError {
121 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 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 pub fn new() -> Self {
184 CliArgs::parse()
185 }
186
187 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 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 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.")]
265pub enum RunWithIoError {
267 Describe {
269 #[source]
270 source: anyhow::Error,
272 output: Vec<u8>,
274 },
275 Other(anyhow::Error),
277}
278
279fn run_external(args: Vec<OsString>) -> Result<()> {
280 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}