From 6b5b7dd295e6fe372b73c731eff9b0439174a03f Mon Sep 17 00:00:00 2001 From: Mostafa Date: Wed, 8 Jan 2025 19:54:47 +0800 Subject: [PATCH] fix: parse arguments with spaces --- internal/engine/engine.go | 88 +++++++++++++++++++++++++--------- internal/engine/engine_test.go | 63 +++++++++++++++++++++--- 2 files changed, 122 insertions(+), 29 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 7d97f1e..ce4c7d0 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -169,7 +169,7 @@ func (be *BotEngine) ParseAndExecute( var cmds []string var args map[string]string - cmds, args, err := parseCommand(input) + cmds, args, err := parseInput(input) if err != nil { return command.CommandResult{ Message: err.Error(), @@ -180,35 +180,79 @@ func (be *BotEngine) ParseAndExecute( return be.executeCommand(appID, callerID, cmds, args) } -// parseCommand parses the input string into commands and arguments. +func parseCommandInput(cmdInput string) []string { + cmds := make([]string, 0) + + tokens := strings.Split(cmdInput, " ") + for _, token := range tokens { + token = strings.TrimSpace(token) + + if token != "" { + cmds = append(cmds, token) + } + } + + return cmds +} + +func parseArgumentInput(argInput string) (map[string]string, error) { + args := make(map[string]string) + + tokens := strings.Split(argInput, "--") + for _, token := range tokens { + token = strings.TrimSpace(token) + + if token != "" { + parts := strings.SplitN(token, "=", 2) + key := strings.TrimSpace(parts[0]) + + if key == "" { + return nil, fmt.Errorf("invalid argument format: %s", argInput) + } + + if len(parts) == 1 { + // Boolean argument + args[key] = "true" + } else { + value := strings.TrimSpace(parts[1]) + value = strings.Trim(value, "\"'") + value = strings.TrimSpace(value) + + args[key] = value + } + } + } + + return args, nil +} + +// parseInput parses the input string into commands and arguments. // The input string should be in the following format: // `command1 command2 --arg1=val1 --arg2=val2` // It returns an error if parsing fails. -func parseCommand(input string) ([]string, map[string]string, error) { - if strings.TrimSpace(input) == "" { +func parseInput(input string) ([]string, map[string]string, error) { + // normalize input + input = strings.ReplaceAll(input, "\t", " ") + input = strings.TrimSpace(input) + if input == "" { return nil, nil, errors.New("input string cannot be empty") } - // Split input by spaces while preserving argument values - parts := strings.Fields(input) + argIndex := strings.Index(input, "--") - // Prepare results - cmds := make([]string, 0) - args := make(map[string]string) + var cmdInput, argInput string + if argIndex != -1 { + cmdInput = input[:argIndex] + argInput = input[argIndex:] + } else { + cmdInput = input + argInput = "" + } - // Iterate over parts to separate commands and arguments - for _, part := range parts { - if strings.HasPrefix(part, "--") { - // Argument: split on '=' - argParts := strings.SplitN(part, "=", 2) - key := strings.TrimPrefix(argParts[0], "--") - if len(argParts) != 2 || strings.TrimSpace(key) == "" || strings.TrimSpace(argParts[1]) == "" { - return nil, nil, fmt.Errorf("invalid argument format: %s", part) - } - args[key] = argParts[1] - } else { - cmds = append(cmds, part) - } + cmds := parseCommandInput(cmdInput) + args, err := parseArgumentInput(argInput) + if err != nil { + return nil, nil, err } return cmds, args, nil diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index a23d332..1da26e5 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -30,6 +30,55 @@ func TestParseCommand(t *testing.T) { wantArgs: map[string]string{"arg1": "val1", "arg2": "val2"}, wantErr: nil, }, + { + name: "arguments with quotation", + input: "command1 --arg1='val1' --arg2=\"val2\"", + wantCmds: []string{"command1"}, + wantArgs: map[string]string{"arg1": "val1", "arg2": "val2"}, + wantErr: nil, + }, + { + name: "arguments with quotation inside value", + input: "command1 --arg1='val ' 1' --arg2=\"val \" 2\"", + wantCmds: []string{"command1"}, + wantArgs: map[string]string{"arg1": "val ' 1", "arg2": "val \" 2"}, + wantErr: nil, + }, + { + name: "arguments with quotation and spaces", + input: "command1 --arg1='val 1' --arg2=\"val 2\" --arg3=val3", + wantCmds: []string{"command1"}, + wantArgs: map[string]string{"arg1": "val 1", "arg2": "val 2", "arg3": "val3"}, + wantErr: nil, + }, + { + name: "arguments with = inside value", + input: "command1 --arg1='val=1'", + wantCmds: []string{"command1"}, + wantArgs: map[string]string{"arg1": "val=1"}, + wantErr: nil, + }, + { + name: "extra spaces", + input: "command1 command2 --arg1=' val 1' --arg2=\"val 2 \"", + wantCmds: []string{"command1", "command2"}, + wantArgs: map[string]string{"arg1": "val 1", "arg2": "val 2"}, + wantErr: nil, + }, + { + name: "with tabs", + input: "command1 --arg1='val 1 ' --arg2=\" val 2\"", + wantCmds: []string{"command1"}, + wantArgs: map[string]string{"arg1": "val 1", "arg2": "val 2"}, + wantErr: nil, + }, + { + name: "argument with empty value", + input: "command1 --arg1=\"\"", + wantCmds: []string{"command1"}, + wantArgs: map[string]string{"arg1": ""}, + wantErr: nil, + }, { name: "input with no arguments", input: "command1 command2", @@ -47,9 +96,9 @@ func TestParseCommand(t *testing.T) { { name: "invalid argument format (missing =)", input: "command1 --arg1", - wantCmds: nil, - wantArgs: nil, - wantErr: fmt.Errorf("invalid argument format: --arg1"), + wantCmds: []string{"command1"}, + wantArgs: map[string]string{"arg1": "true"}, + wantErr: nil, }, { name: "invalid argument format (empty key)", @@ -61,9 +110,9 @@ func TestParseCommand(t *testing.T) { { name: "invalid argument format (empty value)", input: "command1 --arg1=", - wantCmds: nil, - wantArgs: nil, - wantErr: fmt.Errorf("invalid argument format: --arg1="), + wantCmds: []string{"command1"}, + wantArgs: map[string]string{"arg1": ""}, + wantErr: nil, }, { name: "empty input", @@ -76,7 +125,7 @@ func TestParseCommand(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotCmds, gotArgs, gotErr := parseCommand(tt.input) + gotCmds, gotArgs, gotErr := parseInput(tt.input) // Compare commands assert.Equal(t, tt.wantCmds, gotCmds, "commands mismatch: got %v, want %v", gotCmds, tt.wantCmds)