Skip to content

Commit 8afbbef

Browse files
almas-xhwbrzzlcoderabbitai[bot]
authored
feat: improve artisan command output readability (#766)
* feat: improve artisan command output readability * Update console/cli_helper.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Wenbo Han <hwbrzzl@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 003a287 commit 8afbbef

File tree

5 files changed

+517
-5
lines changed

5 files changed

+517
-5
lines changed

console/application.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ func NewApplication(name, usage, usageText, version string, artisan ...bool) con
2121
instance.Usage = usage
2222
instance.UsageText = usageText
2323
instance.Version = version
24+
instance.CommandNotFound = commandNotFound
25+
instance.OnUsageError = onUsageError
2426
isArtisan := len(artisan) > 0 && artisan[0]
2527

2628
return &Application{
@@ -38,8 +40,9 @@ func (r *Application) Register(commands []console.Command) {
3840
Action: func(ctx *cli.Context) error {
3941
return item.Handle(NewCliContext(ctx))
4042
},
41-
Category: item.Extend().Category,
42-
Flags: flagsToCliFlags(item.Extend().Flags),
43+
Category: item.Extend().Category,
44+
Flags: flagsToCliFlags(item.Extend().Flags),
45+
OnUsageError: onUsageError,
4346
}
4447
r.instance.Commands = append(r.instance.Commands, &cliCommand)
4548
}
@@ -105,7 +108,7 @@ func (r *Application) Run(args []string, exitIfArtisan bool) error {
105108
}
106109

107110
if exitIfArtisan {
108-
os.Exit(1)
111+
os.Exit(0)
109112
}
110113
}
111114

console/cli_helper.go

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
package console
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"sort"
8+
"strings"
9+
"text/tabwriter"
10+
"text/template"
11+
12+
"github.com/charmbracelet/huh"
13+
"github.com/urfave/cli/v2"
14+
"github.com/xrash/smetrics"
15+
16+
"github.com/goravel/framework/support/color"
17+
)
18+
19+
func init() {
20+
cli.HelpPrinterCustom = printHelpCustom
21+
cli.AppHelpTemplate = appHelpTemplate
22+
cli.CommandHelpTemplate = commandHelpTemplate
23+
cli.VersionPrinter = printVersion
24+
huh.ErrUserAborted = cli.Exit(color.Red().Sprint("Cancelled."), 0)
25+
}
26+
27+
const maxLineLength = 10000
28+
29+
var usageTemplate = `{{if .UsageText}}{{wrap (colorize .UsageText) 3}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [options]{{end}}{{if .ArgsUsage}}{{.ArgsUsage}}{{else}}{{if .Args}} [arguments...]{{end}}{{end}}{{end}}`
30+
var commandTemplate = `{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCategories}}{{if .Name}}
31+
{{yellow .Name}}:{{end}}{{range .VisibleCommands}}
32+
{{$s := join .Names ", "}}{{green $s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap (colorize .Usage) $cv}}{{end}}{{end}}`
33+
34+
var flagTemplate = `{{ $cv := offsetFlags .VisibleFlags 5}}{{range .VisibleFlags}}
35+
{{$s := getFlagName .}}{{green $s}}{{ $sp := subtract $cv (offset $s 1) }}{{ indent $sp ""}}{{$us := (capitalize .Usage)}}{{wrap (colorize $us) $cv}}{{$df := getFlagDefaultText . }}{{if $df}} {{yellow $df}}{{end}}{{end}}`
36+
37+
var appHelpTemplate = `{{$v := offset .Usage 6}}{{wrap (colorize .Usage) 3}}{{if .Version}} {{green (wrap .Version $v)}}{{end}}
38+
39+
{{ yellow "Usage:" }}
40+
{{if .UsageText}}{{wrap (colorize .UsageText) 3}}{{end}}{{if .VisibleFlags}}
41+
42+
{{ yellow "Options:" }}{{template "flagTemplate" .}}{{end}}{{if .VisibleCommands}}
43+
44+
{{ yellow "Available commands:" }}{{template "commandTemplate" .}}{{end}}
45+
`
46+
47+
var commandHelpTemplate = `{{ yellow "Description:" }}
48+
{{ (colorize .Usage) }}
49+
50+
{{ yellow "Usage:" }}
51+
{{template "usageTemplate" .}}{{if .VisibleFlags}}
52+
53+
{{ yellow "Options:" }}{{template "flagTemplate" .}}{{end}}
54+
`
55+
56+
var colorsFuncMap = template.FuncMap{
57+
"green": color.Green().Sprint,
58+
"red": color.Red().Sprint,
59+
"blue": color.Blue().Sprint,
60+
"yellow": color.Yellow().Sprint,
61+
"cyan": color.Cyan().Sprint,
62+
"white": color.White().Sprint,
63+
"gray": color.Gray().Sprint,
64+
"default": color.Default().Sprint,
65+
"black": color.Black().Sprint,
66+
"magenta": color.Magenta().Sprint,
67+
}
68+
69+
var colorizeTemp = template.New("colorize").Funcs(colorsFuncMap)
70+
71+
func subtract(a, b int) int {
72+
return a - b
73+
}
74+
75+
func indent(spaces int, v string) string {
76+
pad := strings.Repeat(" ", spaces)
77+
return pad + strings.Replace(v, "\n", "\n"+pad, -1)
78+
}
79+
80+
func wrap(input string, offset int) string {
81+
var ss []string
82+
83+
lines := strings.Split(input, "\n")
84+
85+
padding := strings.Repeat(" ", offset)
86+
87+
for i, line := range lines {
88+
if line == "" {
89+
ss = append(ss, line)
90+
} else {
91+
wrapped := wrapLine(line, offset, padding)
92+
if i == 0 {
93+
ss = append(ss, wrapped)
94+
} else {
95+
ss = append(ss, padding+wrapped)
96+
97+
}
98+
99+
}
100+
}
101+
102+
return strings.Join(ss, "\n")
103+
}
104+
105+
func wrapLine(input string, offset int, padding string) string {
106+
if maxLineLength <= offset || len(input) <= maxLineLength-offset {
107+
return input
108+
}
109+
110+
lineWidth := maxLineLength - offset
111+
words := strings.Fields(input)
112+
if len(words) == 0 {
113+
return input
114+
}
115+
116+
wrapped := words[0]
117+
spaceLeft := lineWidth - len(wrapped)
118+
for _, word := range words[1:] {
119+
if len(word)+1 > spaceLeft {
120+
wrapped += "\n" + padding + word
121+
spaceLeft = lineWidth - len(word)
122+
} else {
123+
wrapped += " " + word
124+
spaceLeft -= 1 + len(word)
125+
}
126+
}
127+
128+
return wrapped
129+
}
130+
131+
func offset(input string, fixed int) int {
132+
return len(input) + fixed
133+
}
134+
135+
func offsetCommands(cmd []*cli.Command, fixed int) int {
136+
var maxLen = 0
137+
for i := range cmd {
138+
if s := strings.Join(cmd[i].Names(), ", "); len(s) > maxLen {
139+
maxLen = len(s)
140+
}
141+
}
142+
return maxLen + fixed
143+
}
144+
145+
func offsetFlags(flags []cli.Flag, fixed int) int {
146+
var maxLen = 0
147+
for i := range flags {
148+
if s := cli.FlagNamePrefixer(flags[i].Names(), ""); len(s) > maxLen {
149+
maxLen = len(s)
150+
}
151+
}
152+
return maxLen + fixed
153+
}
154+
155+
func getFlagName(flag cli.DocGenerationFlag) string {
156+
names := flag.Names()
157+
sort.Slice(names, func(i, j int) bool {
158+
return len(names[i]) < len(names[j])
159+
})
160+
161+
return cli.FlagNamePrefixer(names, "")
162+
}
163+
164+
func getFlagDefaultText(flag cli.DocGenerationFlag) string {
165+
defaultValueString := ""
166+
if bf, ok := flag.(*cli.BoolFlag); !ok || !bf.DisableDefaultText {
167+
if s := flag.GetDefaultText(); s != "" {
168+
defaultValueString = fmt.Sprintf(`[default: %s]`, s)
169+
}
170+
}
171+
return defaultValueString
172+
}
173+
174+
func capitalize(s string) string {
175+
s = strings.TrimSpace(s)
176+
if s == "" {
177+
return s
178+
}
179+
return strings.ToUpper(s[:1]) + s[1:]
180+
}
181+
182+
func colorize(tpml string) string {
183+
if strings.Contains(tpml, "{{") && strings.Contains(tpml, "}}") {
184+
if tp, err := colorizeTemp.Parse(tpml); err == nil {
185+
var out strings.Builder
186+
if err = tp.Execute(&out, tpml); err == nil {
187+
return out.String()
188+
}
189+
}
190+
}
191+
192+
return tpml
193+
}
194+
195+
func printVersion(ctx *cli.Context) {
196+
_, _ = fmt.Fprintf(ctx.App.Writer, "%v %v\n", ctx.App.Usage, color.Green().Sprint(ctx.App.Version))
197+
}
198+
199+
func printHelpCustom(out io.Writer, templ string, data interface{}, _ map[string]interface{}) {
200+
201+
funcMap := template.FuncMap{
202+
"join": strings.Join,
203+
"subtract": subtract,
204+
"indent": indent,
205+
"trim": strings.TrimSpace,
206+
"capitalize": capitalize,
207+
"wrap": wrap,
208+
"offset": offset,
209+
"offsetCommands": offsetCommands,
210+
"offsetFlags": offsetFlags,
211+
"getFlagName": getFlagName,
212+
"getFlagDefaultText": getFlagDefaultText,
213+
"colorize": colorize,
214+
}
215+
216+
w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
217+
t := template.Must(template.New("help").Funcs(funcMap).Funcs(colorsFuncMap).Parse(templ))
218+
templates := map[string]string{
219+
"usageTemplate": usageTemplate,
220+
"commandTemplate": commandTemplate,
221+
"flagTemplate": flagTemplate,
222+
}
223+
for name, value := range templates {
224+
if _, err := t.New(name).Parse(value); err != nil {
225+
if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
226+
_, _ = fmt.Fprintf(cli.ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
227+
}
228+
}
229+
}
230+
231+
err := t.Execute(w, data)
232+
if err != nil {
233+
// If the writer is closed, t.Execute will fail, and there's nothing
234+
// we can do to recover.
235+
if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
236+
_, _ = fmt.Fprintf(cli.ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
237+
}
238+
return
239+
}
240+
_ = w.Flush()
241+
}
242+
243+
func commandNotFound(ctx *cli.Context, command string) {
244+
var (
245+
msgTxt = fmt.Sprintf("Command '%s' is not defined.", command)
246+
suggestion string
247+
)
248+
if alternatives := findAlternatives(command, func() (collection []string) {
249+
for i := range ctx.App.Commands {
250+
collection = append(collection, ctx.App.Commands[i].Names()...)
251+
}
252+
return
253+
}()); len(alternatives) > 0 {
254+
if len(alternatives) == 1 {
255+
msgTxt = msgTxt + " Did you mean this?"
256+
} else {
257+
msgTxt = msgTxt + " Did you mean one of these?"
258+
}
259+
suggestion = "\n " + strings.Join(alternatives, "\n ")
260+
}
261+
color.Errorln(msgTxt)
262+
color.Gray().Println(suggestion)
263+
}
264+
265+
func onUsageError(_ *cli.Context, err error, _ bool) error {
266+
if flag, ok := strings.CutPrefix(err.Error(), "flag provided but not defined: -"); ok {
267+
color.Red().Printfln("The '%s' option does not exist.", flag)
268+
return nil
269+
}
270+
if flag, ok := strings.CutPrefix(err.Error(), "flag needs an argument: -"); ok {
271+
color.Red().Printfln("The '%s' option requires a value.", flag)
272+
return nil
273+
}
274+
if errMsg := err.Error(); strings.HasPrefix(errMsg, "invalid value") && strings.Contains(errMsg, "for flag -") {
275+
var value, flag string
276+
if _, parseErr := fmt.Sscanf(errMsg, "invalid value %q for flag -%s", &value, &flag); parseErr == nil {
277+
color.Red().Printfln("Invalid value '%s' for option '%s'.", value, strings.TrimSuffix(flag, ":"))
278+
return nil
279+
}
280+
}
281+
282+
return err
283+
}
284+
285+
func findAlternatives(name string, collection []string) (result []string) {
286+
var (
287+
threshold = 1e3
288+
alternatives = make(map[string]float64)
289+
collectionParts = make(map[string][]string)
290+
)
291+
for i := range collection {
292+
collectionParts[collection[i]] = strings.Split(collection[i], ":")
293+
}
294+
for i, sub := range strings.Split(name, ":") {
295+
for collectionName, parts := range collectionParts {
296+
exists := alternatives[collectionName] != 0
297+
if len(parts) <= i {
298+
if exists {
299+
alternatives[collectionName] += threshold
300+
}
301+
continue
302+
}
303+
lev := smetrics.WagnerFischer(sub, parts[i], 1, 1, 1)
304+
if float64(lev) <= float64(len(sub))/3 || strings.Contains(parts[i], sub) {
305+
if exists {
306+
alternatives[collectionName] += float64(lev)
307+
} else {
308+
alternatives[collectionName] = float64(lev)
309+
}
310+
} else if exists {
311+
alternatives[collectionName] += threshold
312+
}
313+
}
314+
}
315+
for _, item := range collection {
316+
lev := smetrics.WagnerFischer(name, item, 1, 1, 1)
317+
if float64(lev) <= float64(len(name))/3 || strings.Contains(item, name) {
318+
if alternatives[item] != 0 {
319+
alternatives[item] -= float64(lev)
320+
} else {
321+
alternatives[item] = float64(lev)
322+
}
323+
}
324+
}
325+
type scoredItem struct {
326+
name string
327+
score float64
328+
}
329+
var sortedAlternatives []scoredItem
330+
for item, score := range alternatives {
331+
if score < 2*threshold {
332+
sortedAlternatives = append(sortedAlternatives, scoredItem{item, score})
333+
}
334+
}
335+
sort.Slice(sortedAlternatives, func(i, j int) bool {
336+
if sortedAlternatives[i].score == sortedAlternatives[j].score {
337+
return sortedAlternatives[i].name < sortedAlternatives[j].name
338+
}
339+
return sortedAlternatives[i].score < sortedAlternatives[j].score
340+
})
341+
for _, item := range sortedAlternatives {
342+
result = append(result, item.name)
343+
}
344+
return result
345+
}

0 commit comments

Comments
 (0)