Skip to content

Commit 4dd41ac

Browse files
committed
color schema support for help info
1 parent 80b5948 commit 4dd41ac

File tree

8 files changed

+207
-48
lines changed

8 files changed

+207
-48
lines changed

args.go

+18-8
Original file line numberDiff line numberDiff line change
@@ -145,20 +145,30 @@ func (a *arg) getIdentifier() string {
145145
return a.short
146146
}
147147

148-
func (a *arg) formatHelpHeader() string {
148+
func (a *arg) formatHelpHeader(argument, meta Color) (size int, content string) {
149149
metaName := a.getMetaName()
150150
if a.Positional {
151-
return metaName
151+
size = len(metaName)
152+
content = wrapperColor(metaName, argument)
153+
return
152154
}
155+
156+
wrapped := []string{}
153157
watchers := a.getWatchers()
154-
if a.isFlag {
155-
return strings.Join(watchers, ", ")
156-
}
157-
var signedWatchers []string
158158
for _, w := range watchers {
159-
signedWatchers = append(signedWatchers, fmt.Sprintf("%s %s", w, metaName))
159+
if a.isFlag {
160+
wrapped = append(wrapped, wrapperColor(w, argument))
161+
size += len(w)
162+
} else {
163+
wrapped = append(wrapped,
164+
fmt.Sprintf("%s %s", wrapperColor(w, argument),
165+
wrapperColor(metaName, meta)))
166+
size += len(w) + len(metaName) + 1
167+
}
160168
}
161-
return strings.Join(signedWatchers, ", ")
169+
size += (len(wrapped) - 1) * 2
170+
content = strings.Join(wrapped, ", ")
171+
return
162172
}
163173

164174
func (a *arg) formatHelpWithExtraInfo() string {

color.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package argparse
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
)
8+
9+
type Color struct {
10+
Code int
11+
Property int
12+
}
13+
14+
type ColorSchema struct {
15+
Usage Color
16+
Description Color
17+
18+
GroupTitle Color
19+
Command Color
20+
21+
Argument Color
22+
Meta Color
23+
24+
Epilog Color
25+
}
26+
27+
var NoColor = &ColorSchema{}
28+
var DefaultColor = &ColorSchema{
29+
Usage: Color{37, 1},
30+
GroupTitle: Color{32, 1},
31+
Command: Color{33, 1},
32+
Argument: Color{36, 0},
33+
Epilog: Color{37, 1},
34+
}
35+
36+
func checkTerminalColorSupport() bool {
37+
return strings.Contains(os.Getenv("TERM"), "color")
38+
}
39+
40+
func wrapperColor(content string, color Color) string {
41+
if color.Code == 0 && color.Property == 0 {
42+
return content
43+
}
44+
return fmt.Sprintf("\033[0%d;%dm%s\033[00m", color.Property, color.Code, content)
45+
}

examples/colorful/main.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// this is show case for using color in argparse
2+
//
3+
// demo for ColorSchema
4+
package main
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/hellflame/argparse"
10+
)
11+
12+
func main() {
13+
parser := argparse.NewParser("basic", "this is a basic program", &argparse.ParserConfig{
14+
WithColor: true,
15+
WithHint: true,
16+
17+
EpiLog: "more info please visit https://github.com/hellflame/argparse",
18+
})
19+
sub := parser.AddCommand("run", "run your program", nil)
20+
parser.AddCommand("test", "test for your program", nil)
21+
22+
sub.Flag("d", "dir", &argparse.Option{Help: "give me a directory"})
23+
parser.String("n", "name", &argparse.Option{Default: "flame", Help: "your name"})
24+
parser.Ints("t", "times", &argparse.Option{HintInfo: "run times", Group: "base", Help: "how many times"})
25+
parser.Float("s", "size", &argparse.Option{Help: "give me a size", Group: "base", Required: true})
26+
parser.String("u", "url", &argparse.Option{Positional: true, Help: "target url"})
27+
parser.String("l", "", nil)
28+
if e := parser.Parse(nil); e != nil {
29+
switch e {
30+
case argparse.BreakAfterHelpError:
31+
return
32+
default:
33+
fmt.Println(e.Error())
34+
}
35+
return
36+
}
37+
}

examples/inherit/main.go

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// inheritable arguments
2+
13
package main
24

35
import (

parse.go

+73-32
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,21 @@ type Parser struct {
3636

3737
// ParserConfig is the only type to config `Parser`, programmers only need to use this type to control `Parser` action
3838
type ParserConfig struct {
39-
Usage string // manual usage display
40-
EpiLog string // message after help
41-
DisableHelp bool // disable help entry register [-h/--help]
42-
ContinueOnHelp bool // set true to: continue program after default help is printed
43-
DisableDefaultShowHelp bool // set false to: default show help when there is no args to parse (default action)
44-
DefaultAction func() // set default action to replace default help action
45-
AddShellCompletion bool // set true to register shell completion entry [--completion]
46-
WithHint bool // argument help message with argument default value hint
47-
MaxHeaderLength int // max argument header length in help menu, help info will start at new line if argument meta info is too long
39+
Usage string // manual usage display
40+
EpiLog string // message after help
41+
42+
DisableHelp bool // disable help entry register [-h/--help]
43+
ContinueOnHelp bool // set true to: continue program after default help is printed
44+
DisableDefaultShowHelp bool // set false to: default show help when there is no args to parse (default action)
45+
46+
DefaultAction func() // set default action to replace default help action
47+
AddShellCompletion bool // set true to register shell completion entry [--completion]
48+
WithHint bool // argument help message with argument default value hint
49+
MaxHeaderLength int // max argument header length in help menu, help info will start at new line if argument meta info is too long
50+
51+
WithColor bool // enable colorful help message if the terminal has support for color
52+
EnsureColor bool // use color code for sure, skip terminal env check
53+
ColorSchema *ColorSchema // use given color schema to draw help info
4854
}
4955

5056
// NewParser create the parser object with optional name & description & ParserConfig
@@ -56,17 +62,20 @@ func NewParser(name string, description string, config *ParserConfig) *Parser {
5662
name = strings.ReplaceAll(path.Base(os.Args[0]), " ", "") // avoid space for shell complete code generate
5763
}
5864
parser := &Parser{
59-
name: name,
60-
description: description,
61-
config: config,
65+
name: name,
66+
description: description,
67+
config: config,
68+
6269
entries: []*arg{},
6370
entryMap: make(map[string]*arg),
6471
entryGroup: make(map[string][]*arg),
6572
entryGroupOrder: []string{},
66-
positionArgs: []*arg{},
67-
positionalPool: make(map[string]*arg),
68-
subParser: []*Parser{},
69-
subParserMap: make(map[string]*Parser),
73+
74+
positionArgs: []*arg{},
75+
positionalPool: make(map[string]*arg),
76+
77+
subParser: []*Parser{},
78+
subParserMap: make(map[string]*Parser),
7079
}
7180
if !config.DisableHelp {
7281
parser.showHelp = parser.Flag("h", "help",
@@ -156,12 +165,29 @@ func (p *Parser) PrintHelp() {
156165
fmt.Println(p.FormatHelp())
157166
}
158167

159-
// FormatHelp only format help message for manual use, for example: decide when to print help message
168+
// FormatHelp only format help message for manual use, you can decide when to print help message
160169
func (p *Parser) FormatHelp() string {
161-
result := p.formatUsage()
170+
if !p.config.WithColor {
171+
return p.FormatHelpWithColor(NoColor)
172+
}
173+
174+
if p.config.EnsureColor || checkTerminalColorSupport() {
175+
schema := DefaultColor
176+
if p.config.ColorSchema != nil {
177+
schema = p.config.ColorSchema
178+
}
179+
return p.FormatHelpWithColor(schema)
180+
}
181+
182+
return p.FormatHelpWithColor(NoColor)
183+
}
184+
185+
func (p *Parser) FormatHelpWithColor(schema *ColorSchema) string {
186+
result := wrapperColor(p.formatUsage(), schema.Usage)
162187
if p.description != "" {
163-
result += "\n\n" + p.description
188+
result += "\n\n" + wrapperColor(p.description, schema.Description)
164189
}
190+
// calculate header length
165191
headerLength := 10 // here set minimum header length, the code after will find the max length of headers
166192
for _, parser := range p.subParser {
167193
l := len(parser.name)
@@ -170,13 +196,13 @@ func (p *Parser) FormatHelp() string {
170196
}
171197
}
172198
for _, arg := range p.positionArgs {
173-
l := len(arg.formatHelpHeader())
199+
l, _ := arg.formatHelpHeader(schema.Argument, schema.Meta)
174200
if l > headerLength {
175201
headerLength = l
176202
}
177203
}
178204
for _, arg := range p.entries {
179-
l := len(arg.formatHelpHeader())
205+
l, _ := arg.formatHelpHeader(schema.Argument, schema.Meta)
180206
if l > headerLength {
181207
headerLength = l
182208
}
@@ -187,15 +213,19 @@ func (p *Parser) FormatHelp() string {
187213
headerLength = p.config.MaxHeaderLength
188214
helpBreak = true
189215
}
216+
// sub command
190217
if len(p.subParser) > 0 {
191-
section := "\n\ncommands:"
218+
section := "\n\n" + wrapperColor("commands:", schema.GroupTitle)
192219
for _, parser := range p.subParser {
193-
section += "\n" + formatHelpRow(parser.name, parser.description, headerLength, helpBreak)
220+
section += "\n" + formatHelpRow(wrapperColor(parser.name, schema.Command), parser.description,
221+
len(parser.name), headerLength, helpBreak)
194222
}
195223
result += section
196224
}
197225
withHint := p.config.WithHint
198-
if len(p.positionArgs) > 0 { // dealing positional arguments present
226+
227+
// positional arguments
228+
if len(p.positionArgs) > 0 {
199229
section := ""
200230
for _, arg := range p.positionArgs {
201231
if arg.Group != "" || arg.HideEntry {
@@ -205,13 +235,16 @@ func (p *Parser) FormatHelp() string {
205235
if withHint && !arg.NoHint {
206236
help = arg.formatHelpWithExtraInfo()
207237
}
208-
section += "\n" + formatHelpRow(arg.formatHelpHeader(), help, headerLength, helpBreak)
238+
size, header := arg.formatHelpHeader(schema.Argument, schema.Meta)
239+
section += "\n" + formatHelpRow(header, help, size, headerLength, helpBreak)
209240
}
210241
if section != "" {
211-
section = "\n\npositionals:" + section
242+
section = "\n\n" + wrapperColor("positionals:", schema.GroupTitle) + section
212243
}
213244
result += section
214245
}
246+
247+
// optional arguments
215248
if len(p.entries) > 0 { // dealing optional arguments present
216249
parsed := make(map[string]bool)
217250
section := ""
@@ -231,28 +264,36 @@ func (p *Parser) FormatHelp() string {
231264
if withHint && !arg.NoHint {
232265
help = arg.formatHelpWithExtraInfo()
233266
}
234-
section += "\n" + formatHelpRow(arg.formatHelpHeader(), help, headerLength, helpBreak)
267+
size, header := arg.formatHelpHeader(schema.Argument, schema.Meta)
268+
section += "\n" + formatHelpRow(header, help, size, headerLength, helpBreak)
235269
}
236270
if section != "" {
237-
section = "\n\noptions:" + section
271+
section = "\n\n" + wrapperColor("options:", schema.GroupTitle) + section
238272
}
239273
result += section
240274
}
241-
for _, group := range p.entryGroupOrder { // dealing arguments group present
242-
section := fmt.Sprintf("\n\n%s:", group)
275+
276+
// argument groups
277+
for _, group := range p.entryGroupOrder {
278+
section := "\n\n" + wrapperColor(group+":", schema.GroupTitle)
243279
content := ""
244280
for _, arg := range p.entryGroup[group] {
245281
if arg.HideEntry {
246282
continue
247283
}
248-
content += "\n" + formatHelpRow(arg.formatHelpHeader(), arg.Help, headerLength, helpBreak)
284+
help := arg.Help
285+
if withHint && !arg.NoHint {
286+
help = arg.formatHelpWithExtraInfo()
287+
}
288+
size, header := arg.formatHelpHeader(schema.Argument, schema.Meta)
289+
content += "\n" + formatHelpRow(header, help, size, headerLength, helpBreak)
249290
}
250291
if content != "" {
251292
result += section + content
252293
}
253294
}
254295
if p.config.EpiLog != "" {
255-
result += "\n\n" + p.config.EpiLog
296+
result += "\n\n" + wrapperColor(p.config.EpiLog, schema.Epilog)
256297
}
257298

258299
return result

parse_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package argparse
22

33
import (
44
"fmt"
5+
"os"
56
"strconv"
67
"strings"
78
"testing"
@@ -817,3 +818,21 @@ func TestBindParserConflict(t *testing.T) {
817818
a, a,
818819
}})
819820
}
821+
822+
func TestColorful(t *testing.T) {
823+
os.Setenv("TERM", "")
824+
p := NewParser("", "", &ParserConfig{WithColor: true})
825+
p.String("l", "", nil)
826+
if strings.Contains(p.FormatHelp(), "\033[00m") {
827+
t.Error("should be no color")
828+
return
829+
}
830+
831+
p = NewParser("", "", &ParserConfig{WithHint: true,
832+
WithColor: true, EnsureColor: true, ColorSchema: DefaultColor})
833+
p.String("l", "", &Option{HintInfo: "", Group: "base"})
834+
if !strings.Contains(p.FormatHelp(), "\033[00m") {
835+
t.Error("should be colorful")
836+
return
837+
}
838+
}

utils.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ func init() {
2424
}
2525
}
2626

27-
func formatHelpRow(head, content string, maxHeadLength int, withBreak bool) string {
27+
func formatHelpRow(head, content string, bareHeadLength, maxHeadLength int, withBreak bool) string {
2828
content = strings.Replace(content, "\n", "", -1)
2929
result := fmt.Sprintf(" %s ", head)
30-
headLeftPadding := maxHeadLength - len(result)
30+
headLeftPadding := maxHeadLength - bareHeadLength - 3
3131
if headLeftPadding > 0 { // fill left padding
3232
result += strings.Repeat(" ", headLeftPadding)
3333
}

0 commit comments

Comments
 (0)