Skip to content

Commit

Permalink
Add a cli.NoColour option for disabling all colour/style (#122)
Browse files Browse the repository at this point in the history
* Check `$FORCE_COLOR` and `$NO_COLOR` only once

* Add a `cli.NoColour` option

* Tweak the help layout
  • Loading branch information
FollowTheProcess authored Jan 4, 2025
1 parent 9ad2b79 commit 2f803e2
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 126 deletions.
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
ignore:
- examples # Demo programs to showcase the library, coverage tracking not needed
- internal/colour # Done this a bunch of times, annoying to test because of $FORCE_COLOR and $NO_COLOR and sync.OnceValues
2 changes: 1 addition & 1 deletion command.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ func writeArgumentsSection(cmd *Command, s *strings.Builder) error {
case requiredArgMarker:
tab.Row(" %s\t%s [required]\n", colour.Bold(arg.name), arg.description)
case "":
tab.Row(" %s\t%s\n", colour.Bold(arg.name), arg.description)
tab.Row(" %s\t%s [default %q]\n", colour.Bold(arg.name), arg.description, arg.defaultValue)
default:
tab.Row(" %s\t%s [default %s]\n", colour.Bold(arg.name), arg.description, arg.defaultValue)
}
Expand Down
19 changes: 11 additions & 8 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ func TestHelp(t *testing.T) {
cli.OverrideArgs([]string{"--help"}),
cli.RequiredArg("src", "The file to copy"), // This one is required
cli.OptionalArg("dest", "Destination to copy to", "./dest"), // This one is optional
cli.OptionalArg("other", "Something else", ""), // This is optional but default is empty
cli.Run(func(cmd *cli.Command, args []string) error { return nil }),
},
wantErr: false,
Expand Down Expand Up @@ -543,16 +544,17 @@ func TestHelp(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Force no colour in tests
t.Setenv("NO_COLOR", "true")

snap := snapshot.New(t, snapshot.Update(*update))

stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}

// Test specific overrides to the options in the table
options := []cli.Option{cli.Stdout(stdout), cli.Stderr(stderr)}
options := []cli.Option{
cli.Stdout(stdout),
cli.Stderr(stderr),
cli.NoColour(true),
}

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

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

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Force no colour in tests
t.Setenv("NO_COLOR", "true")

stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}

// Test specific overrides to the options in the table
options := []cli.Option{cli.Stdout(stdout), cli.Stderr(stderr)}
options := []cli.Option{
cli.Stdout(stdout),
cli.Stderr(stderr),
cli.NoColour(true),
}

cmd, err := cli.New("version-test", slices.Concat(tt.options, options)...)
test.Ok(t, err)
Expand Down
37 changes: 30 additions & 7 deletions internal/colour/colour.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
// the same length, which means [text/tabwriter] will correctly calculate alignment as long as styles are not mixed within a table.
package colour

import "os"
import (
"os"
"sync"
)

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

// Disable is a flag that disables all colour text, it overrides both
// $FORCE_COLOR and $NO_COLOR, setting it to true will always make this
// package return plain text and not check any other config.
var Disable bool = false

// getColourOnce is a [sync.OnceValues] function that returns the state of
// $NO_COLOR and $FORCE_COLOR, once and only once to avoid us calling
// os.Getenv on every call to a colour function.
var getColourOnce = sync.OnceValues(getColour)

// getColour returns whether $NO_COLOR and $FORCE_COLOR were set.
func getColour() (noColour bool, forceColour bool) {
no := os.Getenv("NO_COLOR") != ""
force := os.Getenv("FORCE_COLOR") != ""

return no, force
}

// Title returns the given text in a title style, bold white and underlined.
//
// If $NO_COLOR is set, text will be returned unmodified.
Expand All @@ -30,13 +51,15 @@ func Bold(text string) string {

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

noColor, forceColor := getColourOnce()

// $FORCE_COLOR overrides $NO_COLOR
if forceColor {
Expand Down
107 changes: 0 additions & 107 deletions internal/colour/colour_test.go

This file was deleted.

16 changes: 16 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"slices"
"strings"

"github.com/FollowTheProcess/cli/internal/colour"
"github.com/FollowTheProcess/cli/internal/flag"
)

Expand Down Expand Up @@ -164,6 +165,21 @@ func Stderr(stderr io.Writer) Option {
return option(f)
}

// NoColour is an [Option] that disables all colour output from the [Command].
//
// CLI respects the values of $NO_COLOR and $FORCE_COLOR automatically so this need
// not be set for most applications.
//
// Setting this option takes precedence over all other colour configuration.
func NoColour(noColour bool) Option {
f := func(cfg *config) error {
// Just disable the internal colour package entirely
colour.Disable = noColour
return nil
}
return option(f)
}

// Short is an [Option] that sets the one line usage summary for a [Command].
//
// The one line usage will appear in the help text as well as alongside
Expand Down
7 changes: 4 additions & 3 deletions testdata/snapshots/TestHelp/with_named_arguments.snap.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
A placeholder for something cool

Usage: test [OPTIONS] SRC [DEST]
Usage: test [OPTIONS] SRC [DEST] [OTHER]

Arguments:
src The file to copy [required]
dest Destination to copy to [default ./dest]
src The file to copy [required]
dest Destination to copy to [default ./dest]
other Something else [default ""]

Options:
-h --help bool Show help for test
Expand Down

0 comments on commit 2f803e2

Please sign in to comment.