├── .gitignore ├── _example ├── figures │ └── sub-expand-example.png └── main.go ├── go.mod ├── utils.go ├── go.sum ├── LICENSE ├── README.md └── colorflag.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /_example/figures/sub-expand-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/relastle/colorflag/HEAD/_example/figures/sub-expand-example.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/relastle/colorflag 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fatih/color v1.7.0 7 | github.com/mattn/go-colorable v0.1.2 8 | ) 9 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package colorflag 2 | 3 | func makeOffsets(strs []string) []int { 4 | max := 0 5 | for _, str := range strs { 6 | if len(str) > max { 7 | max = len(str) 8 | } 9 | } 10 | 11 | res := []int{} 12 | for _, str := range strs { 13 | res = append(res, max-len(str)) 14 | } 15 | return res 16 | } 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 2 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 3 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 4 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 5 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 6 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 7 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 8 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hiroki Konishi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/relastle/colorflag" 7 | ) 8 | 9 | func main() { 10 | // main command 11 | flag.String("main-opt1-string", "", "Description of `quoted target`") 12 | flag.Int("main-opt2-integer", 0, "Description of integer option") 13 | 14 | // sub command 1 15 | sub1FlagSet := flag.NewFlagSet("sub1", flag.ExitOnError) 16 | sub1FlagSet.String("sub1-option-string", "default string value", "Description of string option of sub command 1") 17 | sub1FlagSet.Int("sub1-option-integer", 2, "Description of integer option for sub command") 18 | sub1FlagSet.Bool("sub1-flag-bool", false, "Description of flag for sub command") 19 | 20 | // sub command 2 21 | sub2FlagSet := flag.NewFlagSet("sub2", flag.ExitOnError) 22 | sub2FlagSet.String("sub2-option-string", "", "Description of string option of sub command 2") 23 | 24 | // Optional 25 | colorflag.Indent = 4 // default is 2 26 | colorflag.ExpandsSubCommand = true // default is true 27 | colorflag.TitleColor = "green" // default is yellow 28 | colorflag.FlagColor = "cyan" // default is green 29 | 30 | // Parse (and return selected sub command name) 31 | subCommand := colorflag.Parse([]*flag.FlagSet{ 32 | sub1FlagSet, 33 | sub2FlagSet, 34 | }) 35 | 36 | switch subCommand { 37 | case "sub1": 38 | // Handle sub1 sub-command 39 | case "sub2": 40 | // Handle sub2 sub-command 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # colorflag 2 | 3 | colorflag provides colorized help for CLI applications using Go's `flag` standard library 4 | 5 | ## Installation 6 | 7 | ```zsh 8 | go get github.com/relastle/colorflag 9 | ``` 10 | 11 | ## Example 12 | 13 | #### Sample code 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "flag" 20 | 21 | "github.com/relastle/colorflag" 22 | ) 23 | 24 | func main() { 25 | // main command 26 | flag.String("main-opt1-string", "", "Description of `quoted target`") 27 | flag.Int("main-opt2-integer", 0, "Description of integer option") 28 | 29 | // sub command 1 30 | sub1FlagSet := flag.NewFlagSet("sub1", flag.ExitOnError) 31 | sub1FlagSet.String("sub1-option-string", "default string value", "Description of string option of sub command 1") 32 | sub1FlagSet.Int("sub1-option-integer", 2, "Description of integer option for sub command") 33 | sub1FlagSet.Bool("sub1-flag-bool", false, "Description of flag for sub command") 34 | 35 | // sub command 2 36 | sub2FlagSet := flag.NewFlagSet("sub2", flag.ExitOnError) 37 | sub2FlagSet.String("sub2-option-string", "", "Description of string option of sub command 2") 38 | 39 | // Optional 40 | colorflag.Indent = 4 // default is 2 41 | colorflag.ExpandsSubCommand = true // default is true 42 | colorflag.TitleColor = "green" // default is yellow 43 | colorflag.FlagColor = "cyan" // default is green 44 | 45 | // Parse (and return selected sub command name) 46 | subCommand := colorflag.Parse([]*flag.FlagSet{ 47 | sub1FlagSet, 48 | sub2FlagSet, 49 | }) 50 | 51 | switch subCommand { 52 | case "sub1": 53 | // Handle sub1 sub-command 54 | case "sub2": 55 | // Handle sub2 sub-command 56 | } 57 | } 58 | ``` 59 | 60 | #### Help output 61 | 62 | ![](./_example/figures/sub-expand-example.png) 63 | 64 | 65 | # [License](LICENSE) 66 | 67 | The MIT License (MIT) 68 | -------------------------------------------------------------------------------- /colorflag.go: -------------------------------------------------------------------------------- 1 | package colorflag 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/fatih/color" 9 | "github.com/mattn/go-colorable" 10 | ) 11 | 12 | var ( 13 | // Indent indicates depth of one indent level 14 | // (the number of spaces inserted). 15 | Indent = 2 16 | 17 | // ExpandsSubCommand defines whether options and flags 18 | // of sub-commands are displayed in the top level help 19 | // message. 20 | ExpandsSubCommand = true 21 | 22 | // TitleColor specifies color of title 23 | // such as `subcommand`. 24 | TitleColor = "yellow" 25 | 26 | // FlagColor specifies color of flags 27 | FlagColor = "green" 28 | ) 29 | 30 | // colorFuncMap maps color name to colorize function 31 | var colorFuncMap = map[string](func(format string, a ...interface{}) string){ 32 | "black": color.BlackString, 33 | "red": color.RedString, 34 | "green": color.GreenString, 35 | "yellow": color.YellowString, 36 | "blue": color.BlueString, 37 | "magenda": color.MagentaString, 38 | "cyan": color.CyanString, 39 | "white": color.WhiteString, 40 | } 41 | 42 | // OutputFormatter is a formatter that constructs help 43 | // messages in a structured way. 44 | type OutputFormatter struct { 45 | Indent int 46 | currentIndent int 47 | result string 48 | currentFlags []*flag.Flag 49 | } 50 | 51 | func newOutputFormatter(indent int) *OutputFormatter { 52 | return &OutputFormatter{ 53 | Indent: indent, 54 | } 55 | } 56 | 57 | func (o *OutputFormatter) makeOffsetSpaces(n int) string { 58 | res := "" 59 | for i := 0; i < n; i++ { 60 | res += " " 61 | } 62 | return res 63 | } 64 | 65 | func (o *OutputFormatter) addIndent() { 66 | o.result += o.makeOffsetSpaces(o.currentIndent) 67 | } 68 | 69 | // InitGroup adds group name which is followd by 70 | // multiple options or flags 71 | func (o *OutputFormatter) InitGroup(groupName string) { 72 | o.addIndent() 73 | o.result += colorFuncMap[TitleColor](groupName) + "\n" 74 | o.currentIndent += o.Indent 75 | } 76 | 77 | // AddFlag adds group name which is followd by 78 | // multiple options or flags 79 | func (o *OutputFormatter) AddFlag(flg *flag.Flag) { 80 | o.currentFlags = append(o.currentFlags, flg) 81 | } 82 | 83 | // AddSubCommand adds subcommand 84 | func (o *OutputFormatter) AddSubCommand(subCommand string) { 85 | o.addIndent() 86 | o.result += fmt.Sprintf( 87 | "%v\n", 88 | colorFuncMap[FlagColor](subCommand), 89 | ) 90 | } 91 | 92 | // CloseGroup closes one group. 93 | // which break line and unshift indent 94 | func (o *OutputFormatter) CloseGroup() { 95 | flagNames := []string{} 96 | names := []string{} 97 | usages := []string{} 98 | defValues := []string{} 99 | for _, flg := range o.currentFlags { 100 | name, usage := flag.UnquoteUsage(flg) 101 | flagNames = append(flagNames, flg.Name) 102 | names = append(names, name) 103 | usages = append(usages, usage) 104 | defValues = append(defValues, flg.DefValue) 105 | } 106 | 107 | offsetSlices1 := makeOffsets(flagNames) 108 | offsetSlices2 := makeOffsets(names) 109 | 110 | for i := 0; i < len(o.currentFlags); i++ { 111 | flagName := flagNames[i] 112 | name := names[i] 113 | usage := usages[i] 114 | defValue := defValues[i] 115 | offset1 := offsetSlices1[i] 116 | offset2 := offsetSlices2[i] 117 | 118 | o.addIndent() 119 | o.result += fmt.Sprintf( 120 | "%v%v <%v>%v %v (default: %v)\n", 121 | colorFuncMap[FlagColor]("-"+flagName), 122 | o.makeOffsetSpaces(offset1), 123 | name, 124 | o.makeOffsetSpaces(offset2), 125 | usage, 126 | defValue, 127 | ) 128 | } 129 | o.result += "\n" 130 | o.currentIndent -= o.Indent 131 | o.currentFlags = []*flag.Flag{} 132 | } 133 | 134 | // Print prints constructed help message 135 | func (o *OutputFormatter) Print() { 136 | fmt.Printf(o.result) 137 | } 138 | 139 | func overrideSubCommandUsage(flagSet *flag.FlagSet) { 140 | flagSet.Usage = func() { 141 | outputFormatter := newOutputFormatter(Indent) 142 | outputFormatter.InitGroup(flagSet.Name()) 143 | flagSet.VisitAll(func(flg *flag.Flag) { 144 | outputFormatter.AddFlag(flg) 145 | }) 146 | outputFormatter.CloseGroup() 147 | outputFormatter.Print() 148 | } 149 | } 150 | 151 | func fetchFlagSet(flagSets []*flag.FlagSet, firstArg string) *flag.FlagSet { 152 | for _, flagSet := range flagSets { 153 | if flagSet.Name() == firstArg { 154 | return flagSet 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | // overrideUsages overrides usage help massege of 161 | // main command and sub commands 162 | func overrideUsages(flagSets []*flag.FlagSet) { 163 | // Override main help 164 | flag.Usage = func() { 165 | outputFormatter := newOutputFormatter(Indent) 166 | 167 | outputFormatter.InitGroup(flag.CommandLine.Name()) 168 | flag.CommandLine.VisitAll(func(flg *flag.Flag) { 169 | outputFormatter.AddFlag(flg) 170 | }) 171 | outputFormatter.CloseGroup() 172 | 173 | if len(flagSets) > 0 { 174 | outputFormatter.InitGroup("subcommands") 175 | for _, flagSet := range flagSets { 176 | if ExpandsSubCommand { 177 | outputFormatter.InitGroup(flagSet.Name()) 178 | flagSet.VisitAll(func(flg *flag.Flag) { 179 | outputFormatter.AddFlag(flg) 180 | }) 181 | outputFormatter.CloseGroup() 182 | } else { 183 | outputFormatter.AddSubCommand(flagSet.Name()) 184 | } 185 | } 186 | outputFormatter.CloseGroup() 187 | } 188 | 189 | outputFormatter.Print() 190 | } 191 | // set colorable stderr 192 | flag.CommandLine.SetOutput(colorable.NewColorableStderr()) 193 | 194 | // Override sub command help 195 | for _, flagSet := range flagSets { 196 | overrideSubCommandUsage(flagSet) 197 | // set colorable stderr 198 | flagSet.SetOutput(colorable.NewColorableStderr()) 199 | } 200 | } 201 | 202 | func validateOneColor(color string) { 203 | colorKeys := []string{} 204 | for k := range colorFuncMap { 205 | if color == k { 206 | return 207 | } 208 | colorKeys = append(colorKeys, k) 209 | } 210 | panic(fmt.Sprintf("color should be in %v", colorKeys)) 211 | } 212 | 213 | func validateColors() { 214 | validateOneColor(TitleColor) 215 | validateOneColor(FlagColor) 216 | } 217 | 218 | // Parse parse subcommands and override usage 219 | func Parse(flagSets []*flag.FlagSet) string { 220 | // validate colors 221 | validateColors() 222 | 223 | overrideUsages(flagSets) 224 | 225 | if len(os.Args) == 1 { 226 | flag.Parse() 227 | return "" 228 | } 229 | 230 | firstArg := os.Args[1] 231 | fetchedFlagSet := fetchFlagSet(flagSets, firstArg) 232 | 233 | if fetchedFlagSet == nil { 234 | flag.Parse() 235 | return "" 236 | } 237 | fetchedFlagSet.Parse(os.Args[2:]) 238 | return fetchedFlagSet.Name() 239 | } 240 | --------------------------------------------------------------------------------