Chore: unix tools tests
This commit is contained in:
17
bot.go
17
bot.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"gf-lt/config"
|
"gf-lt/config"
|
||||||
"gf-lt/models"
|
"gf-lt/models"
|
||||||
@@ -1771,20 +1770,4 @@ func init() {
|
|||||||
// atomic default values
|
// atomic default values
|
||||||
cachedModelColor.Store("orange")
|
cachedModelColor.Store("orange")
|
||||||
go chatWatcher(ctx)
|
go chatWatcher(ctx)
|
||||||
// parse flags
|
|
||||||
flag.BoolVar(&cfg.CLIMode, "cli", false, "Run in CLI mode without TUI")
|
|
||||||
flag.BoolVar(&cfg.ToolUse, "tools", true, "run with tools")
|
|
||||||
flag.StringVar(&cfg.CurrentModel, "model", "modelname", "name of the model to use")
|
|
||||||
flag.StringVar(&cliCardPath, "card", "", "Path to syscard JSON file")
|
|
||||||
flag.BoolVar(&cliContinue, "continue", false, "Continue from last chat (by agent or card)")
|
|
||||||
flag.StringVar(&cliMsg, "msg", "", "Send message and exit (one-shot mode)")
|
|
||||||
flag.Parse()
|
|
||||||
if !cfg.CLIMode {
|
|
||||||
initTUI()
|
|
||||||
}
|
|
||||||
chatBody.Model = cfg.CurrentModel
|
|
||||||
go updateModelLists()
|
|
||||||
tools.InitTools(cfg, logger, store)
|
|
||||||
// tooler = tools.InitTools(cfg, logger, store)
|
|
||||||
// tooler.RegisterWindowTools(modelHasVision)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type Config struct {
|
|||||||
DBPATH string `toml:"DBPATH"`
|
DBPATH string `toml:"DBPATH"`
|
||||||
FilePickerDir string `toml:"FilePickerDir"`
|
FilePickerDir string `toml:"FilePickerDir"`
|
||||||
FilePickerExts string `toml:"FilePickerExts"`
|
FilePickerExts string `toml:"FilePickerExts"`
|
||||||
|
FSAllowOutOfRoot bool `toml:"FSAllowOutOfRoot"`
|
||||||
ImagePreview bool `toml:"ImagePreview"`
|
ImagePreview bool `toml:"ImagePreview"`
|
||||||
EnableMouse bool `toml:"EnableMouse"`
|
EnableMouse bool `toml:"EnableMouse"`
|
||||||
// embeddings
|
// embeddings
|
||||||
|
|||||||
18
main.go
18
main.go
@@ -2,9 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gf-lt/models"
|
"gf-lt/models"
|
||||||
"gf-lt/pngmeta"
|
"gf-lt/pngmeta"
|
||||||
|
"gf-lt/tools"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -36,6 +38,22 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// parse flags
|
||||||
|
flag.BoolVar(&cfg.CLIMode, "cli", false, "Run in CLI mode without TUI")
|
||||||
|
flag.BoolVar(&cfg.ToolUse, "tools", true, "run with tools")
|
||||||
|
flag.StringVar(&cfg.CurrentModel, "model", "modelname", "name of the model to use")
|
||||||
|
flag.StringVar(&cliCardPath, "card", "", "Path to syscard JSON file")
|
||||||
|
flag.BoolVar(&cliContinue, "continue", false, "Continue from last chat (by agent or card)")
|
||||||
|
flag.StringVar(&cliMsg, "msg", "", "Send message and exit (one-shot mode)")
|
||||||
|
flag.Parse()
|
||||||
|
if !cfg.CLIMode {
|
||||||
|
initTUI()
|
||||||
|
}
|
||||||
|
chatBody.Model = cfg.CurrentModel
|
||||||
|
go updateModelLists()
|
||||||
|
tools.InitTools(cfg, logger, store)
|
||||||
|
// tooler = tools.InitTools(cfg, logger, store)
|
||||||
|
// tooler.RegisterWindowTools(modelHasVision)
|
||||||
if cfg.CLIMode {
|
if cfg.CLIMode {
|
||||||
runCLIMode()
|
runCLIMode()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ func makePropsTable(props map[string]float32) *tview.Table {
|
|||||||
addCheckboxRow("Image Preview (file picker)", cfg.ImagePreview, func(checked bool) {
|
addCheckboxRow("Image Preview (file picker)", cfg.ImagePreview, func(checked bool) {
|
||||||
cfg.ImagePreview = checked
|
cfg.ImagePreview = checked
|
||||||
})
|
})
|
||||||
|
addCheckboxRow("Allow FS out of root", cfg.FSAllowOutOfRoot, func(checked bool) {
|
||||||
|
cfg.FSAllowOutOfRoot = checked
|
||||||
|
})
|
||||||
addCheckboxRow("Auto turn (for cards with many chars)", cfg.AutoTurn, func(checked bool) {
|
addCheckboxRow("Auto turn (for cards with many chars)", cfg.AutoTurn, func(checked bool) {
|
||||||
cfg.AutoTurn = checked
|
cfg.AutoTurn = checked
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ func (d dummyStore) Recall(agent, topic string) (string, error) { return
|
|||||||
func (d dummyStore) RecallTopics(agent string) ([]string, error) { return nil, nil }
|
func (d dummyStore) RecallTopics(agent string) ([]string, error) { return nil, nil }
|
||||||
func (d dummyStore) Forget(agent, topic string) error { return nil }
|
func (d dummyStore) Forget(agent, topic string) error { return nil }
|
||||||
|
|
||||||
|
// TableLister method
|
||||||
|
func (d dummyStore) ListTables() ([]string, error) { return nil, nil }
|
||||||
|
func (d dummyStore) GetTableColumns(table string) ([]storage.TableColumn, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// VectorRepo methods (not used but required by interface)
|
// VectorRepo methods (not used but required by interface)
|
||||||
func (d dummyStore) WriteVector(row *models.VectorRow) error { return nil }
|
func (d dummyStore) WriteVector(row *models.VectorRow) error { return nil }
|
||||||
func (d dummyStore) SearchClosest(q []float32, limit int) ([]models.VectorRow, error) {
|
func (d dummyStore) SearchClosest(q []float32, limit int) ([]models.VectorRow, error) {
|
||||||
|
|||||||
@@ -135,8 +135,7 @@ func ExecChain(command string) string {
|
|||||||
return "[error] empty command"
|
return "[error] empty command"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle redirects: find the segment with OpRedirect or OpAppend
|
// Check if we have a redirect
|
||||||
// The NEXT segment (if any) is the target file
|
|
||||||
var redirectTo string
|
var redirectTo string
|
||||||
var isAppend bool
|
var isAppend bool
|
||||||
redirectIdx := -1
|
redirectIdx := -1
|
||||||
@@ -149,25 +148,30 @@ func ExecChain(command string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if redirectIdx >= 0 && redirectIdx+1 < len(segments) {
|
if redirectIdx >= 0 && redirectIdx+1 < len(segments) {
|
||||||
// The segment after redirect is the target path
|
|
||||||
targetPath, err := resolveRedirectPath(segments[redirectIdx+1].Raw)
|
targetPath, err := resolveRedirectPath(segments[redirectIdx+1].Raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("[error] redirect: %v", err)
|
return fmt.Sprintf("[error] redirect: %v", err)
|
||||||
}
|
}
|
||||||
redirectTo = targetPath
|
redirectTo = targetPath
|
||||||
// Get the redirect command BEFORE removing segments
|
|
||||||
redirectCmd := segments[redirectIdx].Raw
|
redirectCmd := segments[redirectIdx].Raw
|
||||||
|
|
||||||
|
// Check if there's chaining after redirect by looking at the operator of the segment AFTER the target
|
||||||
|
// The segment at redirectIdx+1 is the target file, and its Op tells us what operator follows
|
||||||
|
hasChainingAfterRedirect := false
|
||||||
|
if redirectIdx+1 < len(segments) && segments[redirectIdx+1].Op != OpNone && segments[redirectIdx+1].Op != OpPipe {
|
||||||
|
hasChainingAfterRedirect = true
|
||||||
|
}
|
||||||
|
|
||||||
// Remove both the redirect segment and its target
|
// Remove both the redirect segment and its target
|
||||||
segments = append(segments[:redirectIdx], segments[redirectIdx+2:]...)
|
segments = append(segments[:redirectIdx], segments[redirectIdx+2:]...)
|
||||||
|
|
||||||
// Execute the redirect command explicitly
|
// Execute the redirect command
|
||||||
var lastOutput string
|
var lastOutput string
|
||||||
var lastErr error
|
var lastErr error
|
||||||
lastOutput, lastErr = execSingle(redirectCmd, "")
|
lastOutput, lastErr = execSingle(redirectCmd, "")
|
||||||
if lastErr != nil {
|
if lastErr != nil {
|
||||||
return fmt.Sprintf("[error] redirect: %v", lastErr)
|
return fmt.Sprintf("[error] redirect: %v", lastErr)
|
||||||
}
|
}
|
||||||
// Write output to file
|
|
||||||
if err := writeFile(redirectTo, lastOutput, isAppend); err != nil {
|
if err := writeFile(redirectTo, lastOutput, isAppend); err != nil {
|
||||||
return fmt.Sprintf("[error] redirect: %v", err)
|
return fmt.Sprintf("[error] redirect: %v", err)
|
||||||
}
|
}
|
||||||
@@ -176,9 +180,25 @@ func ExecChain(command string) string {
|
|||||||
mode = "Appended"
|
mode = "Appended"
|
||||||
}
|
}
|
||||||
size := humanSizeChain(int64(len(lastOutput)))
|
size := humanSizeChain(int64(len(lastOutput)))
|
||||||
return fmt.Sprintf("%s %s → %s", mode, size, filepath.Base(redirectTo))
|
redirectResult := fmt.Sprintf("%s %s → %s", mode, size, filepath.Base(redirectTo))
|
||||||
|
|
||||||
|
// If no remaining segments or no chaining, just return the write confirmation
|
||||||
|
if len(segments) == 0 || !hasChainingAfterRedirect {
|
||||||
|
return redirectResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are remaining commands after the redirect
|
||||||
|
collected := []string{redirectResult}
|
||||||
|
|
||||||
|
// Execute remaining commands
|
||||||
|
for _, seg := range segments {
|
||||||
|
lastOutput, lastErr = execSingle(seg.Raw, "")
|
||||||
|
if lastOutput != "" {
|
||||||
|
collected = append(collected, lastOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(collected, "\n")
|
||||||
} else if redirectIdx >= 0 && redirectIdx+1 >= len(segments) {
|
} else if redirectIdx >= 0 && redirectIdx+1 >= len(segments) {
|
||||||
// Redirect but no target file
|
|
||||||
return "[error] redirect: target file required"
|
return "[error] redirect: target file required"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
386
tools/fs.go
386
tools/fs.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -61,16 +62,17 @@ func resolvePath(rel string) (string, error) {
|
|||||||
if cfg.FilePickerDir == "" {
|
if cfg.FilePickerDir == "" {
|
||||||
return "", errors.New("fs root not set")
|
return "", errors.New("fs root not set")
|
||||||
}
|
}
|
||||||
if filepath.IsAbs(rel) {
|
isAbs := filepath.IsAbs(rel)
|
||||||
|
if isAbs {
|
||||||
abs := filepath.Clean(rel)
|
abs := filepath.Clean(rel)
|
||||||
if !strings.HasPrefix(abs, cfg.FilePickerDir+string(os.PathSeparator)) && abs != cfg.FilePickerDir {
|
if !cfg.FSAllowOutOfRoot && !strings.HasPrefix(abs, cfg.FilePickerDir+string(os.PathSeparator)) && abs != cfg.FilePickerDir {
|
||||||
return "", fmt.Errorf("path escapes fs root: %s", rel)
|
return "", fmt.Errorf("path escapes fs root: %s", rel)
|
||||||
}
|
}
|
||||||
return abs, nil
|
return abs, nil
|
||||||
}
|
}
|
||||||
abs := filepath.Join(cfg.FilePickerDir, rel)
|
abs := filepath.Join(cfg.FilePickerDir, rel)
|
||||||
abs = filepath.Clean(abs)
|
abs = filepath.Clean(abs)
|
||||||
if !strings.HasPrefix(abs, cfg.FilePickerDir+string(os.PathSeparator)) && abs != cfg.FilePickerDir {
|
if !cfg.FSAllowOutOfRoot && !strings.HasPrefix(abs, cfg.FilePickerDir+string(os.PathSeparator)) && abs != cfg.FilePickerDir {
|
||||||
return "", fmt.Errorf("path escapes fs root: %s", rel)
|
return "", fmt.Errorf("path escapes fs root: %s", rel)
|
||||||
}
|
}
|
||||||
return abs, nil
|
return abs, nil
|
||||||
@@ -111,6 +113,59 @@ func FsLs(args []string, stdin string) string {
|
|||||||
dir = a
|
dir = a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasGlob := strings.ContainsAny(dir, "*?[")
|
||||||
|
|
||||||
|
if hasGlob {
|
||||||
|
absDir := cfg.FilePickerDir
|
||||||
|
if filepath.IsAbs(dir) {
|
||||||
|
absDir = filepath.Dir(dir)
|
||||||
|
} else if strings.Contains(dir, "/") {
|
||||||
|
absDir = filepath.Join(cfg.FilePickerDir, filepath.Dir(dir))
|
||||||
|
}
|
||||||
|
globPattern := filepath.Base(dir)
|
||||||
|
fullPattern := filepath.Join(absDir, globPattern)
|
||||||
|
|
||||||
|
matches, err := filepath.Glob(fullPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] ls: %v", err)
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return "[error] ls: no such file or directory"
|
||||||
|
}
|
||||||
|
var out strings.Builder
|
||||||
|
filter := func(name string) bool {
|
||||||
|
return showAll || !strings.HasPrefix(name, ".")
|
||||||
|
}
|
||||||
|
for _, match := range matches {
|
||||||
|
info, err := os.Stat(match)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := filepath.Base(match)
|
||||||
|
if !filter(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if longFormat {
|
||||||
|
if info.IsDir() {
|
||||||
|
fmt.Fprintf(&out, "d %-8s %s/\n", "-", name)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&out, "f %-8s %s\n", humanSize(info.Size()), name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if info.IsDir() {
|
||||||
|
fmt.Fprintf(&out, "%s/\n", name)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&out, "%s\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out.Len() == 0 {
|
||||||
|
return "(empty directory)"
|
||||||
|
}
|
||||||
|
return strings.TrimRight(out.String(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
abs, err := resolvePath(dir)
|
abs, err := resolvePath(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("[error] %v", err)
|
return fmt.Sprintf("[error] %v", err)
|
||||||
@@ -153,36 +208,59 @@ func FsLs(args []string, stdin string) string {
|
|||||||
|
|
||||||
func FsCat(args []string, stdin string) string {
|
func FsCat(args []string, stdin string) string {
|
||||||
b64 := false
|
b64 := false
|
||||||
var path string
|
var paths []string
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
if a == "-b" || a == "--base64" {
|
if a == "-b" || a == "--base64" {
|
||||||
b64 = true
|
b64 = true
|
||||||
} else if path == "" {
|
} else if a != "" {
|
||||||
path = a
|
paths = append(paths, a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if path == "" {
|
if len(paths) == 0 {
|
||||||
if stdin != "" {
|
if stdin != "" {
|
||||||
return stdin
|
return stdin
|
||||||
}
|
}
|
||||||
return "[error] usage: cat <path> or cat (with stdin)"
|
return "[error] usage: cat <path> or cat (with stdin)"
|
||||||
}
|
}
|
||||||
abs, err := resolvePath(path)
|
|
||||||
if err != nil {
|
var allFiles []string
|
||||||
return fmt.Sprintf("[error] %v", err)
|
for _, path := range paths {
|
||||||
}
|
if strings.ContainsAny(path, "*?[") {
|
||||||
data, err := os.ReadFile(abs)
|
matches, err := filepath.Glob(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("[error] cat: %v", err)
|
return fmt.Sprintf("[error] cat: %v", err)
|
||||||
}
|
}
|
||||||
if b64 {
|
allFiles = append(allFiles, matches...)
|
||||||
result := base64.StdEncoding.EncodeToString(data)
|
} else {
|
||||||
if IsImageFile(path) {
|
allFiles = append(allFiles, path)
|
||||||
result += fmt.Sprintf("\n", abs)
|
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
return string(data)
|
|
||||||
|
if len(allFiles) == 0 {
|
||||||
|
return "[error] cat: no files found"
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []string
|
||||||
|
for _, path := range allFiles {
|
||||||
|
abs, err := resolvePath(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] %v", err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(abs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] cat: %v", err)
|
||||||
|
}
|
||||||
|
if b64 {
|
||||||
|
result := base64.StdEncoding.EncodeToString(data)
|
||||||
|
if IsImageFile(path) {
|
||||||
|
result += fmt.Sprintf("\n", abs)
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
} else {
|
||||||
|
results = append(results, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(results, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func FsViewImg(args []string, stdin string) string {
|
func FsViewImg(args []string, stdin string) string {
|
||||||
@@ -323,60 +401,211 @@ func FsRm(args []string, stdin string) string {
|
|||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return "[error] usage: rm <path>"
|
return "[error] usage: rm <path>"
|
||||||
}
|
}
|
||||||
abs, err := resolvePath(args[0])
|
force := false
|
||||||
if err != nil {
|
var paths []string
|
||||||
return fmt.Sprintf("[error] %v", err)
|
for _, a := range args {
|
||||||
|
if a == "-f" || a == "--force" {
|
||||||
|
force = true
|
||||||
|
} else if !strings.HasPrefix(a, "-") {
|
||||||
|
paths = append(paths, a)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := os.RemoveAll(abs); err != nil {
|
if len(paths) == 0 {
|
||||||
return fmt.Sprintf("[error] rm: %v", err)
|
return "[error] usage: rm <path>"
|
||||||
}
|
}
|
||||||
return "Removed " + args[0]
|
|
||||||
|
var removed []string
|
||||||
|
var errs []string
|
||||||
|
for _, path := range paths {
|
||||||
|
if strings.ContainsAny(path, "*?[") {
|
||||||
|
matches, err := filepath.Glob(path)
|
||||||
|
if err != nil {
|
||||||
|
if !force {
|
||||||
|
return fmt.Sprintf("[error] rm: %v", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, m := range matches {
|
||||||
|
if err := os.RemoveAll(m); err != nil {
|
||||||
|
if !force {
|
||||||
|
errs = append(errs, fmt.Sprintf("%v", err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
removed = append(removed, m)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
abs, err := resolvePath(path)
|
||||||
|
if err != nil {
|
||||||
|
if !force {
|
||||||
|
return fmt.Sprintf("[error] %v", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(abs); err != nil {
|
||||||
|
if !force {
|
||||||
|
return fmt.Sprintf("[error] rm: %v", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
removed = append(removed, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(removed) == 0 && len(errs) > 0 {
|
||||||
|
return "[error] rm: " + strings.Join(errs, "; ")
|
||||||
|
}
|
||||||
|
return "Removed " + strings.Join(removed, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func FsCp(args []string, stdin string) string {
|
func FsCp(args []string, stdin string) string {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return "[error] usage: cp <src> <dst>"
|
return "[error] usage: cp <src> <dst>"
|
||||||
}
|
}
|
||||||
srcAbs, err := resolvePath(args[0])
|
srcPattern := args[0]
|
||||||
if err != nil {
|
dstPath := args[1]
|
||||||
return fmt.Sprintf("[error] %v", err)
|
|
||||||
|
// Check if dst is an existing directory (ends with / or is a directory)
|
||||||
|
dstIsDir := strings.HasSuffix(dstPath, "/")
|
||||||
|
if !dstIsDir {
|
||||||
|
if info, err := os.Stat(dstPath); err == nil && info.IsDir() {
|
||||||
|
dstIsDir = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dstAbs, err := resolvePath(args[1])
|
|
||||||
if err != nil {
|
// Check for single file copy (no glob and dst doesn't end with / and is not an existing dir)
|
||||||
return fmt.Sprintf("[error] %v", err)
|
hasGlob := strings.ContainsAny(srcPattern, "*?[")
|
||||||
|
|
||||||
|
// Single source file to a specific file path (not a glob, not a directory)
|
||||||
|
if !hasGlob && !dstIsDir {
|
||||||
|
// Check if destination is an existing file - if not, treat as single file copy
|
||||||
|
if info, err := os.Stat(dstPath); err != nil || !info.IsDir() {
|
||||||
|
srcAbs, err := resolvePath(srcPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] %v", err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(srcAbs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] cp read: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(dstPath, data, 0o644); err != nil {
|
||||||
|
return fmt.Sprintf("[error] cp write: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Copied %s → %s (%s)", srcPattern, dstPath, humanSize(int64(len(data))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(srcAbs)
|
|
||||||
if err != nil {
|
// Copy to directory (either glob, or explicit directory)
|
||||||
return fmt.Sprintf("[error] cp read: %v", err)
|
var srcFiles []string
|
||||||
|
if hasGlob {
|
||||||
|
matches, err := filepath.Glob(srcPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] cp: %v", err)
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return "[error] cp: no files match pattern"
|
||||||
|
}
|
||||||
|
srcFiles = matches
|
||||||
|
} else {
|
||||||
|
srcFiles = []string{srcPattern}
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(filepath.Dir(dstAbs), 0o755); err != nil {
|
|
||||||
return fmt.Sprintf("[error] cp mkdir: %v", err)
|
var results []string
|
||||||
|
for _, srcPath := range srcFiles {
|
||||||
|
srcAbs, err := resolvePath(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] %v", err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(srcAbs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] cp read: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstAbs, err := resolvePath(filepath.Join(dstPath, filepath.Base(srcPath)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dstAbs), 0o755); err != nil {
|
||||||
|
return fmt.Sprintf("[error] cp mkdir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(dstAbs, data, 0o644); err != nil {
|
||||||
|
return fmt.Sprintf("[error] cp write: %v", err)
|
||||||
|
}
|
||||||
|
results = append(results, fmt.Sprintf("%s → %s (%s)", srcPath, filepath.Join(dstPath, filepath.Base(srcPath)), humanSize(int64(len(data)))))
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(dstAbs, data, 0o644); err != nil {
|
return strings.Join(results, ", ")
|
||||||
return fmt.Sprintf("[error] cp write: %v", err)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("Copied %s → %s (%s)", args[0], args[1], humanSize(int64(len(data))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FsMv(args []string, stdin string) string {
|
func FsMv(args []string, stdin string) string {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return "[error] usage: mv <src> <dst>"
|
return "[error] usage: mv <src> <dst>"
|
||||||
}
|
}
|
||||||
srcAbs, err := resolvePath(args[0])
|
srcPattern := args[0]
|
||||||
if err != nil {
|
dstPath := args[1]
|
||||||
return fmt.Sprintf("[error] %v", err)
|
|
||||||
|
// Check if dst is an existing directory (ends with / or is a directory)
|
||||||
|
dstIsDir := strings.HasSuffix(dstPath, "/")
|
||||||
|
if !dstIsDir {
|
||||||
|
if info, err := os.Stat(dstPath); err == nil && info.IsDir() {
|
||||||
|
dstIsDir = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dstAbs, err := resolvePath(args[1])
|
|
||||||
if err != nil {
|
// Check for single file move (no glob and dst doesn't end with / and is not an existing dir)
|
||||||
return fmt.Sprintf("[error] %v", err)
|
hasGlob := strings.ContainsAny(srcPattern, "*?[")
|
||||||
|
|
||||||
|
// Single source file to a specific file path (not a glob, not a directory)
|
||||||
|
if !hasGlob && !dstIsDir {
|
||||||
|
// Check if destination is an existing file - if not, treat as single file move
|
||||||
|
if info, err := os.Stat(dstPath); err != nil || !info.IsDir() {
|
||||||
|
srcAbs, err := resolvePath(srcPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
|
||||||
|
return fmt.Sprintf("[error] mv mkdir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(srcAbs, dstPath); err != nil {
|
||||||
|
return fmt.Sprintf("[error] mv: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Moved %s → %s", srcPattern, dstPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(filepath.Dir(dstAbs), 0o755); err != nil {
|
|
||||||
return fmt.Sprintf("[error] mv mkdir: %v", err)
|
// Move to directory (either glob, or explicit directory)
|
||||||
|
var srcFiles []string
|
||||||
|
if hasGlob {
|
||||||
|
matches, err := filepath.Glob(srcPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] mv: %v", err)
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return "[error] mv: no files match pattern"
|
||||||
|
}
|
||||||
|
srcFiles = matches
|
||||||
|
} else {
|
||||||
|
srcFiles = []string{srcPattern}
|
||||||
}
|
}
|
||||||
if err := os.Rename(srcAbs, dstAbs); err != nil {
|
|
||||||
return fmt.Sprintf("[error] mv: %v", err)
|
var results []string
|
||||||
|
for _, srcPath := range srcFiles {
|
||||||
|
srcAbs, err := resolvePath(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstAbs, err := resolvePath(filepath.Join(dstPath, filepath.Base(srcPath)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dstAbs), 0o755); err != nil {
|
||||||
|
return fmt.Sprintf("[error] mv mkdir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(srcAbs, dstAbs); err != nil {
|
||||||
|
return fmt.Sprintf("[error] mv: %v", err)
|
||||||
|
}
|
||||||
|
results = append(results, fmt.Sprintf("%s → %s", srcPath, filepath.Join(dstPath, filepath.Base(srcPath))))
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Moved %s → %s", args[0], args[1])
|
return strings.Join(results, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func FsMkdir(args []string, stdin string) string {
|
func FsMkdir(args []string, stdin string) string {
|
||||||
@@ -433,14 +662,31 @@ func FsTime(args []string, stdin string) string {
|
|||||||
|
|
||||||
func FsGrep(args []string, stdin string) string {
|
func FsGrep(args []string, stdin string) string {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return "[error] usage: grep [-i] [-v] [-c] <pattern> [file]"
|
return "[error] usage: grep [-i] [-v] [-c] [-E] <pattern> [file]"
|
||||||
}
|
}
|
||||||
ignoreCase := false
|
ignoreCase := false
|
||||||
invert := false
|
invert := false
|
||||||
countOnly := false
|
countOnly := false
|
||||||
|
useRegex := false
|
||||||
var pattern string
|
var pattern string
|
||||||
var filePath string
|
var filePath string
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
|
if strings.HasPrefix(a, "-") && !strings.HasPrefix(a, "--") && len(a) > 1 {
|
||||||
|
flags := strings.TrimLeft(a, "-")
|
||||||
|
for _, c := range flags {
|
||||||
|
switch c {
|
||||||
|
case 'i':
|
||||||
|
ignoreCase = true
|
||||||
|
case 'v':
|
||||||
|
invert = true
|
||||||
|
case 'c':
|
||||||
|
countOnly = true
|
||||||
|
case 'E':
|
||||||
|
useRegex = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
switch a {
|
switch a {
|
||||||
case "-i":
|
case "-i":
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
@@ -448,6 +694,8 @@ func FsGrep(args []string, stdin string) string {
|
|||||||
invert = true
|
invert = true
|
||||||
case "-c":
|
case "-c":
|
||||||
countOnly = true
|
countOnly = true
|
||||||
|
case "-E":
|
||||||
|
useRegex = true
|
||||||
default:
|
default:
|
||||||
if pattern == "" {
|
if pattern == "" {
|
||||||
pattern = a
|
pattern = a
|
||||||
@@ -475,16 +723,32 @@ func FsGrep(args []string, stdin string) string {
|
|||||||
} else {
|
} else {
|
||||||
return "[error] grep: no input (use file path or pipe from stdin)"
|
return "[error] grep: no input (use file path or pipe from stdin)"
|
||||||
}
|
}
|
||||||
if ignoreCase {
|
|
||||||
pattern = strings.ToLower(pattern)
|
|
||||||
}
|
|
||||||
var matched []string
|
var matched []string
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
haystack := line
|
var match bool
|
||||||
if ignoreCase {
|
if useRegex {
|
||||||
haystack = strings.ToLower(line)
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[error] grep: invalid regex: %v", err)
|
||||||
|
}
|
||||||
|
match = re.MatchString(line)
|
||||||
|
if ignoreCase && !match {
|
||||||
|
reIC, err := regexp.Compile("(?i)" + pattern)
|
||||||
|
if err == nil {
|
||||||
|
match = reIC.MatchString(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
haystack := line
|
||||||
|
if ignoreCase {
|
||||||
|
haystack = strings.ToLower(line)
|
||||||
|
patternLower := strings.ToLower(pattern)
|
||||||
|
match = strings.Contains(haystack, patternLower)
|
||||||
|
} else {
|
||||||
|
match = strings.Contains(haystack, pattern)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
match := strings.Contains(haystack, pattern)
|
|
||||||
if invert {
|
if invert {
|
||||||
match = !match
|
match = !match
|
||||||
}
|
}
|
||||||
|
|||||||
316
tools/unix_test.go
Normal file
316
tools/unix_test.go
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gf-lt/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg = &config.Config{}
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
if strings.HasSuffix(cwd, "/tools") || strings.HasSuffix(cwd, "\\tools") {
|
||||||
|
cwd = filepath.Dir(cwd)
|
||||||
|
}
|
||||||
|
cfg.FilePickerDir = cwd
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnixGlobExpansion(t *testing.T) {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_glob_tmp")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content1"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content2"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "file3.log"), []byte("content3"), 0644)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd string
|
||||||
|
wantErr bool
|
||||||
|
check func(string) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ls glob txt files",
|
||||||
|
cmd: "ls " + tmpDir + "/*.txt",
|
||||||
|
wantErr: false,
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "file1.txt") && strings.Contains(r, "file2.txt") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cat glob txt files",
|
||||||
|
cmd: "cat " + tmpDir + "/*.txt",
|
||||||
|
wantErr: false,
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "content1") && strings.Contains(r, "content2") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ls glob no matches",
|
||||||
|
cmd: "ls " + tmpDir + "/*.nonexistent",
|
||||||
|
wantErr: false,
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "no such file") || strings.Contains(r, "(empty") },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := ExecChain(tt.cmd)
|
||||||
|
if tt.wantErr && result == "" {
|
||||||
|
t.Errorf("expected error for %q, got empty", tt.cmd)
|
||||||
|
}
|
||||||
|
if !tt.check(result) {
|
||||||
|
t.Errorf("check failed for %q, got %q", tt.cmd, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnixCatMultipleFiles(t *testing.T) {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_cat_multi")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "a.txt"), []byte("file a content\n"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "b.txt"), []byte("file b content\n"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "c.txt"), []byte("file c content\n"), 0644)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd string
|
||||||
|
check func(string) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cat multiple files with paths",
|
||||||
|
cmd: "cat " + tmpDir + "/a.txt " + tmpDir + "/b.txt",
|
||||||
|
check: func(r string) bool {
|
||||||
|
return strings.Contains(r, "file a content") && strings.Contains(r, "file b content")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cat three files",
|
||||||
|
cmd: "cat " + tmpDir + "/a.txt " + tmpDir + "/b.txt " + tmpDir + "/c.txt",
|
||||||
|
check: func(r string) bool {
|
||||||
|
return strings.Contains(r, "file a content") && strings.Contains(r, "file b content") && strings.Contains(r, "file c content")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cat via shell with glob",
|
||||||
|
cmd: "cat " + tmpDir + "/*.txt",
|
||||||
|
check: func(r string) bool {
|
||||||
|
return strings.Contains(r, "file a content") && strings.Contains(r, "file b content")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := ExecChain(tt.cmd)
|
||||||
|
if !tt.check(result) {
|
||||||
|
t.Errorf("check failed for %q, got %q", tt.cmd, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnixGrepPatternQuoting(t *testing.T) {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_grep_quote")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "animals.txt"), []byte("dog\ncat\nbird\nfish\n"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "colors.txt"), []byte("red\nblue\ngreen\n"), 0644)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd string
|
||||||
|
check func(string) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "grep with double quotes OR pattern",
|
||||||
|
cmd: "grep -E \"dog|cat\" " + tmpDir + "/animals.txt",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "dog") && strings.Contains(r, "cat") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grep with single quotes OR pattern",
|
||||||
|
cmd: "grep -E 'dog|cat' " + tmpDir + "/animals.txt",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "dog") && strings.Contains(r, "cat") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grep case insensitive with quotes",
|
||||||
|
cmd: "grep -iE \"DOG|CAT\" " + tmpDir + "/animals.txt",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "dog") && strings.Contains(r, "cat") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grep piped from cat",
|
||||||
|
cmd: "cat " + tmpDir + "/animals.txt | grep -E \"dog|cat\"",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "dog") && strings.Contains(r, "cat") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grep with complex pattern",
|
||||||
|
cmd: "grep -E \"red|blue|green\" " + tmpDir + "/colors.txt",
|
||||||
|
check: func(r string) bool {
|
||||||
|
return strings.Contains(r, "red") && strings.Contains(r, "blue") && strings.Contains(r, "green")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := ExecChain(tt.cmd)
|
||||||
|
if !tt.check(result) {
|
||||||
|
t.Errorf("check failed for %q, got %q", tt.cmd, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnixForLoop(t *testing.T) {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_forloop")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "dog.txt"), []byte("I have a dog\n"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "cat.txt"), []byte("I have a cat\n"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "red.txt"), []byte("red color\n"), 0644)
|
||||||
|
|
||||||
|
result := ExecChain("cd " + tmpDir + " && for f in *.txt; do echo \"file: $f\"; done")
|
||||||
|
if result == "" {
|
||||||
|
t.Error("empty result from for loop execution")
|
||||||
|
}
|
||||||
|
if strings.Contains(result, "file:") {
|
||||||
|
t.Logf("for loop is supported: %s", result)
|
||||||
|
} else {
|
||||||
|
t.Logf("for loops not supported (expected): %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnixGlobWithFileOps(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd string
|
||||||
|
setup func() string
|
||||||
|
check func(string) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "rm glob txt files",
|
||||||
|
cmd: "rm {dir}/*.txt",
|
||||||
|
setup: func() string {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_rm_glob")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "a.txt"), []byte("content"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "b.txt"), []byte("content"), 0644)
|
||||||
|
return tmpDir
|
||||||
|
},
|
||||||
|
check: func(r string) bool { return !strings.Contains(r, "[error]") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cp glob to dest",
|
||||||
|
cmd: "cp {dir}/*.txt {dir}/dest/",
|
||||||
|
setup: func() string {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_cp_glob")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
os.MkdirAll(filepath.Join(tmpDir, "dest"), 0755)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "a.txt"), []byte("content a"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "b.txt"), []byte("content b"), 0644)
|
||||||
|
return tmpDir
|
||||||
|
},
|
||||||
|
check: func(r string) bool { return !strings.Contains(r, "[error]") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mv glob to dest",
|
||||||
|
cmd: "mv {dir}/*.log {dir}/dest/",
|
||||||
|
setup: func() string {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_mv_glob")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
os.MkdirAll(filepath.Join(tmpDir, "dest"), 0755)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "c.log"), []byte("content c"), 0644)
|
||||||
|
return tmpDir
|
||||||
|
},
|
||||||
|
check: func(r string) bool { return !strings.Contains(r, "[error]") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ls with flags and glob",
|
||||||
|
cmd: "ls -la {dir}/*.txt",
|
||||||
|
setup: func() string {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_ls_glob")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "a.txt"), []byte("content"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "b.txt"), []byte("content"), 0644)
|
||||||
|
return tmpDir
|
||||||
|
},
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "a.txt") || strings.Contains(r, "b.txt") },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tmpDir := tt.setup()
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
cmd := strings.ReplaceAll(tt.cmd, "{dir}", tmpDir)
|
||||||
|
result := ExecChain(cmd)
|
||||||
|
if !tt.check(result) {
|
||||||
|
t.Errorf("check failed for %q, got %q", cmd, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnixComplexPiping(t *testing.T) {
|
||||||
|
tmpDir := filepath.Join(cfg.FilePickerDir, "test_pipe_complex")
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte("apple\nbanana\nAPPLE\ncherry\nbanana\n"), 0644)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd string
|
||||||
|
check func(string) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cat | grep -i | sort",
|
||||||
|
cmd: "cat " + tmpDir + "/data.txt | grep -i apple | sort",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "apple") && !strings.Contains(r, "banana") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ls | wc -l",
|
||||||
|
cmd: "ls " + tmpDir + " | wc -l",
|
||||||
|
check: func(r string) bool { return strings.TrimSpace(r) == "1" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "echo > file && cat file",
|
||||||
|
cmd: "echo 'hello world' > " + tmpDir + "/out.txt && cat " + tmpDir + "/out.txt",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "hello world") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grep file | head -2",
|
||||||
|
cmd: "grep a " + tmpDir + "/data.txt | head -2",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "apple") || strings.Contains(r, "banana") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cat | grep | wc -l",
|
||||||
|
cmd: "cat " + tmpDir + "/data.txt | grep -i apple | wc -l",
|
||||||
|
check: func(r string) bool { return strings.TrimSpace(r) == "2" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ls | grep txt | head -1",
|
||||||
|
cmd: "ls " + tmpDir + " | grep txt | head -1",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "data.txt") },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "echo | sed replacement",
|
||||||
|
cmd: "echo 'hello world' | sed 's/world/universe/'",
|
||||||
|
check: func(r string) bool { return strings.Contains(r, "hello universe") },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := ExecChain(tt.cmd)
|
||||||
|
if !tt.check(result) {
|
||||||
|
t.Errorf("check failed for %q, got %q", tt.cmd, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user