| // 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: | |
| using BufferedOutput = ScopedBufferedOutput::Item; | |
| 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(); | |
| } | |
| // Take a snapshot of the current buffers output items. | |
| std::vector<BufferedOutput> GetItems() { | |
| std::lock_guard<std::mutex> lock(lock_); | |
| return output_buffer_; | |
| } | |
| private: | |
| 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; | |
| // Non-null while ScopedBufferedOutput singleton exists. | |
| QuietModeBuffer* scoped_output_buffer = nullptr; | |
| } // namespace | |
| bool IsColorEnabled() { | |
| EnsureInitialized(); | |
| return is_console; | |
| } | |
| void OutputString(std::string_view output, | |
| TextDecoration dec, | |
| HtmlEscaping escaping) { | |
| if (scoped_output_buffer) { | |
| scoped_output_buffer->Append(output, dec, escaping); | |
| return; | |
| } | |
| WriteOutputString(output, dec, escaping); | |
| } | |
| void OutputLogString(std::string_view output, | |
| TextDecoration dec, | |
| HtmlEscaping escaping) { | |
| if (scoped_output_buffer) { | |
| scoped_output_buffer->Append(output, dec, escaping); | |
| return; | |
| } | |
| 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 FlushBufferedLogOutput() { | |
| if (quiet_mode_buffer) { | |
| quiet_mode_buffer->Flush(); | |
| } | |
| } | |
| ScopedBufferedOutput::ScopedBufferedOutput() { | |
| DCHECK(!scoped_output_buffer) | |
| << "Only one ScopedBufferedOutput instance can exist"; | |
| scoped_output_buffer = new QuietModeBuffer(); | |
| } | |
| ScopedBufferedOutput::~ScopedBufferedOutput() { | |
| DCHECK(scoped_output_buffer); | |
| delete scoped_output_buffer; | |
| scoped_output_buffer = nullptr; | |
| } | |
| std::vector<ScopedBufferedOutput::Item> ScopedBufferedOutput::GetItems() { | |
| DCHECK(scoped_output_buffer); | |
| return scoped_output_buffer->GetItems(); | |
| } | |
| 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"); | |
| } |