aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2023-11-30 00:59:30 +0100
committerThomas Voss <mail@thomasvoss.com> 2023-11-30 00:59:30 +0100
commit9fb540642acc6dfcc6816291a832a8ea56973f09 (patch)
tree15910ae83f43a033368a6cffc5195c12d5c2772f
parent7fbe5d6731df8f6b8601431d8fc42c166c0e0e00 (diff)
Add long-option support
-rw-r--r--err.go20
-rw-r--r--opts.go105
-rw-r--r--opts_test.go114
3 files changed, 229 insertions, 10 deletions
diff --git a/err.go b/err.go
index cbe96b4..23692fa 100644
--- a/err.go
+++ b/err.go
@@ -4,16 +4,28 @@ import "fmt"
// A BadOptionError describes an option that the user attempted to pass
// which the developer did not register.
-type BadOptionError rune
+type BadOptionError struct {
+ r rune
+ s string
+}
func (e BadOptionError) Error() string {
- return fmt.Sprintf("unknown option ‘%c’", e)
+ if e.r != 0 {
+ return fmt.Sprintf("unknown option ‘-%c’", e.r)
+ }
+ return fmt.Sprintf("unknown option ‘--%s’", e.s)
}
// A NoArgumentError describes an option that the user attempted to pass
// without an argument, which required an argument.
-type NoArgumentError rune
+type NoArgumentError struct {
+ r rune
+ s string
+}
func (e NoArgumentError) Error() string {
- return fmt.Sprintf("expected argument for option ‘%c’", e)
+ if e.r != 0 {
+ return fmt.Sprintf("expected argument for option ‘-%c’", e.r)
+ }
+ return fmt.Sprintf("expected argument for option ‘--%s’", e.s)
}
diff --git a/opts.go b/opts.go
index 7f5882c..b8c8b07 100644
--- a/opts.go
+++ b/opts.go
@@ -12,7 +12,10 @@
// user-facing I/O is delegrated to the caller.
package opts
-import "slices"
+import (
+ "slices"
+ "strings"
+)
// ArgMode represents the whether or not a long-option takes an argument.
type ArgMode int
@@ -83,7 +86,7 @@ func Get(args []string, optstr string) (flags []Flag, optind int, err error) {
for j, r := range rs {
k := slices.Index(optrs, r)
if k == -1 {
- return nil, 0, BadOptionError(r)
+ return nil, 0, BadOptionError{r: r}
}
var s string
@@ -93,7 +96,7 @@ func Get(args []string, optstr string) (flags []Flag, optind int, err error) {
case am == Required:
i++
if i >= len(args) {
- return nil, 0, NoArgumentError(r)
+ return nil, 0, NoArgumentError{r: r}
}
s = args[i]
default:
@@ -109,6 +112,102 @@ func Get(args []string, optstr string) (flags []Flag, optind int, err error) {
return flags, i, nil
}
+func GetLong(args []string, opts []LongOpt) (flags []Flag, optind int, err error) {
+ if len(args) == 0 {
+ return
+ }
+
+ var i int
+ for i = 1; i < len(args); i++ {
+ arg := args[i]
+ if len(arg) == 0 || arg == "-" || arg[0] != '-' {
+ break
+ } else if arg == "--" {
+ i++
+ break
+ }
+
+ if strings.HasPrefix(arg, "--") {
+ arg = arg[2:]
+
+ n := arg
+ j := strings.IndexByte(n, '=')
+ if j != -1 {
+ n = arg[:j]
+ }
+
+ var s string
+ o, ok := optStruct(opts, n)
+
+ switch {
+ case !ok:
+ return nil, 0, BadOptionError{s: n}
+ case o.Arg != None && j != -1:
+ s = arg[j+1:]
+ case o.Arg == Required:
+ i++
+ if i >= len(args) {
+ return nil, 0, NoArgumentError{s: n}
+ }
+ s = args[i]
+ }
+
+ flags = append(flags, Flag{Key: o.Short, Value: s})
+ } else {
+ rs := []rune(arg[1:])
+ for j, r := range rs {
+ var s string
+
+ switch am, ok := getModeRune(opts, r); {
+ case !ok:
+ return nil, 0, BadOptionError{r: r}
+ case am != None && j < len(rs)-1:
+ s = string(rs[j+1:])
+ case am == Required:
+ i++
+ if i >= len(args) {
+ return nil, 0, NoArgumentError{r: r}
+ }
+ s = args[i]
+ default:
+ flags = append(flags, Flag{Key: r})
+ continue
+ }
+
+ flags = append(flags, Flag{r, s})
+ break
+ }
+ }
+ }
+
+ return flags, i, nil
+}
+
+func getModeRune(os []LongOpt, r rune) (ArgMode, bool) {
+ for _, o := range os {
+ if o.Short == r {
+ return o.Arg, true
+ }
+ }
+ return 0, false
+}
+
+func optStruct(os []LongOpt, s string) (LongOpt, bool) {
+ i := -1
+ for j, o := range os {
+ if strings.HasPrefix(o.Long, s) {
+ if i != -1 {
+ return LongOpt{}, false
+ }
+ i = j
+ }
+ }
+ if i == -1 {
+ return LongOpt{}, false
+ }
+ return os[i], true
+}
+
func colonsToArgMode(rs []rune) ArgMode {
if len(rs) >= 2 && rs[0] == ':' && rs[1] == ':' {
return Optional
diff --git a/opts_test.go b/opts_test.go
index 1f71057..6ebaa56 100644
--- a/opts_test.go
+++ b/opts_test.go
@@ -9,6 +9,8 @@ func die(t *testing.T, name string, want, got any) {
t.Fatalf("Expected %s to be ‘%s’ but got ‘%s’", name, want, got)
}
+// SHORT OPTS
+
func assertGet(t *testing.T, args []string, fw, ow int, ew error) []Flag {
flags, optind, err := Get(args, "abλc:dßĦ::")
if err != ew {
@@ -35,7 +37,7 @@ func TestNoFlag(t *testing.T) {
func TestCNoArg(t *testing.T) {
args := []string{"foo", "-c"}
- assertGet(t, args, 0, 0, NoArgumentError('c'))
+ assertGet(t, args, 0, 0, NoArgumentError{r: 'c'})
}
func TestCWithArg(t *testing.T) {
@@ -200,12 +202,12 @@ func TestΛAsArgToC(t *testing.T) {
func TestInvalidFlag(t *testing.T) {
args := []string{"foo", "-X"}
- assertGet(t, args, 0, 0, BadOptionError('X'))
+ assertGet(t, args, 0, 0, BadOptionError{r: 'X'})
}
func TestInvalidFlagWithArg(t *testing.T) {
args := []string{"foo", "-X", "bar"}
- assertGet(t, args, 0, 0, BadOptionError('X'))
+ assertGet(t, args, 0, 0, BadOptionError{r: 'X'})
}
func TestAAfterArg(t *testing.T) {
@@ -256,3 +258,109 @@ func TestΛĦWithArg(t *testing.T) {
die(t, "flags[1].Value", "bar", flags[1].Value)
}
}
+
+// LONG OPTS
+
+func assertGetLong(t *testing.T, args []string, fw, ow int, ew error) []Flag {
+ opts := []LongOpt{
+ {Short: 'a', Long: "add", Arg: None},
+ {Short: 'b', Long: "back", Arg: None},
+ {Short: 'λ', Long: "λεωνίδας", Arg: None},
+ {Short: 'c', Long: "change", Arg: Required},
+ {Short: 'C', Long: "count", Arg: None},
+ {Short: 'd', Long: "delete", Arg: None},
+ {Short: 'ß', Long: "scheiße", Arg: None},
+ {Short: 'Ħ', Long: "Ħaġrat", Arg: Optional},
+ }
+ flags, optind, err := GetLong(args, opts)
+ if err != ew {
+ die(t, "err", ew, err)
+ }
+ if optind != ow {
+ die(t, "optind", ow, optind)
+ }
+ if len(flags) != fw {
+ die(t, "flags", fw, flags)
+ }
+ return flags
+}
+
+func TestNoArgL(t *testing.T) {
+ args := []string{}
+ assertGetLong(t, args, 0, 0, nil)
+}
+
+func TestNoFlagL(t *testing.T) {
+ args := []string{"foo"}
+ assertGetLong(t, args, 0, 1, nil)
+}
+
+func TestChangeNoArg(t *testing.T) {
+ args := []string{"foo", "--change"}
+ assertGetLong(t, args, 0, 0, NoArgumentError{s: "change"})
+}
+
+func TestChangeArgEqual(t *testing.T) {
+ args := []string{"foo", "--change=bar"}
+ flags := assertGetLong(t, args, 1, 2, nil)
+ if flags[0].Key != 'c' {
+ die(t, "flags[0].Key", 'c', flags[0].Key)
+ }
+ if flags[0].Value != "bar" {
+ die(t, "flags[0].Value", "bar", flags[0].Value)
+ }
+}
+
+func TestChangeArgSpace(t *testing.T) {
+ args := []string{"foo", "--change", "bar"}
+ flags := assertGetLong(t, args, 1, 3, nil)
+ if flags[0].Key != 'c' {
+ die(t, "flags[0].Key", 'c', flags[0].Key)
+ }
+ if flags[0].Value != "bar" {
+ die(t, "flags[0].Value", "bar", flags[0].Value)
+ }
+}
+
+func TestChangeArgSpaceShort(t *testing.T) {
+ args := []string{"foo", "--ch", "bar"}
+ flags := assertGetLong(t, args, 1, 3, nil)
+ if flags[0].Key != 'c' {
+ die(t, "flags[0].Key", 'c', flags[0].Key)
+ }
+ if flags[0].Value != "bar" {
+ die(t, "flags[0].Value", "bar", flags[0].Value)
+ }
+}
+
+func TestChangeArgSpaceShortFail(t *testing.T) {
+ args := []string{"foo", "--c", "bar"}
+ assertGetLong(t, args, 0, 0, BadOptionError{s: "c"})
+}
+
+func TestĦaġratNoArg(t *testing.T) {
+ args := []string{"foo", "--Ħ"}
+ assertGetLong(t, args, 1, 2, nil)
+}
+
+func TestĦaġratArgEqual(t *testing.T) {
+ args := []string{"foo", "--Ħ=bar"}
+ flags := assertGetLong(t, args, 1, 2, nil)
+ if flags[0].Key != 'Ħ' {
+ die(t, "flags[0].Key", 'Ħ', flags[0].Key)
+ }
+ if flags[0].Value != "bar" {
+ die(t, "flags[0].Value", "bar", flags[0].Value)
+ }
+}
+
+func TestĦaġratArgSpace(t *testing.T) {
+ args := []string{"foo", "--Ħ", "bar"}
+ flags := assertGetLong(t, args, 1, 2, nil)
+ if flags[0].Key != 'Ħ' {
+ die(t, "flags[0].Key", 'Ħ', flags[0].Key)
+ }
+ if flags[0].Value != "" {
+ die(t, "flags[0].Value", "", flags[0].Value)
+ }
+}