image/tree: Respect NO_COLOR env variable

Do not use the fancy colored output if NO_COLOR variable is set to 1
following the https://no-color.org/ convention.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2025-11-13 11:31:05 +01:00
parent 88e324150b
commit be9e6308f5
5 changed files with 46 additions and 15 deletions

View File

@ -292,7 +292,7 @@ func printImageTree(outs command.Streams, view treeView) {
Width: func() int {
maxChipsWidth := 0
for _, chip := range possibleChips {
s := chip.String(isTerm)
s := out.Sprint(chip)
l := tui.Width(s)
maxChipsWidth += l
}
@ -308,9 +308,9 @@ func printImageTree(outs command.Streams, view treeView) {
var b strings.Builder
for _, chip := range possibleChips {
if chip.check(d) {
b.WriteString(chip.String(isTerm))
b.WriteString(out.Sprint(chip))
} else {
b.WriteString(chipPlaceholder.String(isTerm))
b.WriteString(out.Sprint(chipPlaceholder))
}
}
return b.String()

View File

@ -15,6 +15,7 @@ func TestPrintImageTreeAnsiTty(t *testing.T) {
stdoutTty bool
stderrTty bool
expectedAnsi bool
noColorEnv bool
}{
{
name: "non-terminal",
@ -80,6 +81,24 @@ func TestPrintImageTreeAnsiTty(t *testing.T) {
expectedAnsi: false,
},
{
name: "no-color-env",
stdinTty: false,
stdoutTty: false,
stderrTty: false,
noColorEnv: true,
expectedAnsi: false,
},
{
name: "no-color-env-terminal",
stdinTty: true,
stdoutTty: true,
stderrTty: true,
noColorEnv: true,
expectedAnsi: false,
},
}
mockView := treeView{
@ -115,6 +134,11 @@ func TestPrintImageTreeAnsiTty(t *testing.T) {
cli.In().SetIsTerminal(tc.stdinTty)
cli.Out().SetIsTerminal(tc.stdoutTty)
cli.Err().SetIsTerminal(tc.stderrTty)
if tc.noColorEnv {
t.Setenv("NO_COLOR", "1")
} else {
t.Setenv("NO_COLOR", "")
}
printImageTree(cli, mockView)
@ -123,9 +147,9 @@ func TestPrintImageTreeAnsiTty(t *testing.T) {
hasAnsi := strings.Contains(out, "\x1b[")
if tc.expectedAnsi {
assert.Check(t, hasAnsi, "Output should contain ANSI escape codes")
assert.Check(t, hasAnsi, "Output should contain ANSI escape codes, output: %s", out)
} else {
assert.Check(t, !hasAnsi, "Output should not contain ANSI escape codes")
assert.Check(t, !hasAnsi, "Output should not contain ANSI escape codes, output: %s", out)
}
})
}

View File

@ -130,6 +130,8 @@ line:
| `DOCKER_TLS` | Enable TLS for connections made by the `docker` CLI (equivalent of the `--tls` command-line option). Set to a non-empty value to enable TLS. Note that TLS is enabled automatically if any of the other TLS options are set. |
| `DOCKER_TLS_VERIFY` | When set Docker uses TLS and verifies the remote. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) |
| `BUILDKIT_PROGRESS` | Set type of progress output (`auto`, `plain`, `tty`, `rawjson`) when [building](https://docs.docker.com/reference/cli/docker/image/build/) with [BuildKit backend](https://docs.docker.com/build/buildkit/). Use plain to show container output (default `auto`). |
| `NO_COLOR` | Disable any ANSI escape codes in the output in accordance with https://no-color.org/
|
Because Docker is developed using Go, you can also use any environment
variables used by the Go runtime. In particular, you may find these useful:

View File

@ -28,7 +28,7 @@ func withHeader(header Str) noteOptions {
}
func (o Output) printNoteWithOptions(format string, args []any, opts ...noteOptions) {
if o.isTerminal {
if !o.noColor {
// TODO: Handle all flags
format = strings.ReplaceAll(format, "--platform", ColorFlag.Apply("--platform"))
}
@ -51,7 +51,7 @@ func (o Output) printNoteWithOptions(format string, args []any, opts ...noteOpti
}
l := line
if o.isTerminal {
if !o.noColor {
l = aec.Italic.Apply(l)
}
_, _ = fmt.Fprintln(o, l)

View File

@ -5,6 +5,7 @@ package tui
import (
"fmt"
"os"
"github.com/docker/cli/cli/streams"
"github.com/morikuni/aec"
@ -12,7 +13,7 @@ import (
type Output struct {
*streams.Out
isTerminal bool
noColor bool
}
type terminalPrintable interface {
@ -20,24 +21,28 @@ type terminalPrintable interface {
}
func NewOutput(out *streams.Out) Output {
noColor := !out.IsTerminal()
if os.Getenv("NO_COLOR") != "" {
noColor = true
}
return Output{
Out: out,
isTerminal: out.IsTerminal(),
Out: out,
noColor: noColor,
}
}
func (o Output) Color(clr aec.ANSI) aec.ANSI {
if o.isTerminal {
return clr
if o.noColor {
return ColorNone
}
return ColorNone
return clr
}
func (o Output) Sprint(all ...any) string {
var out []any
for _, p := range all {
if s, ok := p.(terminalPrintable); ok {
out = append(out, s.String(o.isTerminal))
out = append(out, s.String(!o.noColor))
} else {
out = append(out, p)
}
@ -47,7 +52,7 @@ func (o Output) Sprint(all ...any) string {
func (o Output) PrintlnWithColor(clr aec.ANSI, args ...any) {
msg := o.Sprint(args...)
if o.isTerminal {
if !o.noColor {
msg = clr.Apply(msg)
}
_, _ = fmt.Fprintln(o.Out, msg)