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 var dryRun 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") flag.BoolVar(&dryRun, "dry-run", false, "Show what files would be collected without creating archive") // Custom usage message flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [--name ]... [--match ]... [--include-dir ]... [--exclude-dir ]... [--dry-run] [--verbose] \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]) fmt.Fprintf(os.Stderr, " %s --dry-run --name .mise.toml ./ 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 (skip validation in dry-run mode) var format string if !dryRun { 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) } // Handle dry-run mode if dryRun { fmt.Printf("DRY RUN: Found %d files that would be collected:\n", len(files)) for _, file := range files { fmt.Printf(" %s\n", file.Path) } fmt.Printf("\nDRY RUN: Would create archive: %s\n", outputPath) fmt.Printf("DRY RUN: No archive created (use without --dry-run to create archive)\n") os.Exit(exitSuccess) } // 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) }