From c9afffefeb7b3e5e8bd834512f46a1f20bdb756b Mon Sep 17 00:00:00 2001 From: OsamaMIT Date: Fri, 30 May 2025 18:33:01 +0400 Subject: [PATCH 1/6] feat: support external subcommands via PATH (#1343) --- Cargo.lock | 1 + hugr-cli/Cargo.toml | 1 + hugr-cli/src/main.rs | 30 +++++++++++++++++++++------ hugr-cli/tests/external.rs | 42 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 hugr-cli/tests/external.rs diff --git a/Cargo.lock b/Cargo.lock index bf072bfb8e..34da7b9dc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1232,6 +1232,7 @@ dependencies = [ "predicates", "rstest", "serde_json", + "tempfile", ] [[package]] diff --git a/hugr-cli/Cargo.toml b/hugr-cli/Cargo.toml index cd7d1fb26a..eee8daed17 100644 --- a/hugr-cli/Cargo.toml +++ b/hugr-cli/Cargo.toml @@ -38,6 +38,7 @@ assert_cmd = { workspace = true } assert_fs = { workspace = true } predicates = { workspace = true } rstest.workspace = true +tempfile = "3" [[bin]] name = "hugr" diff --git a/hugr-cli/src/main.rs b/hugr-cli/src/main.rs index 8af5b6b3d5..4a87fa827d 100644 --- a/hugr-cli/src/main.rs +++ b/hugr-cli/src/main.rs @@ -11,12 +11,30 @@ fn main() { CliArgs::Validate(args) => run_validate(args), CliArgs::GenExtensions(args) => args.run_dump(&hugr::std_extensions::STD_REG), CliArgs::Mermaid(args) => run_mermaid(args), - CliArgs::External(_) => { - // TODO: Implement support for external commands. - // Running `hugr COMMAND` would look for `hugr-COMMAND` in the path - // and run it. - eprintln!("External commands are not supported yet."); - std::process::exit(1); + CliArgs::External(args) => { + // External subcommand support: invoke `hugr-` + 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: '{}'.\nCould not find '{}' in PATH.", subcmd, exe); + std::process::exit(1); + } + Err(e) => { + eprintln!("error: failed to invoke '{}': {}", exe, e); + std::process::exit(1); + } + } } _ => { eprintln!("Unknown command"); diff --git a/hugr-cli/tests/external.rs b/hugr-cli/tests/external.rs new file mode 100644 index 0000000000..18e39bc2a4 --- /dev/null +++ b/hugr-cli/tests/external.rs @@ -0,0 +1,42 @@ +//! Tests for external subcommand support in hugr-cli. +#![cfg(all(test, not(miri)))] + +use assert_cmd::Command; +use predicates::str::contains; +use std::env; +use std::fs; +use std::os::unix::fs::PermissionsExt; +use tempfile::TempDir; + +#[test] +fn test_missing_external_command() { + let mut cmd = Command::cargo_bin("hugr").unwrap(); + cmd.arg("idontexist"); + cmd.assert() + .failure() + .stderr(contains("no such subcommand")); +} + +#[test] +fn test_external_command_invocation() { + // Create a dummy external command in a temp dir + let tempdir = TempDir::new().unwrap(); + let bin_path = tempdir.path().join("hugr-dummy"); + fs::write(&bin_path, b"#!/bin/sh\necho dummy called: $@\nexit 42\n").unwrap(); + let mut perms = fs::metadata(&bin_path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&bin_path, perms).unwrap(); + + // Prepend tempdir to PATH + let orig_path = env::var("PATH").unwrap(); + let new_path = format!("{}:{}", tempdir.path().display(), orig_path); + let mut cmd = Command::cargo_bin("hugr").unwrap(); + cmd.env("PATH", new_path); + cmd.arg("dummy"); + cmd.arg("foo"); + cmd.arg("bar"); + cmd.assert() + .failure() + .stdout(contains("dummy called: foo bar")) + .code(42); +} From f26f4d6719da1b090411f6394d5ed169756a79ea Mon Sep 17 00:00:00 2001 From: Osama El Mahdy <114868997+OsamaMIT@users.noreply.github.com> Date: Mon, 2 Jun 2025 13:54:09 +0400 Subject: [PATCH 2/6] fix(cli): correct string formatting and restrict dummy program test to Unix (#1343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suggestions directly committed Co-authored-by: Agustín Borgna <121866228+aborgna-q@users.noreply.github.com> --- hugr-cli/src/main.rs | 4 ++-- hugr-cli/tests/external.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hugr-cli/src/main.rs b/hugr-cli/src/main.rs index 4a87fa827d..c75c0f3ff2 100644 --- a/hugr-cli/src/main.rs +++ b/hugr-cli/src/main.rs @@ -27,11 +27,11 @@ fn main() { } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - eprintln!("error: no such subcommand: '{}'.\nCould not find '{}' in PATH.", subcmd, exe); + 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); + eprintln!("error: failed to invoke '{exe}': {e}"); std::process::exit(1); } } diff --git a/hugr-cli/tests/external.rs b/hugr-cli/tests/external.rs index 18e39bc2a4..c9b5086627 100644 --- a/hugr-cli/tests/external.rs +++ b/hugr-cli/tests/external.rs @@ -18,6 +18,7 @@ fn test_missing_external_command() { } #[test] +#[cfg_attr(not(unix), ignore = "Dummy program supported on Unix-like systems")] fn test_external_command_invocation() { // Create a dummy external command in a temp dir let tempdir = TempDir::new().unwrap(); From 45e4a9ea5b9afd48b9009448425184c9bd975996 Mon Sep 17 00:00:00 2001 From: OsamaMIT Date: Mon, 2 Jun 2025 14:25:08 +0400 Subject: [PATCH 3/6] fix(cli): update tests and error checks for Windows compatibility (#1343) --- hugr-cli/tests/external.rs | 4 +++- hugr-cli/tests/validate.rs | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/hugr-cli/tests/external.rs b/hugr-cli/tests/external.rs index c9b5086627..7fe4eb48be 100644 --- a/hugr-cli/tests/external.rs +++ b/hugr-cli/tests/external.rs @@ -5,6 +5,7 @@ use assert_cmd::Command; use predicates::str::contains; use std::env; use std::fs; +#[cfg(unix)] use std::os::unix::fs::PermissionsExt; use tempfile::TempDir; @@ -24,7 +25,8 @@ fn test_external_command_invocation() { let tempdir = TempDir::new().unwrap(); let bin_path = tempdir.path().join("hugr-dummy"); fs::write(&bin_path, b"#!/bin/sh\necho dummy called: $@\nexit 42\n").unwrap(); - let mut perms = fs::metadata(&bin_path).unwrap().permissions(); + let perms = fs::metadata(&bin_path).unwrap().permissions(); + #[cfg(unix)] perms.set_mode(0o755); fs::set_permissions(&bin_path, perms).unwrap(); diff --git a/hugr-cli/tests/validate.rs b/hugr-cli/tests/validate.rs index a55a8b5985..5fb09ca091 100644 --- a/hugr-cli/tests/validate.rs +++ b/hugr-cli/tests/validate.rs @@ -69,7 +69,10 @@ fn test_doesnt_exist(mut val_cmd: Command) { val_cmd .assert() .failure() - .stderr(contains("No such file or directory")); + // clap now prints something like: + // error: Invalid value for [INPUT]: Could not open "foobar": (os error 2) + // so just look for "Could not open" + .stderr(contains("Could not open")); } #[rstest] From 11d4816a4d9fbc39be1a35f458910799ab0450b1 Mon Sep 17 00:00:00 2001 From: OsamaMIT Date: Mon, 2 Jun 2025 14:39:01 +0400 Subject: [PATCH 4/6] fix(cli): pin tempfile version and move it to workspace dependencies (#1343) --- Cargo.lock | 6 +++--- Cargo.toml | 1 + hugr-cli/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34da7b9dc2..cfac9fb7b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1232,7 +1232,7 @@ dependencies = [ "predicates", "rstest", "serde_json", - "tempfile", + "tempfile", ] [[package]] @@ -2928,9 +2928,9 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", "getrandom 0.3.2", diff --git a/Cargo.toml b/Cargo.toml index 759eba2350..68f3e8eee5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ serde_yaml = "0.9.34" smol_str = "0.3.1" static_assertions = "1.1.0" strum = "0.27.0" +tempfile = "3.20" thiserror = "2.0.12" typetag = "0.2.20" clap = { version = "4.5.38" } diff --git a/hugr-cli/Cargo.toml b/hugr-cli/Cargo.toml index eee8daed17..7f54a7970d 100644 --- a/hugr-cli/Cargo.toml +++ b/hugr-cli/Cargo.toml @@ -37,8 +37,8 @@ struct_missing = "warn" assert_cmd = { workspace = true } assert_fs = { workspace = true } predicates = { workspace = true } +tempfile = { workspace = true } rstest.workspace = true -tempfile = "3" [[bin]] name = "hugr" From 535d39d676f5942c39c011c6fbd4b48f284e5cc4 Mon Sep 17 00:00:00 2001 From: OsamaMIT Date: Mon, 2 Jun 2025 14:41:20 +0400 Subject: [PATCH 5/6] chore(cli): run cargo fmt to apply standard formatting --- hugr-cli/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hugr-cli/src/main.rs b/hugr-cli/src/main.rs index c75c0f3ff2..fe405101db 100644 --- a/hugr-cli/src/main.rs +++ b/hugr-cli/src/main.rs @@ -19,7 +19,10 @@ fn main() { } 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(); + 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() { @@ -27,7 +30,9 @@ fn main() { } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - eprintln!("error: no such subcommand: '{subcmd}'.\nCould not find '{exe}' in PATH."); + eprintln!( + "error: no such subcommand: '{subcmd}'.\nCould not find '{exe}' in PATH." + ); std::process::exit(1); } Err(e) => { From 6671809be8a04c1e50aa6c9efa4630c5c5901d80 Mon Sep 17 00:00:00 2001 From: OsamaMIT Date: Mon, 2 Jun 2025 14:47:09 +0400 Subject: [PATCH 6/6] fix(cli): restore mut on perms to allow set_mode on Unix --- hugr-cli/tests/external.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-cli/tests/external.rs b/hugr-cli/tests/external.rs index 7fe4eb48be..ec026f2f58 100644 --- a/hugr-cli/tests/external.rs +++ b/hugr-cli/tests/external.rs @@ -25,7 +25,7 @@ fn test_external_command_invocation() { let tempdir = TempDir::new().unwrap(); let bin_path = tempdir.path().join("hugr-dummy"); fs::write(&bin_path, b"#!/bin/sh\necho dummy called: $@\nexit 42\n").unwrap(); - let perms = fs::metadata(&bin_path).unwrap().permissions(); + let mut perms = fs::metadata(&bin_path).unwrap().permissions(); #[cfg(unix)] perms.set_mode(0o755); fs::set_permissions(&bin_path, perms).unwrap();