From 265ccb5ea805d6f4c81a58a70519348bb7097a98 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 15 Mar 2024 14:26:10 -0500 Subject: [PATCH] Try to add usage string parsing Far harder than I first thought. --- README.md | 5 ++-- use.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++ use_test.go | 44 ++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 use.go create mode 100644 use_test.go diff --git a/README.md b/README.md index d217918..d068896 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,9 @@ type Option struct { } ``` -And is used by each [Command](https://pkg.go.dev/github.com/coder/serpent#Command) when -passed as an array to the `Options` field. +And is used by each [Command](https://pkg.go.dev/github.com/coder/serpent#Command) when passed as an array to the `Options` field. + +Each Option stores how it was configured in the `ValueSource` field. ## More coming... This README is a stub for now. We'll better explain the design and usage diff --git a/use.go b/use.go new file mode 100644 index 0000000..b255571 --- /dev/null +++ b/use.go @@ -0,0 +1,68 @@ +package serpent + +import ( + "strings" + + "golang.org/x/xerrors" +) + +type useArg struct { + // name is the name of the argument. + // or "bob" in arg "[bob]" + name string + // array is true if the argument is an array. + // or "true" in arg "[bob...]" + array bool + // required is true if the argument is required. + // or "true" in arg "" + required bool +} + +func parseUse(use string) []useArg { + words := strings.Fields(use) + args := make([]useArg, 0, len(words)) + for _, word := range words { + if len(word) < 2 { + continue + } + + isOptional := word[0] == '[' && word[len(word)-1] == ']' + isRequired := word[0] == '<' && word[len(word)-1] == '>' + if !isOptional && !isRequired { + continue + } + + name := word[1 : len(word)-1] + + const ellipse = "..." + isArray := strings.HasSuffix(name, ellipse) + + if isArray { + name = name[:len(name)-len(ellipse)] + } + + arg := useArg{ + name: name, + array: isArray, + required: isRequired, + } + + args = append(args, arg) + } + + return args +} + +// enforceUse returns a middleware that enforces that a command +// was invoked according to its use. It does not validate flags. +func enforceUse() MiddlewareFunc { + return func(next HandlerFunc) HandlerFunc { + return func(inv *Invocation) error { + if inv.Command.Use == "" { + return xerrors.Errorf("command has no use") + } + + return next(inv) + } + } +} diff --git a/use_test.go b/use_test.go new file mode 100644 index 0000000..3365a88 --- /dev/null +++ b/use_test.go @@ -0,0 +1,44 @@ +package serpent + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_parseUse(t *testing.T) { + t.Parallel() + tests := []struct { + use string + expected []useArg + }{ + { + use: " [optional] [optionalArray...]", + expected: []useArg{ + {name: "required", array: false, required: true}, + {name: "optional", array: false, required: false}, + {name: "requiredArray", array: true, required: true}, + {name: "optionalArray", array: true, required: false}, + }, + }, + { + use: " [singleOptional]", + expected: []useArg{{name: "single", array: false, required: true}, {name: "singleOptional", array: false, required: false}}, + }, + { + use: "noBrackets noBracketsEither", + expected: []useArg{}, + }, + { + use: "", + expected: []useArg{}, + }, + } + + for _, tc := range tests { + t.Run(tc.use, func(t *testing.T) { + result := parseUse(tc.use) + require.Equal(t, tc.expected, result) + }) + } +}