Skip to content
Merged
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
62 changes: 0 additions & 62 deletions param_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,65 +22,3 @@ func (p *Params) Set(value string) error {
(*p)[kv[0]] = kv[1]
return nil
}

// ParseParams parses a string in the format 'key1="value1",key2="value2"' and returns a Params map.
// Values must be quoted with double quotes. If the string is not in the correct format, it returns an error.
func ParseParams(value string) (Params, error) {
p := make(Params)
if value == "" {
return p, nil
}

// Parse comma-separated key="value" pairs
// We need to handle quoted values properly
pairs := parseKeyValuePairs(value)
for _, pair := range pairs {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("invalid parameter format: %s", pair)
}
key := strings.TrimSpace(kv[0])
val := strings.TrimSpace(kv[1])

// Remove quotes from value
if len(val) >= 2 && val[0] == '"' && val[len(val)-1] == '"' {
val = val[1 : len(val)-1]
} else {
return nil, fmt.Errorf("value must be quoted: %s", pair)
}

p[key] = val
}
return p, nil
}

// parseKeyValuePairs splits a string by commas, respecting quoted values
func parseKeyValuePairs(s string) []string {
var pairs []string
var current strings.Builder
inQuotes := false

for i := 0; i < len(s); i++ {
ch := s[i]

if ch == '"' {
inQuotes = !inQuotes
current.WriteByte(ch)
} else if ch == ',' && !inQuotes {
// End of a pair
if current.Len() > 0 {
pairs = append(pairs, strings.TrimSpace(current.String()))
current.Reset()
}
} else {
current.WriteByte(ch)
}
}

// Add the last pair
if current.Len() > 0 {
pairs = append(pairs, strings.TrimSpace(current.String()))
}

return pairs
}
134 changes: 0 additions & 134 deletions param_map_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"strings"
"testing"
)

Expand Down Expand Up @@ -96,136 +95,3 @@ func TestParams_SetMultiple(t *testing.T) {
t.Errorf("Params[key2] = %q, want %q", p["key2"], "value2")
}
}

func TestParseParams(t *testing.T) {
tests := []struct {
name string
value string
want Params
wantErr bool
errMatch string
}{
{
name: "single key=value",
value: `key="value"`,
want: Params{
"key": "value",
},
wantErr: false,
},
{
name: "multiple key=value pairs",
value: `key1="value1",key2="value2"`,
want: Params{
"key1": "value1",
"key2": "value2",
},
wantErr: false,
},
{
name: "value with spaces",
value: `name="John Doe"`,
want: Params{
"name": "John Doe",
},
wantErr: false,
},
{
name: "value with equals sign",
value: `formula="a=b+c"`,
want: Params{
"formula": "a=b+c",
},
wantErr: false,
},
{
name: "value with comma inside quotes",
value: `list="one,two,three"`,
want: Params{
"list": "one,two,three",
},
wantErr: false,
},
{
name: "multiple values with special chars",
value: `key1="value with spaces",key2="a=b",key3="x,y,z"`,
want: Params{
"key1": "value with spaces",
"key2": "a=b",
"key3": "x,y,z",
},
wantErr: false,
},
{
name: "empty value",
value: `key=""`,
want: Params{
"key": "",
},
wantErr: false,
},
{
name: "value without quotes",
value: `key=value`,
wantErr: true,
errMatch: "value must be quoted",
},
{
name: "invalid format - no equals",
value: `keyvalue`,
wantErr: true,
errMatch: "invalid parameter format",
},
{
name: "missing closing quote",
value: `key="value`,
wantErr: true,
errMatch: "value must be quoted",
},
{
name: "empty string",
value: ``,
want: Params{},
wantErr: false,
},
{
name: "spaces around key and value",
value: ` key1 = "value1" , key2 = "value2" `,
want: Params{
"key1": "value1",
"key2": "value2",
},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseParams(tt.value)

if (err != nil) != tt.wantErr {
t.Errorf("ParseParams() error = %v, wantErr %v", err, tt.wantErr)
return
}

if tt.wantErr && tt.errMatch != "" && err != nil {
if !strings.Contains(err.Error(), tt.errMatch) {
t.Errorf("ParseParams() error = %v, should contain %q", err, tt.errMatch)
}
return
}

if !tt.wantErr {
if len(got) != len(tt.want) {
t.Errorf("ParseParams() got %d params, want %d: got=%v, want=%v", len(got), len(tt.want), got, tt.want)
return
}
for k, v := range tt.want {
if got[k] != v {
t.Errorf("ParseParams()[%q] = %q, want %q", k, got[k], v)
}
}
}
})
}
}
136 changes: 136 additions & 0 deletions pkg/slashcommand/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# slashcommand

Package `slashcommand` provides a parser for slash commands commonly used in AI coding assistants.

## Overview

This package parses slash commands using bash-like argument parsing:
```
/task-name arg1 "arg 2" arg3
```

The parser extracts:
- **Task name**: The command identifier (without the leading `/`)
- **Arguments**: Positional arguments accessed via `$ARGUMENTS`, `$1`, `$2`, `$3`, etc.

Arguments are parsed like bash:
- Quoted arguments (single or double quotes) can contain spaces
- Quotes are removed from parsed arguments
- Escape sequences are supported in double quotes (`\"`)

## Installation

```bash
go get github.com/kitproj/coding-context-cli/pkg/slashcommand
```

## Usage

```go
import "github.com/kitproj/coding-context-cli/pkg/slashcommand"

// Parse a simple command
taskName, params, err := slashcommand.ParseSlashCommand("/fix-bug")
// taskName: "fix-bug"
// params: map[]

// Parse a command with arguments
taskName, params, err := slashcommand.ParseSlashCommand("/fix-bug 123")
// taskName: "fix-bug"
// params: map["ARGUMENTS": "123", "1": "123"]

// Parse a command with quoted arguments
taskName, params, err := slashcommand.ParseSlashCommand(`/code-review "Fix login bug" high`)
// taskName: "code-review"
// params: map["ARGUMENTS": "\"Fix login bug\" high", "1": "Fix login bug", "2": "high"]
```

## Command Format

### Basic Structure
```
/task-name arg1 "arg 2" arg3 ...
```

### Argument Parsing Rules
1. Commands **must** start with `/`
2. Task name comes immediately after the `/` (no spaces)
3. Arguments can be quoted with single (`'`) or double (`"`) quotes
4. Quoted arguments can contain spaces
5. Quotes are removed from parsed arguments
6. Double quotes support escape sequences: `\"`
7. Single quotes preserve everything literally (no escapes)

### Returned Parameters
The `params` map contains:
- `ARGUMENTS`: The full argument string (with quotes preserved)
- `1`, `2`, `3`, etc.: Individual positional arguments (with quotes removed)

### Valid Examples
```
/fix-bug # No arguments
/fix-bug 123 # Single argument: $1 = "123"
/deploy staging v1.2.3 # Two arguments: $1 = "staging", $2 = "v1.2.3"
/code-review "PR #42" # Quoted argument: $1 = "PR #42"
/echo 'He said "hello"' # Single quotes preserve quotes: $1 = "He said \"hello\""
/echo "He said \"hello\"" # Escaped quotes in double quotes: $1 = "He said \"hello\""
```

### Invalid Examples
```
fix-bug # Missing leading /
/ # Empty command
/fix-bug "unclosed # Unclosed quote
```

## Error Handling

The parser returns descriptive errors for invalid commands:

```go
_, _, err := slashcommand.ParseSlashCommand("fix-bug")
// Error: slash command must start with '/'

_, _, err := slashcommand.ParseSlashCommand("/")
// Error: slash command cannot be empty

_, _, err := slashcommand.ParseSlashCommand(`/fix-bug "unclosed`)
// Error: unclosed quote in arguments
```

## API

### ParseSlashCommand

```go
func ParseSlashCommand(command string) (taskName string, params map[string]string, err error)
```

Parses a slash command string and extracts the task name and arguments.

**Parameters:**
- `command` (string): The slash command to parse

**Returns:**
- `taskName` (string): The task name without the leading `/`
- `params` (map[string]string): Contains `ARGUMENTS` (full arg string) and `1`, `2`, `3`, etc. (positional args)
- `err` (error): Error if the command format is invalid

## Testing

The package includes comprehensive tests covering:
- Commands without arguments
- Commands with single and multiple arguments
- Quoted arguments (both single and double quotes)
- Escaped quotes
- Empty quoted arguments
- Edge cases and error conditions

Run tests with:
```bash
go test -v ./pkg/slashcommand
```

## License

This package is part of the [coding-context-cli](https://github.com/kitproj/coding-context-cli) project and is licensed under the MIT License.
38 changes: 38 additions & 0 deletions pkg/slashcommand/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package slashcommand_test

import (
"fmt"

"github.com/kitproj/coding-context-cli/pkg/slashcommand"
)

func ExampleParseSlashCommand() {
// Parse a simple command without parameters
taskName, params, err := slashcommand.ParseSlashCommand("/fix-bug")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Task: %s, Params: %v\n", taskName, params)

// Parse a command with single argument
taskName, params, err = slashcommand.ParseSlashCommand("/fix-bug 123")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Task: %s, $1: %s\n", taskName, params["1"])

// Parse a command with multiple arguments
taskName, params, err = slashcommand.ParseSlashCommand(`/implement-feature "User Login" high`)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Task: %s, $1: %s, $2: %s\n", taskName, params["1"], params["2"])

// Output:
// Task: fix-bug, Params: map[]
// Task: fix-bug, $1: 123
// Task: implement-feature, $1: User Login, $2: high
}
Loading