From aff7d73d1626dde297b07509d671fcc3dc7e05f8 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Wed, 8 Apr 2026 13:15:51 +0300 Subject: [PATCH] Fix (tools): tests for tools and some fixes --- tools/fs.go | 144 +++++++++++++++-- tools/fs_test.go | 390 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 523 insertions(+), 11 deletions(-) create mode 100644 tools/fs_test.go diff --git a/tools/fs.go b/tools/fs.go index 61c6880..bcc6c07 100644 --- a/tools/fs.go +++ b/tools/fs.go @@ -162,7 +162,10 @@ func FsCat(args []string, stdin string) string { } } if path == "" { - return "[error] usage: cat " + if stdin != "" { + return stdin + } + return "[error] usage: cat or cat (with stdin)" } abs, err := resolvePath(path) if err != nil { @@ -426,12 +429,13 @@ func FsTime(args []string, stdin string) string { func FsGrep(args []string, stdin string) string { if len(args) == 0 { - return "[error] usage: grep [-i] [-v] [-c] " + return "[error] usage: grep [-i] [-v] [-c] [file]" } ignoreCase := false invert := false countOnly := false var pattern string + var filePath string for _, a := range args { switch a { case "-i": @@ -441,16 +445,35 @@ func FsGrep(args []string, stdin string) string { case "-c": countOnly = true default: - pattern = a + if pattern == "" { + pattern = a + } else if filePath == "" { + filePath = a + } } } if pattern == "" { return "[error] pattern required" } + var lines []string + if filePath != "" { + abs, err := resolvePath(filePath) + if err != nil { + return fmt.Sprintf("[error] grep: %v", err) + } + data, err := os.ReadFile(abs) + if err != nil { + return fmt.Sprintf("[error] grep: %v", err) + } + lines = strings.Split(string(data), "\n") + } else if stdin != "" { + lines = strings.Split(stdin, "\n") + } else { + return "[error] grep: no input (use file path or pipe from stdin)" + } if ignoreCase { pattern = strings.ToLower(pattern) } - lines := strings.Split(stdin, "\n") var matched []string for _, line := range lines { haystack := line @@ -473,6 +496,7 @@ func FsGrep(args []string, stdin string) string { func FsHead(args []string, stdin string) string { n := 10 + var filePath string for i, a := range args { if a == "-n" && i+1 < len(args) { if parsed, err := strconv.Atoi(args[i+1]); err == nil { @@ -482,9 +506,26 @@ func FsHead(args []string, stdin string) string { continue } else if parsed, err := strconv.Atoi(a); err == nil { n = parsed + } else if filePath == "" && !strings.HasPrefix(a, "-") { + filePath = a } } - lines := strings.Split(stdin, "\n") + var lines []string + if filePath != "" { + abs, err := resolvePath(filePath) + if err != nil { + return fmt.Sprintf("[error] head: %v", err) + } + data, err := os.ReadFile(abs) + if err != nil { + return fmt.Sprintf("[error] head: %v", err) + } + lines = strings.Split(string(data), "\n") + } else if stdin != "" { + lines = strings.Split(stdin, "\n") + } else { + return "[error] head: no input (use file path or pipe from stdin)" + } if n > 0 && len(lines) > n { lines = lines[:n] } @@ -493,6 +534,7 @@ func FsHead(args []string, stdin string) string { func FsTail(args []string, stdin string) string { n := 10 + var filePath string for i, a := range args { if a == "-n" && i+1 < len(args) { if parsed, err := strconv.Atoi(args[i+1]); err == nil { @@ -502,9 +544,26 @@ func FsTail(args []string, stdin string) string { continue } else if parsed, err := strconv.Atoi(a); err == nil { n = parsed + } else if filePath == "" && !strings.HasPrefix(a, "-") { + filePath = a } } - lines := strings.Split(stdin, "\n") + var lines []string + if filePath != "" { + abs, err := resolvePath(filePath) + if err != nil { + return fmt.Sprintf("[error] tail: %v", err) + } + data, err := os.ReadFile(abs) + if err != nil { + return fmt.Sprintf("[error] tail: %v", err) + } + lines = strings.Split(string(data), "\n") + } else if stdin != "" { + lines = strings.Split(stdin, "\n") + } else { + return "[error] tail: no input (use file path or pipe from stdin)" + } if n > 0 && len(lines) > n { lines = lines[len(lines)-n:] } @@ -512,9 +571,34 @@ func FsTail(args []string, stdin string) string { } func FsWc(args []string, stdin string) string { - lines := len(strings.Split(stdin, "\n")) - words := len(strings.Fields(stdin)) - chars := len(stdin) + var content string + var filePath string + for _, a := range args { + if strings.HasPrefix(a, "-") { + continue + } + if filePath == "" { + filePath = a + } + } + if filePath != "" { + abs, err := resolvePath(filePath) + if err != nil { + return fmt.Sprintf("[error] wc: %v", err) + } + data, err := os.ReadFile(abs) + if err != nil { + return fmt.Sprintf("[error] wc: %v", err) + } + content = string(data) + } else if stdin != "" { + content = stdin + } else { + return "[error] wc: no input (use file path or pipe from stdin)" + } + lines := len(strings.Split(content, "\n")) + words := len(strings.Fields(content)) + chars := len(content) if len(args) > 0 { switch args[0] { case "-l": @@ -529,17 +613,37 @@ func FsWc(args []string, stdin string) string { } func FsSort(args []string, stdin string) string { - lines := strings.Split(stdin, "\n") reverse := false numeric := false + var filePath string for _, a := range args { switch a { case "-r": reverse = true case "-n": numeric = true + default: + if filePath == "" && !strings.HasPrefix(a, "-") { + filePath = a + } } } + var lines []string + if filePath != "" { + abs, err := resolvePath(filePath) + if err != nil { + return fmt.Sprintf("[error] sort: %v", err) + } + data, err := os.ReadFile(abs) + if err != nil { + return fmt.Sprintf("[error] sort: %v", err) + } + lines = strings.Split(string(data), "\n") + } else if stdin != "" { + lines = strings.Split(stdin, "\n") + } else { + return "[error] sort: no input (use file path or pipe from stdin)" + } sortFunc := func(i, j int) bool { if numeric { ni, _ := strconv.Atoi(lines[i]) @@ -559,13 +663,31 @@ func FsSort(args []string, stdin string) string { } func FsUniq(args []string, stdin string) string { - lines := strings.Split(stdin, "\n") showCount := false + var filePath string for _, a := range args { if a == "-c" { showCount = true + } else if filePath == "" && !strings.HasPrefix(a, "-") { + filePath = a } } + var lines []string + if filePath != "" { + abs, err := resolvePath(filePath) + if err != nil { + return fmt.Sprintf("[error] uniq: %v", err) + } + data, err := os.ReadFile(abs) + if err != nil { + return fmt.Sprintf("[error] uniq: %v", err) + } + lines = strings.Split(string(data), "\n") + } else if stdin != "" { + lines = strings.Split(stdin, "\n") + } else { + return "[error] uniq: no input (use file path or pipe from stdin)" + } var result []string var prev string first := true diff --git a/tools/fs_test.go b/tools/fs_test.go new file mode 100644 index 0000000..767e1ec --- /dev/null +++ b/tools/fs_test.go @@ -0,0 +1,390 @@ +package tools + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "gf-lt/config" +) + +func init() { + cfg = &config.Config{} + cfg.FilePickerDir, _ = os.Getwd() +} + +func TestFsLs(t *testing.T) { + tests := []struct { + name string + args []string + 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, ".") }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsLs(tt.args, tt.stdin) + if !tt.check(result) { + t.Errorf("check failed for %q, got %q", tt.name, result) + } + }) + } +} + +func TestFsCat(t *testing.T) { + tmpFile := filepath.Join(cfg.FilePickerDir, "test_cat.txt") + content := "hello\nworld\n" + os.WriteFile(tmpFile, []byte(content), 0644) + defer os.Remove(tmpFile) + + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"file path", []string{tmpFile}, "", "hello\nworld\n"}, + {"stdin fallback", []string{}, "stdin content", "stdin content"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsCat(tt.args, tt.stdin) + if result != tt.want && !strings.Contains(result, tt.want) { + t.Errorf("expected %q, got %q", tt.want, result) + } + }) + } +} + +func TestFsHead(t *testing.T) { + tmpFile := filepath.Join(cfg.FilePickerDir, "test_head.txt") + content := "line1\nline2\nline3\nline4\nline5\n" + os.WriteFile(tmpFile, []byte(content), 0644) + defer os.Remove(tmpFile) + + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"default from stdin", []string{}, "line1\nline2\nline3", "line1\nline2\nline3"}, + {"n from stdin", []string{"-n", "2"}, "line1\nline2\nline3", "line1\nline2"}, + {"numeric n", []string{"-2"}, "line1\nline2\nline3", "line1\nline2"}, + {"file path", []string{tmpFile}, "", "line1\nline2\nline3\nline4\nline5"}, + {"file with n", []string{"-n", "2", tmpFile}, "", "line1\nline2"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsHead(tt.args, tt.stdin) + if result != tt.want && !strings.Contains(result, tt.want) { + t.Errorf("expected %q, got %q", tt.want, result) + } + }) + } +} + +func TestFsTail(t *testing.T) { + tmpFile := filepath.Join(cfg.FilePickerDir, "test_tail.txt") + content := "line1\nline2\nline3\nline4\nline5\n" + os.WriteFile(tmpFile, []byte(content), 0644) + defer os.Remove(tmpFile) + + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"default from stdin", []string{}, "line1\nline2\nline3", "line1\nline2\nline3"}, + {"n from stdin", []string{"-n", "2"}, "line1\nline2\nline3", "line2\nline3"}, + {"file path", []string{tmpFile}, "", "line1\nline2\nline3\nline4\nline5"}, + {"file with n", []string{"-n", "3", tmpFile}, "", "line3\nline4\nline5"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsTail(tt.args, tt.stdin) + if result != tt.want && !strings.Contains(result, tt.want) { + t.Errorf("expected %q, got %q", tt.want, result) + } + }) + } +} + +func TestFsWc(t *testing.T) { + tmpFile := filepath.Join(cfg.FilePickerDir, "test_wc.txt") + content := "one two three\nfour five\nsix\n" + os.WriteFile(tmpFile, []byte(content), 0644) + defer os.Remove(tmpFile) + + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"default", []string{}, "one two", "1 lines, 2 words, 7 chars"}, + {"lines", []string{"-l"}, "line1\nline2\nline3", "3"}, + {"words", []string{"-w"}, "one two three", "3"}, + {"chars", []string{"-c"}, "abc", "3"}, + {"file lines", []string{"-l", tmpFile}, "", "3"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsWc(tt.args, tt.stdin) + if !strings.Contains(result, tt.want) { + t.Errorf("expected %q in output, got %q", tt.want, result) + } + }) + } +} + +func TestFsSort(t *testing.T) { + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"basic", []string{}, "c\na\nb\n", "a\nb\nc"}, + {"reverse", []string{"-r"}, "a\nb\nc", "c\nb\na"}, + {"numeric", []string{"-n"}, "10\n2\n1\n", "1\n2\n10"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsSort(tt.args, tt.stdin) + if result != tt.want { + t.Errorf("expected %q, got %q", tt.want, result) + } + }) + } +} + +func TestFsUniq(t *testing.T) { + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"basic", []string{}, "a\nb\na\nc", "a\nb\nc"}, + {"count", []string{"-c"}, "a\na\nb", "2 a\n1 b"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsUniq(tt.args, tt.stdin) + if result != tt.want && !strings.Contains(result, tt.want) { + t.Errorf("expected %q, got %q", tt.want, result) + } + }) + } +} + +func TestFsGrep(t *testing.T) { + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"basic", []string{"world"}, "hello\nworld\ntest", "world"}, + {"ignore case", []string{"-i", "WORLD"}, "hello\nworld\ntest", "world"}, + {"invert", []string{"-v", "world"}, "hello\nworld\ntest", "hello\ntest"}, + {"count", []string{"-c", "o"}, "hello\no world\no foo", "3"}, + {"no match", []string{"xyz"}, "hello\nworld", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsGrep(tt.args, tt.stdin) + if tt.want != "" && !strings.Contains(result, tt.want) { + t.Errorf("expected %q, got %q", tt.want, result) + } + }) + } +} + +func TestFsEcho(t *testing.T) { + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"single", []string{"hello"}, "", "hello"}, + {"multiple", []string{"hello", "world"}, "", "hello world"}, + {"with stdin", []string{}, "stdin", "stdin"}, + {"empty", []string{}, "", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsEcho(tt.args, tt.stdin) + if result != tt.want { + t.Errorf("expected %q, got %q", tt.want, result) + } + }) + } +} + +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) + } +} + +func TestFsTime(t *testing.T) { + result := FsTime(nil, "") + if len(result) < 10 { + t.Errorf("expected time output, got %q", result) + } +} + +func TestFsStat(t *testing.T) { + tmpFile := filepath.Join(cfg.FilePickerDir, "test_stat.txt") + os.WriteFile(tmpFile, []byte("content"), 0644) + defer os.Remove(tmpFile) + + result := FsStat([]string{tmpFile}, "") + if !strings.Contains(result, "test_stat.txt") { + t.Errorf("expected filename in output, got %q", result) + } +} + +func TestFsMkdir(t *testing.T) { + testDir := filepath.Join(cfg.FilePickerDir, "test_mkdir_xyz") + defer os.RemoveAll(testDir) + + result := FsMkdir([]string{testDir}, "") + if _, err := os.Stat(testDir); err != nil { + t.Errorf("directory not created: %v, result: %q", err, result) + } +} + +func TestFsCp(t *testing.T) { + src := filepath.Join(cfg.FilePickerDir, "test_cp_src.txt") + dst := filepath.Join(cfg.FilePickerDir, "test_cp_dst.txt") + os.WriteFile(src, []byte("test"), 0644) + defer os.Remove(src) + defer os.Remove(dst) + + result := FsCp([]string{src, dst}, "") + if _, err := os.Stat(dst); err != nil { + t.Errorf("file not copied: %v, result: %q", err, result) + } +} + +func TestFsMv(t *testing.T) { + src := filepath.Join(cfg.FilePickerDir, "test_mv_src.txt") + dst := filepath.Join(cfg.FilePickerDir, "test_mv_dst.txt") + os.WriteFile(src, []byte("test"), 0644) + defer os.Remove(src) + defer os.Remove(dst) + + result := FsMv([]string{src, dst}, "") + if _, err := os.Stat(dst); err != nil { + t.Errorf("file not moved: %v, result: %q", err, result) + } + if _, err := os.Stat(src); err == nil { + t.Errorf("source file still exists") + } +} + +func TestFsRm(t *testing.T) { + tmpFile := filepath.Join(cfg.FilePickerDir, "test_rm_xyz.txt") + os.WriteFile(tmpFile, []byte("test"), 0644) + + result := FsRm([]string{tmpFile}, "") + if _, err := os.Stat(tmpFile); err == nil { + t.Errorf("file not removed, result: %q", result) + } +} + +func TestFsSed(t *testing.T) { + tests := []struct { + name string + args []string + stdin string + want string + }{ + {"replace", []string{"s/hello/bye/"}, "hello world", "bye world"}, + {"global", []string{"s/o/X/g"}, "hello world", "hellX wXrld"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FsSed(tt.args, tt.stdin) + if result != tt.want && !strings.Contains(result, tt.want) { + t.Errorf("expected %q, got %q", tt.want, result) + } + }) + } +} + +func TestPiping(t *testing.T) { + tmpFile := filepath.Join(cfg.FilePickerDir, "test_pipe.txt") + os.WriteFile(tmpFile, []byte("line3\nline1\nline2"), 0644) + defer os.Remove(tmpFile) + + tests := []struct { + name string + cmd string + check func(string) bool + }{ + {"ls | head -3", "ls | head -3", func(r string) bool { return r != "" }}, + {"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") }}, + {"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 | 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" }}, + } + + 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.name, result) + } + }) + } +} + +func TestChaining(t *testing.T) { + tests := []struct { + name string + cmd string + check func(string) bool + }{ + {"ls && echo ok", "ls && echo ok", func(r string) bool { return strings.Contains(r, "ok") }}, + {"ls || echo not run", "ls || echo fallback", func(r string) bool { return !strings.Contains(r, "fallback") }}, + {"false || echo run", "cd /nonexistent123 || echo fallback", func(r string) bool { return strings.Contains(r, "fallback") }}, + {"echo a ; echo b", "echo a ; echo b", func(r string) bool { return strings.Contains(r, "a") && strings.Contains(r, "b") }}, + } + + 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.name, result) + } + }) + } +}