Skip to content

Commit 5fb9588

Browse files
committed
Fix ANSI color env var handling
1 parent bb2b3ef commit 5fb9588

File tree

2 files changed

+98
-22
lines changed

2 files changed

+98
-22
lines changed

src/libraries/System.Console/src/System/ConsolePal.Unix.cs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ internal static class ConsolePal
3939
private static int s_windowHeight; // Cached WindowHeight, invalid when s_windowWidth == -1.
4040
private static int s_invalidateCachedSettings = 1; // Tracks whether we should invalidate the cached settings.
4141

42+
/// <summary>Whether to output ansi color strings.</summary>
43+
private static volatile int s_emitAnsiColorCodes = -1;
44+
4245
public static Stream OpenStandardInput()
4346
{
4447
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)
779782
// Changing the color involves writing an ANSI character sequence out to the output stream.
780783
// We only want to do this if we know that sequence will be interpreted by the output.
781784
// rather than simply displayed visibly.
782-
if (!SupportsAnsiColor())
785+
if (!EmitAnsiColorCodes)
786+
{
783787
return;
788+
}
784789

785790
// See if we've already cached a format string for this foreground/background
786791
// and specific color choice. If we have, just output that format string again.
@@ -813,31 +818,50 @@ private static void WriteSetColorString(bool foreground, ConsoleColor color)
813818
/// <summary>Writes out the ANSI string to reset colors.</summary>
814819
private static void WriteResetColorString()
815820
{
816-
if (SupportsAnsiColor())
821+
if (EmitAnsiColorCodes)
817822
{
818823
WriteStdoutAnsiString(TerminalFormatStrings.Instance.Reset);
819824
}
820825
}
821826

822-
/// <summary>
823-
/// Tests whether ANSI color codes should be emitted or not.
824-
/// <para>
825-
/// If the <c>DOTNET_CONSOLE_ANSI_COLOR</c> environment variable contains <c>true</c> (case insensitive) or <c>1</c>
826-
/// then ANSI color codes are supported. If the <c>DOTNET_CONSOLE_ANSI_COLOR</c> environment variable is not defined
827-
/// or contains a non truthy value, then ANSI color codes are supported if the console output is not redirected.
828-
/// </para>
829-
/// </summary>
830-
/// <returns><c>true</c> if ANSI color escape codes must be emitted, <c>false</c> if they must not be emitted.</returns>
831-
/// <remarks>This was discussed in https://github.com/dotnet/runtime/issues/33980</remarks>
832-
private static bool SupportsAnsiColor()
827+
/// <summary>Get whether to emit ANSI color codes.</summary>
828+
private static bool EmitAnsiColorCodes
833829
{
834-
string? consoleAnsiColor = Environment.GetEnvironmentVariable("DOTNET_CONSOLE_ANSI_COLOR");
835-
if (consoleAnsiColor != null)
830+
get
836831
{
837-
return consoleAnsiColor == "1" || (bool.TryParse(consoleAnsiColor, out bool enabled) && enabled);
838-
}
832+
// The flag starts at -1. If it's no longer -1, it's 0 or 1 to represent false or true.
833+
int emitAnsiColorCodes = s_emitAnsiColorCodes;
834+
if (emitAnsiColorCodes != -1)
835+
{
836+
return Convert.ToBoolean(emitAnsiColorCodes);
837+
}
839838

840-
return !Console.IsOutputRedirected;
839+
// We've not yet computed whether to emit codes or not. Do so now. We may race with
840+
// other threads, and that's ok; this is idempotent unless someone is currently changing
841+
// the value of the relevant environment variables, in which case behavior here is undefined.
842+
843+
// By default, we emit ANSI color codes if output isn't redirected, and suppress them if output is redirected.
844+
bool enabled = !Console.IsOutputRedirected;
845+
846+
if (enabled)
847+
{
848+
// We subscribe to the informal standard from https://no-color.org/. If we'd otherwise emit
849+
// ANSI color codes but the NO_COLOR environment variable is set, disable emitting them.
850+
enabled = Environment.GetEnvironmentVariable("NO_COLOR") is null;
851+
}
852+
else
853+
{
854+
// We also support overriding in the other direction. If we'd otherwise avoid emitting color
855+
// codes but the DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION environment variable is
856+
// set to 1 or true, enable color.
857+
string? envVar = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION");
858+
enabled = envVar is not null && (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
859+
}
860+
861+
// Store and return the computed answer.
862+
s_emitAnsiColorCodes = Convert.ToInt32(enabled);
863+
return enabled;
864+
}
841865
}
842866

843867
/// <summary>

src/libraries/System.Console/tests/Color.cs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5-
using System.IO;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Globalization;
68
using System.Linq;
7-
using System.Runtime.InteropServices;
89
using System.Text;
9-
using Microsoft.DotNet.XUnitExtensions;
10+
using Microsoft.DotNet.RemoteExecutor;
1011
using Xunit;
1112

1213
public class Color
1314
{
15+
private const char Esc = (char)0x1B;
16+
1417
[Fact]
1518
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")]
1619
public static void InvalidColors()
@@ -64,9 +67,58 @@ public static void RedirectedOutputDoesNotUseAnsiSequences()
6467
Console.ResetColor();
6568
Console.Write('4');
6669

67-
const char Esc = (char)0x1B;
6870
Assert.Equal(0, Encoding.UTF8.GetString(data.ToArray()).ToCharArray().Count(c => c == Esc));
6971
Assert.Equal("1234", Encoding.UTF8.GetString(data.ToArray()));
7072
});
7173
}
74+
75+
public static bool TermIsSet => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TERM"));
76+
77+
[ConditionalTheory(nameof(TermIsSet))]
78+
[PlatformSpecific(TestPlatforms.AnyUnix)]
79+
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")]
80+
[InlineData(null)]
81+
[InlineData("1")]
82+
[InlineData("true")]
83+
[InlineData("tRuE")]
84+
[InlineData("0")]
85+
[InlineData("false")]
86+
public static void RedirectedOutput_EnvVarSet_EmitsAnsiCodes(string envVar)
87+
{
88+
var psi = new ProcessStartInfo { RedirectStandardOutput = true };
89+
psi.Environment["DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"] = envVar;
90+
91+
for (int i = 0; i < 3; i++)
92+
{
93+
Action<string> main = i =>
94+
{
95+
Console.Write("SEPARATOR");
96+
switch (i)
97+
{
98+
case "0":
99+
Console.ForegroundColor = ConsoleColor.Blue;
100+
break;
101+
102+
case "1":
103+
Console.BackgroundColor = ConsoleColor.Red;
104+
break;
105+
106+
case "2":
107+
Console.ResetColor();
108+
break;
109+
}
110+
Console.Write("SEPARATOR");
111+
};
112+
113+
using RemoteInvokeHandle remote = RemoteExecutor.Invoke(main, i.ToString(CultureInfo.InvariantCulture), new RemoteInvokeOptions() { StartInfo = psi });
114+
115+
bool expectedEscapes = envVar is not null && (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
116+
117+
string stdout = remote.Process.StandardOutput.ReadToEnd();
118+
string[] parts = stdout.Split("SEPARATOR");
119+
Assert.Equal(3, parts.Length);
120+
121+
Assert.Equal(expectedEscapes, parts[1].Contains(Esc));
122+
}
123+
}
72124
}

0 commit comments

Comments
 (0)