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 "Invalid format: '{_0}'. Valid formats are: json, model, model-exts, s-expression, s-expression-exts"
96 )]
97 InvalidFormat(String),
98 #[error("Error validating HUGR generated by {generator}")]
99 ValidateKnownGenerator {
101 #[source]
102 inner: PackageValidationError,
104 generator: Box<String>,
106 },
107 #[error("Error reading envelope.")]
108 ReadEnvelope(#[from] hugr::envelope::ReadError),
110 #[error("Error loading extension file.")]
112 LoadExtensionFile(#[from] ExtensionResolutionError),
113}
114
115impl CliError {
116 pub fn validation(generator: Option<String>, val_err: PackageValidationError) -> Self {
118 if let Some(g) = generator {
119 Self::ValidateKnownGenerator {
120 inner: val_err,
121 generator: Box::new(g.to_string()),
122 }
123 } else {
124 Self::Validate(val_err)
125 }
126 }
127}
128
129impl CliCommand {
130 fn run_with_io<R: std::io::Read, W: std::io::Write>(
141 self,
142 input_override: Option<R>,
143 output_override: Option<W>,
144 ) -> Result<()> {
145 match self {
146 Self::Validate(mut args) => args.run_with_input(input_override),
147 Self::GenExtensions(args) => {
148 if input_override.is_some() || output_override.is_some() {
149 return Err(anyhow::anyhow!(
150 "GenExtensions command does not support programmatic I/O overrides"
151 ));
152 }
153 args.run_dump(&hugr::std_extensions::STD_REG)
154 }
155 Self::Mermaid(mut args) => args.run_print_with_io(input_override, output_override),
156 Self::Convert(mut args) => args.run_convert_with_io(input_override, output_override),
157 Self::Describe(mut args) => args.run_describe_with_io(input_override, output_override),
158 Self::External(args) => {
159 if input_override.is_some() || output_override.is_some() {
160 return Err(anyhow::anyhow!(
161 "External commands do not support programmatic I/O overrides"
162 ));
163 }
164 run_external(args)
165 }
166 }
167 }
168}
169
170impl Default for CliArgs {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176impl CliArgs {
177 pub fn new() -> Self {
179 CliArgs::parse()
180 }
181
182 pub fn new_from_args<I, T>(args: I) -> Self
184 where
185 I: IntoIterator<Item = T>,
186 T: Into<std::ffi::OsString> + Clone,
187 {
188 CliArgs::parse_from(args)
189 }
190
191 pub fn run_cli(self) {
195 #[cfg(feature = "tracing")]
196 {
197 let level = match self.verbose.filter() {
198 VerbosityFilter::Off => LevelFilter::OFF,
199 VerbosityFilter::Error => LevelFilter::ERROR,
200 VerbosityFilter::Warn => LevelFilter::WARN,
201 VerbosityFilter::Info => LevelFilter::INFO,
202 VerbosityFilter::Debug => LevelFilter::DEBUG,
203 VerbosityFilter::Trace => LevelFilter::TRACE,
204 };
205 tracing_subscriber::fmt()
206 .with_writer(std::io::stderr)
207 .with_max_level(level)
208 .pretty()
209 .init();
210 }
211
212 let result = self
213 .command
214 .run_with_io(None::<std::io::Stdin>, None::<std::io::Stdout>);
215
216 if let Err(err) = result {
217 #[cfg(feature = "tracing")]
218 error!("{:?}", err);
219 #[cfg(not(feature = "tracing"))]
220 eprintln!("{:?}", err);
221 std::process::exit(1);
222 }
223 }
224
225 pub fn run_with_io(self, input: impl std::io::Read) -> Result<Vec<u8>, RunWithIoError> {
246 let mut output = Vec::new();
247 let is_describe = matches!(self.command, CliCommand::Describe(_));
248 let res = self.command.run_with_io(Some(input), Some(&mut output));
249 match (res, is_describe) {
250 (Ok(()), _) => Ok(output),
251 (Err(e), true) => Err(RunWithIoError::Describe { source: e, output }),
252 (Err(e), false) => Err(RunWithIoError::Other(e)),
253 }
254 }
255}
256
257#[derive(Debug, Error)]
258#[non_exhaustive]
259#[error("Error running CLI command with IO.")]
260pub enum RunWithIoError {
262 Describe {
264 #[source]
265 source: anyhow::Error,
267 output: Vec<u8>,
269 },
270 Other(anyhow::Error),
272}
273
274fn run_external(args: Vec<OsString>) -> Result<()> {
275 if args.is_empty() {
277 eprintln!("No external subcommand specified.");
278 std::process::exit(1);
279 }
280 let subcmd = args[0].to_string_lossy();
281 let exe = format!("hugr-{subcmd}");
282 let rest: Vec<_> = args[1..]
283 .iter()
284 .map(|s| s.to_string_lossy().to_string())
285 .collect();
286 match std::process::Command::new(&exe).args(&rest).status() {
287 Ok(status) => {
288 if !status.success() {
289 std::process::exit(status.code().unwrap_or(1));
290 }
291 }
292 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
293 eprintln!("error: no such subcommand: '{subcmd}'.\nCould not find '{exe}' in PATH.");
294 std::process::exit(1);
295 }
296 Err(e) => {
297 eprintln!("error: failed to invoke '{exe}': {e}");
298 std::process::exit(1);
299 }
300 }
301
302 Ok(())
303}