Strip ansi escape sequences from subcommand output when not writing to a smart terminal.
authorNico Weber <thakis@chromium.org>
Thu, 19 Jan 2012 00:09:16 +0000 (16:09 -0800)
committerNico Weber <thakis@chromium.org>
Thu, 19 Jan 2012 00:09:16 +0000 (16:09 -0800)
src/build.cc
src/util.cc
src/util.h
src/util_test.cc

index 94c9d77ebfc73f2e85abf28045f2f554694232ca..8d0abce9182b3958609c5afd4ee25fb6055e4d5f 100644 (file)
@@ -133,8 +133,26 @@ void BuildStatus::BuildEdgeFinished(Edge* edge,
     if (!success)
       printf("FAILED: %s\n", edge->EvaluateCommand().c_str());
 
-    if (!output.empty())
-      printf("%s", output.c_str());
+    // ninja sets stdout and stderr of subprocesses to a pipe, to be able to
+    // check if the output is empty. Some compilers, e.g. clang, check
+    // isatty(stderr) to decide if they should print colored output.
+    // To make it possible to use colored output with ninja, subprocesses should
+    // be run with a flag that forces them to always print color escape codes.
+    // To make sure these escape codes don't show up in a file if ninja's output
+    // is piped to a file, ninja strips ansi escape codes again if it's not
+    // writing to a |smart_terminal_|.
+    // (Launching subprocesses in pseudo ttys doesn't work because there are
+    // only a few hundred available on some systems, and ninja can launch
+    // thousands of parallel compile commands.)
+    // TODO: There should be a flag to disable escape code stripping.
+    string final_output;
+    if (!smart_terminal_)
+      final_output = StripAnsiEscapeCodes(output);
+    else
+      final_output = output;
+
+    if (!final_output.empty())
+      printf("%s", final_output.c_str());
   }
 }
 
index dbf75666aabf71a65bed25e4980c9b33210df323..5f5d8dc1a3f3f995bdd5b62d83b7fab4fbd6362a 100644 (file)
@@ -255,3 +255,31 @@ string GetLastErrorString() {
   return msg;
 }
 #endif
+
+static bool islatinalpha(int c) {
+  // isalpha() is locale-dependent.
+  return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+}
+
+string StripAnsiEscapeCodes(const string& in) {
+  string stripped;
+  stripped.reserve(in.size());
+
+  for (size_t i = 0; i < in.size(); ++i) {
+    if (in[i] != '\33') {
+      // Not an escape code.
+      stripped.push_back(in[i]);
+      continue;
+    }
+
+    // Only strip CSIs for now.
+    if (i + 1 >= in.size()) break;
+    if (in[i + 1] != '[') continue;  // Not a CSI.
+    i += 2;
+
+    // Skip everything up to and including the next [a-zA-Z].
+    while (i < in.size() && !islatinalpha(in[i]))
+      ++i;
+  }
+  return stripped;
+}
index 0ae2743679d60895486412bb04f2570ab159b596..711d476e5b460d0414f4f8b6180e0725870eea6d 100644 (file)
@@ -65,6 +65,9 @@ const char* SpellcheckStringV(const string& text, const vector<const char*>& wor
 /// Like SpellcheckStringV, but takes a NULL-terminated list.
 const char* SpellcheckString(const string& text, ...);
 
+/// Removes all Ansi escape codes (http://www.termsys.demon.co.uk/vtansi.htm).
+string StripAnsiEscapeCodes(const string& in);
+
 #ifdef _WIN32
 #define snprintf _snprintf
 #define fileno _fileno
index 8c3d023300532083b6c7da91e36b92e984c9c605..c44ab772a1623995a163b785df622f5527c6d80b 100644 (file)
@@ -73,3 +73,20 @@ TEST(CanonicalizePath, AbsolutePath) {
   EXPECT_TRUE(CanonicalizePath(&path, &err));
   EXPECT_EQ("/usr/include/stdio.h", path);
 }
+
+TEST(StripAnsiEscapeCodes, EscapeAtEnd) {
+  string stripped = StripAnsiEscapeCodes("foo\33");
+  EXPECT_EQ("foo", stripped);
+
+  stripped = StripAnsiEscapeCodes("foo\33[");
+  EXPECT_EQ("foo", stripped);
+}
+
+TEST(StripAnsiEscapeCodes, StripColors) {
+  // An actual clang warning.
+  string input = "\33[1maffixmgr.cxx:286:15: \33[0m\33[0;1;35mwarning: "
+                 "\33[0m\33[1musing the result... [-Wparentheses]\33[0m";
+  string stripped = StripAnsiEscapeCodes(input);
+  EXPECT_EQ("affixmgr.cxx:286:15: warning: using the result... [-Wparentheses]",
+            stripped);
+}