From 6291a093ca6da07e92fd405ebe9446fdcba33f6e Mon Sep 17 00:00:00 2001 From: JulesDT Date: Mon, 21 Sep 2020 19:15:06 -0400 Subject: [PATCH 1/5] add colors to CLI commands --- command.go | 37 ++++++++++++++++++++++++++++++++++++- command_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 27d39d54e..e6ba8b869 100644 --- a/command.go +++ b/command.go @@ -31,6 +31,30 @@ 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 + ColorDarkGray + 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 @@ -47,6 +71,9 @@ type Command struct { // Example: add [-F file | -D dir]... [-f format] profile Use string + // 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 @@ -493,7 +520,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}} @@ -1293,6 +1320,14 @@ func (c *Command) Name() string { return name } +// ColoredName returns the command's Name in the correct color if specified +func (c *Command) ColoredName() string { + if c.Color != 0 { + 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 { diff --git a/command_test.go b/command_test.go index 16cc41b4c..a521b8b88 100644 --- a/command_test.go +++ b/command_test.go @@ -1989,3 +1989,30 @@ func TestFParseErrWhitelistSiblingCommand(t *testing.T) { } checkStringContains(t, output, "unknown flag: --unknown") } + +func TestColoredName(t *testing.T) { + c := &Command{ + Use: "cmd", + } + if c.Name() != "cmd" { + t.Error("Unexpected name with simple Command") + } + // If no color is specified, the ColoredName should equal the Name + if c.Name() != c.ColoredName() { + t.Error("Name and ColoredName should give the same result") + } + c = &Command{ + Use: "cmd", + Color: ColorRed, + } + if c.Name() != "cmd" { + t.Errorf("Unexpected name with Colored Command: %s\n", c.Name()) + } + // If a color is specified, the ColoredName and the Name should be different + if c.Name() == c.ColoredName() { + t.Error("Name and ColoredName should not give the same result") + } + if c.ColoredName() != "\033[31m"+c.Name()+"\033[0m" { + t.Error("ColoredName should only add color to the name") + } +} From 3e335655aecd3d7b58f61e4d9dd1ce25215422e4 Mon Sep 17 00:00:00 2001 From: JulesDT Date: Mon, 19 Oct 2020 15:44:53 -0600 Subject: [PATCH 2/5] fix light colors and pading when using a color --- command.go | 18 +++++++++++++++--- command_test.go | 6 ++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index e6ba8b869..0d7516179 100644 --- a/command.go +++ b/command.go @@ -45,7 +45,11 @@ const ( ColorMagenta ColorCyan ColorLightGray - ColorDarkGray +) + +// This sequence starts at 90, so we reset iota +const ( + ColorDarkGray = iota + 90 ColorLightRed ColorLightGreen ColorLightYellow @@ -494,10 +498,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. diff --git a/command_test.go b/command_test.go index a521b8b88..34fc5898d 100644 --- a/command_test.go +++ b/command_test.go @@ -2001,6 +2001,9 @@ func TestColoredName(t *testing.T) { if c.Name() != c.ColoredName() { t.Error("Name and ColoredName should give the same result") } + if c.additionalNamePadding() != 0 { + t.Error("With no color, the additionalNamePadding should be 0") + } c = &Command{ Use: "cmd", Color: ColorRed, @@ -2015,4 +2018,7 @@ func TestColoredName(t *testing.T) { if c.ColoredName() != "\033[31m"+c.Name()+"\033[0m" { t.Error("ColoredName should only add color to the name") } + if c.additionalNamePadding() == 0 { + t.Error("With a color, the additionalNamePadding should be more than 0") + } } From 58d9bfdbcb2f6b5d82fdb63a13076073318773ce Mon Sep 17 00:00:00 2001 From: JulesDT Date: Mon, 26 Oct 2020 13:33:09 -0600 Subject: [PATCH 3/5] specify type of 2nd set of terminal colors --- command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command.go b/command.go index 0d7516179..6e48748c4 100644 --- a/command.go +++ b/command.go @@ -49,7 +49,7 @@ const ( // This sequence starts at 90, so we reset iota const ( - ColorDarkGray = iota + 90 + ColorDarkGray TerminalColor = iota + 90 ColorLightRed ColorLightGreen ColorLightYellow From 4ac302ae86f76d27945bc3665102c373f3035382 Mon Sep 17 00:00:00 2001 From: JulesDT Date: Wed, 23 Dec 2020 12:01:45 -0500 Subject: [PATCH 4/5] use the NO_COLOR env variable and a possible command flag --- command.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 6e48748c4..221320571 100644 --- a/command.go +++ b/command.go @@ -75,6 +75,9 @@ 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 @@ -1332,9 +1335,20 @@ 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 { + if c.Color != 0 && c.Root().isColoringEnabled() { return fmt.Sprintf("\033[%dm%s\033[0m", c.Color, c.Name()) } return c.Name() From b2f7bca857a999125429e7e72053624f26258ced Mon Sep 17 00:00:00 2001 From: JulesDT Date: Fri, 22 Jan 2021 18:33:11 -0500 Subject: [PATCH 5/5] add tests for the NO_COLOR env variable --- command_test.go | 71 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/command_test.go b/command_test.go index 34fc5898d..ba0f66df3 100644 --- a/command_test.go +++ b/command_test.go @@ -3,6 +3,7 @@ package cobra import ( "bytes" "context" + "errors" "fmt" "os" "reflect" @@ -1990,35 +1991,75 @@ func TestFParseErrWhitelistSiblingCommand(t *testing.T) { checkStringContains(t, output, "unknown flag: --unknown") } -func TestColoredName(t *testing.T) { - c := &Command{ - Use: "cmd", +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" { - t.Error("Unexpected name with simple Command") + return errors.New("Unexpected name with simple Command") } // If no color is specified, the ColoredName should equal the Name if c.Name() != c.ColoredName() { - t.Error("Name and ColoredName should give the same result") + return errors.New("Name and ColoredName should give the same result") } if c.additionalNamePadding() != 0 { - t.Error("With no color, the additionalNamePadding should be 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, } - if c.Name() != "cmd" { - t.Errorf("Unexpected name with Colored Command: %s\n", c.Name()) + err = commandIsColoredRed(c) + if err != nil { + t.Error(err) } - // If a color is specified, the ColoredName and the Name should be different - if c.Name() == c.ColoredName() { - t.Error("Name and ColoredName should not give the same result") +} + +func TestColoredNameWithNoColorSetup(t *testing.T) { + c := &Command{ + Use: "cmd", + Color: ColorRed, } - if c.ColoredName() != "\033[31m"+c.Name()+"\033[0m" { - t.Error("ColoredName should only add color to the name") + err := commandIsColoredRed(c) + if err != nil { + t.Error(err) } - if c.additionalNamePadding() == 0 { - t.Error("With a color, the additionalNamePadding should be more than 0") + + 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) } }