diff options
| author | Thomas Voss <mail@thomasvoss.com> | 2023-12-05 01:32:52 +0100 | 
|---|---|---|
| committer | Thomas Voss <mail@thomasvoss.com> | 2023-12-05 01:32:52 +0100 | 
| commit | 264780bb60dda33e8d06c48be5f1991212d62a9a (patch) | |
| tree | 684d61e3e7aaddf73ce4e7c6fc731f72a23a7fac | |
| parent | 799bb3be1d099ce6979cd6baa8b84acc507130bd (diff) | |
| -rw-r--r-- | README.md | 27 | ||||
| -rw-r--r-- | v2/err.go | 31 | ||||
| -rw-r--r-- | v2/go.mod | 3 | ||||
| -rw-r--r-- | v2/opts.go | 234 | ||||
| -rw-r--r-- | v2/opts_test.go | 366 | 
5 files changed, 651 insertions, 10 deletions
| @@ -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) +	} +} |