Repository: taiki-e/cargo-hack (language: rust)

Issue:
Allow outputting the commands that will be run without running them
It would be really helpful to allow outputting the commands that will be run without running them.

That way the commands could be used to generate a dynamic matrix for GitHub Actions, and the feature runs could be split over multiple instances.


Discussion / hints:
Filed #175. Could you confirm that it is the interface you were expecting?
Oh wow that was fast. Taking a look 👀.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07619b1f..80fa70fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
 
 ## [Unreleased]
 
+- Add unstable `--print-command-list` flag. ([#175](https://github.com/taiki-e/cargo-hack/pull/175))
+
 ## [0.5.27] - 2023-01-25
 
 - Update `toml_edit` to 0.18.
diff --git a/README.md b/README.md
index 74656297..ff668914 100644
--- a/README.md
+++ b/README.md
@@ -183,6 +183,9 @@ OPTIONS:
         --keep-going
             Keep going on failure.
 
+        --print-command-list
+            Print commands without run (Unstable).
+
         --no-manifest-path
             Do not pass --manifest-path option to cargo (Unstable).
 
diff --git a/src/cli.rs b/src/cli.rs
index 2e08b879..42b36c59 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -47,6 +47,8 @@ pub(crate) struct Args {
     pub(crate) clean_per_version: bool,
     /// --keep-going
     pub(crate) keep_going: bool,
+    /// --print-command-list
+    pub(crate) print_command_list: bool,
     /// --version-range
     pub(crate) version_range: Option<String>,
     /// --version-step
@@ -55,7 +57,7 @@ pub(crate) struct Args {
     // options for --each-feature and --feature-powerset
     /// --optional-deps [DEPS]...
     pub(crate) optional_deps: Option<Vec<String>>,
-    /// --include-features
+    /// --include-features <FEATURES>...
     pub(crate) include_features: Vec<Feature>,
     /// --include-deps-features
     pub(crate) include_deps_features: bool,
@@ -139,6 +141,7 @@ impl Args {
         let mut clean_per_run = false;
         let mut clean_per_version = false;
         let mut keep_going = false;
+        let mut print_command_list = false;
         let mut no_manifest_path = false;
         let mut version_range = None;
         let mut version_step = None;
@@ -278,6 +281,7 @@ impl Args {
                 Long("clean-per-run") => parse_flag!(clean_per_run),
                 Long("clean-per-version") => parse_flag!(clean_per_version),
                 Long("keep-going") => parse_flag!(keep_going),
+                Long("print-command-list") => parse_flag!(print_command_list),
                 Long("no-manifest-path") => parse_flag!(no_manifest_path),
                 Long("ignore-unknown-features") => parse_flag!(ignore_unknown_features),
                 Short('v') | Long("verbose") => verbose += 1,
@@ -555,6 +559,7 @@ impl Args {
             clean_per_run,
             clean_per_version,
             keep_going,
+            print_command_list,
             no_manifest_path,
             include_features: include_features.into_iter().map(Into::into).collect(),
             include_deps_features,
@@ -717,6 +722,7 @@ const HELP: &[HelpText<'_>] = &[
         "This flag can only be used together with --version-range flag.",
     ]),
     ("", "--keep-going", "", "Keep going on failure", &[]),
+    ("", "--print-command-list", "", "Print commands without run (Unstable)", &[]),
     ("", "--no-manifest-path", "", "Do not pass --manifest-path option to cargo (Unstable)", &[]),
     ("-v", "--verbose", "", "Use verbose output", &[]),
     ("", "--color", "<WHEN>", "Coloring: auto, always, never", &[
diff --git a/src/main.rs b/src/main.rs
index 72d7b608..6736350e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -494,7 +494,7 @@ fn exec_cargo_inner(
     line: &mut ProcessBuilder<'_>,
     progress: &mut Progress,
 ) -> Result<()> {
-    if progress.count != 0 {
+    if progress.count != 0 && !cx.print_command_list {
         eprintln!();
     }
     progress.count += 1;
@@ -503,6 +503,11 @@ fn exec_cargo_inner(
         cargo_clean(cx, Some(id))?;
     }
 
+    if cx.print_command_list {
+        print_command(line.clone());
+        return Ok(());
+    }
+
     // running `<command>` (on <package>) (<count>/<total>)
     let mut msg = String::new();
     if term::verbose() {
@@ -524,6 +529,11 @@ fn cargo_clean(cx: &Context, id: Option<&PackageId>) -> Result<()> {
         line.arg(&cx.packages(id).name);
     }
 
+    if cx.print_command_list {
+        print_command(line);
+        return Ok(());
+    }
+
     if term::verbose() {
         // running `cargo clean [--package <package>]`
         info!("running {line}");
@@ -531,3 +541,10 @@ fn cargo_clean(cx: &Context, id: Option<&PackageId>) -> Result<()> {
 
     line.run()
 }
+
+fn print_command(mut line: ProcessBuilder<'_>) {
+    let _guard = term::verbose::scoped(true);
+    line.strip_program_path = true;
+    let l = line.to_string();
+    println!("{}", &l[1..l.len() - 1]);
+}
diff --git a/src/process.rs b/src/process.rs
index 2e310a93..1f9f5274 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -44,6 +44,7 @@ pub(crate) struct ProcessBuilder<'a> {
     /// This list always has a trailing comma if it is not empty.
     // cargo less than Rust 1.38 cannot handle multiple '--features' flags, so it creates another String.
     features: String,
+    pub(crate) strip_program_path: bool,
 }
 
 impl<'a> ProcessBuilder<'a> {
@@ -56,6 +57,7 @@ impl<'a> ProcessBuilder<'a> {
             leading_args: Vec::new(),
             args: Vec::new(),
             features: String::new(),
+            strip_program_path: false,
         }
     }
 
@@ -185,7 +187,7 @@ impl fmt::Display for ProcessBuilder<'_> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(f, "`")?;
 
-        if f.alternate() || term::verbose() {
+        if !self.strip_program_path && (f.alternate() || term::verbose()) {
             write!(f, "{}", self.program.to_string_lossy())?;
         } else {
             write!(f, "{}", Path::new(&*self.program).file_stem().unwrap().to_string_lossy())?;


Test patch:
diff --git a/tests/long-help.txt b/tests/long-help.txt
index 1bb83a9f..77809b33 100644
--- a/tests/long-help.txt
+++ b/tests/long-help.txt
@@ -153,6 +153,9 @@ OPTIONS:
         --keep-going
             Keep going on failure.
 
+        --print-command-list
+            Print commands without run (Unstable).
+
         --no-manifest-path
             Do not pass --manifest-path option to cargo (Unstable).
 
diff --git a/tests/short-help.txt b/tests/short-help.txt
index 66bdd548..8c88e722 100644
--- a/tests/short-help.txt
+++ b/tests/short-help.txt
@@ -39,6 +39,7 @@ OPTIONS:
                                          command
         --clean-per-version              Remove artifacts per Rust version
         --keep-going                     Keep going on failure
+        --print-command-list             Print commands without run (Unstable)
         --no-manifest-path               Do not pass --manifest-path option to cargo (Unstable)
     -v, --verbose                        Use verbose output
         --color <WHEN>                   Coloring: auto, always, never
diff --git a/tests/test.rs b/tests/test.rs
index d7a8e482..18d7b12d 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -1461,3 +1461,20 @@ fn empty_string() {
         .assert_success("real")
         .stderr_not_contains("not found");
 }
+
+#[test]
+fn print_command_list() {
+    cargo_hack(["check", "--each-feature", "--print-command-list"])
+        .assert_success("real")
+        .stdout_contains(
+            "
+            cargo check --manifest-path Cargo.toml --no-default-features
+            cargo check --manifest-path Cargo.toml --no-default-features --features a
+            cargo check --manifest-path Cargo.toml --no-default-features --features b
+            cargo check --manifest-path Cargo.toml --no-default-features --features c
+            cargo check --manifest-path Cargo.toml --no-default-features --features default
+            cargo check --manifest-path Cargo.toml --no-default-features --all-features
+            ",
+        )
+        .stdout_not_contains("`");
+}