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::package::PackageValidationError;
30use thiserror::Error;
31#[cfg(feature = "tracing")]
32use tracing::{error, metadata::LevelFilter};
33
34pub mod convert;
35pub mod describe;
36pub mod extensions;
37pub mod hugr_io;
38pub mod mermaid;
39pub mod validate;
40
41#[derive(Parser, Debug)]
43#[clap(version = crate_version!(), long_about = None)]
44#[clap(about = "HUGR CLI tools.")]
45#[group(id = "hugr")]
46pub struct CliArgs {
47 #[command(subcommand)]
49 pub command: CliCommand,
50 #[command(flatten)]
52 pub verbose: Verbosity<InfoLevel>,
53}
54
55#[derive(Debug, clap::Subcommand)]
57#[non_exhaustive]
58pub enum CliCommand {
59 Validate(validate::ValArgs),
61 GenExtensions(extensions::ExtArgs),
63 Mermaid(mermaid::MermaidArgs),
65 Convert(convert::ConvertArgs),
67 #[command(external_subcommand)]
69 External(Vec<OsString>),
70
71 Describe(describe::DescribeArgs),
77}
78
79#[derive(Debug, Error)]
81#[non_exhaustive]
82pub enum CliError {
83 #[error("Error reading from path.")]
85 InputFile(#[from] std::io::Error),
86 #[error("Error parsing package.")]
88 Parse(#[from] serde_json::Error),
89 #[error("Error validating HUGR.")]
90 Validate(#[from] PackageValidationError),
92 #[error(
94 "Input file is not a HUGR envelope. Invalid magic number.\n\nUse `--hugr-json` to read a raw HUGR JSON file instead."
95 )]
96 NotAnEnvelope,
97 #[error(
99 "Invalid format: '{_0}'. Valid formats are: json, model, model-exts, model-text, model-text-exts"
100 )]
101 InvalidFormat(String),
102 #[error("Error validating HUGR generated by {generator}")]
103 ValidateKnownGenerator {
105 #[source]
106 inner: PackageValidationError,
108 generator: Box<String>,
110 },
111 #[error("Error reading envelope.")]
112 ReadEnvelope(#[from] hugr::envelope::ReadError),
114}
115
116impl CliError {
117 pub fn validation(generator: Option<String>, val_err: PackageValidationError) -> Self {
119 if let Some(g) = generator {
120 Self::ValidateKnownGenerator {
121 inner: val_err,
122 generator: Box::new(g.to_string()),
123 }
124 } else {
125 Self::Validate(val_err)
126 }
127 }
128}
129
130impl CliCommand {
131 fn run_with_io<R: std::io::Read, W: std::io::Write>(
142 self,
143 input_override: Option<R>,
144 output_override: Option<W>,
145 ) -> Result<()> {
146 match self {
147 Self::Validate(mut args) => args.run_with_input(input_override),
148 Self::GenExtensions(args) => {
149 if input_override.is_some() || output_override.is_some() {
150 return Err(anyhow::anyhow!(
151 "GenExtensions command does not support programmatic I/O overrides"
152 ));
153 }
154 args.run_dump(&hugr::std_extensions::STD_REG)
155 }
156 Self::Mermaid(mut args) => args.run_print_with_io(input_override, output_override),
157 Self::Convert(mut args) => args.run_convert_with_io(input_override, output_override),
158 Self::Describe(mut args) => args.run_describe_with_io(input_override, output_override),
159 Self::External(args) => {
160 if input_override.is_some() || output_override.is_some() {
161 return Err(anyhow::anyhow!(
162 "External commands do not support programmatic I/O overrides"
163 ));
164 }
165 run_external(args)
166 }
167 }
168 }
169}
170
171impl Default for CliArgs {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177impl CliArgs {
178 pub fn new() -> Self {
180 CliArgs::parse()
181 }
182
183 pub fn new_from_args<I, T>(args: I) -> Self
185 where
186 I: IntoIterator<Item = T>,
187 T: Into<std::ffi::OsString> + Clone,
188 {
189 CliArgs::parse_from(args)
190 }
191
192 pub fn run_cli(self) {
196 #[cfg(feature = "tracing")]
197 {
198 let level = match self.verbose.filter() {
199 VerbosityFilter::Off => LevelFilter::OFF,
200 VerbosityFilter::Error => LevelFilter::ERROR,
201 VerbosityFilter::Warn => LevelFilter::WARN,
202 VerbosityFilter::Info => LevelFilter::INFO,
203 VerbosityFilter::Debug => LevelFilter::DEBUG,
204 VerbosityFilter::Trace => LevelFilter::TRACE,
205 };
206 tracing_subscriber::fmt()
207 .with_writer(std::io::stderr)
208 .with_max_level(level)
209 .pretty()
210 .init();
211 }
212
213 let result = self
214 .command
215 .run_with_io(None::<std::io::Stdin>, None::<std::io::Stdout>);
216
217 if let Err(err) = result {
218 #[cfg(feature = "tracing")]
219 error!("{:?}", err);
220 #[cfg(not(feature = "tracing"))]
221 eprintln!("{:?}", err);
222 std::process::exit(1);
223 }
224 }
225
226 pub fn run_with_io(self, input: impl std::io::Read) -> Result<Vec<u8>, RunWithIoError> {
247 let mut output = Vec::new();
248 let is_describe = matches!(self.command, CliCommand::Describe(_));
249 let res = self.command.run_with_io(Some(input), Some(&mut output));
250 match (res, is_describe) {
251 (Ok(()), _) => Ok(output),
252 (Err(e), true) => Err(RunWithIoError::Describe { source: e, output }),
253 (Err(e), false) => Err(RunWithIoError::Other(e)),
254 }
255 }
256}
257
258#[derive(Debug, Error)]
259#[non_exhaustive]
260#[error("Error running CLI command with IO.")]
261pub enum RunWithIoError {
263 Describe {
265 #[source]
266 source: anyhow::Error,
268 output: Vec<u8>,
270 },
271 Other(anyhow::Error),
273}
274
275fn run_external(args: Vec<OsString>) -> Result<()> {
276 if args.is_empty() {
278 eprintln!("No external subcommand specified.");
279 std::process::exit(1);
280 }
281 let subcmd = args[0].to_string_lossy();
282 let exe = format!("hugr-{subcmd}");
283 let rest: Vec<_> = args[1..]
284 .iter()
285 .map(|s| s.to_string_lossy().to_string())
286 .collect();
287 match std::process::Command::new(&exe).args(&rest).status() {
288 Ok(status) => {
289 if !status.success() {
290 std::process::exit(status.code().unwrap_or(1));
291 }
292 }
293 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
294 eprintln!("error: no such subcommand: '{subcmd}'.\nCould not find '{exe}' in PATH.");
295 std::process::exit(1);
296 }
297 Err(e) => {
298 eprintln!("error: failed to invoke '{exe}': {e}");
299 std::process::exit(1);
300 }
301 }
302
303 Ok(())
304}