diff options
author | Thomas Voss <mail@thomasvoss.com> | 2023-11-30 00:59:30 +0100 |
---|---|---|
committer | Thomas Voss <mail@thomasvoss.com> | 2023-11-30 00:59:30 +0100 |
commit | 9fb540642acc6dfcc6816291a832a8ea56973f09 (patch) | |
tree | 15910ae83f43a033368a6cffc5195c12d5c2772f | |
parent | 7fbe5d6731df8f6b8601431d8fc42c166c0e0e00 (diff) |
Add long-option support
-rw-r--r-- | err.go | 20 | ||||
-rw-r--r-- | opts.go | 105 | ||||
-rw-r--r-- | opts_test.go | 114 |
3 files changed, 229 insertions, 10 deletions
@@ -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) } @@ -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) + } +} |