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
75 changes: 49 additions & 26 deletions pkg/slashcommand/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ Package `slashcommand` provides a parser for slash commands commonly used in AI

## Overview

This package parses slash commands using bash-like argument parsing:
This package parses slash commands using bash-like argument parsing. The parser can find slash commands anywhere in the input text, not just at the beginning:
```
/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.
- **Found status**: Boolean indicating whether a slash command was found

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 (`\"`)
- Arguments are extracted until end of line

## Installation

Expand All @@ -30,19 +32,34 @@ go get github.com/kitproj/coding-context-cli/pkg/slashcommand
import "github.com/kitproj/coding-context-cli/pkg/slashcommand"

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

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

// Parse a command with quoted arguments
taskName, params, err := slashcommand.ParseSlashCommand(`/code-review "Fix login bug" high`)
taskName, params, found, 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"]
// found: true

// Command found in middle of text
taskName, params, found, err := slashcommand.ParseSlashCommand("Please /deploy production now")
// taskName: "deploy"
// params: map["ARGUMENTS": "production now", "1": "production", "2": "now"]
// found: true

// No command found
taskName, params, found, err := slashcommand.ParseSlashCommand("No command here")
// taskName: ""
// params: nil
// found: false
```

## Command Format
Expand All @@ -53,13 +70,16 @@ taskName, params, err := slashcommand.ParseSlashCommand(`/code-review "Fix login
```

### Argument Parsing Rules
1. Commands **must** start with `/`
1. Slash commands can appear **anywhere** in the input text
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)
3. Arguments are extracted until end of line (newline stops argument collection)
4. Arguments can be quoted with single (`'`) or double (`"`) quotes
5. Quoted arguments can contain spaces
6. Quotes are removed from parsed arguments
7. Double quotes support escape sequences: `\"`
8. Single quotes preserve everything literally (no escapes)
9. Text before the `/` is ignored (prefix lost)
10. Text after a newline is ignored (suffix lost)

### Returned Parameters
The `params` map contains:
Expand All @@ -74,47 +94,48 @@ The `params` map contains:
/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\""
Please /fix-bug 123 today # Command in middle: task = "fix-bug", $1 = "123", $2 = "today"
Text /deploy prod\nNext line # Arguments stop at newline: task = "deploy", $1 = "prod"
```

### Invalid Examples
### Cases with No Command Found
```
fix-bug # Missing leading /
/ # Empty command
/fix-bug "unclosed # Unclosed quote
fix-bug # Missing leading /: found = false
No command here # No slash: found = false
```

## Error Handling

The parser returns descriptive errors for invalid commands:
The parser returns errors only for malformed commands (e.g., unclosed quotes). If no slash command is found, the function returns `found=false` without an error.

```go
_, _, err := slashcommand.ParseSlashCommand("fix-bug")
// Error: slash command must start with '/'
// No command found - not an error
_, _, found, err := slashcommand.ParseSlashCommand("No command here")
// found: false, err: nil

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

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

## API

### ParseSlashCommand

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

Parses a slash command string and extracts the task name and arguments.
Parses a slash command string and extracts the task name and arguments. The function searches for a slash command anywhere in the input text.

**Parameters:**
- `command` (string): The slash command to parse
- `command` (string): The text that may contain a slash command

**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
- `found` (bool): True if a slash command was found, false otherwise
- `err` (error): Error if the command format is invalid (e.g., unclosed quotes)

## Testing

Expand All @@ -124,6 +145,8 @@ The package includes comprehensive tests covering:
- Quoted arguments (both single and double quotes)
- Escaped quotes
- Empty quoted arguments
- Commands embedded in text (prefix/suffix text)
- Commands with newlines
- Edge cases and error conditions

Run tests with:
Expand Down
18 changes: 12 additions & 6 deletions pkg/slashcommand/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,34 @@ import (

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

// Parse a command with single argument
taskName, params, err = slashcommand.ParseSlashCommand("/fix-bug 123")
taskName, params, found, 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"])
if found {
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`)
taskName, params, found, 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"])
if found {
fmt.Printf("Task: %s, $1: %s, $2: %s\n", taskName, params["1"], params["2"])
}

// Output:
// Task: fix-bug, Params: map[]
Expand Down
67 changes: 44 additions & 23 deletions pkg/slashcommand/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,70 +6,91 @@ import (
)

// ParseSlashCommand parses a slash command string and extracts the task name and parameters.
// It searches for a slash command anywhere in the input string, not just at the beginning.
// The expected format is: /task-name arg1 "arg 2" arg3
//
// The function will find the slash command even if it's embedded in other text. For example:
// - "Please /fix-bug 123 today" -> taskName: "fix-bug", params: {"ARGUMENTS": "123 today", "1": "123", "2": "today"}, found: true
// - "Some text /code-review" -> taskName: "code-review", params: {}, found: true
//
// Arguments are parsed like Bash:
// - Quoted arguments can contain spaces
// - Both single and double quotes are supported
// - Quotes are removed from the parsed arguments
// - Arguments are extracted until end of line
//
// Examples:
// - "/fix-bug 123" -> taskName: "fix-bug", params: {"ARGUMENTS": "123", "1": "123"}
// - "/code-review \"PR #42\" high" -> taskName: "code-review", params: {"ARGUMENTS": "\"PR #42\" high", "1": "PR #42", "2": "high"}
// - "/fix-bug 123" -> taskName: "fix-bug", params: {"ARGUMENTS": "123", "1": "123"}, found: true
// - "/code-review \"PR #42\" high" -> taskName: "code-review", params: {"ARGUMENTS": "\"PR #42\" high", "1": "PR #42", "2": "high"}, found: true
// - "no command here" -> taskName: "", params: nil, found: false
//
// Returns:
// - taskName: the task name (without the leading slash)
// - params: a map containing:
// - "ARGUMENTS": the full argument string (with quotes preserved)
// - "1", "2", "3", etc.: positional arguments (with quotes removed)
// - err: an error if the command format is invalid
func ParseSlashCommand(command string) (taskName string, params map[string]string, err error) {
command = strings.TrimSpace(command)

// Check if command starts with '/'
if !strings.HasPrefix(command, "/") {
return "", nil, fmt.Errorf("slash command must start with '/'")
// - found: true if a slash command was found, false otherwise
// - err: an error if the command format is invalid (e.g., unclosed quotes)
func ParseSlashCommand(command string) (taskName string, params map[string]string, found bool, err error) {
// Find the slash command anywhere in the string
slashIdx := strings.Index(command, "/")
if slashIdx == -1 {
return "", nil, false, nil
}

// Remove leading slash
command = command[1:]
// Extract from the slash onwards
command = command[slashIdx+1:]

if command == "" {
return "", nil, fmt.Errorf("slash command cannot be empty")
return "", nil, false, nil
}

// Find the task name (first word)
spaceIdx := strings.IndexAny(command, " \t")
if spaceIdx == -1 {
// No arguments, just the task name
return command, make(map[string]string), nil
// Find the task name (first word after the slash)
// Task name ends at first whitespace or newline
endIdx := strings.IndexAny(command, " \t\n\r")
if endIdx == -1 {
// No arguments, just the task name (rest of the string)
return command, make(map[string]string), true, nil
}

taskName = command[:spaceIdx]
argsString := strings.TrimSpace(command[spaceIdx:])
taskName = command[:endIdx]

// Extract arguments until end of line
restOfString := command[endIdx:]
newlineIdx := strings.IndexAny(restOfString, "\n\r")
var argsString string
if newlineIdx == -1 {
// No newline, use everything
argsString = strings.TrimSpace(restOfString)
} else {
// Only use up to the newline
argsString = strings.TrimSpace(restOfString[:newlineIdx])
}

params = make(map[string]string)

// Store the full argument string (with quotes preserved)
params["ARGUMENTS"] = argsString
if argsString != "" {
params["ARGUMENTS"] = argsString
}

// If there are no arguments, return early
if argsString == "" {
return taskName, params, nil
return taskName, params, true, nil
}

// Parse positional arguments using bash-like parsing
args, err := parseBashArgs(argsString)
if err != nil {
return "", nil, err
return "", nil, false, err
}

// Add positional arguments as $1, $2, $3, etc.
for i, arg := range args {
params[fmt.Sprintf("%d", i+1)] = arg
}

return taskName, params, nil
return taskName, params, true, nil
}

// parseBashArgs parses a string into arguments like bash does, respecting quoted values
Expand Down
Loading