zutils/cmd/search.go
selamanapps 5b334071e0 feat: add user-friendly help system and new commands
New Commands:
- z read - Smart file reader with auto-detection (markdown, syntax highlighting)
- z sys - System overview (CPU, RAM, Disk, processes)
- z find - File search with pattern/size/time filters
- z search - User-friendly grep alternative
- z usage - Process resource usage monitor

Enhancements:
- Unified help system with consistent error messages
- Help on error shows usage and examples for each command
- "Command not found" suggests similar commands
- Directory info now detects .gitignore and shows ignored files
- File handler registry for extensible file type detection
- Added Italic, Dim, Underline color constants
2026-05-02 01:12:55 +03:00

180 lines
4.2 KiB
Go

package cmd
import (
"bufio"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/zemenawi/zutils/pkg/colors"
)
type SearchOptions struct {
Pattern string
Path string
CaseInsensitive bool
WholeWord bool
LineNumbers bool
ContextLines int
FilePattern string
}
type SearchResult struct {
File string
Line int
Content string
}
func SearchCommand(args []string) error {
fs := flag.NewFlagSet("search", flag.ContinueOnError)
options := SearchOptions{}
fs.BoolVar(&options.CaseInsensitive, "i", false, "Case insensitive search")
fs.BoolVar(&options.WholeWord, "w", false, "Match whole word")
fs.BoolVar(&options.LineNumbers, "n", true, "Show line numbers")
fs.IntVar(&options.ContextLines, "C", 0, "Show N lines of context")
fs.StringVar(&options.FilePattern, "f", "*", "Search only in files matching pattern")
if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return nil
}
return fmt.Errorf("invalid arguments\n\nUsage: z search [options] pattern [path]\n\nExamples:\n z search func cmd/ Search for 'func' in cmd directory\n z search -i 'error' . Case-insensitive search\n z search -w 'import' *.go Whole word match in Go files")
}
if fs.NArg() < 1 {
return fmt.Errorf("missing search pattern\n\nUsage: z search [options] pattern [path]\n\nExamples:\n z search func cmd/ Search for 'func' in cmd directory\n z search -i 'error' . Case-insensitive search\n z search -w 'import' *.go Whole word match in Go files")
}
options.Pattern = fs.Arg(0)
if fs.NArg() > 1 {
options.Path = fs.Arg(1)
} else {
options.Path = "."
}
results, err := searchFiles(options)
if err != nil {
return fmt.Errorf("search error: %v", err)
}
if len(results) == 0 {
fmt.Printf("%sNo matches found for '%s'%s\n", colors.Gray, options.Pattern, colors.Reset)
return nil
}
displaySearchResults(results, options)
return nil
}
func searchFiles(opts SearchOptions) ([]SearchResult, error) {
var results []SearchResult
pattern := opts.Pattern
if opts.CaseInsensitive {
pattern = strings.ToLower(pattern)
}
var regex *regexp.Regexp
if opts.WholeWord {
regex = regexp.MustCompile(`\b` + regexp.QuoteMeta(pattern) + `\b`)
} else {
regex = regexp.MustCompile(regexp.QuoteMeta(pattern))
}
err := filepath.Walk(opts.Path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() {
return nil
}
baseName := filepath.Base(path)
matched, err := filepath.Match(opts.FilePattern, baseName)
if err != nil || !matched {
return nil
}
fileResults := searchFile(path, regex, opts)
results = append(results, fileResults...)
return nil
})
return results, err
}
func searchFile(path string, regex *regexp.Regexp, opts SearchOptions) []SearchResult {
var results []SearchResult
file, err := os.Open(path)
if err != nil {
return results
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
check := line
if opts.CaseInsensitive {
check = strings.ToLower(check)
}
if regex.MatchString(check) {
results = append(results, SearchResult{
File: path,
Line: lineNum,
Content: line,
})
}
}
return results
}
func displaySearchResults(results []SearchResult, opts SearchOptions) {
files := make(map[string][]SearchResult)
for _, r := range results {
files[r.File] = append(files[r.File], r)
}
PrintBoxHeader(fmt.Sprintf("SEARCH RESULTS (%d matches in %d files)", len(results), len(files)), colors.Cyan)
for file, fileResults := range files {
relPath := file
if strings.Contains(relPath, "/") {
parts := strings.Split(relPath, "/")
if len(parts) > 3 {
relPath = ".../" + strings.Join(parts[len(parts)-2:], "/")
}
}
fmt.Printf("\n%s%s%s:%s\n", colors.Bold, colors.Yellow, relPath, colors.Reset)
for _, r := range fileResults {
content := r.Content
if len(content) > 100 {
content = content[:97] + "..."
}
if opts.LineNumbers {
fmt.Printf("%s%4d:%s %s\n", colors.Gray, r.Line, colors.Reset, content)
} else {
fmt.Printf(" %s\n", content)
}
}
}
fmt.Println()
}