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() }