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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ There might be a case where you want to explicitly disable/enable color output.
The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment
variable is set to a non-empty string.

It also supports the [`FORCE_COLOR`](https://force-color.org) environment variable. When
`FORCE_COLOR` is set (to any value other than `"0"`), colors will be enabled regardless of
terminal detection. This is useful in CI environments like GitHub Actions and GitLab CI that
support ANSI colors but are not detected as TTYs. `NO_COLOR` takes precedence over
`FORCE_COLOR`.

`Color` has support to disable/enable colors programmatically both globally and
for single color definitions. For example suppose you have a CLI app and a
`-no-color` bool flag. You can easily disable the color output with:
Expand Down Expand Up @@ -176,7 +182,9 @@ c.Println("This prints again cyan...")

## GitHub Actions

To output color in GitHub Actions (or other CI systems that support ANSI colors), make sure to set `color.NoColor = false` so that it bypasses the check for non-tty output streams.
To output color in GitHub Actions (or other CI systems that support ANSI colors), you can
set the `FORCE_COLOR=1` environment variable. Alternatively, set `color.NoColor = false`
programmatically to bypass the check for non-tty output streams.


## Credits
Expand Down
16 changes: 15 additions & 1 deletion color.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ var (
// set (regardless of its value). This is a global option and affects all
// colors. For more control over each color block use the methods
// DisableColor() individually.
NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || !stdoutIsTerminal()
//
// If the FORCE_COLOR environment variable is set (to any value other than
// "0"), colors will be enabled regardless of terminal detection. NO_COLOR
// takes precedence over FORCE_COLOR.
NoColor = noColorIsSet() || (!forceColorIsSet() && (os.Getenv("TERM") == "dumb" || !stdoutIsTerminal()))

// Output defines the standard output of the print functions. By default,
// stdOut() is used.
Expand Down Expand Up @@ -67,6 +71,13 @@ func stdErr() io.Writer {
return colorable.NewColorableStderr()
}

// forceColorIsSet returns true if the environment variable FORCE_COLOR is set
// to a value other than "0". See https://force-color.org for more details.
func forceColorIsSet() bool {
val, ok := os.LookupEnv("FORCE_COLOR")
return ok && val != "0"
}

// Color defines a custom color object which is defined by SGR parameters.
type Color struct {
params []Attribute
Expand Down Expand Up @@ -178,6 +189,9 @@ func New(value ...Attribute) *Color {
if noColorIsSet() {
c.noColor = boolPtr(true)
}
if forceColorIsSet() && !noColorIsSet() {
c.noColor = boolPtr(false)
}

c.Add(value...)
return c
Expand Down
91 changes: 91 additions & 0 deletions color_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,3 +662,94 @@ func TestRGB(t *testing.T) {
})
}
}

func Test_forceColorIsSet(t *testing.T) {
t.Run("default", func(t *testing.T) {
if forceColorIsSet() {
t.Errorf("forceColorIsSet() = true, want false")
}
})

tests := []struct {
name string
value string
want bool
}{
{
name: "FORCE_COLOR=1",
value: "1",
want: true,
},
{
name: "FORCE_COLOR=0",
value: "0",
want: false,
},
{
name: "FORCE_COLOR=true",
value: "true",
want: true,
},
{
name: "FORCE_COLOR=",
value: "",
want: true,
},
Comment on lines +693 to +697

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This surprised me

I went to read the spec you quoted

Command-line software which outputs colored text should check for a FORCE_COLOR environment variable. When this variable is present and not an empty string (regardless of its value), it should force the addition of ANSI color.

So here it state that empty string should not enable color.

But then it also state that having FORCE_COLOR=0 implies having colors?

Which mean your lookup env should check for values different than empty string, and not "0"

Note: I understand the spec is unclear

Because this activates colors

FORCE_COLOR=" "

While this doesn't

FORCE_COLOR=""

I'm unsure what is the best here, because I would expect this to disable colors

FORCE_COLOR=0

So here as it's open to interpretation, but that the spec seems pretty clear I would state with the spec and use a lookup and check for != "" and add tests for "0" " " "0 "

{
name: "FORCE_COLOR=whatever",
value: "whatever",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("FORCE_COLOR", tt.value)
if got := forceColorIsSet(); got != tt.want {
t.Errorf("forceColorIsSet() = %v, want %v", got, tt.want)
}
})
}
}
Comment on lines +666 to +712

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I would like to recommend this

Suggested change
func Test_forceColorIsSet(t *testing.T) {
tests := []struct {
name string
act func()
want bool
}{
{
name: "default",
act: func() {},
want: false,
},
{
name: "FORCE_COLOR=1",
act: func() { os.Setenv("FORCE_COLOR", "1") },
want: true,
},
{
name: "FORCE_COLOR=0",
act: func() { os.Setenv("FORCE_COLOR", "0") },
want: false,
},
{
name: "FORCE_COLOR=true",
act: func() { os.Setenv("FORCE_COLOR", "true") },
want: true,
},
{
name: "FORCE_COLOR=",
act: func() { os.Setenv("FORCE_COLOR", "") },
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Cleanup(func() {
os.Unsetenv("FORCE_COLOR")
})
tt.act()
if got := forceColorIsSet(); got != tt.want {
t.Errorf("forceColorIsSet() = %v, want %v", got, tt.want)
}
})
}
}
func Test_forceColorIsSet(t *testing.T) {
t.Run("default", func(t *testing.T){
if forceColorIsSet() {
t.Errorf("forceColorIsSet() = true, want false")
}
})
tests := []struct {
name string
value string
want bool
}{
{
name: "FORCE_COLOR=1",
value: "1",
want: true,
},
{
name: "FORCE_COLOR=0",
value: "0",
want: false,
},
{
name: "FORCE_COLOR=true",
value: "true",
want: true,
},
{
name: "FORCE_COLOR=",
value: "",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.SetEnv("FORCE_COLOR", tt.value)
if got := forceColorIsSet(); got != tt.want {
t.Errorf("forceColorIsSet() = %v, want %v", got, tt.want)
}
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You could also test a value like "whatever"


func TestForceColor(t *testing.T) {
rb := new(bytes.Buffer)
Output = rb

t.Setenv("FORCE_COLOR", "1")

NoColor = false

testColors := []struct {
text string
code Attribute
}{
{text: "black", code: FgBlack},
{text: "red", code: FgRed},
{text: "green", code: FgGreen},
}

for _, c := range testColors {
p := New(c.code)
p.Print(c.text)

line, _ := rb.ReadString('\n')
scannedLine := fmt.Sprintf("%q", line)
colored := fmt.Sprintf("\x1b[%dm%s\x1b[0m", c.code, c.text)
escapedForm := fmt.Sprintf("%q", colored)

if scannedLine != escapedForm {
t.Errorf("Expecting %s, got '%s'\n", escapedForm, scannedLine)
}
}
}

func TestForceColorOverriddenByNoColor(t *testing.T) {
// NO_COLOR should take precedence over FORCE_COLOR
t.Setenv("FORCE_COLOR", "1")
t.Setenv("NO_COLOR", "1")

c := New(FgRed)
if !c.isNoColorSet() {
t.Error("Expected NO_COLOR to take precedence over FORCE_COLOR")
}
}