diff --git a/command.go b/command.go index 27d39d54e..221320571 100644 --- a/command.go +++ b/command.go @@ -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 @@ -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 @@ -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. @@ -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}} @@ -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 { diff --git a/command_test.go b/command_test.go index 16cc41b4c..ba0f66df3 100644 --- a/command_test.go +++ b/command_test.go @@ -3,6 +3,7 @@ package cobra import ( "bytes" "context" + "errors" "fmt" "os" "reflect" @@ -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) + } +}