Skip to content

Commit 2018304

Browse files
OsamaMITaborgna-q
andauthored
feat: support external subcommands via PATH (#1343) (#2278)
## **Summary** This PR adds support for external subcommands in the `hugr-cli`binary, as per the feature request from issue #1343. Now when you run: ```bash hugr <subcommand> [args…] ``` if an executable named `hugr-<subcommand>` is on your `PATH`, it will be called with the same `[args…]`. If the binary is missing or fails, you get a clear error and non-zero exit code. --- ## **Details** **Functionality:** * **`src/main.rs`** 1. Checks for no subcommand and errors out. 2. Builds `hugr-<subcommand>` from the first arg. 3. Runs it with `std::process::Command`, passing the remaining args. 4. On error: * If not found → ``` error: no such subcommand: '<subcommand>'. Could not find 'hugr-<subcommand>' in PATH. ``` * Other I/O errors → ``` error: failed to invoke '<executable>': <error> ``` **Testing:** * **`hugr-cli/tests/external.rs`** * **`test_missing_external_command`** Runs `hugr idontexist` and checks for a “no such subcommand” error. * **`test_external_command_invocation`** 1. Creates a temp dir with a dummy `hugr-dummy` script that echoes its args and exits `42`. 2. Prepends that dir to `PATH`. 3. Runs `hugr dummy foo bar` and checks it prints `dummy called: foo bar` and exits `42`. * Adds `tempfile` to `dev-dependencies` in `Cargo.toml` for these tests. --- Closes #1343 --------- Co-authored-by: Agustín Borgna <121866228+aborgna-q@users.noreply.github.com>
1 parent 84fb200 commit 2018304

File tree

6 files changed

+83
-9
lines changed

6 files changed

+83
-9
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ serde_yaml = "0.9.34"
7070
smol_str = "0.3.1"
7171
static_assertions = "1.1.0"
7272
strum = "0.27.0"
73+
tempfile = "3.20"
7374
thiserror = "2.0.12"
7475
typetag = "0.2.20"
7576
clap = { version = "4.5.38" }

hugr-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct_missing = "warn"
3737
assert_cmd = { workspace = true }
3838
assert_fs = { workspace = true }
3939
predicates = { workspace = true }
40+
tempfile = { workspace = true }
4041
rstest.workspace = true
4142

4243
[[bin]]

hugr-cli/src/main.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,35 @@ fn main() {
1111
CliArgs::Validate(args) => run_validate(args),
1212
CliArgs::GenExtensions(args) => args.run_dump(&hugr::std_extensions::STD_REG),
1313
CliArgs::Mermaid(args) => run_mermaid(args),
14-
CliArgs::External(_) => {
15-
// TODO: Implement support for external commands.
16-
// Running `hugr COMMAND` would look for `hugr-COMMAND` in the path
17-
// and run it.
18-
eprintln!("External commands are not supported yet.");
19-
std::process::exit(1);
14+
CliArgs::External(args) => {
15+
// External subcommand support: invoke `hugr-<subcommand>`
16+
if args.is_empty() {
17+
eprintln!("No external subcommand specified.");
18+
std::process::exit(1);
19+
}
20+
let subcmd = args[0].to_string_lossy();
21+
let exe = format!("hugr-{}", subcmd);
22+
let rest: Vec<_> = args[1..]
23+
.iter()
24+
.map(|s| s.to_string_lossy().to_string())
25+
.collect();
26+
match std::process::Command::new(&exe).args(&rest).status() {
27+
Ok(status) => {
28+
if !status.success() {
29+
std::process::exit(status.code().unwrap_or(1));
30+
}
31+
}
32+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
33+
eprintln!(
34+
"error: no such subcommand: '{subcmd}'.\nCould not find '{exe}' in PATH."
35+
);
36+
std::process::exit(1);
37+
}
38+
Err(e) => {
39+
eprintln!("error: failed to invoke '{exe}': {e}");
40+
std::process::exit(1);
41+
}
42+
}
2043
}
2144
_ => {
2245
eprintln!("Unknown command");

hugr-cli/tests/external.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Tests for external subcommand support in hugr-cli.
2+
#![cfg(all(test, not(miri)))]
3+
4+
use assert_cmd::Command;
5+
use predicates::str::contains;
6+
use std::env;
7+
use std::fs;
8+
#[cfg(unix)]
9+
use std::os::unix::fs::PermissionsExt;
10+
use tempfile::TempDir;
11+
12+
#[test]
13+
fn test_missing_external_command() {
14+
let mut cmd = Command::cargo_bin("hugr").unwrap();
15+
cmd.arg("idontexist");
16+
cmd.assert()
17+
.failure()
18+
.stderr(contains("no such subcommand"));
19+
}
20+
21+
#[test]
22+
#[cfg_attr(not(unix), ignore = "Dummy program supported on Unix-like systems")]
23+
fn test_external_command_invocation() {
24+
// Create a dummy external command in a temp dir
25+
let tempdir = TempDir::new().unwrap();
26+
let bin_path = tempdir.path().join("hugr-dummy");
27+
fs::write(&bin_path, b"#!/bin/sh\necho dummy called: $@\nexit 42\n").unwrap();
28+
let mut perms = fs::metadata(&bin_path).unwrap().permissions();
29+
#[cfg(unix)]
30+
perms.set_mode(0o755);
31+
fs::set_permissions(&bin_path, perms).unwrap();
32+
33+
// Prepend tempdir to PATH
34+
let orig_path = env::var("PATH").unwrap();
35+
let new_path = format!("{}:{}", tempdir.path().display(), orig_path);
36+
let mut cmd = Command::cargo_bin("hugr").unwrap();
37+
cmd.env("PATH", new_path);
38+
cmd.arg("dummy");
39+
cmd.arg("foo");
40+
cmd.arg("bar");
41+
cmd.assert()
42+
.failure()
43+
.stdout(contains("dummy called: foo bar"))
44+
.code(42);
45+
}

hugr-cli/tests/validate.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ fn test_doesnt_exist(mut val_cmd: Command) {
6969
val_cmd
7070
.assert()
7171
.failure()
72-
.stderr(contains("No such file or directory"));
72+
// clap now prints something like:
73+
// error: Invalid value for [INPUT]: Could not open "foobar": (os error 2)
74+
// so just look for "Could not open"
75+
.stderr(contains("Could not open"));
7376
}
7477

7578
#[rstest]

0 commit comments

Comments
 (0)