1
0
Fork 0
collect/main.go
Andrew Tomaka 64a2bbace4 Add --verbose flag for real-time file discovery feedback
Implement verbose mode that shows files as they are discovered during
collection rather than just a summary. This provides better user feedback
during the typically slow filesystem traversal phase.

Features:
- Real-time "found: filename" output as files are discovered
- Verbose feedback during collection phase (not archiving phase)
- Simple, focused output that shows filtering effects
- Helps users track progress during potentially slow operations

Changes:
- Add --verbose boolean flag to CLI
- Pass verbose flag to collector constructors
- Display "found: filename" output during file collection
- Add test case to verify verbose output contains expected messages
- Update test suite with helper function to verify output contents
2025-06-12 22:39:32 -04:00

165 lines
4.9 KiB
Go

package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/atomaka/collect/archiver"
"github.com/atomaka/collect/collector"
)
const (
exitSuccess = 0
exitNoFiles = 1
exitArchiveError = 2
exitInvalidArgs = 3
)
// stringSlice is a custom flag type that accumulates string values
type stringSlice []string
func (s *stringSlice) String() string {
return strings.Join(*s, ", ")
}
func (s *stringSlice) Set(value string) error {
*s = append(*s, value)
return nil
}
func main() {
// Define flags using custom type for multiple values
var nameFlags stringSlice
var matchFlags stringSlice
var includeDirFlags stringSlice
var excludeDirFlags stringSlice
var verbose bool
flag.Var(&nameFlags, "name", "Match exact filename (can be specified multiple times)")
flag.Var(&matchFlags, "match", "Match directory pattern (can be specified multiple times)")
flag.Var(&includeDirFlags, "include-dir", "Only traverse directories matching pattern (can be specified multiple times)")
flag.Var(&excludeDirFlags, "exclude-dir", "Skip directories matching pattern (can be specified multiple times)")
flag.BoolVar(&verbose, "verbose", false, "Enable verbose output for debugging")
// Custom usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [--name <filename>]... [--match <pattern>]... [--include-dir <pattern>]... [--exclude-dir <pattern>]... <source-dir> <output-archive>\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Collects files recursively matching specific criteria and archives them.\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " %s --name .mise.toml ./ backup.tgz\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s --match 'aet-*/' ./ backup.zip\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s --name .mise.toml --name README.md --match 'test-*' ./ backup.tgz\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s --include-dir src --exclude-dir 'temp-*' --name '*.go' ./ backup.tgz\n", os.Args[0])
}
flag.Parse()
// Validate flags
if len(nameFlags) == 0 && len(matchFlags) == 0 {
fmt.Fprintf(os.Stderr, "Error: At least one --name or --match must be specified\n\n")
flag.Usage()
os.Exit(exitInvalidArgs)
}
// Check positional arguments
args := flag.Args()
if len(args) != 2 {
fmt.Fprintf(os.Stderr, "Error: Expected 2 arguments (source directory and output archive), got %d\n\n", len(args))
flag.Usage()
os.Exit(exitInvalidArgs)
}
sourceDir := args[0]
outputPath := args[1]
// Determine archive format
format := collector.GetArchiveFormat(outputPath)
if format == "" {
fmt.Fprintf(os.Stderr, "Error: Unsupported archive format. Use .tar.gz, .tgz, or .zip\n")
os.Exit(exitInvalidArgs)
}
// Create matchers
var matchers []collector.Matcher
// Add name matchers
for _, name := range nameFlags {
matchers = append(matchers, collector.NewNameMatcher(name))
}
// Add pattern matchers
for _, pattern := range matchFlags {
matchers = append(matchers, collector.NewPatternMatcher(pattern))
}
// Create a composite matcher if we have multiple matchers, otherwise use the single one
var matcher collector.Matcher
if len(matchers) == 1 {
matcher = matchers[0]
} else {
matcher = collector.NewCompositeMatcher(matchers)
}
// Create directory filters
var dirFilter collector.DirectoryFilter
if len(includeDirFlags) > 0 || len(excludeDirFlags) > 0 {
var includeFilters []collector.DirectoryFilter
var excludeFilters []collector.DirectoryFilter
// Create include filters
if len(includeDirFlags) > 0 {
includeFilters = append(includeFilters, collector.NewIncludeDirectoryFilter(includeDirFlags))
}
// Create exclude filters
if len(excludeDirFlags) > 0 {
excludeFilters = append(excludeFilters, collector.NewExcludeDirectoryFilter(excludeDirFlags))
}
dirFilter = collector.NewCompositeDirectoryFilter(includeFilters, excludeFilters)
}
// Create collector and collect files
var c *collector.Collector
if dirFilter != nil {
c = collector.NewWithDirectoryFilter(matcher, dirFilter, verbose)
} else {
c = collector.New(matcher, verbose)
}
files, err := c.Collect(sourceDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
if err.Error() == "no files found matching criteria" {
os.Exit(exitNoFiles)
}
os.Exit(exitArchiveError)
}
// Report number of files found
fmt.Printf("Found %d files to archive\n", len(files))
// Create appropriate archiver
var arch archiver.Archiver
switch format {
case "tar.gz":
arch = archiver.NewTarArchiver()
case "zip":
arch = archiver.NewZipArchiver()
}
// Create archive
if err := arch.Create(outputPath, files); err != nil {
fmt.Fprintf(os.Stderr, "Error creating archive: %v\n", err)
os.Exit(exitArchiveError)
}
// Get absolute path for cleaner output
absOutput, _ := filepath.Abs(outputPath)
fmt.Printf("Archive created successfully: %s\n", absOutput)
}