Skip to content
Open
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
27 changes: 14 additions & 13 deletions src/libraries/Common/src/System/Console/ConsoleUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,43 @@ 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.

// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,20 @@ 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)))
// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ILoggerProvider>();

var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(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()
{
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/System.Console/src/System/ConsolePal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
84 changes: 61 additions & 23 deletions src/libraries/System.Console/tests/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,46 @@ public static void BackgroundColor_Throws_PlatformNotSupportedException()
Assert.Throws<PlatformNotSupportedException>(() => 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
Expand All @@ -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++)
{
Expand All @@ -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));
}
}
}
Loading