diff --git a/weed/shell/shell_liner.go b/weed/shell/shell_liner.go index 220b04343..00831d42e 100644 --- a/weed/shell/shell_liner.go +++ b/weed/shell/shell_liner.go @@ -7,7 +7,6 @@ import ( "math/rand/v2" "os" "path" - "regexp" "slices" "strings" @@ -43,8 +42,6 @@ func RunShell(options ShellOptions) { defer saveHistory() - reg, _ := regexp.Compile(`'.*?'|".*?"|\S+`) - commandEnv := NewCommandEnv(&options) ctx := context.Background() @@ -89,25 +86,21 @@ func RunShell(options ShellOptions) { } for _, c := range util.StringSplit(cmd, ";") { - if processEachCmd(reg, c, commandEnv) { + if processEachCmd(c, commandEnv) { return } } } } -func processEachCmd(reg *regexp.Regexp, cmd string, commandEnv *CommandEnv) bool { - cmds := reg.FindAllString(cmd, -1) +func processEachCmd(cmd string, commandEnv *CommandEnv) bool { + cmds := splitCommandLine(cmd) if len(cmds) == 0 { return false } else { - args := make([]string, len(cmds[1:])) - - for i := range args { - args[i] = strings.Trim(string(cmds[1+i]), "\"'") - } + args := cmds[1:] cmd := cmds[0] if cmd == "help" || cmd == "?" { @@ -133,6 +126,70 @@ func processEachCmd(reg *regexp.Regexp, cmd string, commandEnv *CommandEnv) bool return false } +func stripQuotes(s string) string { + tokens, unbalanced := parseShellInput(s, false) + if unbalanced { + return s + } + if len(tokens) > 0 { + return tokens[0] + } + return "" +} + +func splitCommandLine(line string) []string { + tokens, _ := parseShellInput(line, true) + return tokens +} + +func parseShellInput(line string, split bool) (args []string, unbalanced bool) { + var current strings.Builder + inDoubleQuotes := false + inSingleQuotes := false + escaped := false + + for i := 0; i < len(line); i++ { + c := line[i] + + if escaped { + current.WriteByte(c) + escaped = false + continue + } + + if c == '\\' && !inSingleQuotes { + escaped = true + continue + } + + if c == '"' && !inSingleQuotes { + inDoubleQuotes = !inDoubleQuotes + continue + } + + if c == '\'' && !inDoubleQuotes { + inSingleQuotes = !inSingleQuotes + continue + } + + if split && (c == ' ' || c == '\t' || c == '\n' || c == '\r') && !inDoubleQuotes && !inSingleQuotes { + if current.Len() > 0 { + args = append(args, current.String()) + current.Reset() + } + continue + } + + current.WriteByte(c) + } + + if current.Len() > 0 { + args = append(args, current.String()) + } + + return args, inDoubleQuotes || inSingleQuotes || escaped +} + func printGenericHelp() { msg := `Type: "help " for help on . Most commands support " -h" also for options. diff --git a/weed/shell/shell_liner_test.go b/weed/shell/shell_liner_test.go new file mode 100644 index 000000000..bfdd2b378 --- /dev/null +++ b/weed/shell/shell_liner_test.go @@ -0,0 +1,105 @@ +package shell + +import ( + "flag" + "reflect" + "testing" +) + +func TestSplitCommandLine(t *testing.T) { + tests := []struct { + input string + expected []string + }{ + { + input: `s3.configure -user=test`, + expected: []string{`s3.configure`, `-user=test`}, + }, + { + input: `s3.configure -user=Test_number_004 -account_display_name="Test number 004" -actions=write -apply`, + expected: []string{`s3.configure`, `-user=Test_number_004`, `-account_display_name=Test number 004`, `-actions=write`, `-apply`}, + }, + { + input: `s3.configure -user=Test_number_004 -account_display_name='Test number 004' -actions=write -apply`, + expected: []string{`s3.configure`, `-user=Test_number_004`, `-account_display_name=Test number 004`, `-actions=write`, `-apply`}, + }, + { + input: `s3.configure -flag="a b"c'd e'`, + expected: []string{`s3.configure`, `-flag=a bcd e`}, + }, + { + input: `s3.configure -name="a\"b"`, + expected: []string{`s3.configure`, `-name=a"b`}, + }, + { + input: `s3.configure -path='a\ b'`, + expected: []string{`s3.configure`, `-path=a\ b`}, + }, + } + + for _, tt := range tests { + got := splitCommandLine(tt.input) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("input: %s\ngot: %v\nwant: %v", tt.input, got, tt.expected) + } + } +} + +func TestStripQuotes(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {input: `"Test number 004"`, expected: `Test number 004`}, + {input: `'Test number 004'`, expected: `Test number 004`}, + {input: `-account_display_name="Test number 004"`, expected: `-account_display_name=Test number 004`}, + {input: `-flag="a"b'c'`, expected: `-flag=abc`}, + {input: `-name="a\"b"`, expected: `-name=a"b`}, + {input: `-path='a\ b'`, expected: `-path=a\ b`}, + {input: `"unbalanced`, expected: `"unbalanced`}, + {input: `'unbalanced`, expected: `'unbalanced`}, + {input: `-name="a\"b`, expected: `-name="a\"b`}, + {input: `trailing\`, expected: `trailing\`}, + } + + for _, tt := range tests { + got := stripQuotes(tt.input) + if got != tt.expected { + t.Errorf("input: %s, got: %s, want: %s", tt.input, got, tt.expected) + } + } +} + +func TestFlagParsing(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + displayName := fs.String("account_display_name", "", "display name") + + rawArg := `-account_display_name="Test number 004"` + args := []string{stripQuotes(rawArg)} + err := fs.Parse(args) + if err != nil { + t.Fatal(err) + } + + expected := "Test number 004" + if *displayName != expected { + t.Errorf("got: [%s], want: [%s]", *displayName, expected) + } +} + +func TestEscapedFlagParsing(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + name := fs.String("name", "", "name") + + rawArg := `-name="a\"b"` + args := []string{stripQuotes(rawArg)} + err := fs.Parse(args) + if err != nil { + t.Fatal(err) + } + + expected := `a"b` + if *name != expected { + t.Errorf("got: [%s], want: [%s]", *name, expected) + } +}