diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
index eacf15a203efdb..5f2e485aca5e43 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
@@ -39,6 +39,9 @@ internal static class ConsolePal
private static int s_windowHeight; // Cached WindowHeight, invalid when s_windowWidth == -1.
private static int s_invalidateCachedSettings = 1; // Tracks whether we should invalidate the cached settings.
+ /// Whether to output ansi color strings.
+ private static volatile int s_emitAnsiColorCodes = -1;
+
public static Stream OpenStandardInput()
{
return new UnixConsoleStream(SafeFileHandleHelper.Open(() => Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDIN_FILENO)), FileAccess.Read,
@@ -779,8 +782,10 @@ private static void WriteSetColorString(bool foreground, ConsoleColor color)
// Changing the color involves writing an ANSI character sequence out to the output stream.
// We only want to do this if we know that sequence will be interpreted by the output.
// rather than simply displayed visibly.
- if (Console.IsOutputRedirected)
+ if (!EmitAnsiColorCodes)
+ {
return;
+ }
// See if we've already cached a format string for this foreground/background
// and specific color choice. If we have, just output that format string again.
@@ -813,13 +818,52 @@ private static void WriteSetColorString(bool foreground, ConsoleColor color)
/// Writes out the ANSI string to reset colors.
private static void WriteResetColorString()
{
- // We only want to send the reset string if we're targeting a TTY device
- if (!Console.IsOutputRedirected)
+ if (EmitAnsiColorCodes)
{
WriteStdoutAnsiString(TerminalFormatStrings.Instance.Reset);
}
}
+ /// Get whether to emit ANSI color codes.
+ private static bool EmitAnsiColorCodes
+ {
+ get
+ {
+ // The flag starts at -1. If it's no longer -1, it's 0 or 1 to represent false or true.
+ int emitAnsiColorCodes = s_emitAnsiColorCodes;
+ if (emitAnsiColorCodes != -1)
+ {
+ return Convert.ToBoolean(emitAnsiColorCodes);
+ }
+
+ // We've not yet computed whether to emit codes or not. Do so now. We may race with
+ // other threads, and that's ok; this is idempotent unless someone is currently changing
+ // the value of the relevant environment variables, in which case behavior here is undefined.
+
+ // By default, we emit ANSI color codes if output isn't redirected, and suppress them if output is redirected.
+ bool enabled = !Console.IsOutputRedirected;
+
+ if (enabled)
+ {
+ // We subscribe to the informal standard from https://no-color.org/. If we'd otherwise emit
+ // ANSI color codes but the NO_COLOR environment variable is set, disable emitting them.
+ enabled = Environment.GetEnvironmentVariable("NO_COLOR") is null;
+ }
+ else
+ {
+ // We also support overriding in the other direction. If we'd otherwise avoid emitting color
+ // codes but the DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION environment variable is
+ // set to 1 or true, enable color.
+ string? envVar = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION");
+ enabled = envVar is not null && (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
+ }
+
+ // Store and return the computed answer.
+ s_emitAnsiColorCodes = Convert.ToInt32(enabled);
+ return enabled;
+ }
+ }
+
///
/// The values of the ConsoleColor enums unfortunately don't map to the
/// corresponding ANSI values. We need to do the mapping manually.
diff --git a/src/libraries/System.Console/tests/Color.cs b/src/libraries/System.Console/tests/Color.cs
index 567faad01ca4c4..55843aa2f37246 100644
--- a/src/libraries/System.Console/tests/Color.cs
+++ b/src/libraries/System.Console/tests/Color.cs
@@ -2,15 +2,18 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.IO;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Text;
-using Microsoft.DotNet.XUnitExtensions;
+using Microsoft.DotNet.RemoteExecutor;
using Xunit;
public class Color
{
+ private const char Esc = (char)0x1B;
+
[Fact]
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")]
public static void InvalidColors()
@@ -64,9 +67,58 @@ public static void RedirectedOutputDoesNotUseAnsiSequences()
Console.ResetColor();
Console.Write('4');
- const char Esc = (char)0x1B;
Assert.Equal(0, Encoding.UTF8.GetString(data.ToArray()).ToCharArray().Count(c => c == Esc));
Assert.Equal("1234", Encoding.UTF8.GetString(data.ToArray()));
});
}
+
+ public static bool TermIsSet => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TERM"));
+
+ [ConditionalTheory(nameof(TermIsSet))]
+ [PlatformSpecific(TestPlatforms.AnyUnix)]
+ [SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")]
+ [InlineData(null)]
+ [InlineData("1")]
+ [InlineData("true")]
+ [InlineData("tRuE")]
+ [InlineData("0")]
+ [InlineData("false")]
+ public static void RedirectedOutput_EnvVarSet_EmitsAnsiCodes(string envVar)
+ {
+ var psi = new ProcessStartInfo { RedirectStandardOutput = true };
+ psi.Environment["DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"] = envVar;
+
+ for (int i = 0; i < 3; i++)
+ {
+ Action main = i =>
+ {
+ Console.Write("SEPARATOR");
+ switch (i)
+ {
+ case "0":
+ Console.ForegroundColor = ConsoleColor.Blue;
+ break;
+
+ case "1":
+ Console.BackgroundColor = ConsoleColor.Red;
+ break;
+
+ case "2":
+ Console.ResetColor();
+ break;
+ }
+ Console.Write("SEPARATOR");
+ };
+
+ using RemoteInvokeHandle remote = RemoteExecutor.Invoke(main, i.ToString(CultureInfo.InvariantCulture), new RemoteInvokeOptions() { StartInfo = psi });
+
+ bool expectedEscapes = envVar is not null && (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
+
+ string stdout = remote.Process.StandardOutput.ReadToEnd();
+ string[] parts = stdout.Split("SEPARATOR");
+ Assert.Equal(3, parts.Length);
+
+ Assert.Equal(expectedEscapes, parts[1].Contains(Esc));
+ }
+ }
}