Skip to content

Commit 2f803e2

Browse files
Add a cli.NoColour option for disabling all colour/style (#122)
* Check `$FORCE_COLOR` and `$NO_COLOR` only once * Add a `cli.NoColour` option * Tweak the help layout
1 parent 9ad2b79 commit 2f803e2

7 files changed

Lines changed: 63 additions & 126 deletions

File tree

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
ignore:
22
- examples # Demo programs to showcase the library, coverage tracking not needed
3+
- internal/colour # Done this a bunch of times, annoying to test because of $FORCE_COLOR and $NO_COLOR and sync.OnceValues

command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ func writeArgumentsSection(cmd *Command, s *strings.Builder) error {
600600
case requiredArgMarker:
601601
tab.Row(" %s\t%s [required]\n", colour.Bold(arg.name), arg.description)
602602
case "":
603-
tab.Row(" %s\t%s\n", colour.Bold(arg.name), arg.description)
603+
tab.Row(" %s\t%s [default %q]\n", colour.Bold(arg.name), arg.description, arg.defaultValue)
604604
default:
605605
tab.Row(" %s\t%s [default %s]\n", colour.Bold(arg.name), arg.description, arg.defaultValue)
606606
}

command_test.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ func TestHelp(t *testing.T) {
464464
cli.OverrideArgs([]string{"--help"}),
465465
cli.RequiredArg("src", "The file to copy"), // This one is required
466466
cli.OptionalArg("dest", "Destination to copy to", "./dest"), // This one is optional
467+
cli.OptionalArg("other", "Something else", ""), // This is optional but default is empty
467468
cli.Run(func(cmd *cli.Command, args []string) error { return nil }),
468469
},
469470
wantErr: false,
@@ -543,16 +544,17 @@ func TestHelp(t *testing.T) {
543544

544545
for _, tt := range tests {
545546
t.Run(tt.name, func(t *testing.T) {
546-
// Force no colour in tests
547-
t.Setenv("NO_COLOR", "true")
548-
549547
snap := snapshot.New(t, snapshot.Update(*update))
550548

551549
stderr := &bytes.Buffer{}
552550
stdout := &bytes.Buffer{}
553551

554552
// Test specific overrides to the options in the table
555-
options := []cli.Option{cli.Stdout(stdout), cli.Stderr(stderr)}
553+
options := []cli.Option{
554+
cli.Stdout(stdout),
555+
cli.Stderr(stderr),
556+
cli.NoColour(true),
557+
}
556558

557559
cmd, err := cli.New("test", slices.Concat(options, tt.options)...)
558560

@@ -681,14 +683,15 @@ func TestVersion(t *testing.T) {
681683

682684
for _, tt := range tests {
683685
t.Run(tt.name, func(t *testing.T) {
684-
// Force no colour in tests
685-
t.Setenv("NO_COLOR", "true")
686-
687686
stderr := &bytes.Buffer{}
688687
stdout := &bytes.Buffer{}
689688

690689
// Test specific overrides to the options in the table
691-
options := []cli.Option{cli.Stdout(stdout), cli.Stderr(stderr)}
690+
options := []cli.Option{
691+
cli.Stdout(stdout),
692+
cli.Stderr(stderr),
693+
cli.NoColour(true),
694+
}
692695

693696
cmd, err := cli.New("version-test", slices.Concat(tt.options, options)...)
694697
test.Ok(t, err)

internal/colour/colour.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
// the same length, which means [text/tabwriter] will correctly calculate alignment as long as styles are not mixed within a table.
55
package colour
66

7-
import "os"
7+
import (
8+
"os"
9+
"sync"
10+
)
811

912
// ANSI codes for coloured output, they are all the same length so as not to throw off
1013
// alignment of [text/tabwriter].
@@ -14,6 +17,24 @@ const (
1417
CodeBold = "\x1b[1;0039m" // Bold & white
1518
)
1619

20+
// Disable is a flag that disables all colour text, it overrides both
21+
// $FORCE_COLOR and $NO_COLOR, setting it to true will always make this
22+
// package return plain text and not check any other config.
23+
var Disable bool = false
24+
25+
// getColourOnce is a [sync.OnceValues] function that returns the state of
26+
// $NO_COLOR and $FORCE_COLOR, once and only once to avoid us calling
27+
// os.Getenv on every call to a colour function.
28+
var getColourOnce = sync.OnceValues(getColour)
29+
30+
// getColour returns whether $NO_COLOR and $FORCE_COLOR were set.
31+
func getColour() (noColour bool, forceColour bool) {
32+
no := os.Getenv("NO_COLOR") != ""
33+
force := os.Getenv("FORCE_COLOR") != ""
34+
35+
return no, force
36+
}
37+
1738
// Title returns the given text in a title style, bold white and underlined.
1839
//
1940
// If $NO_COLOR is set, text will be returned unmodified.
@@ -30,13 +51,15 @@ func Bold(text string) string {
3051

3152
// sprint returns a string with a given colour and the reset code.
3253
//
33-
// It handles checking for NO_COLOR and FORCE_COLOR.
54+
// It handles checking for NO_COLOR and FORCE_COLOR. If the global var
55+
// [Disable] is true then nothing else is checked and plain text is returned.
3456
func sprint(code, text string) string {
35-
// TODO(@FollowTheProcess): I don't like checking *every* time but doing it
36-
// via e.g. sync.Once means that tests are annoying unless we ensure env vars are
37-
// set at the process level
38-
noColor := os.Getenv("NO_COLOR") != ""
39-
forceColor := os.Getenv("FORCE_COLOR") != ""
57+
// Our global variable is above all else
58+
if Disable {
59+
return text
60+
}
61+
62+
noColor, forceColor := getColourOnce()
4063

4164
// $FORCE_COLOR overrides $NO_COLOR
4265
if forceColor {

internal/colour/colour_test.go

Lines changed: 0 additions & 107 deletions
This file was deleted.

option.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"slices"
88
"strings"
99

10+
"github.com/FollowTheProcess/cli/internal/colour"
1011
"github.com/FollowTheProcess/cli/internal/flag"
1112
)
1213

@@ -164,6 +165,21 @@ func Stderr(stderr io.Writer) Option {
164165
return option(f)
165166
}
166167

168+
// NoColour is an [Option] that disables all colour output from the [Command].
169+
//
170+
// CLI respects the values of $NO_COLOR and $FORCE_COLOR automatically so this need
171+
// not be set for most applications.
172+
//
173+
// Setting this option takes precedence over all other colour configuration.
174+
func NoColour(noColour bool) Option {
175+
f := func(cfg *config) error {
176+
// Just disable the internal colour package entirely
177+
colour.Disable = noColour
178+
return nil
179+
}
180+
return option(f)
181+
}
182+
167183
// Short is an [Option] that sets the one line usage summary for a [Command].
168184
//
169185
// The one line usage will appear in the help text as well as alongside

testdata/snapshots/TestHelp/with_named_arguments.snap.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
A placeholder for something cool
22

3-
Usage: test [OPTIONS] SRC [DEST]
3+
Usage: test [OPTIONS] SRC [DEST] [OTHER]
44

55
Arguments:
6-
src The file to copy [required]
7-
dest Destination to copy to [default ./dest]
6+
src The file to copy [required]
7+
dest Destination to copy to [default ./dest]
8+
other Something else [default ""]
89

910
Options:
1011
-h --help bool Show help for test

0 commit comments

Comments
 (0)