Repository: denoland/deno
Pull request: fix(task): strip ansi codes and control chars when printing tasks

Strip out ansi codes and ASCII control characters when
printing the commands, descriptions and dependencies
of tasks.

Changed files:
filename: cli/tools/task.rs
patch: @@ -8,6 +8,7 @@ use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
 
+use console_static_text::ansi::strip_ansi_codes;
 use deno_config::workspace::FolderConfigs;
 use deno_config::workspace::TaskDefinition;
 use deno_config::workspace::TaskOrScript;
@@ -722,19 +723,47 @@ fn print_available_tasks(
     if let Some(description) = &desc.task.description {
       let slash_slash = colors::italic_gray("//");
       for line in description.lines() {
-        writeln!(writer, "    {slash_slash} {}", colors::italic_gray(line))?;
+        writeln!(
+          writer,
+          "    {slash_slash} {}",
+          colors::italic_gray(strip_ansi_codes_and_escape_control_chars(line))
+        )?;
       }
     }
-    writeln!(writer, "    {}", desc.task.command)?;
+    writeln!(
+      writer,
+      "    {}",
+      strip_ansi_codes_and_escape_control_chars(&desc.task.command)
+    )?;
     if !desc.task.dependencies.is_empty() {
+      let dependencies = desc
+        .task
+        .dependencies
+        .into_iter()
+        .map(|d| strip_ansi_codes_and_escape_control_chars(&d))
+        .collect::<Vec<_>>()
+        .join(", ");
       writeln!(
         writer,
         "    {} {}",
         colors::gray("depends on:"),
-        colors::cyan(desc.task.dependencies.join(", "))
+        colors::cyan(dependencies)
       )?;
     }
   }
 
   Ok(())
 }
+
+fn strip_ansi_codes_and_escape_control_chars(s: &str) -> String {
+  strip_ansi_codes(s)
+    .chars()
+    .map(|c| match c {
+      '\n' => "\\n".to_string(),
+      '\r' => "\\r".to_string(),
+      '\t' => "\\t".to_string(),
+      c if c.is_control() => format!("\\x{:02x}", c as u8),
+      c => c.to_string(),
+    })
+    .collect()
+}

filename: tests/integration/task_tests.rs
patch: @@ -3,6 +3,9 @@
 // Most of the tests for this are in deno_task_shell.
 // These tests are intended to only test integration.
 
+use test_util as util;
+use util::TestContextBuilder;
+
 // use test_util::env_vars_for_npm_tests;
 // use test_util::itest;
 // use test_util::TestContext;
@@ -28,3 +31,59 @@
 //   exit_code: 1,
 //   http_server: true,
 // });
+
+#[test]
+fn deno_task_ansi_escape_codes() {
+  let context = TestContextBuilder::default().use_temp_cwd().build();
+  let temp_dir = context.temp_dir();
+  temp_dir.write("deno.json", r#"{
+  "tasks": {
+    "dev": "echo 'BOOO!!!'",
+    "next": "\u001b[3F\u001b[0G- dev\u001b[1E\u001b[2K    echo 'I am your friend.'"
+  }
+}
+"#);
+
+  context
+    .new_command()
+    .args_vec(["task"])
+    .with_pty(|mut console| {
+      console.expect("Available tasks:");
+      console.expect("- dev");
+      console.expect("    echo 'BOOO!!!'");
+      console.expect("- next");
+      console.expect("    - dev    echo 'I am your friend.'");
+    });
+}
+
+#[test]
+fn deno_task_control_chars() {
+  let context = TestContextBuilder::default().use_temp_cwd().build();
+  let temp_dir = context.temp_dir();
+  temp_dir.write(
+    "deno.json",
+    r#"{
+  "tasks": {
+    "dev": "echo 'BOOO!!!' && \r    echo hi there is my command",
+    "serve": {
+      "description": "this is a\tm\rangled description",
+      "command": "echo hello"
+    }
+  }
+}
+"#,
+  );
+
+  context
+    .new_command()
+    .args_vec(["task"])
+    .with_pty(|mut console| {
+      console.expect("Available tasks:");
+      console.expect("- dev");
+      console
+        .expect("    echo 'BOOO!!!' && \\r    echo hi there is my command");
+      console.expect("- serve");
+      console.expect("    // this is a\\tm\\rangled description");
+      console.expect("    echo hello");
+    });
+}