summaryrefslogtreecommitdiff
path: root/internal/arg
diff options
context:
space:
mode:
Diffstat (limited to 'internal/arg')
-rw-r--r--internal/arg/arg.go124
-rw-r--r--internal/arg/arg_test.go122
2 files changed, 246 insertions, 0 deletions
diff --git a/internal/arg/arg.go b/internal/arg/arg.go
new file mode 100644
index 0000000..0577d74
--- /dev/null
+++ b/internal/arg/arg.go
@@ -0,0 +1,124 @@
+package arg
+
+import "strings"
+
+import "github.com/posener/complete/internal/tokener"
+
+// Arg is typed a command line argument.
+type Arg struct {
+ Text string
+ Completed bool
+ Parsed
+}
+
+// Parsed contains information about the argument.
+type Parsed struct {
+ Flag string
+ HasFlag bool
+ Value string
+ Dashes string
+ HasValue bool
+}
+
+// Parse parses a typed command line argument list, and returns a list of arguments.
+func Parse(line string) []Arg {
+ var args []Arg
+ for {
+ arg, after := next(line)
+ if arg.Text != "" {
+ args = append(args, arg)
+ }
+ line = after
+ if line == "" {
+ break
+ }
+ }
+ return args
+}
+
+// next returns the first argument in the line and the rest of the line.
+func next(line string) (arg Arg, after string) {
+ defer arg.parse()
+ // Start and end of the argument term.
+ var start, end int
+ // Stack of quote marks met during the paring of the argument.
+ var token tokener.Tokener
+ // Skip prefix spaces.
+ for start = 0; start < len(line); start++ {
+ token.Visit(line[start])
+ if !token.LastSpace() {
+ break
+ }
+ }
+ // If line is only spaces, return empty argument and empty leftovers.
+ if start == len(line) {
+ return
+ }
+
+ for end = start + 1; end < len(line); end++ {
+ token.Visit(line[end])
+ if token.LastSpace() {
+ arg.Completed = true
+ break
+ }
+ }
+ arg.Text = line[start:end]
+ if !arg.Completed {
+ return
+ }
+ start2 := end
+
+ // Skip space after word.
+ for start2 < len(line) {
+ token.Visit(line[start2])
+ if !token.LastSpace() {
+ break
+ }
+ start2++
+ }
+ after = line[start2:]
+ return
+}
+
+// parse a flag from an argument. The flag can have value attached when it is given in the
+// `-key=value` format.
+func (a *Arg) parse() {
+ if len(a.Text) == 0 {
+ return
+ }
+
+ // A pure value, no flag.
+ if a.Text[0] != '-' {
+ a.Value = a.Text
+ a.HasValue = true
+ return
+ }
+
+ // Seprate the dashes from the flag name.
+ dahsI := 1
+ if len(a.Text) > 1 && a.Text[1] == '-' {
+ dahsI = 2
+ }
+ a.Dashes = a.Text[:dahsI]
+ a.HasFlag = true
+ a.Flag = a.Text[dahsI:]
+
+ // Empty flag
+ if a.Flag == "" {
+ return
+ }
+ // Third dash or empty flag with equal is forbidden.
+ if a.Flag[0] == '-' || a.Flag[0] == '=' {
+ a.Parsed = Parsed{}
+ return
+ }
+ // The flag is valid.
+
+ // Check if flag has a value.
+ if equal := strings.IndexRune(a.Flag, '='); equal != -1 {
+ a.Flag, a.Value = a.Flag[:equal], a.Flag[equal+1:]
+ a.HasValue = true
+ return
+ }
+
+}
diff --git a/internal/arg/arg_test.go b/internal/arg/arg_test.go
new file mode 100644
index 0000000..11130ce
--- /dev/null
+++ b/internal/arg/arg_test.go
@@ -0,0 +1,122 @@
+package arg
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParse(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ line string
+ args []Arg
+ }{
+ {
+ line: "a b",
+ args: []Arg{{Text: "a", Completed: true}, {Text: "b", Completed: false}},
+ },
+ {
+ line: " a b ",
+ args: []Arg{{Text: "a", Completed: true}, {Text: "b", Completed: true}},
+ },
+ {
+ line: "a b",
+ args: []Arg{{Text: "a", Completed: true}, {Text: "b", Completed: false}},
+ },
+ {
+ line: " a ",
+ args: []Arg{{Text: "a", Completed: true}},
+ },
+ {
+ line: " a",
+ args: []Arg{{Text: "a", Completed: false}},
+ },
+ {
+ line: " ",
+ args: nil,
+ },
+ {
+ line: "",
+ args: nil,
+ },
+ {
+ line: `\ a\ b c\ `,
+ args: []Arg{{Text: `\ a\ b`, Completed: true}, {Text: `c\ `, Completed: false}},
+ },
+ {
+ line: `"\"'\''" '"'`,
+ args: []Arg{{Text: `"\"'\''"`, Completed: true}, {Text: `'"'`, Completed: false}},
+ },
+ {
+ line: `"a b"`,
+ args: []Arg{{Text: `"a b"`, Completed: false}},
+ },
+ {
+ line: `"a b" `,
+ args: []Arg{{Text: `"a b"`, Completed: true}},
+ },
+ {
+ line: `"a b"c`,
+ args: []Arg{{Text: `"a b"c`, Completed: false}},
+ },
+ {
+ line: `"a b"c `,
+ args: []Arg{{Text: `"a b"c`, Completed: true}},
+ },
+ {
+ line: `"a b" c`,
+ args: []Arg{{Text: `"a b"`, Completed: true}, {Text: "c", Completed: false}},
+ },
+ {
+ line: `"a `,
+ args: []Arg{{Text: `"a `, Completed: false}},
+ },
+ {
+ line: `\"a b`,
+ args: []Arg{{Text: `\"a`, Completed: true}, {Text: "b", Completed: false}},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.line, func(t *testing.T) {
+ args := Parse(tt.line)
+ // Clear parsed part of the arguments. It is tested in the TestArgsParsed test.
+ for i := range args {
+ arg := args[i]
+ arg.Parsed = Parsed{}
+ args[i] = arg
+ }
+ assert.Equal(t, tt.args, args)
+ })
+ }
+}
+
+func TestArgsParsed(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ text string
+ parsed Parsed
+ }{
+ {text: "-", parsed: Parsed{Dashes: "-", HasFlag: true}},
+ {text: "--", parsed: Parsed{Dashes: "--", HasFlag: true}},
+ {text: "---"}, // Forbidden.
+ {text: "--="}, // Forbidden.
+ {text: "-="}, // Forbidden.
+ {text: "-a-b", parsed: Parsed{Dashes: "-", Flag: "a-b", HasFlag: true}},
+ {text: "--a-b", parsed: Parsed{Dashes: "--", Flag: "a-b", HasFlag: true}},
+ {text: "-a-b=c-d=e", parsed: Parsed{Dashes: "-", Flag: "a-b", HasFlag: true, Value: "c-d=e", HasValue: true}},
+ {text: "--a-b=c-d=e", parsed: Parsed{Dashes: "--", Flag: "a-b", HasFlag: true, Value: "c-d=e", HasValue: true}},
+ {text: "--a-b=", parsed: Parsed{Dashes: "--", Flag: "a-b", HasFlag: true, Value: "", HasValue: true}},
+ {text: "a", parsed: Parsed{Value: "a", HasValue: true}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.text, func(t *testing.T) {
+ arg := Parse(tt.text)[0]
+ assert.Equal(t, tt.parsed, arg.Parsed)
+ })
+ }
+}