Skip to content

Add FORCE_COLOR environment variable support#280

Open
veeceey wants to merge 1 commit into
fatih:mainfrom
veeceey:feat/issue-155-force-color
Open

Add FORCE_COLOR environment variable support#280
veeceey wants to merge 1 commit into
fatih:mainfrom
veeceey:feat/issue-155-force-color

Conversation

@veeceey
Copy link
Copy Markdown

@veeceey veeceey commented Feb 17, 2026

Fixes #155

Adds support for the FORCE_COLOR environment variable. When FORCE_COLOR is set to any value other than "0", colors are enabled regardless of TTY detection. NO_COLOR takes precedence over FORCE_COLOR.

This is useful for CI systems like GitHub Actions and GitLab CI that support ANSI colors but aren't detected as TTYs. The FORCE_COLOR convention has been standardized at https://force-color.org and is already widely supported in the Node.js ecosystem (chalk, etc.) and increasingly in other languages.

Changes

  • color.go: Added forceColorIsSet() helper and integrated it into the NoColor global initialization and New() constructor
  • color_test.go: Added tests for forceColorIsSet(), color output with FORCE_COLOR, and NO_COLOR precedence over FORCE_COLOR
  • README.md: Documented FORCE_COLOR support and updated the GitHub Actions section

Testing

$ go test ./... -v
--- PASS: Test_forceColorIsSet (0.00s)
    --- PASS: Test_forceColorIsSet/default (0.00s)
    --- PASS: Test_forceColorIsSet/FORCE_COLOR=1 (0.00s)
    --- PASS: Test_forceColorIsSet/FORCE_COLOR=0 (0.00s)
    --- PASS: Test_forceColorIsSet/FORCE_COLOR=true (0.00s)
    --- PASS: Test_forceColorIsSet/FORCE_COLOR= (0.00s)
--- PASS: TestForceColor (0.00s)
--- PASS: TestForceColorOverriddenByNoColor (0.00s)
PASS
ok  	github.com/fatih/color	0.239s

@veeceey
Copy link
Copy Markdown
Author

veeceey commented Mar 10, 2026

just following up here, happy to tweak anything if needed!

@veeceey
Copy link
Copy Markdown
Author

veeceey commented Mar 12, 2026

just checking in on this one

@veeceey veeceey force-pushed the feat/issue-155-force-color branch from 345c765 to b78d43c Compare March 18, 2026 05:23
Comment thread color_test.go Outdated
Comment on lines +665 to +668
os.Setenv("FORCE_COLOR", "1")
t.Cleanup(func() {
os.Unsetenv("FORCE_COLOR")
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
os.Setenv("FORCE_COLOR", "1")
t.Cleanup(func() {
os.Unsetenv("FORCE_COLOR")
})
t.Setenv("FORCE_COLOR", "1")

Comment thread color_test.go Outdated
Comment on lines +698 to +703
os.Setenv("FORCE_COLOR", "1")
os.Setenv("NO_COLOR", "1")
t.Cleanup(func() {
os.Unsetenv("FORCE_COLOR")
os.Unsetenv("NO_COLOR")
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
os.Setenv("FORCE_COLOR", "1")
os.Setenv("NO_COLOR", "1")
t.Cleanup(func() {
os.Unsetenv("FORCE_COLOR")
os.Unsetenv("NO_COLOR")
})
t.Setenv("FORCE_COLOR", "1")
t.Setenv("NO_COLOR", "1")

Comment thread color_test.go
Comment on lines +616 to +659
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)
}
})
}
}
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"

Add support for the FORCE_COLOR environment variable (https://force-color.org).
When FORCE_COLOR is set to any value other than "0", colors are enabled
regardless of terminal detection. NO_COLOR takes precedence over FORCE_COLOR.

This is useful in CI environments like GitHub Actions and GitLab CI that
support ANSI colors but are not detected as TTYs.

Fixes fatih#155
@veeceey veeceey force-pushed the feat/issue-155-force-color branch from b78d43c to 2b93d01 Compare March 25, 2026 04:36
@veeceey
Copy link
Copy Markdown
Author

veeceey commented Mar 25, 2026

Rebased on latest main and addressed all your feedback — switched to t.Setenv, restructured the test with a separate default subtest, and added a "whatever" test case. Thanks for the suggestions!

Comment thread color_test.go
Comment on lines +693 to +697
{
name: "FORCE_COLOR=",
value: "",
want: true,
},
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 "

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for FORCE_COLOR

2 participants