diff --git a/src/libraries/Common/src/System/Console/ConsoleUtils.cs b/src/libraries/Common/src/System/Console/ConsoleUtils.cs
index f8472390d1f858..059ccbdc9e3d79 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,29 +13,37 @@ 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.
+ // FORCE_COLOR (per https://force-color.org/) always overrides other settings
+ string? forceColor = Environment.GetEnvironmentVariable("FORCE_COLOR");
+ if (!string.IsNullOrEmpty(forceColor))
+ {
+ 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
+ // 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
+ // 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");
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs
index 68a356de9ba748..b2e290751b7dc4 100644
--- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs
+++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs
@@ -68,6 +68,19 @@ public ConsoleLoggerProvider(IOptionsMonitor options, IEnu
[UnsupportedOSPlatformGuard("windows")]
private static bool DoesConsoleSupportAnsi()
{
+ // Check FORCE_COLOR first (per https://force-color.org/)
+ string? forceColor = Environment.GetEnvironmentVariable("FORCE_COLOR");
+ if (!string.IsNullOrEmpty(forceColor))
+ {
+ return true;
+ }
+
+ // Then check NO_COLOR
+ if (Environment.GetEnvironmentVariable("NO_COLOR") is not null)
+ {
+ return false;
+ }
+
string? envVar = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION");
if (envVar is not null && (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase)))
{
diff --git a/src/libraries/System.Console/tests/Color.cs b/src/libraries/System.Console/tests/Color.cs
index 0c43c50c03b424..654d6e45baa2d1 100644
--- a/src/libraries/System.Console/tests/Color.cs
+++ b/src/libraries/System.Console/tests/Color.cs
@@ -67,8 +67,9 @@ public static void RedirectedOutputDoesNotUseAnsiSequences()
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()));
+ string output = Encoding.UTF8.GetString(data.ToArray());
+ Assert.Equal(0, output.ToCharArray().Count(c => c == Esc));
+ Assert.Contains("1234", output);
});
}
@@ -78,16 +79,25 @@ public static bool TermIsSetAndRemoteExecutorIsSupported
[ConditionalTheory(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", null, false)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "1", true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "true", true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "tRuE", true)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "0", false)]
+ [InlineData("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "false", false)]
+ [InlineData("FORCE_COLOR", "1", true)]
+ [InlineData("FORCE_COLOR", "true", true)]
+ [InlineData("FORCE_COLOR", "any-value", true)]
+ [InlineData("NO_COLOR", "1", false)]
+ [InlineData("NO_COLOR", "true", false)]
+ [InlineData("NO_COLOR", "any-value", false)]
+ public static void RedirectedOutput_EnvironmentVariableSet_RespectColorPreference(string envVarName, string? envVarValue, bool shouldEmitEscapes)
{
var psi = new ProcessStartInfo { RedirectStandardOutput = true };
- psi.Environment["DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"] = envVar;
+ if (envVarValue is not null)
+ {
+ psi.Environment[envVarName] = envVarValue;
+ }
for (int i = 0; i < 3; i++)
{
@@ -113,13 +123,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));
}
}
}