Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions src/libraries/Common/src/System/Console/ConsoleUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,46 @@ namespace System
{
internal static partial class ConsoleUtils
{
/// <summary>Whether to output ansi color strings.</summary>
/// <summary>Whether to output ANSI color strings.</summary>
private static volatile int s_emitAnsiColorCodes = -1;

/// <summary>Get whether to emit ANSI color codes.</summary>
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.
// DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is a legacy alias for the same behavior.
ReadOnlySpan<string> forceColorNames = ["FORCE_COLOR", "DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"];
foreach (string forceColorName in forceColorNames)
{
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(forceColorName)))
{
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
// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,23 @@ public ConsoleLoggerProvider(IOptionsMonitor<ConsoleLoggerOptions> 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)))
// FORCE_COLOR (per https://force-color.org/) always overrides other settings.
// DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is a legacy alias for the same behavior.
ReadOnlySpan<string> forceColorNames = ["FORCE_COLOR", "DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"];
foreach (string forceColorName in forceColorNames)
{
// ANSI color support forcibly enabled via environment variable. This logic matches the behaviour
// found in System.ConsoleUtils.EmitAnsiColorCodes.
return true;
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(forceColorName)))
{
return true;
}
}

// NO_COLOR disables ANSI color output unless explicitly overridden above.
if (Environment.GetEnvironmentVariable("NO_COLOR") is not null)
{
return false;
}

if (
#if NETFRAMEWORK
Environment.OSVersion.Platform != PlatformID.Win32NT
Expand Down
34 changes: 21 additions & 13 deletions src/libraries/System.Console/tests/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ public static void RedirectedOutputDoesNotUseAnsiSequences()
Console.ResetColor();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests for System.Console. What about for M.E.Logging.Console?

Also, these tests are only validating a single env var. Can we have a few tests that validate when multiple env vars are set and thus their precedence?

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);
});
}

Expand All @@ -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++)
{
Expand All @@ -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));
}
}
}
Loading