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 colors to CLI commands #1231

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
67 changes: 64 additions & 3 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ import (
// FParseErrWhitelist configures Flag parse errors to be ignored
type FParseErrWhitelist flag.ParseErrorsWhitelist

// TerminalColor is a type used for the names of the different
// colors in the terminal
type TerminalColor int

// Colors represents the different colors one can use in the terminal
const (
ColorBlack TerminalColor = iota + 30
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorLightGray
)

// This sequence starts at 90, so we reset iota
const (
ColorDarkGray TerminalColor = iota + 90
ColorLightRed
ColorLightGreen
ColorLightYellow
ColorLightBlue
ColorLightMagenta
ColorLightCyan
ColorWhite
)

// Command is just that, a command for your application.
// E.g. 'go run ...' - 'run' is the command. Cobra requires
// you to define the usage and description as part of your command
Expand All @@ -47,6 +75,12 @@ type Command struct {
// Example: add [-F file | -D dir]... [-f format] profile
Use string

// DisableColors is a boolean used to disable the coloring in the command line
DisableColors bool

// Color represents the color to use to print the command in the terminal
Color TerminalColor

// Aliases is an array of aliases that can be used instead of the first word in Use.
Aliases []string

Expand Down Expand Up @@ -467,10 +501,18 @@ var minNamePadding = 11

// NamePadding returns padding for the name.
func (c *Command) NamePadding() int {
additionalPadding := c.additionalNamePadding()
if c.parent == nil || minNamePadding > c.parent.commandsMaxNameLen {
return minNamePadding
return minNamePadding + additionalPadding
}
return c.parent.commandsMaxNameLen
return c.parent.commandsMaxNameLen + additionalPadding
}

func (c *Command) additionalNamePadding() int {
// additionalPadding is used to pad non visible characters
// This happens for example when using colors, where \033[31m isn't seen
// but still is counted towards the padding
return len(c.ColoredName()) - len(c.Name())
}

// UsageTemplate returns usage template for the command.
Expand All @@ -493,7 +535,7 @@ Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}

Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
{{rpad .ColoredName .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}

Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Expand Down Expand Up @@ -1293,6 +1335,25 @@ func (c *Command) Name() string {
return name
}

// isColoringEnabled will be queried to know whether or not we should enable
// the coloring on a command. This will usually be called on the Root command
// and applied for every command.
func (c *Command) isColoringEnabled() bool {
_, noColorEnv := os.LookupEnv("NO_COLOR")
if c.DisableColors || noColorEnv {
return false
}
return true
}

// ColoredName returns the command's Name in the correct color if specified
func (c *Command) ColoredName() string {
if c.Color != 0 && c.Root().isColoringEnabled() {
return fmt.Sprintf("\033[%dm%s\033[0m", c.Color, c.Name())
}
return c.Name()
}

// HasAlias determines if a given string is an alias of the command.
func (c *Command) HasAlias(s string) bool {
for _, a := range c.Aliases {
Expand Down
74 changes: 74 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cobra
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"reflect"
Expand Down Expand Up @@ -1989,3 +1990,76 @@ func TestFParseErrWhitelistSiblingCommand(t *testing.T) {
}
checkStringContains(t, output, "unknown flag: --unknown")
}

func commandIsColoredRed(c *Command) error {
if c.Name() != "cmd" {
return fmt.Errorf("Unexpected name with Colored Command: %s", c.Name())
}
// If a color is specified, the ColoredName and the Name should be different
if c.Name() == c.ColoredName() {
return errors.New("Name and ColoredName should not give the same result")
}
if c.ColoredName() != "\033[31m"+c.Name()+"\033[0m" {
return errors.New("ColoredName should only add color to the name")
}
if c.additionalNamePadding() == 0 {
return errors.New("With a color, the additionalNamePadding should be more than 0")
}
return nil
}

func commandIsNotColored(c *Command) error {
if c.Name() != "cmd" {
return errors.New("Unexpected name with simple Command")
}
// If no color is specified, the ColoredName should equal the Name
if c.Name() != c.ColoredName() {
return errors.New("Name and ColoredName should give the same result")
}
if c.additionalNamePadding() != 0 {
return errors.New("With no color, the additionalNamePadding should be 0")
}
return nil
}

func TestColoredName(t *testing.T) {
c := &Command{
Use: "cmd",
}
err := commandIsNotColored(c)
if err != nil {
t.Error(err)
}
c = &Command{
Use: "cmd",
Color: ColorRed,
}
err = commandIsColoredRed(c)
if err != nil {
t.Error(err)
}
}

func TestColoredNameWithNoColorSetup(t *testing.T) {
c := &Command{
Use: "cmd",
Color: ColorRed,
}
err := commandIsColoredRed(c)
if err != nil {
t.Error(err)
}

os.Setenv("NO_COLOR", "true")
err = commandIsNotColored(c)
if err != nil {
t.Error(err)
}
os.Unsetenv("NO_COLOR")

c.DisableColors = true
err = commandIsNotColored(c)
if err != nil {
t.Error(err)
}
}