From 30ccc33f3499161e755e642c0cb39ee9c96d95e5 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Thu, 19 Mar 2026 14:07:07 +1300 Subject: [PATCH] getopt: init module --- cli.go | 2 +- getopt/.LICENSE.~undo-tree~ | 4 + getopt/LICENSE | 26 ++++ getopt/bool.go | 43 ++++++ getopt/commandline.go | 180 +++++++++++++++++++++++++ getopt/duration.go | 39 ++++++ getopt/flag_test.go | 74 +++++++++++ getopt/flagset.go | 259 ++++++++++++++++++++++++++++++++++++ getopt/float.go | 33 +++++ getopt/getopt.go | 155 +++++++++++++++++++++ getopt/getopt_test.go | 89 +++++++++++++ getopt/go.mod | 3 + getopt/go.sum | 7 + getopt/int.go | 61 +++++++++ getopt/string.go | 29 ++++ getopt/uint.go | 73 ++++++++++ go.mod | 2 - 17 files changed, 1076 insertions(+), 3 deletions(-) create mode 100644 getopt/.LICENSE.~undo-tree~ create mode 100644 getopt/LICENSE create mode 100644 getopt/bool.go create mode 100644 getopt/commandline.go create mode 100644 getopt/duration.go create mode 100644 getopt/flag_test.go create mode 100644 getopt/flagset.go create mode 100644 getopt/float.go create mode 100644 getopt/getopt.go create mode 100644 getopt/getopt_test.go create mode 100644 getopt/go.mod create mode 100644 getopt/go.sum create mode 100644 getopt/int.go create mode 100644 getopt/string.go create mode 100644 getopt/uint.go diff --git a/cli.go b/cli.go index acdaf96..8764eca 100644 --- a/cli.go +++ b/cli.go @@ -6,7 +6,7 @@ import ( "path/filepath" - "git.sr.ht/~sircmpwn/getopt" + "git.baxters.nz/jeremy/cli/getopt" ) var undef = "(undefined)" diff --git a/getopt/.LICENSE.~undo-tree~ b/getopt/.LICENSE.~undo-tree~ new file mode 100644 index 0000000..3bb90d3 --- /dev/null +++ b/getopt/.LICENSE.~undo-tree~ @@ -0,0 +1,4 @@ +(undo-tree-save-format-version . 1) +"ea9995832c3e0239223cbc9357def034cfcdda62" +[nil current nil nil (27067 8166 957871 684000) 0 nil] +nil diff --git a/getopt/LICENSE b/getopt/LICENSE new file mode 100644 index 0000000..b156508 --- /dev/null +++ b/getopt/LICENSE @@ -0,0 +1,26 @@ +Copyright 2019 Drew DeVault + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/getopt/bool.go b/getopt/bool.go new file mode 100644 index 0000000..291b71b --- /dev/null +++ b/getopt/bool.go @@ -0,0 +1,43 @@ +package getopt + +import ( + "flag" + "strconv" +) + +type boolFlag interface { + flag.Value + IsBoolFlag() bool +} + +type boolVal bool + +func (b *boolVal) String() string { + return strconv.FormatBool(bool(*b)) +} + +func (b *boolVal) Set(val string) error { + *b = boolVal(true) + return nil +} + +func (b *boolVal) IsBoolFlag() bool { + return true +} + +// BoolVar defines a bool flag with specified name, default value, and +// usage string. The argument p points to a bool variable in which to +// store the value of the flag. +func (set *FlagSet) BoolVar(p *bool, name string, value bool, usage string) { + *p = value + set.Var((*boolVal)(p), name, usage) +} + +// Bool defines a bool flag with specified name, default value, and +// usage string. The return value is the address of a bool variable that +// stores the value of the flag. +func (set *FlagSet) Bool(name string, value bool, usage string) *bool { + var b bool + set.BoolVar(&b, name, value, usage) + return &b +} diff --git a/getopt/commandline.go b/getopt/commandline.go new file mode 100644 index 0000000..13ec667 --- /dev/null +++ b/getopt/commandline.go @@ -0,0 +1,180 @@ +package getopt + +import ( + "flag" + "io" + "os" + "time" +) + +// CommandLine is the default set of command-line flags, parsed from +// os.Args. The top-level functions such as BoolVar, Arg, and so on are +// wrappers for the methods of CommandLine. +var CommandLine = NewFlagSet(os.Args[0], flag.ExitOnError) + +// Usage prints a usage message documenting all defined command-line +// flags to os.Stderr. It is called when an error occurs while parsing +// flags. The function is a variable that may be changed to point to a +// custom function. By default it prints a simple header and calls +// PrintDefaults; for details about the format of the output and how to +// control it, see the documentation for PrintDefaults. Custom usage +// functions may choose to exit the program; by default exiting happens +// anyway as the command line's error handling strategy is set to +// ExitOnError. +var Usage = CommandLine.Usage + +// PrintDefaults prints, to standard error unless configured otherwise, +// a usage message showing the default settings of all defined +// command-line flags. +func PrintDefaults() { CommandLine.PrintDefaults() } + +// Arg returns the i'th command-line argument. Arg(0) is the first +// remaining argument after flags have been processed. Arg returns an +// empty string if the requested element does not exist. +func Arg(i int) string { return CommandLine.Arg(i) } + +// Args returns the non-flag command-line arguments. +func Args() []string { return CommandLine.Args() } + +// NArg is the number of arguments remaining after flags have been +// processed. +func NArg() int { return CommandLine.NArg() } + +// NFlag returns the number of command-line flags that have been set. +func NFlag() int { return CommandLine.NFlag() } + +// Parsed reports whether the command-line flags have been parsed. +func Parsed() bool { return CommandLine.Parsed() } + +// Parse parses the command-line flags from os.Args. Must be called +// after all flags are defined and before flags are accessed by the +// program. +func Parse() error { return CommandLine.Parse() } + +// Var defines a flag with the specified name and usage string. The type +// and value of the flag are represented by the first argument, of type +// Value, which typically holds a user-defined implementation of Value. +// For instance, the caller could create a flag that turns a +// comma-separated string into a slice of strings by giving the slice +// the methods of Value; in particular, Set would decompose the +// comma-separated string into the slice. +func Var(value flag.Value, name string, usage string) { + CommandLine.Var(value, name, usage) +} + +// Lookup returns the Flag structure of the named flag, returning nil if +// none exists. +func Lookup(name string) *Flag { + return CommandLine.Lookup(name) +} + +// SetOutput sets the destination for usage and error messages. If +// output is nil, os.Stderr is used. +func SetOutput(output io.Writer) { + CommandLine.SetOutput(output) +} + +// BoolVar defines a bool flag with specified name, default value, and +// usage string. The argument p points to a bool variable in which to +// store the value of the flag. +func BoolVar(p *bool, name string, value bool, usage string) { + CommandLine.BoolVar(p, name, value, usage) +} + +// Bool defines a bool flag with specified name, default value, and +// usage string. The return value is the address of a bool variable that +// stores the value of the flag. +func Bool(name string, value bool, usage string) *bool { + return CommandLine.Bool(name, value, usage) +} + +// DurationVar defines a time.Duration flag with specified name, default +// value, and usage string. The argument p points to a time.Duration +// variable in which to store the value of the flag. The flag accepts a +// value acceptable to time.ParseDuration. +func DurationVar(p *time.Duration, name string, value time.Duration, usage string) { + CommandLine.DurationVar(p, name, value, usage) +} + +// Duration defines a time.Duration flag with specified name, default +// value, and usage string. The return value is the address of a +// time.Duration variable that stores the value of the flag. The flag +// accepts a value acceptable to time.ParseDuration. +func Duration(name string, value time.Duration, usage string) *time.Duration { + return CommandLine.Duration(name, value, usage) +} + +// Float64Var defines a float64 flag with specified name, default value, +// and usage string. The argument p points to a float64 variable in +// which to store the value of the flag. +func Float64Var(p *float64, name string, value float64, usage string) { + CommandLine.Float64Var(p, name, value, usage) +} + +// Float64 defines a float64 flag with specified name, default value, +// and usage string. The return value is the address of a float64 +// variable that stores the value of the flag. +func Float64(name string, value float64, usage string) *float64 { + return CommandLine.Float64(name, value, usage) +} + +// IntVar defines a int flag with specified name, default value, +// and usage string. The argument p points to a int variable in +// which to store the value of the flag. +func IntVar(p *int, name string, value int, usage string) { + CommandLine.IntVar(p, name, value, usage) +} + +// Int defines an int flag with specified name, default value, and usage +// string. The return value is the address of an int variable that +// stores the value of the flag. +func Int(name string, value int, usage string) *int { + return CommandLine.Int(name, value, usage) +} + +// Int64Var defines an int64 flag with specified name, default value, +// and usage string. The argument p points to an int64 variable in which +// to store the value of the flag. +func Int64Var(p *int64, name string, value int64, usage string) { + CommandLine.Int64Var(p, name, value, usage) +} + +// Int64 defines an int64 flag with specified name, default value, and +// usage string. The return value is the address of an int64 variable +// that stores the value of the flag. +func Int64(name string, value int64, usage string) *int64 { + return CommandLine.Int64(name, value, usage) +} + +// StringVar defines a string flag with specified name, default value, +// and usage string. The argument p points to a string variable in which +// to store the value of the flag. +func StringVar(p *string, name string, value string, usage string) { + CommandLine.StringVar(p, name, value, usage) +} + +// String defines a string flag with specified name, default value, and +// usage string. The return value is the address of a string variable +// that stores the value of the flag. +func String(name string, value string, usage string) *string { + return CommandLine.String(name, value, usage) +} + +// Uint64Var defines a uint64 flag with specified name, default value, +// and usage string. The argument p points to a uint64 variable in which +// to store the value of the flag. +func Uint64Var(p *uint64, name string, value uint64, usage string) { + CommandLine.Uint64Var(p, name, value, usage) +} + +// Uint64 defines a uint64 flag with specified name, default value, and +// usage string. The return value is the address of a uint64 variable +// that stores the value of the flag. +func Uint64(name string, value uint64, usage string) *uint64 { + return CommandLine.Uint64(name, value, usage) +} + +// Set sets the value of the named flag. +func Set(name, value string) error { + return CommandLine.Set(name, value) +} diff --git a/getopt/duration.go b/getopt/duration.go new file mode 100644 index 0000000..53e96d7 --- /dev/null +++ b/getopt/duration.go @@ -0,0 +1,39 @@ +package getopt + +import ( + "time" +) + +type durationVal time.Duration + +func (d *durationVal) String() string { + return time.Duration(*d).String() +} + +func (d *durationVal) Set(val string) error { + v, err := time.ParseDuration(val) + if err != nil { + return err + } + *d = durationVal(v) + return nil +} + +// DurationVar defines a time.Duration flag with specified name, default +// value, and usage string. The argument p points to a time.Duration +// variable in which to store the value of the flag. The flag accepts a +// value acceptable to time.ParseDuration. +func (set *FlagSet) DurationVar(p *time.Duration, name string, value time.Duration, usage string) { + *p = value + set.Var((*durationVal)(p), name, usage) +} + +// Duration defines a time.Duration flag with specified name, default +// value, and usage string. The return value is the address of a +// time.Duration variable that stores the value of the flag. The flag +// accepts a value acceptable to time.ParseDuration. +func (set *FlagSet) Duration(name string, value time.Duration, usage string) *time.Duration { + var d time.Duration + set.DurationVar(&d, name, value, usage) + return &d +} diff --git a/getopt/flag_test.go b/getopt/flag_test.go new file mode 100644 index 0000000..ecce0d6 --- /dev/null +++ b/getopt/flag_test.go @@ -0,0 +1,74 @@ +package getopt + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestInt(t *testing.T) { + assert := assert.New(t) + + p := NewFlagSet("", 0) + var k int + p.IntVar(&k, "k", 16, "set k") + i := p.Int64("i", -1, "set i") + j := p.Uint("j", 64, "set j") + + err := p.parse([]string{"bin", "-i", "32", "normal arg"}) + assert.Nil(err, "Expected err to be nil") + assert.Equal(3, p.optindex, "Expected to only parse two arguments") + assert.Equal(int64(32), *i, "Expected -i argument to equal 32") + assert.Equal(uint(64), *j, "Expected -j argument to equal 64, since unset") + assert.Equal(16, k, "Expected -k argument to equal 16, since unset") +} + +func TestBool(t *testing.T) { + assert := assert.New(t) + + p := NewFlagSet("", 0) + var a bool + p.BoolVar(&a, "a", false, "set a") + b := p.Bool("b", false, "set b") + + err := p.parse([]string{"bin", "-a", "normal arg"}) + assert.Nil(err, "Expected err to be nil") + assert.Equal(2, p.optindex, "Expected to only parse two arguments") + assert.Equal(true, a, "Expected -a argument to be set") + assert.Equal(false, *b, "Expected -b argument to not be set") +} + +func TestString(t *testing.T) { + assert := assert.New(t) + + p := NewFlagSet("", 0) + get := p.String("c", "default", "get -c") + + opt := "some options" + err := p.parse([]string{"bin", "-c", opt, "normal arg"}) + assert.Nil(err, "Expected err to be nil") + assert.Equal(opt, *get, "Expected argument to be parsed") +} + +func TestFloat64(t *testing.T) { + assert := assert.New(t) + + p := NewFlagSet("", 0) + f := p.Float64("f", -3.14, "get -f") + + err := p.parse([]string{"bin", "-f", "3.14", "normal arg"}) + assert.Nil(err, "Expected err to be nil") + assert.Equal(3.14, *f, "Expected -f to equal 3.14") +} + +func TestDuration(t *testing.T) { + assert := assert.New(t) + + p := NewFlagSet("", 0) + d := p.Duration("d", 0, "get -d") + + err := p.parse([]string{"bin", "-d", "1h3m", "normal arg"}) + assert.Nil(err, "Expected err to be nil") + assert.Equal(time.Hour+3*time.Minute, *d, "Expected -d to equal 1 hour and 3 minutes") +} diff --git a/getopt/flagset.go b/getopt/flagset.go new file mode 100644 index 0000000..adb24ad --- /dev/null +++ b/getopt/flagset.go @@ -0,0 +1,259 @@ +package getopt + +import ( + "bytes" + "flag" + "fmt" + "io" + "os" + "strings" + "unicode/utf8" +) + +const ( + typeBool = iota + typeString + typeInt + typeUint + typeInt64 + typeUint64 + typeFloat64 + typeDuration +) + +// A Flag represents the state of a flag. +type Flag struct { + Name string + Rune rune + Value flag.Value + Usage string + used bool +} + +// A FlagSet represents a set of defined flags. The zero value of a +// FlagSet has no name and has ContinueOnError error handling. +type FlagSet struct { + parsed bool + optindex int + + flags map[rune]*Flag + output io.Writer + + name string + errorHandling flag.ErrorHandling + + Usage func() +} + +// NewFlagSet returns a new, empty flag set. +func NewFlagSet(name string, err flag.ErrorHandling) *FlagSet { + set := FlagSet{ + name: name, + flags: make(map[rune]*Flag), + output: os.Stderr, + errorHandling: err, + } + set.Usage = func() { + if set.name != "" { + fmt.Fprintf(set.output, "Usage of %s:\n\n", set.name) + } else { + fmt.Fprintf(set.output, "Usage:\n\n") + } + set.PrintDefaults() + } + return &set +} + +func (set *FlagSet) parse(args []string) error { + var buf bytes.Buffer + for r, f := range set.flags { + buf.WriteRune(r) + if f, ok := f.Value.(boolFlag); !ok || !f.IsBoolFlag() { + buf.WriteByte(':') + } + } + + options, optind, err := Getopts(args, buf.String()) + if err != nil { + return err + } + set.optindex = optind + + for _, opt := range options { + err = set.Set(fmt.Sprintf("%c", opt.Option), opt.Value) + if err != nil { + return err + } + } + + set.parsed = true + return nil +} + +// Set sets the value of the named flag. +func (set *FlagSet) Set(name, value string) error { + r, _ := utf8.DecodeRuneInString(name) + flag, ok := set.flags[r] + if !ok { + return fmt.Errorf("no such flag -%v", name) + } + + flag.used = true + return flag.Value.Set(value) +} + +// Arg returns the i'th command-line argument. Arg(0) is the first +// remaining argument after flags have been processed. Arg returns an +// empty string if the requested element does not exist. +func (set *FlagSet) Arg(i int) string { + var arg string + if len(set.Args()) < i { + arg = set.Args()[i] + } + return arg +} + +// Args returns the non-flag command-line arguments. +func (set *FlagSet) Args() []string { + return os.Args[set.optindex:] +} + +// NArg is the number of arguments remaining after flags have been +// processed. +func (set *FlagSet) NArg() int { + return len(set.Args()) +} + +// NFlag returns the number of command-line flags that have been set. +func (set *FlagSet) NFlag() int { + return len(set.flags) +} + +// Parsed reports whether the command-line flags have been parsed. +func (set *FlagSet) Parsed() bool { + return set.parsed +} + +// ParseSlice parses the command-line flags from args. Must be called +// after all flags are defined and before flags are accessed by the +// program. +func (set *FlagSet) ParseSlice(args []string) (err error) { + err = set.parse(args) + if err != nil { + switch set.errorHandling { + case flag.PanicOnError: + panic(err) + case flag.ExitOnError: + fmt.Println(err) + os.Exit(2) + } + } + return +} + +// Parse parses the command-line flags from os.Args. Must be called +// after all flags are defined and before flags are accessed by the +// program. +func (set *FlagSet) Parse() error { + return set.ParseSlice(os.Args) +} + +// ErrorHandling returns the error handling behavior of the flag set. +func (set *FlagSet) ErrorHandling() flag.ErrorHandling { + return set.errorHandling +} + +// Var defines a flag with the specified name and usage string. The type +// and value of the flag are represented by the first argument, of type +// Value, which typically holds a user-defined implementation of Value. +// For instance, the caller could create a flag that turns a +// comma-separated string into a slice of strings by giving the slice +// the methods of Value; in particular, Set would decompose the +// comma-separated string into the slice. +func (set *FlagSet) Var(value flag.Value, name string, usage string) { + r, _ := utf8.DecodeRuneInString(name) + set.flags[r] = &Flag{ + Name: name, + Rune: r, + Value: value, + Usage: usage, + } +} + +func (set *FlagSet) visitIf(cond func(*Flag) bool, fn func(*Flag)) { + var runes []rune + for r := range set.flags { + runes = append(runes, r) + } + + for i := range runes { + for j := i; j > 0; j-- { + if runes[j] < runes[j-1] { + runes[j], runes[j-1] = runes[j-1], runes[j] + } + } + } + + for _, r := range runes { + if cond == nil || cond(set.flags[r]) { + fn(set.flags[r]) + } + } +} + +// VisitAll visits the command-line flags in lexicographical order, +// calling fn for each. It visits all flags, even those not set. +func (set *FlagSet) VisitAll(fn func(*Flag)) { + set.visitIf(nil, fn) +} + +// Visit visits the command-line flags in lexicographical order, +// calling fn for each. +func (set *FlagSet) Visit(fn func(*Flag)) { + set.visitIf(func(flag *Flag) bool { return flag.used }, fn) +} + +// Lookup returns the Flag structure of the named flag, returning nil if +// none exists. +func (set *FlagSet) Lookup(name string) *Flag { + r, _ := utf8.DecodeRuneInString(name) + return set.flags[r] +} + +// PrintDefaults prints, to standard error unless configured otherwise, +// a usage message showing the default settings of all defined +// command-line flags. +func (set *FlagSet) PrintDefaults() { + out := set.output + set.VisitAll(func(flag *Flag) { + fmt.Fprintf(out, " -%c", flag.Rune) + + val := flag.Value + + usage := strings.Replace(flag.Usage, "\n", "\n \t", -1) + fmt.Fprintf(out, "\t%s", usage) + + if _, ok := val.(*stringVal); ok { + fmt.Fprintf(out, " (default %q)", val) + } else if _, ok := val.(*boolVal); !ok { + fmt.Fprintf(out, " (default %s)", val) + } + fmt.Fprintf(out, "\n") + }) +} + +// SetOutput sets the destination for usage and error messages. If +// output is nil, os.Stderr is used. +func (set *FlagSet) SetOutput(output io.Writer) { + if output == nil { + set.output = os.Stderr + } else { + set.output = output + } +} + +// Output returns the destination for usage and error messages. +// os.Stderr is returned if output was not set or was set to nil. +func (set *FlagSet) Output() io.Writer { + return set.output +} diff --git a/getopt/float.go b/getopt/float.go new file mode 100644 index 0000000..3f78100 --- /dev/null +++ b/getopt/float.go @@ -0,0 +1,33 @@ +package getopt + +import ( + "fmt" +) + +type floatVal float64 + +func (f *floatVal) String() string { + return fmt.Sprint(float64(*f)) +} + +func (f *floatVal) Set(val string) error { + _, err := fmt.Sscanf(val, "%g", f) + return err +} + +// Float64Var defines a float64 flag with specified name, default value, +// and usage string. The argument p points to a float64 variable in +// which to store the value of the flag. +func (set *FlagSet) Float64Var(p *float64, name string, value float64, usage string) { + *p = value + set.Var((*floatVal)(p), name, usage) +} + +// Float64 defines a float64 flag with specified name, default value, +// and usage string. The return value is the address of a float64 +// variable that stores the value of the flag. +func (set *FlagSet) Float64(name string, value float64, usage string) *float64 { + var f float64 + set.Float64Var(&f, name, value, usage) + return &f +} diff --git a/getopt/getopt.go b/getopt/getopt.go new file mode 100644 index 0000000..0a22477 --- /dev/null +++ b/getopt/getopt.go @@ -0,0 +1,155 @@ +// getopt is a POSIX-compatible implementation of getopt(3) for Go. +// +// Example usage: +// +// import ( +// "os" +// "git.sr.ht/~sircmpwn/getopt" +// ) +// +// func main() { +// opts, optind, err := getopt.Getopts(os.Args, "abc:d:") +// if err != nil { +// panic(err) +// } +// for _, opt := range opts { +// switch opt.Option { +// case 'a': +// println("Option -a specified") +// case 'b': +// println("Option -b specified") +// case 'c': +// println("Option -c specified: " + opt.Value) +// case 'd': +// println("Option -d specified: " + opt.Value) +// } +// } +// println("Remaining arguments:") +// for _, arg := range os.Args[optind:] { +// println(arg) +// } +// } +// +// A flag[0]-like interface is also supported. +// +// import ( +// "git.sr.ht/~sircmpwn/getopt" +// ) +// +// func main() { +// a := getopt.Bool("a", false, "turn on option a") +// b := getopt.Int("b", 1, "set b to a numerical value") +// var opt string +// getopt.StringVar(&opt, "c", "", "let c be specified string") +// +// err := getopt.Parse() +// if err != nil { +// panic(err) +// } +// +// print("Value of a: ") +// println(*a) +// print("Value of b: ") +// println(*b) +// println("Value of c: " + opt) +// +// println("Remaining arguments:") +// for _, arg := range getopt.Args() { +// println(arg) +// } +// } +// +// [0]: https://golang.org/pkg/flag/ +package getopt + +import ( + "fmt" + "os" +) + +// In the case of "-o example", Option is 'o' and "example" is Value. For +// options which do not take an argument, Value is "". +type Option struct { + Option rune + Value string +} + +// This is returned when an unknown option is found in argv, but not in the +// option spec. +type UnknownOptionError rune + +func (e UnknownOptionError) Error() string { + return fmt.Sprintf("%s: unknown option -%c", os.Args[0], rune(e)) +} + +// This is returned when an option with a mandatory argument is missing that +// argument. +type MissingOptionError rune + +func (e MissingOptionError) Error() string { + return fmt.Sprintf("%s: expected argument for -%c", os.Args[0], rune(e)) +} + +// Getopts implements a POSIX-compatible options interface. +// +// Returns a slice of options and the index of the first non-option argument. +// +// If an error is returned, you must print it to stderr to be POSIX complaint. +func Getopts(argv []string, spec string) ([]Option, int, error) { + optmap := make(map[rune]bool) + runes := []rune(spec) + for i, rn := range spec { + if rn == ':' { + if i == 0 { + continue + } + optmap[runes[i-1]] = true + } else { + optmap[rn] = false + } + } + + var ( + i int + opts []Option + ) + for i = 1; i < len(argv); i++ { + arg := argv[i] + runes = []rune(arg) + if len(arg) == 0 || arg == "-" { + break + } + if arg[0] != '-' { + break + } + if arg == "--" { + i++ + break + } + for j, opt := range runes[1:] { + if optopt, ok := optmap[opt]; !ok { + opts = append(opts, Option{'?', ""}) + return opts, i, UnknownOptionError(opt) + } else if optopt { + if j+1 < len(runes)-1 { + opts = append(opts, Option{opt, string(runes[j+2:])}) + break + } else { + if i+1 >= len(argv) { + if len(spec) >= 1 && spec[0] == ':' { + opts = append(opts, Option{':', string(opt)}) + } else { + return opts, i, MissingOptionError(opt) + } + } else { + opts = append(opts, Option{opt, argv[i+1]}) + i++ + } + } + } else { + opts = append(opts, Option{opt, ""}) + } + } + } + return opts, i, nil +} diff --git a/getopt/getopt_test.go b/getopt/getopt_test.go new file mode 100644 index 0000000..d13f2cd --- /dev/null +++ b/getopt/getopt_test.go @@ -0,0 +1,89 @@ +package getopt + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSimpleCase(t *testing.T) { + assert := assert.New(t) + opts, i, err := Getopts([]string{ + "test_bin", "-afo", "output-file", "normal arg"}, "afo:") + assert.Nil(err, "Expected err to be nil") + assert.Equal(len(opts), 3, "Expected 3 options to be parsed") + assert.Equal(i, 3, "Expected non-option args to start at index 2") + assert.Equal(opts[0], Option{'a', ""}) + assert.Equal(opts[1], Option{'f', ""}) + assert.Equal(opts[2], Option{'o', "output-file"}) +} + +func TestShortFormArgument(t *testing.T) { + assert := assert.New(t) + opts, i, err := Getopts([]string{ + "test_bin", "-afooutput-file", "normal arg"}, "afo:") + assert.Nil(err, "Expected err to be nil") + assert.Equal(len(opts), 3, "Expected 3 options to be parsed") + assert.Equal(i, 2, "Expected non-option args to start at index 2") + assert.Equal(opts[0], Option{'a', ""}) + assert.Equal(opts[1], Option{'f', ""}) + assert.Equal(opts[2], Option{'o', "output-file"}) +} + +func TestSeparateArgs(t *testing.T) { + assert := assert.New(t) + opts, i, err := Getopts([]string{ + "test_bin", "-a", "-f", "-o", "output-file", "normal arg"}, "afo:") + assert.Nil(err, "Expected err to be nil") + assert.Equal(len(opts), 3, "Expected 3 options to be parsed") + assert.Equal(i, 5, "Expected non-option args to start at index 5") + assert.Equal(opts[0], Option{'a', ""}) + assert.Equal(opts[1], Option{'f', ""}) + assert.Equal(opts[2], Option{'o', "output-file"}) +} + +func TestTwoDashes(t *testing.T) { + assert := assert.New(t) + opts, i, err := Getopts([]string{ + "test_bin", "-afo", "output-file", "--", "-f", "normal arg"}, "afo:") + assert.Nil(err, "Expected err to be nil") + assert.Equal(len(opts), 3, "Expected 3 options to be parsed") + assert.Equal(i, 4, "Expected non-option args to start at index 4") + assert.Equal(opts[0], Option{'a', ""}) + assert.Equal(opts[1], Option{'f', ""}) + assert.Equal(opts[2], Option{'o', "output-file"}) +} + +func TestUnknownOption(t *testing.T) { + assert := assert.New(t) + _, _, err := Getopts([]string{"test_bin", "-x"}, "y") + var errt UnknownOptionError + assert.IsType(err, errt, "Expected unknown option error") + assert.Equal(err.Error(), fmt.Sprintf("%s: unknown option -x", os.Args[0]), + "Expected POSIX-compatible error message") +} + +func TestMissingOption(t *testing.T) { + assert := assert.New(t) + _, _, err := Getopts([]string{"test_bin", "-x"}, "x:") + var errt MissingOptionError + assert.IsType(err, errt, "Expected missing option error") + assert.Equal(err.Error(), fmt.Sprintf("%s: expected argument for -x", + os.Args[0]), "Expected POSIX-compatible error message") +} + +func TestExpectedMissingOption(t *testing.T) { + assert := assert.New(t) + opts, _, err := Getopts([]string{"test_bin", "-x"}, ":x:") + assert.Nil(err, "Expected err to be nil") + assert.Equal(len(opts), 1, "Expected 1 option to be parsed") + assert.Equal(opts[0], Option{':', "x"}) +} + +func TestNoOption(t *testing.T) { + assert := assert.New(t) + _, i, _ := Getopts([]string{"test_bin"}, "") + assert.Equal(i, 1, "Expected non-option args to start at index 1") +} diff --git a/getopt/go.mod b/getopt/go.mod new file mode 100644 index 0000000..2ce0e28 --- /dev/null +++ b/getopt/go.mod @@ -0,0 +1,3 @@ +module git.sr.ht/~sircmpwn/getopt + +require github.com/stretchr/testify v1.3.0 diff --git a/getopt/go.sum b/getopt/go.sum new file mode 100644 index 0000000..4347755 --- /dev/null +++ b/getopt/go.sum @@ -0,0 +1,7 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/getopt/int.go b/getopt/int.go new file mode 100644 index 0000000..b714727 --- /dev/null +++ b/getopt/int.go @@ -0,0 +1,61 @@ +package getopt + +import ( + "fmt" +) + +type intVal int + +func (i *intVal) String() string { + return fmt.Sprint(*i) +} + +func (i *intVal) Set(val string) error { + _, err := fmt.Sscanf(val, "%d", i) + return err +} + +// IntVar defines a int flag with specified name, default value, +// and usage string. The argument p points to a int variable in +// which to store the value of the flag. +func (set *FlagSet) IntVar(p *int, name string, value int, usage string) { + *p = value + set.Var((*intVal)(p), name, usage) +} + +// Int defines an int flag with specified name, default value, and usage +// string. The return value is the address of an int variable that +// stores the value of the flag. +func (set *FlagSet) Int(name string, value int, usage string) *int { + p := new(int) + set.IntVar(p, name, value, usage) + return p +} + +type int64Val int64 + +func (i *int64Val) String() string { + return fmt.Sprint(int64(*i)) +} + +func (i *int64Val) Set(val string) error { + _, err := fmt.Sscanf(val, "%d", i) + return err +} + +// Int64Var defines an int64 flag with specified name, default value, +// and usage string. The argument p points to an int64 variable in which +// to store the value of the flag. +func (set *FlagSet) Int64Var(p *int64, name string, value int64, usage string) { + *p = value + set.Var((*int64Val)(p), name, usage) +} + +// Int64 defines an int64 flag with specified name, default value, and +// usage string. The return value is the address of an int64 variable +// that stores the value of the flag. +func (set *FlagSet) Int64(name string, value int64, usage string) *int64 { + var i int64 + set.Int64Var(&i, name, value, usage) + return &i +} diff --git a/getopt/string.go b/getopt/string.go new file mode 100644 index 0000000..5ae3a48 --- /dev/null +++ b/getopt/string.go @@ -0,0 +1,29 @@ +package getopt + +type stringVal string + +func (s *stringVal) String() string { + return string(*s) +} + +func (s *stringVal) Set(val string) error { + *s = stringVal(val) + return nil +} + +// StringVar defines a string flag with specified name, default value, +// and usage string. The argument p points to a string variable in which +// to store the value of the flag. +func (set *FlagSet) StringVar(p *string, name string, value string, usage string) { + *p = value + set.Var((*stringVal)(p), name, usage) +} + +// String defines a string flag with specified name, default value, and +// usage string. The return value is the address of a string variable +// that stores the value of the flag. +func (set *FlagSet) String(name string, value string, usage string) *string { + var s string + set.StringVar(&s, name, value, usage) + return &s +} diff --git a/getopt/uint.go b/getopt/uint.go new file mode 100644 index 0000000..31446ef --- /dev/null +++ b/getopt/uint.go @@ -0,0 +1,73 @@ +package getopt + +import "fmt" + +type uintVal uint + +func (i *uintVal) String() string { + return fmt.Sprint(uint(*i)) +} + +func (i *uintVal) Set(val string) error { + _, err := fmt.Sscanf(val, "%d", i) + return err +} + +// UintVar defines a uint flag with specified name, default value, and +// usage string. The argument p points to a uint variable in which to +// store the value of the flag. +func (set *FlagSet) UintVar(p *uint, name string, value uint, usage string) { + *p = value + set.Var((*uintVal)(p), name, usage) +} + +// UintVar defines a uint flag with specified name, default value, and +// usage string. The argument p points to a uint variable in which to +// store the value of the flag. +func UintVar(p *uint, name string, value uint, usage string) { + CommandLine.UintVar(p, name, value, usage) +} + +// Uint defines a uint flag with specified name, default value, and +// usage string. The return value is the address of a uint variable that +// stores the value of the flag. +func (set *FlagSet) Uint(name string, value uint, usage string) *uint { + var b uint + set.UintVar(&b, name, value, usage) + return &b +} + +// Uint defines a uint flag with specified name, default value, and +// usage string. The return value is the address of a uint variable that +// stores the value of the flag. +func Uint(name string, value uint, usage string) *uint { + return CommandLine.Uint(name, value, usage) +} + +type uint64Val uint64 + +func (i *uint64Val) String() string { + return fmt.Sprint(uint64(*i)) +} + +func (i *uint64Val) Set(val string) error { + _, err := fmt.Sscanf(val, "%d", i) + return err +} + +// Uint64Var defines a uint64 flag with specified name, default value, +// and usage string. The argument p points to a uint64 variable in which +// to store the value of the flag. +func (set *FlagSet) Uint64Var(p *uint64, name string, value uint64, usage string) { + *p = value + set.Var((*uint64Val)(p), name, usage) +} + +// Uint64 defines a uint64 flag with specified name, default value, and +// usage string. The return value is the address of a uint64 variable +// that stores the value of the flag. +func (set *FlagSet) Uint64(name string, value uint64, usage string) *uint64 { + var b uint64 + set.Uint64Var(&b, name, value, usage) + return &b +} diff --git a/go.mod b/go.mod index 167a092..c7e9a86 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module git.baxters.nz/jeremy/cli go 1.26.1 - -require git.sr.ht/~sircmpwn/getopt v1.0.0