| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "gn/standard_out.h" |
| |
| #include <stddef.h> |
| |
| #include <mutex> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/logging.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "gn/switches.h" |
| #include "util/build_config.h" |
| |
| #if defined(OS_WIN) |
| #include <windows.h> |
| #else |
| #include <stdio.h> |
| #include <unistd.h> |
| #endif |
| |
| namespace { |
| |
| bool initialized = false; |
| |
| #if defined(OS_WIN) |
| HANDLE hstdout; |
| WORD default_attributes; |
| #endif |
| bool is_console = false; |
| |
| bool is_markdown = false; |
| |
| // True while output is going into a markdown ```...``` code block. |
| bool in_body = false; |
| |
| void EnsureInitialized() { |
| if (initialized) |
| return; |
| initialized = true; |
| |
| const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess(); |
| if (cmdline->HasSwitch(switches::kMarkdown)) { |
| // Output help in Markdown's syntax, not color-highlighted. |
| is_markdown = true; |
| } |
| |
| if (cmdline->HasSwitch(switches::kNoColor)) { |
| // Force color off. |
| is_console = false; |
| return; |
| } |
| |
| #if defined(OS_WIN) |
| // On Windows, we can't force the color on. If the output handle isn't a |
| // console, there's nothing we can do about it. |
| hstdout = ::GetStdHandle(STD_OUTPUT_HANDLE); |
| CONSOLE_SCREEN_BUFFER_INFO info; |
| is_console = !!::GetConsoleScreenBufferInfo(hstdout, &info); |
| default_attributes = info.wAttributes; |
| #else |
| if (cmdline->HasSwitch(switches::kColor)) |
| is_console = true; |
| else |
| is_console = isatty(fileno(stdout)); |
| #endif |
| } |
| |
| #if !defined(OS_WIN) |
| void WriteToStdOut(const std::string& output) { |
| size_t written_bytes = fwrite(output.data(), 1, output.size(), stdout); |
| DCHECK_EQ(output.size(), written_bytes); |
| } |
| #endif // !defined(OS_WIN) |
| |
| void OutputMarkdownDec(TextDecoration dec) { |
| // The markdown rendering turns "dim" text to italics and any |
| // other colored text to bold. |
| |
| #if defined(OS_WIN) |
| DWORD written = 0; |
| if (dec == DECORATION_DIM) |
| ::WriteFile(hstdout, "*", 1, &written, nullptr); |
| else if (dec != DECORATION_NONE) |
| ::WriteFile(hstdout, "**", 2, &written, nullptr); |
| #else |
| if (dec == DECORATION_DIM) |
| WriteToStdOut("*"); |
| else if (dec != DECORATION_NONE) |
| WriteToStdOut("**"); |
| #endif |
| } |
| |
| #if defined(OS_WIN) |
| |
| void WriteOutputString(std::string_view output, |
| TextDecoration dec, |
| HtmlEscaping escaping) { |
| EnsureInitialized(); |
| DWORD written = 0; |
| |
| if (is_markdown) { |
| OutputMarkdownDec(dec); |
| } else if (is_console) { |
| switch (dec) { |
| case DECORATION_NONE: |
| break; |
| case DECORATION_DIM: |
| ::SetConsoleTextAttribute(hstdout, FOREGROUND_INTENSITY); |
| break; |
| case DECORATION_RED: |
| ::SetConsoleTextAttribute(hstdout, |
| FOREGROUND_RED | FOREGROUND_INTENSITY); |
| break; |
| case DECORATION_GREEN: |
| // Keep green non-bold. |
| ::SetConsoleTextAttribute(hstdout, FOREGROUND_GREEN); |
| break; |
| case DECORATION_BLUE: |
| ::SetConsoleTextAttribute(hstdout, |
| FOREGROUND_BLUE | FOREGROUND_INTENSITY); |
| break; |
| case DECORATION_YELLOW: |
| ::SetConsoleTextAttribute(hstdout, FOREGROUND_RED | FOREGROUND_GREEN); |
| break; |
| case DECORATION_MAGENTA: |
| ::SetConsoleTextAttribute( |
| hstdout, FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY); |
| break; |
| } |
| } |
| |
| std::string tmpstr = std::string(output); |
| if (is_markdown && dec == DECORATION_YELLOW) { |
| // https://code.google.com/p/gitiles/issues/detail?id=77 |
| // Gitiles will replace "--" with an em dash in non-code text. |
| // Figuring out all instances of this might be difficult, but we can |
| // at least escape the instances where this shows up in a heading. |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "--", "\\--"); |
| } |
| if (is_markdown && !in_body && escaping == DEFAULT_ESCAPING) { |
| // Markdown auto-escapes < and > in code sections (and converts < to |
| // &tl; there), but not elsewhere. |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "<", "<"); |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, ">", ">"); |
| } |
| ::WriteFile(hstdout, tmpstr.c_str(), static_cast<DWORD>(tmpstr.size()), |
| &written, nullptr); |
| |
| if (is_markdown) { |
| OutputMarkdownDec(dec); |
| } else if (is_console) { |
| ::SetConsoleTextAttribute(hstdout, default_attributes); |
| } |
| } |
| |
| #else |
| |
| void WriteOutputString(std::string_view output, |
| TextDecoration dec, |
| HtmlEscaping escaping) { |
| EnsureInitialized(); |
| if (is_markdown) { |
| OutputMarkdownDec(dec); |
| } else if (is_console) { |
| switch (dec) { |
| case DECORATION_NONE: |
| break; |
| case DECORATION_DIM: |
| WriteToStdOut("\e[2m"); |
| break; |
| case DECORATION_RED: |
| WriteToStdOut("\e[31m\e[1m"); |
| break; |
| case DECORATION_GREEN: |
| WriteToStdOut("\e[32m"); |
| break; |
| case DECORATION_BLUE: |
| WriteToStdOut("\e[34m\e[1m"); |
| break; |
| case DECORATION_YELLOW: |
| WriteToStdOut("\e[33m"); |
| break; |
| case DECORATION_MAGENTA: |
| WriteToStdOut("\e[35m\e[1m"); |
| break; |
| } |
| } |
| |
| std::string tmpstr = std::string(output); |
| if (is_markdown && dec == DECORATION_YELLOW) { |
| // https://code.google.com/p/gitiles/issues/detail?id=77 |
| // Gitiles will replace "--" with an em dash in non-code text. |
| // Figuring out all instances of this might be difficult, but we can |
| // at least escape the instances where this shows up in a heading. |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "--", "\\--"); |
| } |
| if (is_markdown && !in_body && escaping == DEFAULT_ESCAPING) { |
| // Markdown auto-escapes < and > in code sections (and converts < to |
| // &tl; there), but not elsewhere. |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "<", "<"); |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, ">", ">"); |
| } |
| WriteToStdOut(tmpstr.data()); |
| |
| if (is_markdown) { |
| OutputMarkdownDec(dec); |
| } else if (is_console && dec != DECORATION_NONE) { |
| WriteToStdOut("\e[0m"); |
| } |
| } |
| |
| #endif |
| |
| // Collects buffered log output for quiet mode. |
| class QuietModeBuffer { |
| public: |
| void Append(std::string_view output, |
| TextDecoration decoration, |
| HtmlEscaping escaping) { |
| { |
| std::lock_guard<std::mutex> lock(lock_); |
| if (!is_flushed_) { |
| output_buffer_.push_back({std::string(output), decoration, escaping}); |
| return; |
| } |
| // Otherwise fall-through to printing to stdout outside the lock. |
| } |
| WriteOutputString(output, decoration, escaping); |
| } |
| |
| // Outputs any queued output to the standard out. |
| void Flush() { |
| // Do everything inside the lock so we guarantee we flush all previous |
| // output before writing something else from another thread. Flushing will |
| // only happen in error cases so this will not be performance-critical. |
| std::lock_guard<std::mutex> lock(lock_); |
| |
| if (is_flushed_) |
| return; |
| is_flushed_ = true; |
| |
| for (const BufferedOutput& output : output_buffer_) { |
| WriteOutputString(output.output, output.decoration, output.escaping); |
| } |
| output_buffer_.clear(); |
| } |
| |
| private: |
| struct BufferedOutput { |
| std::string output; |
| TextDecoration decoration; |
| HtmlEscaping escaping; |
| }; |
| |
| std::mutex lock_; |
| |
| // Set when we're in quiet mode but then flush the output. This means that |
| // future output should go directly to the terminal (this solves the problem |
| // of safely turning "off" quiet mode when we hit an error because |
| // quiet_mode_buffer isn't threadsafe). |
| bool is_flushed_ = false; |
| |
| std::vector<BufferedOutput> output_buffer_; |
| }; |
| |
| // Non-null while buffering standard output. Deliberately leaked on shutdown. |
| QuietModeBuffer* quiet_mode_buffer = nullptr; |
| |
| } // namespace |
| |
| bool IsColorEnabled() { |
| EnsureInitialized(); |
| return is_console; |
| } |
| |
| void OutputString(std::string_view output, |
| TextDecoration dec, |
| HtmlEscaping escaping) { |
| WriteOutputString(output, dec, escaping); |
| } |
| |
| void OutputLogString(std::string_view output, |
| TextDecoration dec, |
| HtmlEscaping escaping) { |
| if (quiet_mode_buffer) { |
| quiet_mode_buffer->Append(output, dec, escaping); |
| return; |
| } |
| WriteOutputString(output, dec, escaping); |
| } |
| |
| void BufferLogOutput() { |
| if (quiet_mode_buffer) { |
| DCHECK(false); // Expecting to only be called once. |
| return; |
| } |
| quiet_mode_buffer = new QuietModeBuffer(); |
| } |
| |
| void FlushBufferedOutput() { |
| if (quiet_mode_buffer) { |
| quiet_mode_buffer->Flush(); |
| } |
| } |
| |
| void PrintSectionHelp(const std::string& line, |
| const std::string& topic, |
| const std::string& tag) { |
| EnsureInitialized(); |
| |
| if (is_markdown) { |
| OutputString("* [" + line + "](#" + tag + ")\n"); |
| } else if (topic.size()) { |
| OutputString("\n" + line + " (type \"gn help " + topic + |
| "\" for more help):\n"); |
| } else { |
| OutputString("\n" + line + ":\n"); |
| } |
| } |
| |
| void PrintShortHelp(const std::string& line, const std::string& link_tag) { |
| EnsureInitialized(); |
| |
| if (is_markdown) { |
| if (link_tag.empty()) |
| OutputString(" * " + line + "\n"); |
| else |
| OutputString(" * [" + line + "](#" + link_tag + ")\n"); |
| return; |
| } |
| |
| size_t colon_offset = line.find(':'); |
| size_t first_normal = 0; |
| if (colon_offset != std::string::npos) { |
| OutputString(" " + line.substr(0, colon_offset), DECORATION_YELLOW); |
| first_normal = colon_offset; |
| } |
| |
| // See if the colon is followed by a " [" and if so, dim the contents of [ ]. |
| if (first_normal > 0 && line.size() > first_normal + 2 && |
| line[first_normal + 1] == ' ' && line[first_normal + 2] == '[') { |
| size_t begin_bracket = first_normal + 2; |
| OutputString(": "); |
| first_normal = line.find(']', begin_bracket); |
| if (first_normal == std::string::npos) |
| first_normal = line.size(); |
| else |
| first_normal++; |
| OutputString(line.substr(begin_bracket, first_normal - begin_bracket), |
| DECORATION_DIM); |
| } |
| |
| OutputString(line.substr(first_normal) + "\n"); |
| } |
| |
| void PrintLongHelp(const std::string& text, const std::string& tag) { |
| EnsureInitialized(); |
| |
| bool first_header = true; |
| in_body = false; |
| std::size_t empty_lines = 0; |
| for (const std::string& line : base::SplitString( |
| text, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) { |
| // Check for a heading line. |
| if (!line.empty() && line[0] != ' ') { |
| // New paragraph, just skip any trailing empty lines. |
| empty_lines = 0; |
| |
| if (is_markdown) { |
| // GN's block-level formatting is converted to markdown as follows: |
| // * The first heading is treated as an H3. |
| // * Subsequent heading are treated as H4s. |
| // * Any other text is wrapped in a code block and displayed as-is. |
| // |
| // Span-level formatting (the decorations) is converted inside |
| // OutputString(). |
| if (in_body) { |
| OutputString("```\n\n", DECORATION_NONE); |
| in_body = false; |
| } |
| |
| if (first_header && !tag.empty()) { |
| OutputString("### <a name=\"" + tag + "\"></a>", DECORATION_NONE, |
| NO_ESCAPING); |
| } else { |
| OutputString("#### ", DECORATION_NONE); |
| } |
| } |
| |
| // Highlight up to the colon (if any). |
| size_t chars_to_highlight = line.find(':'); |
| if (chars_to_highlight == std::string::npos) |
| chars_to_highlight = line.size(); |
| |
| OutputString(line.substr(0, chars_to_highlight), DECORATION_YELLOW); |
| OutputString(line.substr(chars_to_highlight)); |
| if (first_header) { |
| if (is_markdown) { |
| OutputString(" [Back to Top](#gn-reference)", DECORATION_NONE); |
| } |
| first_header = false; |
| } |
| OutputString("\n"); |
| continue; |
| } else if (is_markdown && !line.empty() && !in_body) { |
| OutputString("```\n", DECORATION_NONE); |
| in_body = true; |
| } |
| |
| // We buffer empty lines, so we can skip them if needed |
| // (i.e. new paragraph body, end of final paragraph body). |
| if (in_body && is_markdown) { |
| if (!line.empty() && empty_lines != 0) { |
| OutputString(std::string(empty_lines, '\n')); |
| empty_lines = 0; |
| } else if (line.empty()) { |
| ++empty_lines; |
| continue; |
| } |
| } |
| |
| // Check for a comment. |
| TextDecoration dec = DECORATION_NONE; |
| for (const auto& elem : line) { |
| if (elem == '#' && !is_markdown) { |
| // Got a comment, draw dimmed. |
| dec = DECORATION_DIM; |
| break; |
| } else if (elem != ' ') { |
| break; |
| } |
| } |
| |
| OutputString(line + "\n", dec); |
| } |
| |
| if (is_markdown && in_body) |
| OutputString("```\n"); |
| } |