Skip to content

Commit 9e8d93e

Browse files
committed
Iterate on parse; add another example cli
1 parent 8831926 commit 9e8d93e

File tree

7 files changed

+636
-188
lines changed

7 files changed

+636
-188
lines changed

command.go

Lines changed: 1 addition & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
package cli
22

33
import (
4-
"cmp"
54
"context"
65
"flag"
76
"fmt"
8-
"slices"
97
"strings"
108

119
"github.com/mfridman/cli/pkg/suggest"
12-
"github.com/mfridman/cli/pkg/textutil"
1310
)
1411

1512
// NoExecError is returned when a command has no execution function.
@@ -86,7 +83,7 @@ type FlagMetadata struct {
8683
// f.String("output", "", "output file")
8784
// f.Int("count", 0, "number of items")
8885
// })
89-
func FlagsFunc(fn func(*flag.FlagSet)) *flag.FlagSet {
86+
func FlagsFunc(fn func(f *flag.FlagSet)) *flag.FlagSet {
9087
fset := flag.NewFlagSet("", flag.ContinueOnError)
9188
fn(fset)
9289
return fset
@@ -117,174 +114,6 @@ func (c *Command) formatUnknownCommandError(unknownCmd string) error {
117114
return fmt.Errorf("unknown command %q", unknownCmd)
118115
}
119116

120-
func defaultUsage(c *Command) string {
121-
var b strings.Builder
122-
123-
// Handle custom usage function
124-
if c.UsageFunc != nil {
125-
return c.UsageFunc(c)
126-
}
127-
128-
// Short help section
129-
if c.ShortHelp != "" {
130-
for _, line := range textutil.Wrap(c.ShortHelp, 80) {
131-
b.WriteString(line)
132-
b.WriteRune('\n')
133-
}
134-
b.WriteRune('\n')
135-
}
136-
137-
// Usage section
138-
b.WriteString("Usage:\n ")
139-
if c.Usage != "" {
140-
b.WriteString(c.Usage)
141-
b.WriteRune('\n')
142-
} else {
143-
usage := c.Name
144-
if c.state != nil && len(c.state.commandPath) > 0 {
145-
usage = getCommandPath(c.state.commandPath)
146-
}
147-
if c.Flags != nil {
148-
usage += " [flags]"
149-
}
150-
if len(c.SubCommands) > 0 {
151-
usage += " <command>"
152-
}
153-
b.WriteString(usage)
154-
b.WriteRune('\n')
155-
}
156-
157-
// Available Commands section
158-
if len(c.SubCommands) > 0 {
159-
b.WriteString("Available Commands:\n")
160-
161-
sortedCommands := slices.Clone(c.SubCommands)
162-
slices.SortFunc(sortedCommands, func(a, b *Command) int {
163-
return cmp.Compare(a.Name, b.Name)
164-
})
165-
166-
maxLen := 0
167-
for _, sub := range sortedCommands {
168-
if len(sub.Name) > maxLen {
169-
maxLen = len(sub.Name)
170-
}
171-
}
172-
173-
for _, sub := range sortedCommands {
174-
if sub.ShortHelp == "" {
175-
fmt.Fprintf(&b, " %s\n", sub.Name)
176-
continue
177-
}
178-
179-
nameWidth := maxLen + 4
180-
wrapWidth := 80 - nameWidth
181-
182-
lines := textutil.Wrap(sub.ShortHelp, wrapWidth)
183-
padding := strings.Repeat(" ", maxLen-len(sub.Name)+4)
184-
fmt.Fprintf(&b, " %s%s%s\n", sub.Name, padding, lines[0])
185-
186-
indentPadding := strings.Repeat(" ", nameWidth+2)
187-
for _, line := range lines[1:] {
188-
fmt.Fprintf(&b, "%s%s\n", indentPadding, line)
189-
}
190-
}
191-
b.WriteRune('\n')
192-
}
193-
194-
var flags []flagInfo
195-
196-
if c.state != nil && len(c.state.commandPath) > 0 {
197-
for i, cmd := range c.state.commandPath {
198-
if cmd.Flags == nil {
199-
continue
200-
}
201-
isGlobal := i < len(c.state.commandPath)-1
202-
cmd.Flags.VisitAll(func(f *flag.Flag) {
203-
flags = append(flags, flagInfo{
204-
name: "-" + f.Name,
205-
usage: f.Usage,
206-
defval: f.DefValue,
207-
global: isGlobal,
208-
})
209-
})
210-
}
211-
}
212-
213-
if len(flags) > 0 {
214-
slices.SortFunc(flags, func(a, b flagInfo) int {
215-
return cmp.Compare(a.name, b.name)
216-
})
217-
218-
maxLen := 0
219-
for _, f := range flags {
220-
if len(f.name) > maxLen {
221-
maxLen = len(f.name)
222-
}
223-
}
224-
225-
hasLocal := false
226-
hasGlobal := false
227-
for _, f := range flags {
228-
if f.global {
229-
hasGlobal = true
230-
} else {
231-
hasLocal = true
232-
}
233-
}
234-
235-
if hasLocal {
236-
b.WriteString("Flags:\n")
237-
writeFlagSection(&b, flags, maxLen, false)
238-
b.WriteRune('\n')
239-
}
240-
241-
if hasGlobal {
242-
b.WriteString("Global Flags:\n")
243-
writeFlagSection(&b, flags, maxLen, true)
244-
b.WriteRune('\n')
245-
}
246-
}
247-
248-
// Help suggestion for subcommands
249-
if len(c.SubCommands) > 0 {
250-
fmt.Fprintf(&b, "Use \"%s [command] --help\" for more information about a command.\n",
251-
getCommandPath(c.state.commandPath))
252-
}
253-
254-
return strings.TrimRight(b.String(), "\n")
255-
}
256-
257-
// writeFlagSection writes either the local or global flags section
258-
func writeFlagSection(b *strings.Builder, flags []flagInfo, maxLen int, global bool) {
259-
for _, f := range flags {
260-
if f.global == global {
261-
nameWidth := maxLen + 4
262-
wrapWidth := 80 - nameWidth
263-
264-
usageText := f.usage
265-
if f.defval != "" && f.defval != "false" {
266-
usageText += fmt.Sprintf(" (default %s)", f.defval)
267-
}
268-
269-
lines := textutil.Wrap(usageText, wrapWidth)
270-
padding := strings.Repeat(" ", maxLen-len(f.name)+4)
271-
fmt.Fprintf(b, " %s%s%s\n", f.name, padding, lines[0])
272-
273-
indentPadding := strings.Repeat(" ", nameWidth+2)
274-
for _, line := range lines[1:] {
275-
fmt.Fprintf(b, "%s%s\n", indentPadding, line)
276-
}
277-
}
278-
}
279-
}
280-
281-
type flagInfo struct {
282-
name string
283-
usage string
284-
defval string
285-
global bool
286-
}
287-
288117
func formatFlagName(name string) string {
289118
return "-" + name
290119
}

examples/cmd/echo/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func main() {
3838
}
3939
if err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil); err != nil {
4040
if errors.Is(err, flag.ErrHelp) {
41+
fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root))
4142
return
4243
}
4344
fmt.Fprintf(os.Stderr, "error: %v\n", err)

0 commit comments

Comments
 (0)