diff --git a/src/libraries/Common/src/System/Console/ConsoleUtils.cs b/src/libraries/Common/src/System/Console/ConsoleUtils.cs
index f8472390d1f858..7da5e30a088205 100644
--- a/src/libraries/Common/src/System/Console/ConsoleUtils.cs
+++ b/src/libraries/Common/src/System/Console/ConsoleUtils.cs
@@ -5,7 +5,7 @@ namespace System
{
internal static partial class ConsoleUtils
{
- /// Whether to output ansi color strings.
+ /// Whether to output ANSI color strings.
private static volatile int s_emitAnsiColorCodes = -1;
/// Get whether to emit ANSI color codes.
@@ -13,34 +13,35 @@ public 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.
+ // 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
+ // We've not yet computed whether to emit codes or not. 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.
+ // Per https://force-color.org/, FORCE_COLOR forces ANSI color output when set to a non-empty value.
+ // DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is treated as a legacy alias for the same behavior.
+ // These take highest priority and override all other checks.
+ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FORCE_COLOR")) ||
+ !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION")))
+ {
+ s_emitAnsiColorCodes = 1;
+ return true;
+ }
+
// 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.
+ // Per https://no-color.org/, NO_COLOR disables ANSI color output when set.
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);
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs
index 68a356de9ba748..f05da9785bd457 100644
--- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs
@@ -68,13 +68,20 @@ public ConsoleLoggerProvider(IOptionsMonitor options, IEnu
[UnsupportedOSPlatformGuard("windows")]
private static bool DoesConsoleSupportAnsi()
{
- string? envVar = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION");
- if (envVar is not null && (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase)))
+ // Per https://force-color.org/, FORCE_COLOR forces ANSI color output when set to a non-empty value.
+ // DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is treated as a legacy alias for the same behavior.
+ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FORCE_COLOR")) ||
+ !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION")))
{
- // ANSI color support forcibly enabled via environment variable. This logic matches the behaviour
- // found in System.ConsoleUtils.EmitAnsiColorCodes.
return true;
}
+
+ // Per https://no-color.org/, NO_COLOR disables ANSI color output when set.
+ if (Environment.GetEnvironmentVariable("NO_COLOR") is not null)
+ {
+ return false;
+ }
+
if (
#if NETFRAMEWORK
Environment.OSVersion.Platform != PlatformID.Win32NT
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs
index 796604d60a53c6..c7b1d4cc1bcf0c 100644
--- a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs
@@ -445,6 +445,43 @@ public void AddConsole_IsOutputRedirected_ColorDisabled()
}, new RemoteInvokeOptions { StartInfo = new ProcessStartInfo() { RedirectStandardOutput = true } }).Dispose();
}
+ [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
+ [InlineData("FORCE_COLOR", "1", null, null, "AnsiLogConsole")]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "1", null, null, "AnsiLogConsole")]
+ [InlineData(null, null, "NO_COLOR", "1", "AnsiParsingLogConsole")]
+ [InlineData("FORCE_COLOR", "1", "NO_COLOR", "1", "AnsiLogConsole")]
+ public void DoesConsoleSupportAnsi_RespectsColorEnvVars(
+ string? envVarName1, string? envVarValue1,
+ string? envVarName2, string? envVarValue2,
+ string expectedConsoleTypeName)
+ {
+ var psi = new ProcessStartInfo { RedirectStandardOutput = true };
+ if (envVarName1 is not null)
+ {
+ psi.Environment[envVarName1] = envVarValue1;
+ }
+ if (envVarName2 is not null)
+ {
+ psi.Environment[envVarName2] = envVarValue2;
+ }
+
+ RemoteExecutor.Invoke(static (expectedTypeName) =>
+ {
+ var loggerProvider = new ServiceCollection()
+ .AddLogging(builder => builder.AddConsole())
+ .BuildServiceProvider()
+ .GetRequiredService();
+
+ var consoleLoggerProvider = Assert.IsType(loggerProvider);
+
+ var messageQueueField = typeof(ConsoleLoggerProvider).GetField("_messageQueue", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ var processor = (ConsoleLoggerProcessor)messageQueueField!.GetValue(consoleLoggerProvider)!;
+ Assert.Equal(expectedTypeName, processor.Console.GetType().Name);
+
+ loggerProvider.Dispose();
+ }, expectedConsoleTypeName, new RemoteInvokeOptions { StartInfo = psi }).Dispose();
+ }
+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
public void WriteDebug_LogsCorrectColors()
{
diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
index 57d4ef11fd62e3..0aef24b28da7c7 100644
--- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
+++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs
@@ -1112,9 +1112,9 @@ private static void InvalidateTerminalSettings()
Volatile.Write(ref s_invalidateCachedSettings, 1);
}
- // Ansi colors are enabled when stdout is a terminal or when
- // DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is set.
- // In both cases, they are written to stdout.
+ // Ansi colors are enabled when stdout is a terminal, when
+ // FORCE_COLOR is set, or when DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is set.
+ // In all cases, they are written to stdout.
internal static void WriteTerminalAnsiColorString(string? value)
=> WriteTerminalAnsiString(value, OpenStandardOutputHandle(), mayChangeCursorPosition: false);
diff --git a/src/libraries/System.Console/tests/Color.cs b/src/libraries/System.Console/tests/Color.cs
index 3e56d816b16a0c..520ebadde8366c 100644
--- a/src/libraries/System.Console/tests/Color.cs
+++ b/src/libraries/System.Console/tests/Color.cs
@@ -51,25 +51,46 @@ public static void BackgroundColor_Throws_PlatformNotSupportedException()
Assert.Throws(() => Console.BackgroundColor = ConsoleColor.Red);
}
- [Fact]
+ [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")]
public static void RedirectedOutputDoesNotUseAnsiSequences()
{
- // Make sure that redirecting to a memory stream causes Console not to write out the ANSI sequences
-
- Helpers.RunInRedirectedOutput((data) =>
+ // Run in a child process with redirected stdout so that no in-process
+ // test framework output (e.g. xunit skip messages) can pollute the stream.
+ var startInfo = new ProcessStartInfo { RedirectStandardOutput = true };
+ using RemoteInvokeHandle handle = RemoteExecutor.Invoke(static () =>
{
- Console.Write('1');
+ Console.Error.WriteLine($"IsOutputRedirected: {Console.IsOutputRedirected}");
+ Console.Error.WriteLine($"TERM: {Environment.GetEnvironmentVariable("TERM")}");
+ Console.Write("1");
Console.ForegroundColor = ConsoleColor.Blue;
- Console.Write('2');
+ Console.Write("2");
Console.BackgroundColor = ConsoleColor.Red;
- Console.Write('3');
+ Console.Write("3");
Console.ResetColor();
- Console.Write('4');
-
- Assert.Equal(0, Encoding.UTF8.GetString(data.ToArray()).ToCharArray().Count(c => c == Esc));
- Assert.Equal("1234", Encoding.UTF8.GetString(data.ToArray()));
- });
+ Console.Write("4");
+ Console.Error.WriteLine($"Done writing");
+ }, new RemoteInvokeOptions { StartInfo = startInfo });
+
+ string capturedOutput = handle.Process.StandardOutput.ReadToEnd();
+ byte[] rawBytes = System.Text.Encoding.UTF8.GetBytes(capturedOutput);
+ Console.Error.WriteLine($"Output length: {rawBytes.Length}");
+ Console.Error.WriteLine($"Output hex: {BitConverter.ToString(rawBytes)}");
+ Console.Error.Write("Output chars: ");
+ foreach (char c in capturedOutput)
+ {
+ if (c >= 32 && c < 127)
+ Console.Error.Write(c);
+ else
+ Console.Error.Write($"[0x{(int)c:X2}]");
+ }
+ Console.Error.WriteLine();
+ Console.Error.WriteLine($"capturedOutput[0] = 0x{(int)capturedOutput[0]:X2}");
+ Console.Error.WriteLine($"capturedOutput == \"1234\": {capturedOutput == "1234"}");
+ Console.Error.WriteLine($"Contains ESC: {capturedOutput.Contains(Esc)}");
+ Console.Error.WriteLine($"Esc char value: 0x{(int)Esc:X2}");
+ Assert.DoesNotContain(Esc.ToString(), capturedOutput);
+ Assert.Equal("1234", capturedOutput);
}
public static bool TermIsSetAndRemoteExecutorIsSupported
@@ -78,16 +99,35 @@ public static bool TermIsSetAndRemoteExecutorIsSupported
[ConditionalTheory(typeof(Color), nameof(TermIsSetAndRemoteExecutorIsSupported))]
[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)
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "1", null, null, true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "true", null, null, true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "tRuE", null, null, true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "0", null, null, true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "any-value", null, null, true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "", null, null, false)]
+ [InlineData(null, null, "FORCE_COLOR", "1", true)]
+ [InlineData(null, null, "FORCE_COLOR", "true", true)]
+ [InlineData(null, null, "FORCE_COLOR", "any-value", true)]
+ [InlineData(null, null, "FORCE_COLOR", "", false)]
+ [InlineData(null, null, "NO_COLOR", "1", false)]
+ [InlineData(null, null, "NO_COLOR", "true", false)]
+ [InlineData(null, null, "NO_COLOR", "any-value", false)]
+ [InlineData("FORCE_COLOR", "1", "NO_COLOR", "1", true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "1", "NO_COLOR", "1", true)]
+ public static void RedirectedOutput_ColorEnvVars_RespectColorPreference(
+ string? envVarName1, string? envVarValue1,
+ string? envVarName2, string? envVarValue2,
+ bool shouldEmitEscapes)
{
var psi = new ProcessStartInfo { RedirectStandardOutput = true };
- psi.Environment["DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"] = envVar;
+ if (envVarName1 is not null)
+ {
+ psi.Environment[envVarName1] = envVarValue1;
+ }
+ if (envVarName2 is not null)
+ {
+ psi.Environment[envVarName2] = envVarValue2;
+ }
for (int i = 0; i < 3; i++)
{
@@ -113,13 +153,11 @@ public static void RedirectedOutput_EnvVarSet_EmitsAnsiCodes(string? envVar)
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));
+ Assert.Equal(shouldEmitEscapes, parts[1].Contains(Esc));
}
}
}