Skip to content

Commit

Permalink
Add possibility to write ANSI color escape codes when the console out…
Browse files Browse the repository at this point in the history
…put is redirected (#47935)

* Add possibility to write ANSI color escape codes when the console output is redirected

This  introduces an opt-in mechanism to allow writing ANSI color escape codes even when the console output is redirected.

To enable ANSI color codes, the `DOTNET_CONSOLE_ANSI_COLOR` environment variable must be set to `true` (case insensitive) or `1`.

Fixes #33980

* Fix ANSI color env var handling

Co-authored-by: Stephen Toub <stoub@microsoft.com>
  • Loading branch information
0xced and stephentoub authored Jul 9, 2021
1 parent e0b9fe5 commit 0d848f6
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 7 deletions.
50 changes: 47 additions & 3 deletions src/libraries/System.Console/src/System/ConsolePal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ internal static class ConsolePal
private static int s_windowHeight; // Cached WindowHeight, invalid when s_windowWidth == -1.
private static int s_invalidateCachedSettings = 1; // Tracks whether we should invalidate the cached settings.

/// <summary>Whether to output ansi color strings.</summary>
private static volatile int s_emitAnsiColorCodes = -1;

public static Stream OpenStandardInput()
{
return new UnixConsoleStream(SafeFileHandleHelper.Open(() => Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDIN_FILENO)), FileAccess.Read,
Expand Down Expand Up @@ -779,8 +782,10 @@ private static void WriteSetColorString(bool foreground, ConsoleColor color)
// Changing the color involves writing an ANSI character sequence out to the output stream.
// We only want to do this if we know that sequence will be interpreted by the output.
// rather than simply displayed visibly.
if (Console.IsOutputRedirected)
if (!EmitAnsiColorCodes)
{
return;
}

// See if we've already cached a format string for this foreground/background
// and specific color choice. If we have, just output that format string again.
Expand Down Expand Up @@ -813,13 +818,52 @@ private static void WriteSetColorString(bool foreground, ConsoleColor color)
/// <summary>Writes out the ANSI string to reset colors.</summary>
private static void WriteResetColorString()
{
// We only want to send the reset string if we're targeting a TTY device
if (!Console.IsOutputRedirected)
if (EmitAnsiColorCodes)
{
WriteStdoutAnsiString(TerminalFormatStrings.Instance.Reset);
}
}

/// <summary>Get whether to emit ANSI color codes.</summary>
private 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.
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
// 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.

// 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.
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);
return enabled;
}
}

/// <summary>
/// The values of the ConsoleColor enums unfortunately don't map to the
/// corresponding ANSI values. We need to do the mapping manually.
Expand Down
60 changes: 56 additions & 4 deletions src/libraries/System.Console/tests/Color.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.DotNet.XUnitExtensions;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;

public class Color
{
private const char Esc = (char)0x1B;

[Fact]
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "Not supported on Browser, iOS, MacCatalyst, or tvOS.")]
public static void InvalidColors()
Expand Down Expand Up @@ -64,9 +67,58 @@ public static void RedirectedOutputDoesNotUseAnsiSequences()
Console.ResetColor();
Console.Write('4');
const char Esc = (char)0x1B;
Assert.Equal(0, Encoding.UTF8.GetString(data.ToArray()).ToCharArray().Count(c => c == Esc));
Assert.Equal("1234", Encoding.UTF8.GetString(data.ToArray()));
});
}

public static bool TermIsSet => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TERM"));

[ConditionalTheory(nameof(TermIsSet))]
[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)
{
var psi = new ProcessStartInfo { RedirectStandardOutput = true };
psi.Environment["DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"] = envVar;

for (int i = 0; i < 3; i++)
{
Action<string> main = i =>
{
Console.Write("SEPARATOR");
switch (i)
{
case "0":
Console.ForegroundColor = ConsoleColor.Blue;
break;
case "1":
Console.BackgroundColor = ConsoleColor.Red;
break;
case "2":
Console.ResetColor();
break;
}
Console.Write("SEPARATOR");
};

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

0 comments on commit 0d848f6

Please sign in to comment.