Skip to content

Commit 73cbc83

Browse files
committed
fix usage text and github testing action
1 parent 49917cc commit 73cbc83

4 files changed

Lines changed: 138 additions & 8 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
go-version: ['1.24']
17+
go-version: ['1.24', '1.25']
1818
steps:
1919
- uses: actions/checkout@v6
2020

cli.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"flag"
66
"io"
77
"os"
8+
"strconv"
89
"strings"
910
)
1011

@@ -17,6 +18,15 @@ type Group struct {
1718
Title string
1819
}
1920

21+
// flagMeta stores metadata about a flag that has both long and short forms.
22+
type flagMeta struct {
23+
longName string
24+
shortName string
25+
usage string
26+
value flag.Value // the underlying flag.Value
27+
defValue string // default value as string
28+
}
29+
2030
// Command represents a CLI command with subcommands, flags, and an action.
2131
type Command struct {
2232
// Use is the one-line usage message. Recommended syntax: "cmd [flags] [arg...]"
@@ -102,6 +112,9 @@ type Command struct {
102112
// NEW fields for flag parsing results
103113
parsedFlags *flag.FlagSet // combined set used for parsing
104114
leftoverArgs []string // arguments after flag parsing
115+
116+
// flagMetaMap maps each flag name (both long and short) to its metadata.
117+
flagMetaMap map[string]*flagMeta
105118
}
106119

107120
// NewCommand creates a new command with the given name and description.
@@ -114,6 +127,7 @@ func NewCommand(use, short, long string) *Command {
114127
inReader: os.Stdin,
115128
outWriter: os.Stdout,
116129
errWriter: os.Stderr,
130+
flagMetaMap: make(map[string]*flagMeta),
117131
}
118132
cmd.localFlags = flag.NewFlagSet(use, flag.ContinueOnError)
119133
cmd.persistentFlags = flag.NewFlagSet(use, flag.ContinueOnError)
@@ -232,3 +246,71 @@ func (c *Command) persistentFlagSets() []*flag.FlagSet {
232246
}
233247
return sets
234248
}
249+
250+
// ---------- Combined Flag Methods ----------
251+
252+
// IntVarP defines an integer flag with both long and short names.
253+
// It sets the variable p to the value given on the command line.
254+
func (c *Command) IntVarP(p *int, long, short string, value int, usage string) {
255+
// Register long flag (visible)
256+
c.localFlags.IntVar(p, long, value, usage)
257+
// Register short flag (hidden by skipping in help)
258+
c.localFlags.IntVar(p, short, value, usage)
259+
// Store metadata for help merging
260+
meta := &flagMeta{
261+
longName: long,
262+
shortName: short,
263+
usage: usage,
264+
value: c.localFlags.Lookup(long).Value,
265+
defValue: flagValueToString(value),
266+
}
267+
c.flagMetaMap[long] = meta
268+
c.flagMetaMap[short] = meta
269+
}
270+
271+
// StringVarP defines a string flag with both long and short names.
272+
func (c *Command) StringVarP(p *string, long, short string, value string, usage string) {
273+
c.localFlags.StringVar(p, long, value, usage)
274+
c.localFlags.StringVar(p, short, value, usage)
275+
meta := &flagMeta{
276+
longName: long,
277+
shortName: short,
278+
usage: usage,
279+
value: c.localFlags.Lookup(long).Value,
280+
defValue: value,
281+
}
282+
c.flagMetaMap[long] = meta
283+
c.flagMetaMap[short] = meta
284+
}
285+
286+
// BoolVarP defines a boolean flag with both long and short names.
287+
func (c *Command) BoolVarP(p *bool, long, short string, value bool, usage string) {
288+
c.localFlags.BoolVar(p, long, value, usage)
289+
c.localFlags.BoolVar(p, short, value, usage)
290+
meta := &flagMeta{
291+
longName: long,
292+
shortName: short,
293+
usage: usage,
294+
value: c.localFlags.Lookup(long).Value,
295+
defValue: flagValueToString(value),
296+
}
297+
c.flagMetaMap[long] = meta
298+
c.flagMetaMap[short] = meta
299+
}
300+
301+
// helper to convert a flag's default value to a string representation
302+
func flagValueToString(v interface{}) string {
303+
switch val := v.(type) {
304+
case int:
305+
return strconv.Itoa(val)
306+
case bool:
307+
if val {
308+
return "true"
309+
}
310+
return "false"
311+
case string:
312+
return val
313+
default:
314+
return ""
315+
}
316+
}

help.go

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (c *Command) Help() {
4343
fmt.Fprintf(w, " %s:\n", group.Title)
4444
}
4545
for _, cmd := range group.Commands {
46-
line := fmt.Sprintf(" %s\t%s", cmd.Name(), cmd.Short)
46+
line := fmt.Sprintf(" %s\t\t%s", cmd.Name(), cmd.Short)
4747
if cmd.Deprecated != "" {
4848
line += " (deprecated)"
4949
}
@@ -61,17 +61,63 @@ func (c *Command) usageLine() string {
6161
return c.Use
6262
}
6363

64+
// printFlags prints flags, merging long and short versions where defined.
6465
func (c *Command) printFlags(w io.Writer, fs *flag.FlagSet) {
66+
// Track which flag names we've already printed as part of a combined pair
67+
printed := make(map[string]bool)
68+
69+
// First, print combined flags
70+
for _, meta := range c.flagMetaMap {
71+
// We only want to print each meta once, but it's stored under two keys.
72+
// Use a set of the meta pointers to avoid duplicates.
73+
// For simplicity, we'll just check if we've printed the long name.
74+
if printed[meta.longName] {
75+
continue
76+
}
77+
printed[meta.longName] = true
78+
printed[meta.shortName] = true
79+
80+
// check if empty
81+
if meta.shortName == "" {
82+
line := fmt.Sprintf(" --%s", meta.longName)
83+
if meta.defValue != "" {
84+
line += fmt.Sprintf("\t\t%s (Default: %s)", meta.usage, meta.defValue)
85+
}
86+
fmt.Fprintf(w, "%s\n", line)
87+
continue
88+
} else {
89+
// check if long name is empty (should not happen in combined flags, but just in case)
90+
if meta.longName == "" {
91+
line := fmt.Sprintf(" -%s", meta.shortName)
92+
if meta.defValue != "" {
93+
line += fmt.Sprintf("\t\t%s (Default: %s)", meta.usage, meta.defValue)
94+
}
95+
fmt.Fprintf(w, "%s\n", line)
96+
continue
97+
}
98+
}
99+
100+
// Build the flag line with both short and long names
101+
line := fmt.Sprintf(" -%s, --%s", meta.shortName, meta.longName)
102+
if meta.defValue != "" {
103+
line += fmt.Sprintf("\t\t%s (Default: %s)", meta.usage, meta.defValue)
104+
}
105+
fmt.Fprintf(w, "%s\n", line)
106+
}
107+
108+
// Then, print any remaining flags that are not part of a combined pair
65109
fs.VisitAll(func(f *flag.Flag) {
66-
// skip help flags to avoid duplication
67110
if f.Name == "help" || f.Name == "h" {
68111
return
69112
}
70-
fmt.Fprintf(w, " -%s", f.Name)
113+
if printed[f.Name] {
114+
return
115+
}
116+
line := fmt.Sprintf(" -%s", f.Name)
71117
if f.DefValue != "" {
72-
fmt.Fprintf(w, "=%s", f.DefValue)
118+
line += fmt.Sprintf("\t\t%s (Default: %s)", f.Usage, f.DefValue)
73119
}
74-
fmt.Fprintf(w, "\n \t%s\n", f.Usage)
120+
fmt.Fprintf(w, "%s\n \t%s\n", line, f.Usage)
75121
})
76122
}
77123

help_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ func TestHelpOutput(t *testing.T) {
3131
"Examples:\nexample text",
3232
"Deprecated: use newcmd instead",
3333
"Flags:",
34-
"-flag1=default",
34+
"-flag1",
35+
"Default: default",
3536
"flag1 usage",
3637
"Persistent Flags:",
3738
"-pflag",
3839
"persistent flag",
3940
"Available Commands:",
4041
"Group One:",
41-
"sub sub short", // было "sub\tsub short"
42+
"sub",
43+
"sub short",
4244
}
4345
for _, check := range checks {
4446
if !strings.Contains(out, check) {

0 commit comments

Comments
 (0)