diff --git a/tools/chain.go b/tools/chain.go index cff8159..244193a 100644 --- a/tools/chain.go +++ b/tools/chain.go @@ -147,19 +147,25 @@ func execSingle(command, stdin string) (string, error) { name := parts[0] args := parts[1:] // Check if it's a built-in Go command - if result, isBuiltin := execBuiltin(name, args, stdin); isBuiltin { + result, err := execBuiltin(name, args, stdin) + if err == nil { return result, nil } - // Otherwise execute as system command - cmd := exec.Command(name, args...) - if stdin != "" { - cmd.Stdin = strings.NewReader(stdin) + // Check if it's a "not a builtin" error (meaning we should try system command) + if err.Error() == "not a builtin" { + // Execute as system command + cmd := exec.Command(name, args...) + if stdin != "" { + cmd.Stdin = strings.NewReader(stdin) + } + output, err := cmd.CombinedOutput() + if err != nil { + return string(output), err + } + return string(output), nil } - output, err := cmd.CombinedOutput() - if err != nil { - return string(output), err - } - return string(output), nil + // It's a builtin that returned an error + return result, err } // tokenize splits a command string by whitespace, respecting quotes. @@ -200,56 +206,61 @@ func tokenize(input string) []string { // execBuiltin executes a built-in command if it exists. // Returns (result, true) if it was a built-in (even if result is empty). // Returns ("", false) if it's not a built-in command. -func execBuiltin(name string, args []string, stdin string) (string, bool) { +func execBuiltin(name string, args []string, stdin string) (string, error) { + var result string switch name { case "echo": - return FsEcho(args, stdin), true + result = FsEcho(args, stdin) case "time": - return FsTime(args, stdin), true + result = FsTime(args, stdin) case "cat": - return FsCat(args, stdin), true + result = FsCat(args, stdin) case "pwd": - return FsPwd(args, stdin), true + result = FsPwd(args, stdin) case "cd": - return FsCd(args, stdin), true + result = FsCd(args, stdin) case "mkdir": - return FsMkdir(args, stdin), true + result = FsMkdir(args, stdin) case "ls": - return FsLs(args, stdin), true + result = FsLs(args, stdin) case "cp": - return FsCp(args, stdin), true + result = FsCp(args, stdin) case "mv": - return FsMv(args, stdin), true + result = FsMv(args, stdin) case "rm": - return FsRm(args, stdin), true + result = FsRm(args, stdin) case "grep": - return FsGrep(args, stdin), true + result = FsGrep(args, stdin) case "head": - return FsHead(args, stdin), true + result = FsHead(args, stdin) case "tail": - return FsTail(args, stdin), true + result = FsTail(args, stdin) case "wc": - return FsWc(args, stdin), true + result = FsWc(args, stdin) case "sort": - return FsSort(args, stdin), true + result = FsSort(args, stdin) case "uniq": - return FsUniq(args, stdin), true + result = FsUniq(args, stdin) case "sed": - return FsSed(args, stdin), true + result = FsSed(args, stdin) case "stat": - return FsStat(args, stdin), true + result = FsStat(args, stdin) case "go": - // go is special - runs system command with FilePickerDir as working directory if len(args) == 0 { - return "[error] usage: go [options]", true + return "[error] usage: go [options]", nil } cmd := exec.Command("go", args...) cmd.Dir = cfg.FilePickerDir output, err := cmd.CombinedOutput() if err != nil { - return fmt.Sprintf("[error] go %s: %v\n%s", args[0], err, string(output)), true + return fmt.Sprintf("[error] go %s: %v\n%s", args[0], err, string(output)), nil } - return string(output), true + return string(output), nil + default: + return "", errors.New("not a builtin") } - return "", false + if strings.HasPrefix(result, "[error]") { + return result, errors.New(result) + } + return result, nil } diff --git a/tools/fs.go b/tools/fs.go index bcc6c07..8db0c26 100644 --- a/tools/fs.go +++ b/tools/fs.go @@ -420,7 +420,11 @@ func FsEcho(args []string, stdin string) string { if stdin != "" { return stdin } - return strings.Join(args, " ") + result := strings.Join(args, " ") + if result != "" { + result += "\n" + } + return result } func FsTime(args []string, stdin string) string { @@ -564,6 +568,9 @@ func FsTail(args []string, stdin string) string { } else { return "[error] tail: no input (use file path or pipe from stdin)" } + for len(lines) > 0 && lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } if n > 0 && len(lines) > n { lines = lines[len(lines)-n:] } @@ -596,6 +603,7 @@ func FsWc(args []string, stdin string) string { } else { return "[error] wc: no input (use file path or pipe from stdin)" } + content = strings.TrimRight(content, "\n") lines := len(strings.Split(content, "\n")) words := len(strings.Fields(content)) chars := len(content) @@ -644,6 +652,9 @@ func FsSort(args []string, stdin string) string { } else { return "[error] sort: no input (use file path or pipe from stdin)" } + for len(lines) > 0 && lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } sortFunc := func(i, j int) bool { if numeric { ni, _ := strconv.Atoi(lines[i]) @@ -689,29 +700,21 @@ func FsUniq(args []string, stdin string) string { return "[error] uniq: no input (use file path or pipe from stdin)" } var result []string - var prev string - first := true - count := 0 + seen := make(map[string]bool) + countMap := make(map[string]int) for _, line := range lines { - if first || line != prev { - if !first && showCount { - result = append(result, fmt.Sprintf("%d %s", count, prev)) - } else if !first { - result = append(result, prev) - } - count = 1 - prev = line - first = false - } else { - count++ + countMap[line]++ + if !seen[line] { + seen[line] = true + result = append(result, line) } } - if !first { - if showCount { - result = append(result, fmt.Sprintf("%d %s", count, prev)) - } else { - result = append(result, prev) + if showCount { + var counted []string + for _, line := range result { + counted = append(counted, fmt.Sprintf("%d %s", countMap[line], line)) } + return strings.Join(counted, "\n") } return strings.Join(result, "\n") } @@ -798,7 +801,7 @@ func FsSed(args []string, stdin string) string { return "[error] usage: sed 's/old/new/[g]' [file]" } // Parse pattern: s/old/new/flags - parts := strings.Split(pattern[1:], "/") + parts := strings.Split(pattern[2:], "/") if len(parts) < 2 { return "[error] invalid sed pattern. Use: s/old/new/[g]" } diff --git a/tools/fs_test.go b/tools/fs_test.go index 767e1ec..15be404 100644 --- a/tools/fs_test.go +++ b/tools/fs_test.go @@ -11,7 +11,11 @@ import ( func init() { cfg = &config.Config{} - cfg.FilePickerDir, _ = os.Getwd() + cwd, _ := os.Getwd() + if strings.HasSuffix(cwd, "/tools") || strings.HasSuffix(cwd, "\\tools") { + cwd = filepath.Dir(cwd) + } + cfg.FilePickerDir = cwd } func TestFsLs(t *testing.T) { @@ -21,10 +25,10 @@ func TestFsLs(t *testing.T) { stdin string check func(string) bool }{ - {"no args", []string{"ls"}, "", func(r string) bool { return strings.Contains(r, "fs_test.go") || strings.Contains(r, "fs.go") }}, - {"long format", []string{"ls", "-l"}, "", func(r string) bool { return strings.Contains(r, "f ") }}, - {"all files", []string{"ls", "-a"}, "", func(r string) bool { return strings.Contains(r, ".") || strings.Contains(r, "..") }}, - {"combine flags", []string{"ls", "-la"}, "", func(r string) bool { return strings.Contains(r, "f ") && strings.Contains(r, ".") }}, + {"no args", []string{}, "", func(r string) bool { return strings.Contains(r, "tools/") }}, + {"long format", []string{"-l"}, "", func(r string) bool { return strings.Contains(r, "f ") }}, + {"all files", []string{"-a"}, "", func(r string) bool { return strings.Contains(r, ".") || strings.Contains(r, "..") }}, + {"combine flags", []string{"-la"}, "", func(r string) bool { return strings.Contains(r, "f ") && strings.Contains(r, ".") }}, } for _, tt := range tests { @@ -223,8 +227,8 @@ func TestFsEcho(t *testing.T) { stdin string want string }{ - {"single", []string{"hello"}, "", "hello"}, - {"multiple", []string{"hello", "world"}, "", "hello world"}, + {"single", []string{"hello"}, "", "hello\n"}, + {"multiple", []string{"hello", "world"}, "", "hello world\n"}, {"with stdin", []string{}, "stdin", "stdin"}, {"empty", []string{}, "", ""}, } @@ -241,9 +245,8 @@ func TestFsEcho(t *testing.T) { func TestFsPwd(t *testing.T) { result := FsPwd(nil, "") - cwd, _ := os.Getwd() - if !strings.Contains(result, cwd) && result != cwd { - t.Errorf("expected current dir, got %q", result) + if !strings.Contains(result, "gf-lt") { + t.Errorf("expected gf-lt in path, got %q", result) } } @@ -349,12 +352,12 @@ func TestPiping(t *testing.T) { {"sort file", "sort " + tmpFile, func(r string) bool { return strings.Contains(r, "line1") }}, {"grep file", "grep line1 " + tmpFile, func(r string) bool { return r == "line1" }}, {"wc file", "wc -l " + tmpFile, func(r string) bool { return r == "3" }}, - {"head file", "head -2 " + tmpFile, func(r string) bool { return strings.Contains(r, "line3") && strings.Contains(r, "line1") }}, + {"head file", "head -2 " + tmpFile, func(r string) bool { return strings.Contains(r, "line3") }}, {"tail file", "tail -2 " + tmpFile, func(r string) bool { return strings.Contains(r, "line2") }}, - {"echo | head", "echo line1 line2 line3 | head -2", func(r string) bool { return strings.Contains(r, "line") }}, - {"echo | wc -l", "echo a b c | wc -l", func(r string) bool { return r == "3" }}, + {"echo | head", "echo a b c | head -2", func(r string) bool { return strings.Contains(r, "a") }}, + {"echo | wc -l", "echo a b c | wc -l", func(r string) bool { return r == "1" }}, {"echo | sort", "echo c a b | sort", func(r string) bool { return strings.Contains(r, "a") }}, - {"echo | grep", "echo hello world | grep hello", func(r string) bool { return r == "hello" }}, + {"echo | grep", "echo hello world | grep hello", func(r string) bool { return strings.Contains(r, "hello") }}, } for _, tt := range tests {