diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2e5c0b6..7ba176c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,16 +16,14 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: 1.21.0 + go-version: 1.22.0 - name: Build run: go build -v ./... - - name: Install goimports - run: go install golang.org/x/tools/cmd/goimports@latest - - - name: Install gofumpt - run: go install mvdan.cc/gofumpt@latest + - run: go install golang.org/x/tools/cmd/goimports@latest + - run: go install mvdan.cc/gofumpt@latest + - run: go install honnef.co/go/tools/cmd/staticcheck@latest - name: Format run: goimports -w . && gofumpt -w . @@ -43,8 +41,5 @@ jobs: - name: Vet run: go vet -v ./... - - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@latest - - name: staticcheck run: staticcheck ./... diff --git a/clap.gen.go b/clap.gen.go index 07d0cee..f525dd9 100644 --- a/clap.gen.go +++ b/clap.gen.go @@ -2,7 +2,98 @@ package main -import "github.com/steverusso/goclap/clap" +import ( + "flag" + "fmt" + "io" + "os" + "strconv" +) + +type clapCommand struct { + usage func() string + opts []clapInput + args []clapInput +} + +type clapInput struct { + name string + value flag.Value + required bool +} + +func clapFatalf(cmdName, format string, args ...any) { + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "error: %s.\nRun '%s -h' for usage.\n", msg, cmdName) + os.Exit(2) +} + +func (cc *clapCommand) parse(args []string) ([]string, error) { + f := flag.FlagSet{Usage: func() {}} + f.SetOutput(io.Discard) + for i := range cc.opts { + o := &cc.opts[i] + f.Var(o.value, o.name, "") + } + + if err := f.Parse(args); err != nil { + if err == flag.ErrHelp { + fmt.Println(cc.usage()) + os.Exit(0) + } + return nil, err + } + + // TODO(steve): check for missing required flags when supported + + rest := f.Args() + + if len(cc.args) > 0 { + for i := range cc.args { + arg := &cc.args[i] + if len(rest) <= i { + if arg.required { + return nil, fmt.Errorf("missing required arg '%s'", arg.name) + } + return nil, nil + } + if err := arg.value.Set(rest[i]); err != nil { + return nil, fmt.Errorf("parsing positional argument '%s': %v", arg.name, err) + } + } + return nil, nil + } + + return rest, nil +} + +type clapBool bool + +func clapNewBool(p *bool) *clapBool { return (*clapBool)(p) } + +func (v *clapBool) String() string { return strconv.FormatBool(bool(*v)) } + +func (v *clapBool) Set(s string) error { + b, err := strconv.ParseBool(s) + if err != nil { + return fmt.Errorf(`invalid boolean value "%s"`, s) + } + *v = clapBool(b) + return err +} + +func (*clapBool) IsBoolFlag() bool { return true } + +type clapString string + +func clapNewString(p *string) *clapString { return (*clapString)(p) } + +func (v *clapString) String() string { return string(*v) } + +func (v *clapString) Set(s string) error { + *v = clapString(s) + return nil +} func (*goclap) UsageHelp() string { return `goclap - Pre-build tool to generate command line argument parsing code from Go comments @@ -20,12 +111,18 @@ options: } func (c *goclap) Parse(args []string) { - p := clap.NewCommandParser("goclap") - p.CustomUsage = c.UsageHelp - p.Flag("type", clap.NewString(&c.rootCmdType)) - p.Flag("srcdir", clap.NewString(&c.srcDir)) - p.Flag("with-version", clap.NewBool(&c.withVersion)) - p.Flag("out", clap.NewString(&c.outFilePath)) - p.Flag("version", clap.NewBool(&c.version)) - p.Parse(args) + p := clapCommand{ + usage: c.UsageHelp, + opts: []clapInput{ + {name: "type", value: clapNewString(&c.rootCmdType)}, + {name: "srcdir", value: clapNewString(&c.srcDir)}, + {name: "with-version", value: clapNewBool(&c.withVersion)}, + {name: "out", value: clapNewString(&c.outFilePath)}, + {name: "version", value: clapNewBool(&c.version)}, + }, + } + _, err := p.parse(args) + if err != nil { + clapFatalf("goclap", err.Error()) + } } diff --git a/clap/command.go b/clap/command.go deleted file mode 100644 index 0bc1234..0000000 --- a/clap/command.go +++ /dev/null @@ -1,118 +0,0 @@ -package clap - -import ( - "flag" - "fmt" - "os" -) - -type CommandParser struct { - CustomUsage func() string - - path string - flags []Input - args []Input -} - -func NewCommandParser(path string) CommandParser { - return CommandParser{ - path: path, - } -} - -func (p *CommandParser) Flag(name string, v flag.Value) *Input { - p.flags = append(p.flags, Input{ - name: name, - value: v, - }) - return &p.flags[len(p.flags)-1] -} - -func (p *CommandParser) Arg(name string, v flag.Value) *Input { - p.args = append(p.args, Input{ - name: name, - value: v, - }) - return &p.args[len(p.args)-1] -} - -func (p *CommandParser) Fatalf(format string, args ...any) { - msg := fmt.Sprintf(format, args...) - fmt.Fprintf(os.Stderr, "error: %s.\nRun '%s -h' for usage.\n", msg, p.path) - os.Exit(2) -} - -func (p *CommandParser) Parse(args []string) []string { - f := flag.FlagSet{Usage: func() {}} - for _, opt := range p.flags { - if err := opt.parseEnv(); err != nil { - p.Fatalf("%v", err) - } - f.Var(opt.value, opt.name, "") - } - - if err := f.Parse(args); err != nil { - if err == flag.ErrHelp { - fmt.Println(p.CustomUsage()) - os.Exit(0) - } - p.Fatalf("%v", err) - } - - // TODO(steve): check for missing required flags when supported - - rest := f.Args() - - if len(p.args) > 0 { - for i, arg := range p.args { - if len(rest) < i { - if arg.isRequired { - p.Fatalf("missing required arg '%s'", arg.name) - } - return nil - } - if err := arg.parseEnv(); err != nil { - p.Fatalf("%v", err) - } - if err := arg.value.Set(rest[i]); err != nil { - p.Fatalf("parsing positional argument '%s': %v", arg.name, err) - } - } - return nil - } - - return f.Args() -} - -type Input struct { - name string - envVarName string - value flag.Value - isRequired bool - isPresent bool -} - -func (in *Input) Env(name string) *Input { - in.envVarName = name - return in -} - -func (in *Input) Require() *Input { - in.isRequired = true - return in -} - -func (in *Input) parseEnv() error { - if in.envVarName == "" { - return nil - } - s, ok := os.LookupEnv(in.envVarName) - if !ok { - return nil - } - in.isPresent = true - if err := in.value.Set(s); err != nil { - return fmt.Errorf("parsing env var '%s': %w", in.envVarName, err) - } - return nil -} diff --git a/clap/values.go b/clap/values.go deleted file mode 100644 index de6f311..0000000 --- a/clap/values.go +++ /dev/null @@ -1,241 +0,0 @@ -package clap - -import ( - "flag" - "fmt" - "strconv" -) - -var ( - _ = (flag.Value)((*Bool)(nil)) - _ = (flag.Value)((*String)(nil)) - _ = (flag.Value)((*Float32)(nil)) - _ = (flag.Value)((*Float64)(nil)) - - _ = (flag.Value)((*Int)(nil)) - _ = (flag.Value)((*Int8)(nil)) - _ = (flag.Value)((*Int16)(nil)) - _ = (flag.Value)((*Int32)(nil)) - _ = (flag.Value)((*Int64)(nil)) - - _ = (flag.Value)((*Uint)(nil)) - _ = (flag.Value)((*Uint8)(nil)) - _ = (flag.Value)((*Uint16)(nil)) - _ = (flag.Value)((*Uint32)(nil)) - _ = (flag.Value)((*Uint64)(nil)) -) - -type Bool bool - -func NewBool(p *bool) *Bool { return (*Bool)(p) } - -func (v *Bool) String() string { return strconv.FormatBool(bool(*v)) } - -func (v *Bool) Set(s string) error { - b, err := strconv.ParseBool(s) - if err != nil { - return fmt.Errorf(`invalid boolean value "%s"`, s) - } - *v = Bool(b) - return err -} - -func (*Bool) IsBoolFlag() bool { return true } - -type String string - -func NewString(p *string) *String { return (*String)(p) } - -func (v *String) String() string { return string(*v) } - -func (v *String) Set(s string) error { - *v = String(s) - return nil -} - -type Float32 float32 - -func NewFloat32(p *float32) *Float32 { return (*Float32)(p) } - -func (v *Float32) String() string { return strconv.FormatFloat(float64(*v), 'g', -1, 32) } - -func (v *Float32) Set(s string) error { - f64, err := strconv.ParseFloat(s, 32) - if err != nil { - return numError(err) - } - *v = Float32(f64) - return err -} - -type Float64 float64 - -func NewFloat64(p *float64) *Float64 { return (*Float64)(p) } - -func (v *Float64) String() string { return strconv.FormatFloat(float64(*v), 'g', -1, 64) } - -func (v *Float64) Set(s string) error { - f64, err := strconv.ParseFloat(s, 64) - if err != nil { - return numError(err) - } - *v = Float64(f64) - return err -} - -type Int int - -func NewInt(p *int) *Int { return (*Int)(p) } - -func (v *Int) String() string { return strconv.Itoa(int(*v)) } - -func (v *Int) Set(s string) error { - i64, err := strconv.ParseInt(s, 0, strconv.IntSize) - if err != nil { - return numError(err) - } - *v = Int(i64) - return err -} - -type Int8 int8 - -func NewInt8(p *int8) *Int8 { return (*Int8)(p) } - -func (v *Int8) String() string { return strconv.FormatInt(int64(*v), 10) } - -func (v *Int8) Set(s string) error { - i64, err := strconv.ParseInt(s, 0, 8) - if err != nil { - return numError(err) - } - *v = Int8(i64) - return nil -} - -type Int16 int16 - -func NewInt16(p *int16) *Int16 { return (*Int16)(p) } - -func (v *Int16) String() string { return strconv.FormatInt(int64(*v), 10) } - -func (v *Int16) Set(s string) error { - i64, err := strconv.ParseInt(s, 0, 16) - if err != nil { - return numError(err) - } - *v = Int16(i64) - return nil -} - -type Int32 int32 - -func NewInt32(p *int32) *Int32 { return (*Int32)(p) } - -func (v *Int32) String() string { return strconv.FormatInt(int64(*v), 10) } - -func (v *Int32) Set(s string) error { - i64, err := strconv.ParseInt(s, 0, 32) - if err != nil { - return numError(err) - } - *v = Int32(i64) - return nil -} - -type Int64 int64 - -func NewInt64(p *int64) *Int64 { return (*Int64)(p) } - -func (v *Int64) String() string { return strconv.FormatInt(int64(*v), 10) } - -func (v *Int64) Set(s string) error { - i64, err := strconv.ParseInt(s, 0, 64) - if err != nil { - return numError(err) - } - *v = Int64(i64) - return nil -} - -type Uint uint - -func NewUint(p *uint) *Uint { return (*Uint)(p) } - -func (v *Uint) String() string { return strconv.FormatUint(uint64(*v), 10) } - -func (v *Uint) Set(s string) error { - u64, err := strconv.ParseUint(s, 0, strconv.IntSize) - if err != nil { - return numError(err) - } - *v = Uint(u64) - return err -} - -type Uint8 uint8 - -func NewUint8(p *uint8) *Uint8 { return (*Uint8)(p) } - -func (v *Uint8) String() string { return strconv.FormatUint(uint64(*v), 10) } - -func (v *Uint8) Set(s string) error { - u64, err := strconv.ParseUint(s, 0, 8) - if err != nil { - return numError(err) - } - *v = Uint8(u64) - return nil -} - -type Uint16 uint16 - -func NewUint16(p *uint16) *Uint16 { return (*Uint16)(p) } - -func (v *Uint16) String() string { return strconv.FormatUint(uint64(*v), 10) } - -func (v *Uint16) Set(s string) error { - u64, err := strconv.ParseUint(s, 0, 16) - if err != nil { - return numError(err) - } - *v = Uint16(u64) - return nil -} - -type Uint32 uint32 - -func NewUint32(p *uint32) *Uint32 { return (*Uint32)(p) } - -func (v *Uint32) String() string { return strconv.FormatUint(uint64(*v), 10) } - -func (v *Uint32) Set(s string) error { - u64, err := strconv.ParseUint(s, 0, 32) - if err != nil { - return numError(err) - } - *v = Uint32(u64) - return nil -} - -type Uint64 uint64 - -func NewUint64(p *uint64) *Uint64 { return (*Uint64)(p) } - -func (v *Uint64) String() string { return strconv.FormatUint(uint64(*v), 10) } - -func (v *Uint64) Set(s string) error { - u64, err := strconv.ParseUint(s, 0, 64) - if err != nil { - return numError(err) - } - *v = Uint64(u64) - return nil -} - -func numError(err error) error { - if ne, ok := err.(*strconv.NumError); ok { - return ne.Err - } - return err -} diff --git a/examples/posargs/clap.gen.go b/examples/posargs/clap.gen.go index 09ba627..929b5e6 100644 --- a/examples/posargs/clap.gen.go +++ b/examples/posargs/clap.gen.go @@ -2,7 +2,121 @@ package main -import "github.com/steverusso/goclap/clap" +import ( + "flag" + "fmt" + "io" + "os" + "reflect" + "strconv" +) + +type clapCommand struct { + usage func() string + opts []clapInput + args []clapInput +} + +type clapInput struct { + name string + value flag.Value + required bool +} + +func clapFatalf(cmdName, format string, args ...any) { + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "error: %s.\nRun '%s -h' for usage.\n", msg, cmdName) + os.Exit(2) +} + +func (cc *clapCommand) parse(args []string) ([]string, error) { + f := flag.FlagSet{Usage: func() {}} + f.SetOutput(io.Discard) + for i := range cc.opts { + o := &cc.opts[i] + f.Var(o.value, o.name, "") + } + + if err := f.Parse(args); err != nil { + if err == flag.ErrHelp { + fmt.Println(cc.usage()) + os.Exit(0) + } + return nil, err + } + + // TODO(steve): check for missing required flags when supported + + rest := f.Args() + + if len(cc.args) > 0 { + for i := range cc.args { + arg := &cc.args[i] + if len(rest) <= i { + if arg.required { + return nil, fmt.Errorf("missing required arg '%s'", arg.name) + } + return nil, nil + } + if err := arg.value.Set(rest[i]); err != nil { + return nil, fmt.Errorf("parsing positional argument '%s': %v", arg.name, err) + } + } + return nil, nil + } + + return rest, nil +} + +type clapString string + +func clapNewString(p *string) *clapString { return (*clapString)(p) } + +func (v *clapString) String() string { return string(*v) } + +func (v *clapString) Set(s string) error { + *v = clapString(s) + return nil +} + +type clapFloat[T float32 | float64] struct{ v *T } + +func clapNewFloat[T float32 | float64](p *T) clapFloat[T] { return clapFloat[T]{p} } + +func (v clapFloat[T]) String() string { + return strconv.FormatFloat(float64(*v.v), 'g', -1, reflect.TypeFor[T]().Bits()) +} + +func (v clapFloat[T]) Set(s string) error { + f64, err := strconv.ParseFloat(s, reflect.TypeFor[T]().Bits()) + if err != nil { + return numError(err) + } + *v.v = T(f64) + return err +} + +type clapUint[T uint | uint8 | uint16 | uint32 | uint64] struct{ v *T } + +func clapNewUint[T uint | uint8 | uint16 | uint32 | uint64](p *T) clapUint[T] { return clapUint[T]{p} } + +func (v clapUint[T]) String() string { return strconv.FormatUint(uint64(*v.v), 10) } + +func (v clapUint[T]) Set(s string) error { + u64, err := strconv.ParseUint(s, 0, reflect.TypeFor[T]().Bits()) + if err != nil { + return numError(err) + } + *v.v = T(u64) + return nil +} + +func numError(err error) error { + if ne, ok := err.(*strconv.NumError); ok { + return ne.Err + } + return err +} func (*mycli) UsageHelp() string { return `mycli - Print a few positional args @@ -20,10 +134,16 @@ arguments: } func (c *mycli) Parse(args []string) { - p := clap.NewCommandParser("mycli") - p.CustomUsage = c.UsageHelp - p.Arg("", clap.NewFloat32(&c.f32)).Require() - p.Arg("", clap.NewString(&c.str)).Require() - p.Arg("", clap.NewUint16(&c.u16)).Require() - p.Parse(args) + p := clapCommand{ + usage: c.UsageHelp, + args: []clapInput{ + {name: "", value: clapNewFloat(&c.f32), required: true}, + {name: "", value: clapNewString(&c.str), required: true}, + {name: "", value: clapNewUint(&c.u16), required: true}, + }, + } + _, err := p.parse(args) + if err != nil { + clapFatalf("mycli", err.Error()) + } } diff --git a/examples/simple/clap.gen.go b/examples/simple/clap.gen.go index 01b4ad9..0f54594 100644 --- a/examples/simple/clap.gen.go +++ b/examples/simple/clap.gen.go @@ -2,7 +2,98 @@ package main -import "github.com/steverusso/goclap/clap" +import ( + "flag" + "fmt" + "io" + "os" + "strconv" +) + +type clapCommand struct { + usage func() string + opts []clapInput + args []clapInput +} + +type clapInput struct { + name string + value flag.Value + required bool +} + +func clapFatalf(cmdName, format string, args ...any) { + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "error: %s.\nRun '%s -h' for usage.\n", msg, cmdName) + os.Exit(2) +} + +func (cc *clapCommand) parse(args []string) ([]string, error) { + f := flag.FlagSet{Usage: func() {}} + f.SetOutput(io.Discard) + for i := range cc.opts { + o := &cc.opts[i] + f.Var(o.value, o.name, "") + } + + if err := f.Parse(args); err != nil { + if err == flag.ErrHelp { + fmt.Println(cc.usage()) + os.Exit(0) + } + return nil, err + } + + // TODO(steve): check for missing required flags when supported + + rest := f.Args() + + if len(cc.args) > 0 { + for i := range cc.args { + arg := &cc.args[i] + if len(rest) <= i { + if arg.required { + return nil, fmt.Errorf("missing required arg '%s'", arg.name) + } + return nil, nil + } + if err := arg.value.Set(rest[i]); err != nil { + return nil, fmt.Errorf("parsing positional argument '%s': %v", arg.name, err) + } + } + return nil, nil + } + + return rest, nil +} + +type clapBool bool + +func clapNewBool(p *bool) *clapBool { return (*clapBool)(p) } + +func (v *clapBool) String() string { return strconv.FormatBool(bool(*v)) } + +func (v *clapBool) Set(s string) error { + b, err := strconv.ParseBool(s) + if err != nil { + return fmt.Errorf(`invalid boolean value "%s"`, s) + } + *v = clapBool(b) + return err +} + +func (*clapBool) IsBoolFlag() bool { return true } + +type clapString string + +func clapNewString(p *string) *clapString { return (*clapString)(p) } + +func (v *clapString) String() string { return string(*v) } + +func (v *clapString) Set(s string) error { + *v = clapString(s) + return nil +} func (*mycli) UsageHelp() string { return `mycli - Print a string with the option to make it uppercase @@ -19,9 +110,17 @@ arguments: } func (c *mycli) Parse(args []string) { - p := clap.NewCommandParser("mycli") - p.CustomUsage = c.UsageHelp - p.Flag("upper", clap.NewBool(&c.toUpper)) - p.Arg("", clap.NewString(&c.input)).Require() - p.Parse(args) + p := clapCommand{ + usage: c.UsageHelp, + opts: []clapInput{ + {name: "upper", value: clapNewBool(&c.toUpper)}, + }, + args: []clapInput{ + {name: "", value: clapNewString(&c.input), required: true}, + }, + } + _, err := p.parse(args) + if err != nil { + clapFatalf("mycli", err.Error()) + } } diff --git a/examples/simple_env/clap.gen.go b/examples/simple_env/clap.gen.go index 122b035..9e238df 100644 --- a/examples/simple_env/clap.gen.go +++ b/examples/simple_env/clap.gen.go @@ -2,7 +2,128 @@ package main -import "github.com/steverusso/goclap/clap" +import ( + "flag" + "fmt" + "io" + "os" + "reflect" + "strconv" +) + +type clapCommand struct { + usage func() string + opts []clapInput + args []clapInput +} + +type clapInput struct { + name string + envName string + value flag.Value + required bool +} + +func (in *clapInput) parseEnv() error { + if in.envName == "" { + return nil + } + s, ok := os.LookupEnv(in.envName) + if !ok { + return nil + } + if err := in.value.Set(s); err != nil { + return fmt.Errorf("parsing env var '%s': %w", in.envName, err) + } + return nil +} + +func clapFatalf(cmdName, format string, args ...any) { + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "error: %s.\nRun '%s -h' for usage.\n", msg, cmdName) + os.Exit(2) +} + +func (cc *clapCommand) parse(args []string) ([]string, error) { + f := flag.FlagSet{Usage: func() {}} + f.SetOutput(io.Discard) + for i := range cc.opts { + o := &cc.opts[i] + if err := o.parseEnv(); err != nil { + return nil, err + } + f.Var(o.value, o.name, "") + } + + if err := f.Parse(args); err != nil { + if err == flag.ErrHelp { + fmt.Println(cc.usage()) + os.Exit(0) + } + return nil, err + } + + // TODO(steve): check for missing required flags when supported + + rest := f.Args() + + if len(cc.args) > 0 { + for i := range cc.args { + arg := &cc.args[i] + if err := arg.parseEnv(); err != nil { + return nil, err + } + } + for i := range cc.args { + arg := &cc.args[i] + if len(rest) <= i { + if arg.required { + return nil, fmt.Errorf("missing required arg '%s'", arg.name) + } + return nil, nil + } + if err := arg.value.Set(rest[i]); err != nil { + return nil, fmt.Errorf("parsing positional argument '%s': %v", arg.name, err) + } + } + return nil, nil + } + + return rest, nil +} + +type clapString string + +func clapNewString(p *string) *clapString { return (*clapString)(p) } + +func (v *clapString) String() string { return string(*v) } + +func (v *clapString) Set(s string) error { + *v = clapString(s) + return nil +} + +type clapUint[T uint | uint8 | uint16 | uint32 | uint64] struct{ v *T } + +func clapNewUint[T uint | uint8 | uint16 | uint32 | uint64](p *T) clapUint[T] { return clapUint[T]{p} } + +func (v clapUint[T]) String() string { return strconv.FormatUint(uint64(*v.v), 10) } + +func (v clapUint[T]) Set(s string) error { + u64, err := strconv.ParseUint(s, 0, reflect.TypeFor[T]().Bits()) + if err != nil { + return numError(err) + } + *v.v = T(u64) + return nil +} + +func numError(err error) error { + if ne, ok := err.(*strconv.NumError); ok { + return ne.Err + } + return err +} func (*mycli) UsageHelp() string { return `mycli - Print a string with a prefix @@ -20,10 +141,18 @@ arguments: } func (c *mycli) Parse(args []string) { - p := clap.NewCommandParser("mycli") - p.CustomUsage = c.UsageHelp - p.Flag("prefix", clap.NewString(&c.prefix)).Env("MY_PREFIX") - p.Flag("count", clap.NewUint(&c.count)).Env("MY_COUNT") - p.Arg("[input]", clap.NewString(&c.input)).Env("MY_INPUT") - p.Parse(args) + p := clapCommand{ + usage: c.UsageHelp, + opts: []clapInput{ + {name: "prefix", value: clapNewString(&c.prefix), envName: "MY_PREFIX"}, + {name: "count", value: clapNewUint(&c.count), envName: "MY_COUNT"}, + }, + args: []clapInput{ + {name: "[input]", value: clapNewString(&c.input), envName: "MY_INPUT"}, + }, + } + _, err := p.parse(args) + if err != nil { + clapFatalf("mycli", err.Error()) + } } diff --git a/examples/strops/clap.gen.go b/examples/strops/clap.gen.go index 76cc360..1865954 100644 --- a/examples/strops/clap.gen.go +++ b/examples/strops/clap.gen.go @@ -2,7 +2,121 @@ package main -import "github.com/steverusso/goclap/clap" +import ( + "flag" + "fmt" + "io" + "os" + "reflect" + "strconv" +) + +type clapCommand struct { + usage func() string + opts []clapInput + args []clapInput +} + +type clapInput struct { + name string + value flag.Value + required bool +} + +func clapFatalf(cmdName, format string, args ...any) { + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "error: %s.\nRun '%s -h' for usage.\n", msg, cmdName) + os.Exit(2) +} + +func (cc *clapCommand) parse(args []string) ([]string, error) { + f := flag.FlagSet{Usage: func() {}} + f.SetOutput(io.Discard) + for i := range cc.opts { + o := &cc.opts[i] + f.Var(o.value, o.name, "") + } + + if err := f.Parse(args); err != nil { + if err == flag.ErrHelp { + fmt.Println(cc.usage()) + os.Exit(0) + } + return nil, err + } + + // TODO(steve): check for missing required flags when supported + + rest := f.Args() + + if len(cc.args) > 0 { + for i := range cc.args { + arg := &cc.args[i] + if len(rest) <= i { + if arg.required { + return nil, fmt.Errorf("missing required arg '%s'", arg.name) + } + return nil, nil + } + if err := arg.value.Set(rest[i]); err != nil { + return nil, fmt.Errorf("parsing positional argument '%s': %v", arg.name, err) + } + } + return nil, nil + } + + return rest, nil +} + +type clapBool bool + +func clapNewBool(p *bool) *clapBool { return (*clapBool)(p) } + +func (v *clapBool) String() string { return strconv.FormatBool(bool(*v)) } + +func (v *clapBool) Set(s string) error { + b, err := strconv.ParseBool(s) + if err != nil { + return fmt.Errorf(`invalid boolean value "%s"`, s) + } + *v = clapBool(b) + return err +} + +func (*clapBool) IsBoolFlag() bool { return true } + +type clapString string + +func clapNewString(p *string) *clapString { return (*clapString)(p) } + +func (v *clapString) String() string { return string(*v) } + +func (v *clapString) Set(s string) error { + *v = clapString(s) + return nil +} + +type clapInt[T int | int8 | int16 | int32 | int64] struct{ v *T } + +func clapNewInt[T int | int8 | int16 | int32 | int64](p *T) clapInt[T] { return clapInt[T]{p} } + +func (v clapInt[T]) String() string { return strconv.FormatInt(int64(*v.v), 10) } + +func (v clapInt[T]) Set(s string) error { + u64, err := strconv.ParseInt(s, 0, reflect.TypeFor[T]().Bits()) + if err != nil { + return numError(err) + } + *v.v = T(u64) + return nil +} + +func numError(err error) error { + if ne, ok := err.(*strconv.NumError); ok { + return ne.Err + } + return err +} func (*strops) UsageHelp() string { return `strops - Perform different string operations @@ -22,12 +136,20 @@ arguments: } func (c *strops) Parse(args []string) { - p := clap.NewCommandParser("strops") - p.CustomUsage = c.UsageHelp - p.Flag("upper", clap.NewBool(&c.toUpper)) - p.Flag("reverse", clap.NewBool(&c.reverse)) - p.Flag("repeat", clap.NewInt(&c.repeat)) - p.Flag("prefix", clap.NewString(&c.prefix)) - p.Arg("", clap.NewString(&c.input)).Require() - p.Parse(args) + p := clapCommand{ + usage: c.UsageHelp, + opts: []clapInput{ + {name: "upper", value: clapNewBool(&c.toUpper)}, + {name: "reverse", value: clapNewBool(&c.reverse)}, + {name: "repeat", value: clapNewInt(&c.repeat)}, + {name: "prefix", value: clapNewString(&c.prefix)}, + }, + args: []clapInput{ + {name: "", value: clapNewString(&c.input), required: true}, + }, + } + _, err := p.parse(args) + if err != nil { + clapFatalf("strops", err.Error()) + } } diff --git a/generate.go b/generate.go index b8cb3c2..f82391d 100644 --- a/generate.go +++ b/generate.go @@ -11,14 +11,10 @@ import ( const maxUsgLineLen = 90 -const headerTmplText = `// generated by goclap{{ with .Version }} ({{ . }}){{ end }}; DO NOT EDIT - -package {{ .PkgName }} - -import "github.com/steverusso/goclap/clap" -` - var ( + //go:embed tmpls/base-unexported.go.tmpl + baseUnexportedTmplText string + //go:embed tmpls/usagefunc.go.tmpl usgFnTmplText string @@ -31,7 +27,7 @@ func generate(incVersion bool, pkgName string, root *command) ([]byte, error) { if err != nil { return nil, fmt.Errorf("initializing generator: %w", err) } - if err = g.writeHeader(incVersion, pkgName, root); err != nil { + if err = g.writeBase(incVersion, pkgName, root); err != nil { return nil, err } if err = g.genCommandCode(root); err != nil { @@ -65,26 +61,44 @@ func newGenerator() (generator, error) { } type headerData struct { - PkgName string - Version string -} - -func (g *generator) writeHeader(incVersion bool, pkgName string, root *command) error { + PkgName string + Version string + HasBool bool + HasFloat bool + HasInt bool + HasUint bool + HasNumber bool + HasSubcmds bool + Types typeSet + NeedsEnvCode bool +} + +func (g *generator) writeBase(incVersion bool, pkgName string, root *command) error { ts := typeSet{} root.getTypes(ts) - t, err := template.New("header").Parse(headerTmplText) - if err != nil { - return fmt.Errorf("parsing header template: %w", err) + hasFloat := ts.HasAny("float32", "float64") + hasInt := ts.HasAny("int", "int8", "int16", "int32", "int64") + hasUint := ts.HasAny("uint", "uint8", "uint16", "uint32", "uint64") + + data := headerData{ + PkgName: pkgName, + Types: ts, + HasBool: ts.HasAny("bool"), + HasFloat: hasFloat, + HasInt: hasInt, + HasUint: hasUint, + HasNumber: hasFloat || hasInt || hasUint, + HasSubcmds: root.HasSubcmds(), + NeedsEnvCode: root.HasEnvArgOrOptSomewhere(), } - - data := headerData{PkgName: pkgName} if incVersion { data.Version = getBuildVersionInfo().String() } - if err = t.Execute(&g.buf, data); err != nil { - return fmt.Errorf("executing header template: %w", err) + baseTmpl := template.Must(template.New("clapbase").Parse(baseUnexportedTmplText)) + if err := baseTmpl.Execute(&g.buf, &data); err != nil { + return fmt.Errorf("executing base template: %w", err) } return nil } @@ -105,31 +119,30 @@ func (c *command) getTypes(ts typeSet) { type typeSet map[basicType]struct{} -var clapValueTypes = map[basicType]string{ - // int* - "int": "Int", - "int8": "Int8", - "int16": "Int16", - "int32": "Int32", - "int64": "Int64", - // uint* - "uint": "Uint", - "uint8": "Uint8", - "uint16": "Uint16", - "uint32": "Uint32", - "uint64": "Uint64", - // float* - "float32": "Float32", - "float64": "Float64", - // misc - "bool": "Bool", - "string": "String", - "byte": "Uint8", - "rune": "Int32", +func (ts typeSet) HasAny(names ...basicType) bool { + for i := range names { + if _, ok := ts[names[i]]; ok { + return true + } + } + return false } func (t basicType) ClapValueType() string { - return clapValueTypes[t] + switch t { + case "bool": + return "Bool" + case "string": + return "String" + case "float32", "float64": + return "Float" + case "int", "int8", "int16", "int32", "int64", "rune": + return "Int" + case "uint", "uint8", "uint16", "uint32", "uint64", "byte": + return "Uint" + default: + panic("unknown basic type: " + t) + } } func (g *generator) genCommandCode(c *command) error { @@ -322,6 +335,36 @@ func (c *command) Usg(nameWidth int) string { return paddedName + wrapBlurb(c.Data.Blurb, len(paddedName), maxUsgLineLen) } +// HasEnvArgOrOptSomewhere returns true if this command or one of its subcommands contains +// an option or an argument that uses an environment variable config. +func (c *command) HasEnvArgOrOptSomewhere() bool { + for i := range c.Opts { + if _, ok := c.Opts[i].data.getConfig("env"); ok { + return true + } + } + for i := range c.Args { + if _, ok := c.Args[i].data.getConfig("env"); ok { + return true + } + } + for i := range c.Subcmds { + if c.Subcmds[i].HasEnvArgOrOptSomewhere() { + return true + } + } + return false +} + +func (c *command) HasNonHelpOpts() bool { + for i := range c.Opts { + if c.Opts[i].Name != "h" { + return true + } + } + return false +} + func (c *command) HasSubcmds() bool { return len(c.Subcmds) > 0 } func wrapBlurb(v string, indentLen, lineLen int) string { diff --git a/go.mod b/go.mod index 20f474d..c4f0a67 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/steverusso/goclap -go 1.20 +go 1.22 retract v0.0.1-alpha // Incorrect module path diff --git a/tmpls/base-unexported.go.tmpl b/tmpls/base-unexported.go.tmpl new file mode 100644 index 0000000..cbb77ec --- /dev/null +++ b/tmpls/base-unexported.go.tmpl @@ -0,0 +1,222 @@ +// generated by goclap{{ with .Version }} ({{ . }}){{ end }}; DO NOT EDIT + +package {{ .PkgName }} + +import ( + {{- if .HasSubcmds }} + "errors"{{ end }} + "flag" + "fmt" + "io" + "os" + {{- if or .HasNumber }} + "reflect"{{ end }} + {{- if or .HasNumber .HasBool }} + "strconv"{{ end }} +) + +type clapCommand struct { + usage func() string + opts []clapInput + args []clapInput + {{- if .HasSubcmds }} + cmds []string{{ end }} +} + +type clapInput struct { + name string + {{- if .NeedsEnvCode }} + envName string{{ end }} + value flag.Value + required bool +} + +{{- if .NeedsEnvCode }} + +func (in *clapInput) parseEnv() error { + if in.envName == "" { + return nil + } + s, ok := os.LookupEnv(in.envName) + if !ok { + return nil + } + if err := in.value.Set(s); err != nil { + return fmt.Errorf("parsing env var '%s': %w", in.envName, err) + } + return nil +} +{{- end }} + +func clapFatalf(cmdName, format string, args ...any) { + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "error: %s.\nRun '%s -h' for usage.\n", msg, cmdName) + os.Exit(2) +} + +func (cc *clapCommand) parse(args []string) ([]string, error) { + f := flag.FlagSet{Usage: func() {}} + f.SetOutput(io.Discard) + for i := range cc.opts { + o := &cc.opts[i] + {{- if .NeedsEnvCode }} + if err := o.parseEnv(); err != nil { + return nil, err + } + {{- end }} + f.Var(o.value, o.name, "") + } + + if err := f.Parse(args); err != nil { + if err == flag.ErrHelp { + fmt.Println(cc.usage()) + os.Exit(0) + } + return nil, err + } + + // TODO(steve): check for missing required flags when supported + + rest := f.Args() + + if len(cc.args) > 0 { + {{- if .NeedsEnvCode }} + for i := range cc.args { + arg := &cc.args[i] + if err := arg.parseEnv(); err != nil { + return nil, err + } + } + {{- end }} + for i := range cc.args { + arg := &cc.args[i] + if len(rest) <= i { + if arg.required { + return nil, fmt.Errorf("missing required arg '%s'", arg.name) + } + return nil, nil + } + if err := arg.value.Set(rest[i]); err != nil { + return nil, fmt.Errorf("parsing positional argument '%s': %v", arg.name, err) + } + } + return nil, nil + } + + {{- if .HasSubcmds }} + + if len(cc.cmds) > 0 { + if len(rest) == 0 { + return rest, errors.New("no subcommand provided") + } + for i := range cc.cmds { + if rest[0] == cc.cmds[i] { + return rest, nil + } + } + return rest, fmt.Errorf("unknown subcommand '%s'", rest[0]) + } + {{- end }} + + return rest, nil +} + +{{- if .Types.HasAny "bool" }} + +type clapBool bool + +func clapNewBool(p *bool) *clapBool { return (*clapBool)(p) } + +func (v *clapBool) String() string { return strconv.FormatBool(bool(*v)) } + +func (v *clapBool) Set(s string) error { + b, err := strconv.ParseBool(s) + if err != nil { + return fmt.Errorf(`invalid boolean value "%s"`, s) + } + *v = clapBool(b) + return err +} + +func (*clapBool) IsBoolFlag() bool { return true } +{{- end }} + +{{- if .Types.HasAny "string" }} + +type clapString string + +func clapNewString(p *string) *clapString { return (*clapString)(p) } + +func (v *clapString) String() string { return string(*v) } + +func (v *clapString) Set(s string) error { + *v = clapString(s) + return nil +} +{{- end }} + +{{- if .HasFloat }} + +type clapFloat[T float32 | float64] struct{ v *T } + +func clapNewFloat[T float32 | float64](p *T) clapFloat[T] { return clapFloat[T]{p} } + +func (v clapFloat[T]) String() string { + return strconv.FormatFloat(float64(*v.v), 'g', -1, reflect.TypeFor[T]().Bits()) +} + +func (v clapFloat[T]) Set(s string) error { + f64, err := strconv.ParseFloat(s, reflect.TypeFor[T]().Bits()) + if err != nil { + return numError(err) + } + *v.v = T(f64) + return err +} +{{- end }} + +{{- if .HasInt }} + +type clapInt[T int | int8 | int16 | int32 | int64] struct{ v *T } + +func clapNewInt[T int | int8 | int16 | int32 | int64](p *T) clapInt[T] { return clapInt[T]{p} } + +func (v clapInt[T]) String() string { return strconv.FormatInt(int64(*v.v), 10) } + +func (v clapInt[T]) Set(s string) error { + u64, err := strconv.ParseInt(s, 0, reflect.TypeFor[T]().Bits()) + if err != nil { + return numError(err) + } + *v.v = T(u64) + return nil +} +{{- end }} + +{{- if .HasUint }} + +type clapUint[T uint | uint8 | uint16 | uint32 | uint64] struct{ v *T } + +func clapNewUint[T uint | uint8 | uint16 | uint32 | uint64](p *T) clapUint[T] { return clapUint[T]{p} } + +func (v clapUint[T]) String() string { return strconv.FormatUint(uint64(*v.v), 10) } + +func (v clapUint[T]) Set(s string) error { + u64, err := strconv.ParseUint(s, 0, reflect.TypeFor[T]().Bits()) + if err != nil { + return numError(err) + } + *v.v = T(u64) + return nil +} +{{- end }} + +{{- if .HasNumber }} + +func numError(err error) error { + if ne, ok := err.(*strconv.NumError); ok { + return ne.Err + } + return err +} +{{- end }} diff --git a/tmpls/parsefunc.go.tmpl b/tmpls/parsefunc.go.tmpl index 79938ab..10e8f28 100644 --- a/tmpls/parsefunc.go.tmpl +++ b/tmpls/parsefunc.go.tmpl @@ -1,37 +1,53 @@ func (c *{{ .TypeName }}) Parse(args []string) { - p := clap.NewCommandParser("{{ .Parents }}{{ .UsgName }}") - p.CustomUsage = c.UsageHelp + p := clapCommand{ + usage: c.UsageHelp, - {{- range .Opts }} + {{- /* Options. */ -}} + {{- if .HasNonHelpOpts }} + opts: []clapInput{ + {{- range .Opts }} {{- if ne .Name "h" }} - p.Flag("{{ .Name }}", clap.New{{ .FieldType.ClapValueType }}(&c.{{ .FieldName }})) - {{- with .EnvVar }}.Env("{{ . }}"){{ end }} + {name: "{{ .Name }}", value: clapNew{{ .FieldType.ClapValueType }}(&c.{{ .FieldName }}) + {{- with .EnvVar }}, envName: "{{ . }}"{{ end }}}, {{- end }} + {{- end }} + }, {{- end }} {{- /* Arguments. */ -}} - {{- range .Args }} - p.Arg("{{ .UsgName }}", clap.New{{ .FieldType.ClapValueType }}(&c.{{ .FieldName }})) - {{- with .EnvVar }}.Env("{{ . }}"){{ end }} - {{- if .IsRequired }}.Require(){{ end }} + {{- with .Args }} + args: []clapInput{ + {{- range . }} + {name: "{{ .UsgName }}", value: clapNew{{ .FieldType.ClapValueType }}(&c.{{ .FieldName }}) + {{- if .IsRequired }}, required: true{{ end }} + {{- with .EnvVar }}, envName: "{{ . }}"{{ end }}}, + {{- end }} + }, {{- end }} - {{ with .Subcmds }}rest := {{ end }}p.Parse(args) {{- /* Subcommands. */ -}} {{- with .Subcmds }} - - if len(rest) == 0 { - p.Fatalf("no subcommand provided") + cmds: []string{ + {{- range . }} + {{ .QuotedNames }}, + {{- end }} + }, + {{- end }} } + {{ with .Subcmds }}rest{{ else }}_{{ end }}, err := p.parse(args) + if err != nil { + clapFatalf("{{ .Parents }}{{ .UsgName }}", err.Error()) + } + + {{- /* Subcommands. */ -}} + {{- with .Subcmds }} switch rest[0] { {{- range . }} case {{ .QuotedNames }}: c.{{ .FieldName }} = &{{ .TypeName }}{} c.{{ .FieldName }}.Parse(rest[1:]) {{- end }} - default: - p.Fatalf("unknown subcommand '%s'", rest[0]) } {{- end }} }