Files
kvs-sh/utils.go
ryyst efaa5cdcc9 feat: add shell scripting with variables
Implemented roadmap #9: Shell scripts execution with variables.

Features:
- set <name> <value> - Set shell variable
- unset <name> - Remove variable
- vars - List all variables
- Variable substitution in all commands

Variable syntax:
- $VAR or ${VAR} - Shell variables
- $ENV:VARNAME - Environment variables

Variables expand before command execution, enabling:
- Dynamic key paths: put $BASE/$ID '{"data":"value"}'
- Reusable values: set TOKEN xyz && auth $TOKEN
- Script parameterization in batch files

Example batch script:
  set USER alice
  set KEY_PREFIX users
  put $KEY_PREFIX/$USER '{"name":"$USER"}'
  get $KEY_PREFIX/$USER

Variables are session-scoped and work in both interactive
and batch modes. Environment variables can be injected using
$ENV:HOME syntax.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 00:01:36 +03:00

143 lines
4.1 KiB
Go

package main
import (
"fmt"
"strings"
"github.com/chzyer/readline"
"github.com/fatih/color"
)
// Color helpers
var (
cyan = color.New(color.FgCyan).SprintFunc()
green = color.New(color.FgGreen).SprintFunc()
yellow = color.New(color.FgYellow).SprintFunc()
red = color.New(color.FgRed).SprintFunc()
magenta = color.New(color.FgMagenta).SprintFunc()
)
// parseCommand parses command line respecting quotes
func parseCommand(line string) []string {
var parts []string
var current strings.Builder
var quoteChar rune = 0 // Use a rune to store the active quote character (' or ")
for _, ch := range line {
switch {
// If we see a quote character
case ch == '\'' || ch == '"':
if quoteChar == 0 {
// Not currently inside a quoted string, so start one
quoteChar = ch
} else if quoteChar == ch {
// Inside a quoted string and found the matching quote, so end it
quoteChar = 0
} else {
// Inside a quoted string but found the *other* type of quote, treat it as a literal character
current.WriteRune(ch)
}
// If we see a space or tab and are NOT inside a quoted string
case (ch == ' ' || ch == '\t') && quoteChar == 0:
if current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
}
// Any other character
default:
current.WriteRune(ch)
}
}
// Add the last part if it exists
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
// printJSONError displays a user-friendly JSON parsing error with helpful hints
func printJSONError(input string, err error) {
fmt.Println(red("❌ Invalid JSON:"), err)
fmt.Println()
// Show what was received
if len(input) > 100 {
fmt.Println(yellow("Received:"), input[:100]+"...")
} else {
fmt.Println(yellow("Received:"), input)
}
fmt.Println()
// Provide helpful hints based on common errors
errMsg := err.Error()
if strings.Contains(errMsg, "looking for beginning of") ||
strings.Contains(errMsg, "invalid character") {
fmt.Println(cyan("💡 Tip:"), "JSON with spaces needs quotes:")
fmt.Println(green(" ✓ Correct:"), "put key '{\"hello\":\"world\"}'")
fmt.Println(green(" ✓ Correct:"), "put key '{\"a\":1,\"b\":2}'")
fmt.Println(green(" ✓ Compact:"), "put key {\"a\":1,\"b\":2} (no spaces)")
fmt.Println(red(" ✗ Wrong: "), "put key {\"a\": 1, \"b\": 2} (spaces without quotes)")
} else if strings.Contains(errMsg, "unexpected end of JSON") {
fmt.Println(cyan("💡 Tip:"), "JSON appears incomplete or cut off")
fmt.Println(green(" Check:"), "Are all brackets/braces matched? {}, []")
fmt.Println(green(" Check:"), "Did spaces split your JSON into multiple arguments?")
} else {
fmt.Println(cyan("💡 Tip:"), "Wrap JSON in quotes for complex values:")
fmt.Println(green(" Single quotes:"), "put key '{\"your\":\"json\"}'")
fmt.Println(green(" Double quotes:"), "put key \"{\\\"your\\\":\\\"json\\\"}\"")
}
}
// Auto-completion setup
var completer = readline.NewPrefixCompleter(
readline.PcItem("connect"),
readline.PcItem("auth"),
readline.PcItem("profile",
readline.PcItem("add"),
readline.PcItem("use"),
readline.PcItem("remove"),
readline.PcItem("save"),
readline.PcItem("load"),
),
readline.PcItem("get"),
readline.PcItem("put"),
readline.PcItem("delete"),
readline.PcItem("meta",
readline.PcItem("get"),
readline.PcItem("set",
readline.PcItem("--owner"),
readline.PcItem("--group"),
readline.PcItem("--permissions"),
),
),
readline.PcItem("members"),
readline.PcItem("health"),
readline.PcItem("user",
readline.PcItem("get"),
readline.PcItem("create"),
),
readline.PcItem("group",
readline.PcItem("create"),
readline.PcItem("get"),
readline.PcItem("update"),
readline.PcItem("delete"),
readline.PcItem("add-member"),
readline.PcItem("remove-member"),
),
readline.PcItem("export"),
readline.PcItem("import"),
readline.PcItem("export-list"),
readline.PcItem("set"),
readline.PcItem("unset"),
readline.PcItem("vars"),
readline.PcItem("batch"),
readline.PcItem("source"),
readline.PcItem("help"),
readline.PcItem("exit"),
readline.PcItem("quit"),
readline.PcItem("clear"),
)