diff --git a/docs/reference/cli.md b/docs/reference/cli.md index a53d3b63..6a74b1c0 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -387,9 +387,14 @@ Quoted arguments preserve spaces: ### Named Parameters -Named parameters use the format `key="value"` with **mandatory double quotes**: +Named parameters use the format `key="value"` or `key='value'` with **quoted values**: - `/fix-bug issue="PROJ-123"` → `$1` = `issue="PROJ-123"`, `$issue` = `PROJ-123` - `/deploy env="production" version="1.2.3"` → `$1` = `env="production"`, `$2` = `version="1.2.3"`, `$env` = `production`, `$version` = `1.2.3` +- `/run path='/usr/local/bin'` → `$1` = `path='/usr/local/bin'`, `$path` = `/usr/local/bin` + +Both double quotes (`"`) and single quotes (`'`) are supported for named parameter values: +- Double quotes support escape sequences: `msg="He said \"hello\""` → `$msg` = `He said "hello"` +- Single quotes are literal (no escaping): `path='C:\Users\test'` → `$path` = `C:\Users\test` Named parameters are counted as positional arguments (retaining their original form) while also being available by their key name: - `/task arg1 key="value" arg2` → `$1` = `arg1`, `$2` = `key="value"`, `$3` = `arg2`, `$key` = `value` @@ -398,7 +403,10 @@ Named parameter values can contain spaces and special characters: - `/run message="Hello, World!"` → `$1` = `message="Hello, World!"`, `$message` = `Hello, World!` - `/config query="x=y+z"` → `$1` = `query="x=y+z"`, `$query` = `x=y+z` -**Note:** Unquoted values (e.g., `key=value`) or single-quoted values (e.g., `key='value'`) are treated as regular positional arguments, not named parameters. +You can mix quote types in a single command: +- `/deploy env="production" region='us-east'` → `$env` = `production`, `$region` = `us-east` + +**Note:** Unquoted values (e.g., `key=value`) are treated as regular positional arguments, not named parameters. ### Example with Positional Parameters diff --git a/pkg/codingcontext/slashcommand.go b/pkg/codingcontext/slashcommand.go index 68dbb098..e09b0517 100644 --- a/pkg/codingcontext/slashcommand.go +++ b/pkg/codingcontext/slashcommand.go @@ -200,8 +200,8 @@ func parseBashArgsWithNamed(s string) (map[string]string, error) { return params, nil } -// parseNamedParamWithQuotes checks if an argument is a named parameter in key="value" format. -// Double quotes are mandatory for the value portion. +// parseNamedParamWithQuotes checks if an argument is a named parameter in key="value" or key='value' format. +// Both double quotes and single quotes are supported for the value portion. // Returns the key, value (with quotes stripped), and whether it was a valid named parameter. // Key must be non-empty and cannot contain spaces or tabs. func parseNamedParamWithQuotes(rawArg string) (key string, value string, isNamed bool) { @@ -220,27 +220,36 @@ func parseNamedParamWithQuotes(rawArg string) (key string, value string, isNamed // The value portion (after '=') valuePart := rawArg[eqIdx+1:] - // Value must start with double quote (mandatory) - if len(valuePart) < 2 || valuePart[0] != '"' { + // Value must be quoted (either double or single quotes) + if len(valuePart) < 2 { return "", "", false } - // Value must end with double quote - if valuePart[len(valuePart)-1] != '"' { - return "", "", false - } + firstChar := valuePart[0] + lastChar := valuePart[len(valuePart)-1] + + // Check if value starts and ends with matching quotes + if (firstChar == '"' && lastChar == '"') || (firstChar == '\'' && lastChar == '\'') { + // Extract the value between quotes + quotedValue := valuePart[1 : len(valuePart)-1] - // Extract the value between quotes and handle escaped quotes - quotedValue := valuePart[1 : len(valuePart)-1] - var unescaped strings.Builder - for i := 0; i < len(quotedValue); i++ { - if quotedValue[i] == '\\' && i+1 < len(quotedValue) && quotedValue[i+1] == '"' { - unescaped.WriteByte('"') - i++ // Skip the escaped quote + // Handle escaped quotes (only for double quotes) + if firstChar == '"' { + var unescaped strings.Builder + for i := 0; i < len(quotedValue); i++ { + if quotedValue[i] == '\\' && i+1 < len(quotedValue) && quotedValue[i+1] == '"' { + unescaped.WriteByte('"') + i++ // Skip the escaped quote + } else { + unescaped.WriteByte(quotedValue[i]) + } + } + return key, unescaped.String(), true } else { - unescaped.WriteByte(quotedValue[i]) + // Single quotes: no escape handling (literal value) + return key, quotedValue, true } } - return key, unescaped.String(), true + return "", "", false } diff --git a/pkg/codingcontext/slashcommand_test.go b/pkg/codingcontext/slashcommand_test.go index 3b8ddc81..67513203 100644 --- a/pkg/codingcontext/slashcommand_test.go +++ b/pkg/codingcontext/slashcommand_test.go @@ -319,13 +319,64 @@ func TestParseSlashCommand(t *testing.T) { wantErr: false, }, { - name: "single-quoted key=value is treated as positional argument not named parameter", + name: "single-quoted named parameter", command: `/task key='value'`, wantFound: true, wantTask: "task", wantParams: map[string]string{ "ARGUMENTS": `key='value'`, - "1": "key=value", + "1": `key='value'`, + "key": "value", + }, + wantErr: false, + }, + { + name: "single-quoted named parameter with spaces in value", + command: `/task message='Hello World'`, + wantFound: true, + wantTask: "task", + wantParams: map[string]string{ + "ARGUMENTS": `message='Hello World'`, + "1": `message='Hello World'`, + "message": "Hello World", + }, + wantErr: false, + }, + { + name: "single-quoted named parameter with special characters", + command: `/run path='/usr/local/bin'`, + wantFound: true, + wantTask: "run", + wantParams: map[string]string{ + "ARGUMENTS": `path='/usr/local/bin'`, + "1": `path='/usr/local/bin'`, + "path": "/usr/local/bin", + }, + wantErr: false, + }, + { + name: "mixed quote types - double and single quotes", + command: `/deploy env="production" region='us-east'`, + wantFound: true, + wantTask: "deploy", + wantParams: map[string]string{ + "ARGUMENTS": `env="production" region='us-east'`, + "1": `env="production"`, + "2": `region='us-east'`, + "env": "production", + "region": "us-east", + }, + wantErr: false, + }, + { + name: "single-quoted named parameter with backslash", + command: `/task path='C:\Users\test'`, + wantFound: true, + wantTask: "task", + wantParams: map[string]string{ + "ARGUMENTS": `path='C:\Users\test'`, + "1": `path='C:\Users\test'`, + "path": `C:\Users\test`, }, wantErr: false, },