Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a cli.NoColour option for disabling all colour/style #122

Merged
merged 3 commits into from
Jan 4, 2025
Merged
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
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
Loading