Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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

Expand Down
43 changes: 26 additions & 17 deletions pkg/codingcontext/slashcommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
55 changes: 53 additions & 2 deletions pkg/codingcontext/slashcommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Loading