From 264780bb60dda33e8d06c48be5f1991212d62a9a Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Tue, 5 Dec 2023 01:32:52 +0100 Subject: Replace ‘optind’ with ‘rest’ in Get() and GetLong() returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 27 +++-- v2/err.go | 31 +++++ v2/go.mod | 3 + v2/opts.go | 234 ++++++++++++++++++++++++++++++++++++ v2/opts_test.go | 366 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 651 insertions(+), 10 deletions(-) create mode 100644 v2/err.go create mode 100644 v2/go.mod create mode 100644 v2/opts.go create mode 100644 v2/opts_test.go diff --git a/README.md b/README.md index b15aaf4..34e26d9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +## Update 6th December, 2023 + +The v2 of this library has been released, and you should probably use +that instead: + +```sh +$ go get -u git.sr.ht/~mango/opts/v2 +``` + # Opts Opts is a simple Go library implementing unicode-aware getopt(3)- and @@ -21,7 +30,7 @@ import ( "fmt" "os" - "git.sr.ht/~mango/opts" + "git.sr.ht/~mango/opts/v2" ) func usage() { @@ -30,7 +39,7 @@ func usage() { } func main() { - flags, optind, err := opts.Get(os.Args, "a:ßλ") + flags, rest, err := opts.Get(os.Args, "a:ßλ") if err != nil { fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) usage() @@ -47,8 +56,7 @@ func main() { } } - // The remaining arguments - rest := os.Args[optind:] + fmt.Println("The remaining arguments are:", rest) } ``` @@ -65,19 +73,18 @@ import ( "git.sr.ht/~mango/opts" ) -const noShortFlag = -1 - func usage() { fmt.Fprintf(os.Stderr, "Usage: %s [-ßλ] [-a arg] [--no-short]\n", os.Args[0]) os.Exit(1) } func main() { - flags, optind, err := opts.GetLong(os.Args, []opts.LongOpt{ + // The fourth long-option has no short-option equivalent + flags, rest, err := opts.GetLong(os.Args, []opts.LongOpt{ {Short: 'a', Long: "add", Arg: opts.Required}, {Short: 'ß', Long: "sheiße", Arg: opts.None}, {Short: 'λ', Long: "λεωνίδας", Arg: opts.None}, - {Short: noShortFlag, Long: "no-short", Arg: opts.None}, + {Short: -1, Long: "no-short", Arg: opts.None}, }) if err != nil { fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) @@ -92,12 +99,12 @@ func main() { fmt.Println("-ß or --sheiße given") case 'λ': fmt.Println("-λ or --λεωνίδας given") - case noShortFlag: + case -1: fmt.Println("--no-short given") } } // The remaining arguments - rest := os.Args[optind:] + fmt.Println("The remaining arguments are:", rest) } ``` diff --git a/v2/err.go b/v2/err.go new file mode 100644 index 0000000..23692fa --- /dev/null +++ b/v2/err.go @@ -0,0 +1,31 @@ +package opts + +import "fmt" + +// A BadOptionError describes an option that the user attempted to pass +// which the developer did not register. +type BadOptionError struct { + r rune + s string +} + +func (e BadOptionError) Error() string { + 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 struct { + r rune + s string +} + +func (e NoArgumentError) Error() string { + 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/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..f12c3e9 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,3 @@ +module git.sr.ht/~mango/opts/v2 + +go 1.21.4 diff --git a/v2/opts.go b/v2/opts.go new file mode 100644 index 0000000..314acd8 --- /dev/null +++ b/v2/opts.go @@ -0,0 +1,234 @@ +// Package opts implements unicode-aware getopt(3)- and getopt_long(3) +// flag parsing. +// +// The opts package aims to provide as simple an API as possible. If +// your usecase requires more advanced argument-parsing or a more robust +// API, this may not be the ideal package for you. +// +// While the opts package aims to closely follow the POSIX getopt(3) and +// GNU getopt_long(3) C functions, there are some notable differences. +// This package properly supports unicode flags, but also does not +// support a leading ‘:’ in the [Get] function’s option string; all +// user-facing I/O is delegrated to the caller. +package opts + +import ( + "slices" + "strings" +) + +// ArgMode represents whether or not a long-option takes an argument. +type ArgMode int + +// These tokens can be used to specify whether or not a long-option takes +// an argument. +const ( + None ArgMode = iota // long opt takes no argument + Required // long opt takes an argument + Optional // long opt optionally takes an argument +) + +// Flag represents a parsed command-line flag. Key corresponds to the +// rune that was passed on the command-line, and Value corresponds to the +// flags argument if one was provided. In the case of long-options Key +// will map to the corresponding short-code, even if a long-option was +// used. +type Flag struct { + Key rune // the flag that was passed + Value string // the flags argument +} + +// LongOpt represents a long-option to attempt to parse. All long +// options have a short-hand form represented by Short and a long-form +// represented by Long. Arg is used to represent whether or not the +// long-option takes an argument. +// +// In the case that you want to parse a long-option which doesn’t have a +// short-hand form, you can set Short to a negative integer. +type LongOpt struct { + Short rune + Long string + Arg ArgMode +} + +// Get parses the command-line arguments in args according to optstr. +// Unlike POSIX-getopt(3), a leading ‘:’ in optstr is not supported and +// will be ignored and no I/O is ever performed. +// +// Get will look for the flags listed in optstr (i.e., it will look for +// ‘-a’, ‘-ß’, and ‘λ’ given optstr == "aßλ"). The optstr need not be +// sorted in any particular order. If an option takes a required +// argument, it can be suffixed by a colon. If an option takes an +// optional argument, it can be suffixed by two colons. As an example, +// optstr == "a::ßλ:" will search for ‘-a’ with an optional argument, +// ‘-ß’ with no argument, and ‘-λ’ with a required argument. +// +// A successful parse returns the flags in the flags slice and a slice of +// the remaining non-option arguments in rest. In the case of failure, +// err will be one of [BadOptionError] or [NoArgumentError]. +func Get(args []string, optstr string) (flags []Flag, rest []string, err error) { + if len(args) == 0 { + return + } + + optrs := []rune(optstr) + 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 + } + + rs := []rune(arg[1:]) + for j, r := range rs { + k := slices.Index(optrs, r) + if k == -1 { + return nil, nil, BadOptionError{r: r} + } + + var s string + switch am := colonsToArgMode(optrs[k+1:]); { + case am != None && j < len(rs)-1: + s = string(rs[j+1:]) + case am == Required: + i++ + if i >= len(args) { + return nil, nil, NoArgumentError{r: r} + } + s = args[i] + default: + flags = append(flags, Flag{Key: r}) + continue + } + + flags = append(flags, Flag{r, s}) + break + } + } + + return flags, args[i:], nil +} + +// GetLong parses the command-line arguments in args according to opts. +// +// This function is identical to [Get] except it parses according to a +// [LongOpt] slice instead of an opt-string, and it parses long-options. +// When parsing, GetLong will also accept incomplete long-options if they +// are unambiguous. For example, given the following definition of opts: +// +// opts := []LongOpt{ +// {Short: 'a', Long: "add", Arg: None}, +// {Short: 'd', Long: "delete", Arg: None}, +// {Short: 'D', Long: "defer", Arg: None}, +// } +// +// The options ‘--a’ and ‘--ad’ will parse as ‘--add’. The option ‘--de’ +// will not parse however as it is ambiguous. +func GetLong(args []string, opts []LongOpt) (flags []Flag, rest []string, 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, nil, BadOptionError{s: n} + case o.Arg != None && j != -1: + s = arg[j+1:] + case o.Arg == Required: + i++ + if i >= len(args) { + return nil, nil, 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, nil, 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, nil, NoArgumentError{r: r} + } + s = args[i] + default: + flags = append(flags, Flag{Key: r}) + continue + } + + flags = append(flags, Flag{r, s}) + break + } + } + } + + return flags, args[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 + } + if len(rs) >= 1 && rs[0] == ':' { + return Required + } + return None +} diff --git a/v2/opts_test.go b/v2/opts_test.go new file mode 100644 index 0000000..1e35910 --- /dev/null +++ b/v2/opts_test.go @@ -0,0 +1,366 @@ +package opts + +import ( + "fmt" + "testing" +) + +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, rw int, ew error) []Flag { + flags, rest, err := Get(args, "abλc:dßĦ::") + if err != ew { + die(t, "err", ew, err) + } + if len(rest) != rw { + die(t, "rest", rw, rest) + } + if len(flags) != fw { + die(t, "flags", fw, flags) + } + return flags +} + +func TestNoArg(t *testing.T) { + args := []string{} + assertGet(t, args, 0, 0, nil) +} + +func TestNoFlag(t *testing.T) { + args := []string{"foo"} + assertGet(t, args, 0, 0, nil) +} + +func TestCNoArg(t *testing.T) { + args := []string{"foo", "-c"} + assertGet(t, args, 0, 0, NoArgumentError{r: 'c'}) +} + +func TestCWithArg(t *testing.T) { + args := []string{"foo", "-c", "bar"} + flags := assertGet(t, args, 1, 0, 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 TestCWithArgNoSpace(t *testing.T) { + args := []string{"foo", "-cbar"} + flags := assertGet(t, args, 1, 0, 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 TestANoArg(t *testing.T) { + args := []string{"foo", "-a"} + flags := assertGet(t, args, 1, 0, nil) + if flags[0].Key != 'a' { + die(t, "flags[0].Key", 'a', flags[0].Key) + } + if flags[0].Value != "" { + die(t, "flags[0].Value", "", flags[0].Value) + } +} + +func TestAWithArg(t *testing.T) { + args := []string{"foo", "-a", "bar"} + flags := assertGet(t, args, 1, 1, nil) + if flags[0].Key != 'a' { + die(t, "flags[0].Key", 'a', flags[0].Key) + } + if flags[0].Value != "" { + die(t, "flags[0].Value", "", flags[0].Value) + } +} + +func TestAAndCWithArg(t *testing.T) { + args := []string{"foo", "-a", "-c", "bar"} + flags := assertGet(t, args, 2, 0, nil) + if flags[0].Key != 'a' { + die(t, "flags[0].Key", 'a', flags[0].Key) + } + if flags[0].Value != "" { + die(t, "flags[0].Value", "", flags[0].Value) + } + if flags[1].Key != 'c' { + die(t, "flags[0].Key", 'c', flags[1].Key) + } + if flags[1].Value != "bar" { + die(t, "flags[0].Value", "bar", flags[1].Value) + } +} + +func TestACWithArg(t *testing.T) { + args := []string{"foo", "-ac", "bar"} + flags := assertGet(t, args, 2, 0, nil) + if flags[0].Key != 'a' { + die(t, "flags[0].Key", 'a', flags[0].Key) + } + if flags[0].Value != "" { + die(t, "flags[0].Value", "", flags[0].Value) + } + if flags[1].Key != 'c' { + die(t, "flags[0].Key", 'c', flags[1].Key) + } + if flags[1].Value != "bar" { + die(t, "flags[0].Value", "bar", flags[1].Value) + } +} + +func TestCAWithArg(t *testing.T) { + args := []string{"foo", "-ca", "bar"} + flags := assertGet(t, args, 1, 1, nil) + if flags[0].Key != 'c' { + die(t, "flags[0].Key", 'c', flags[0].Key) + } + if flags[0].Value != "a" { + die(t, "flags[0].Value", "a", flags[0].Value) + } +} + +func TestBAfterDashDash(t *testing.T) { + args := []string{"foo", "--", "-b"} + assertGet(t, args, 0, 1, nil) +} + +func TestCWithArgAfterDashDash(t *testing.T) { + args := []string{"foo", "--", "-c", "bar", "baz"} + assertGet(t, args, 0, 3, nil) +} + +func TestCWithArgThenDAfterDashDash(t *testing.T) { + args := []string{"foo", "-c", "bar", "baz", "--", "-d"} + flags := assertGet(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 TestCWithArgThenDAfterEmpty(t *testing.T) { + args := []string{"foo", "-c", "bar", "baz", "", "-d"} + flags := assertGet(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 TestBChainedThrice(t *testing.T) { + args := []string{"foo", "-bbb"} + flags := assertGet(t, args, 3, 0, nil) + for i := 0; i < 3; i++ { + s := fmt.Sprintf("flags[%d].", i) + if flags[i].Key != 'b' { + die(t, s+"Key", 'b', flags[i].Key) + } + if flags[i].Value != "" { + die(t, s+"Value", "", flags[i].Value) + } + } +} + +func TestẞChainedTwice(t *testing.T) { + args := []string{"foo", "-ßß"} + flags := assertGet(t, args, 2, 0, nil) + for i := 0; i < 2; i++ { + s := fmt.Sprintf("flags[%d].", i) + if flags[i].Key != 'ß' { + die(t, s+"Key", 'ß', flags[i].Key) + } + if flags[i].Value != "" { + die(t, s+"Value", "", flags[i].Value) + } + } +} + +func TestΛAsArgToC(t *testing.T) { + args := []string{"foo", "-c", "-λ"} + flags := assertGet(t, args, 1, 0, nil) + if flags[0].Key != 'c' { + die(t, "flags[0].Key", 'c', flags[0].Key) + } + if flags[0].Value != "-λ" { + die(t, "flags[0].Value", "-λ", flags[0].Value) + } +} + +func TestInvalidFlag(t *testing.T) { + args := []string{"foo", "-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{r: 'X'}) +} + +func TestAAfterArg(t *testing.T) { + args := []string{"foo", "bar", "-a"} + assertGet(t, args, 0, 2, nil) +} + +func TestXAfterDash(t *testing.T) { + args := []string{"foo", "-", "-x"} + assertGet(t, args, 0, 2, nil) +} + +func TestĦWithSpaceAndArg(t *testing.T) { + args := []string{"foo", "-Ħ", "bar"} + flags := assertGet(t, args, 1, 1, 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) + } +} + +func TestĦWithArg(t *testing.T) { + args := []string{"foo", "-Ħbar"} + flags := assertGet(t, args, 1, 0, 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ΛĦWithArg(t *testing.T) { + args := []string{"foo", "-λĦbar"} + flags := assertGet(t, args, 2, 0, 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) + } + if flags[1].Key != 'Ħ' { + die(t, "flags[1].Key", 'Ħ', flags[1].Key) + } + if flags[1].Value != "bar" { + die(t, "flags[1].Value", "bar", flags[1].Value) + } +} + +// LONG OPTS + +func assertGetLong(t *testing.T, args []string, fw, rw 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, rest, err := GetLong(args, opts) + if err != ew { + die(t, "err", ew, err) + } + if len(rest) != rw { + die(t, "rest", rw, rest) + } + 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, 0, 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, 0, 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, 0, 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, 0, 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, 0, nil) +} + +func TestĦaġratArgEqual(t *testing.T) { + args := []string{"foo", "--Ħ=bar"} + flags := assertGetLong(t, args, 1, 0, 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, 1, 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) + } +} -- cgit v1.2.3