From bde7aeed90b5e1fcb20a097a153be6f9f294ae15 Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Thu, 12 Jun 2025 22:01:45 -0400 Subject: [PATCH] Apply standard Go formatting and update project documentation - Run go fmt on all Go files to ensure consistent formatting - Add official Go tooling commands to CLAUDE.md for code quality - Update project status to reflect current implementation state --- CLAUDE.md | 32 ++++++++++++++++++++++++++++++++ archiver/archiver.go | 2 +- archiver/tar.go | 22 +++++++++++----------- archiver/zip.go | 22 +++++++++++----------- collector/collector.go | 32 ++++++++++++++++---------------- collector/matcher.go | 24 ++++++++++++------------ main.go | 42 +++++++++++++++++++++--------------------- 7 files changed, 104 insertions(+), 72 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e05b9ca --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,32 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a CLI tool called "collect" that recursively collects files matching specific criteria, maintains their file structure, and archives them for backup purposes. + +## Project Status + +The collect CLI tool has been implemented with the following features: +- Multiple `--name` flags for exact filename matching +- Multiple `--match` flags for directory pattern matching +- Support for combining `--name` and `--match` flags with OR logic +- Preserving original directory structure in the output +- Creating archives in tar.gz or zip format + +## Implementation Notes + +The tool accepts: +1. Multiple `--name` and `--match` flags in any combination +2. Two positional arguments: source directory and output archive path +3. Archive format determined by output file extension (.tgz/.tar.gz or .zip) +4. Recursive directory traversal while preserving original structure + +## Code Quality + +Before committing changes, run these official Go tools: +- `go fmt ./...` - Format all Go files to standard style +- `go vet ./...` - Check for common mistakes and suspicious code +- `go test ./...` - Run unit tests (when available) +- `./test.sh` - Run integration tests diff --git a/archiver/archiver.go b/archiver/archiver.go index 96aa2f8..c7a867e 100644 --- a/archiver/archiver.go +++ b/archiver/archiver.go @@ -5,4 +5,4 @@ import "github.com/atomaka/collect/collector" // Archiver defines the interface for creating archives type Archiver interface { Create(outputPath string, files []collector.FileEntry) error -} \ No newline at end of file +} diff --git a/archiver/tar.go b/archiver/tar.go index edc4566..f69019f 100644 --- a/archiver/tar.go +++ b/archiver/tar.go @@ -7,7 +7,7 @@ import ( "io" "os" "path/filepath" - + "github.com/atomaka/collect/collector" ) @@ -27,22 +27,22 @@ func (a *TarArchiver) Create(outputPath string, files []collector.FileEntry) err return fmt.Errorf("failed to create output file: %w", err) } defer outFile.Close() - + // Create gzip writer gzipWriter := gzip.NewWriter(outFile) defer gzipWriter.Close() - + // Create tar writer tarWriter := tar.NewWriter(gzipWriter) defer tarWriter.Close() - + // Add each file to the archive for _, file := range files { if err := a.addFileToTar(tarWriter, file); err != nil { return fmt.Errorf("failed to add file %s: %w", file.Path, err) } } - + return nil } @@ -59,28 +59,28 @@ func (a *TarArchiver) addFileToTar(tw *tar.Writer, file collector.FileEntry) err return err } defer f.Close() - + // Get file info info, err := f.Stat() if err != nil { return err } - + // Create tar header header, err := tar.FileInfoHeader(info, "") if err != nil { return err } - + // Use the relative path in the archive header.Name = filepath.ToSlash(file.Path) - + // Write header if err := tw.WriteHeader(header); err != nil { return err } - + // Copy file contents _, err = io.Copy(tw, f) return err -} \ No newline at end of file +} diff --git a/archiver/zip.go b/archiver/zip.go index 8ae20f7..7a8f262 100644 --- a/archiver/zip.go +++ b/archiver/zip.go @@ -6,7 +6,7 @@ import ( "io" "os" "path/filepath" - + "github.com/atomaka/collect/collector" ) @@ -26,18 +26,18 @@ func (a *ZipArchiver) Create(outputPath string, files []collector.FileEntry) err return fmt.Errorf("failed to create output file: %w", err) } defer outFile.Close() - + // Create zip writer zipWriter := zip.NewWriter(outFile) defer zipWriter.Close() - + // Add each file to the archive for _, file := range files { if err := a.addFileToZip(zipWriter, file); err != nil { return fmt.Errorf("failed to add file %s: %w", file.Path, err) } } - + return nil } @@ -54,32 +54,32 @@ func (a *ZipArchiver) addFileToZip(zw *zip.Writer, file collector.FileEntry) err return err } defer f.Close() - + // Get file info info, err := f.Stat() if err != nil { return err } - + // Create zip file header header, err := zip.FileInfoHeader(info) if err != nil { return err } - + // Use the relative path in the archive header.Name = filepath.ToSlash(file.Path) - + // Set compression method header.Method = zip.Deflate - + // Create writer for this file writer, err := zw.CreateHeader(header) if err != nil { return err } - + // Copy file contents _, err = io.Copy(writer, f) return err -} \ No newline at end of file +} diff --git a/collector/collector.go b/collector/collector.go index 701ca10..cb93fb7 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -33,7 +33,7 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) { if err != nil { return nil, fmt.Errorf("failed to get absolute path: %w", err) } - + // Check if source directory exists info, err := os.Stat(absSourceDir) if err != nil { @@ -42,9 +42,9 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) { if !info.IsDir() { return nil, fmt.Errorf("source path is not a directory: %s", sourceDir) } - + var files []FileEntry - + err = filepath.WalkDir(absSourceDir, func(path string, d fs.DirEntry, err error) error { if err != nil { // Log permission errors but continue walking @@ -54,7 +54,7 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) { } return err } - + // Get file info info, err := d.Info() if err != nil { @@ -64,12 +64,12 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) { } return err } - + // Skip symlinks if info.Mode()&os.ModeSymlink != 0 { return nil } - + // Check if this file should be included if c.matcher.ShouldInclude(path, info) { // Calculate relative path from source directory @@ -77,42 +77,42 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) { if err != nil { return fmt.Errorf("failed to get relative path: %w", err) } - + // Clean the relative path to ensure consistent formatting relPath = filepath.ToSlash(relPath) - + files = append(files, FileEntry{ Path: relPath, FullPath: path, }) } - + return nil }) - + if err != nil { return nil, fmt.Errorf("walk error: %w", err) } - + // Check if any files were found if len(files) == 0 { return nil, fmt.Errorf("no files found matching criteria") } - + return files, nil } // GetArchiveFormat determines the archive format from the filename func GetArchiveFormat(filename string) string { lower := strings.ToLower(filename) - + if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") { return "tar.gz" } - + if strings.HasSuffix(lower, ".zip") { return "zip" } - + return "" -} \ No newline at end of file +} diff --git a/collector/matcher.go b/collector/matcher.go index d063d72..0fd0210 100644 --- a/collector/matcher.go +++ b/collector/matcher.go @@ -40,7 +40,7 @@ type PatternMatcher struct { func NewPatternMatcher(pattern string) *PatternMatcher { // Remove trailing slash if present pattern = strings.TrimSuffix(pattern, "/") - + return &PatternMatcher{ pattern: pattern, matchedDirs: make(map[string]bool), @@ -58,27 +58,27 @@ func (m *PatternMatcher) ShouldInclude(path string, info os.FileInfo) bool { } return false // Don't include the directory itself, only files within } - + // For files, check if any parent directory is in the matched set dir := filepath.Dir(path) for { if m.matchedDirs[dir] { return true } - + // Also check if this directory matches the pattern (in case we haven't seen it yet) if matched, err := m.dirMatchesPattern(dir); err == nil && matched { m.matchedDirs[dir] = true return true } - + parent := filepath.Dir(dir) if parent == dir || parent == "." { break } dir = parent } - + return false } @@ -86,26 +86,26 @@ func (m *PatternMatcher) ShouldInclude(path string, info os.FileInfo) bool { func (m *PatternMatcher) dirMatchesPattern(dirPath string) (bool, error) { // Get the directory name dirName := filepath.Base(dirPath) - + // For simple patterns (no path separators), just match the directory name if len(m.patternSegments) == 1 { return filepath.Match(m.pattern, dirName) } - + // For complex patterns, we need to match the full path segments pathSegments := strings.Split(dirPath, string(os.PathSeparator)) - + // Try to match the pattern segments against the path segments if len(pathSegments) < len(m.patternSegments) { return false, nil } - + // Check each pattern segment against the corresponding path segment for i := 0; i < len(m.patternSegments); i++ { // Start from the end of both slices patternIdx := len(m.patternSegments) - 1 - i pathIdx := len(pathSegments) - 1 - i - + matched, err := filepath.Match(m.patternSegments[patternIdx], pathSegments[pathIdx]) if err != nil { return false, err @@ -114,7 +114,7 @@ func (m *PatternMatcher) dirMatchesPattern(dirPath string) (bool, error) { return false, nil } } - + return true, nil } @@ -136,4 +136,4 @@ func (m *CompositeMatcher) ShouldInclude(path string, info os.FileInfo) bool { } } return false -} \ No newline at end of file +} diff --git a/main.go b/main.go index fb36195..3fb7a08 100644 --- a/main.go +++ b/main.go @@ -6,16 +6,16 @@ import ( "os" "path/filepath" "strings" - + "github.com/atomaka/collect/archiver" "github.com/atomaka/collect/collector" ) const ( - exitSuccess = 0 - exitNoFiles = 1 + exitSuccess = 0 + exitNoFiles = 1 exitArchiveError = 2 - exitInvalidArgs = 3 + exitInvalidArgs = 3 ) // stringSlice is a custom flag type that accumulates string values @@ -34,10 +34,10 @@ func main() { // Define flags using custom type for multiple values var nameFlags stringSlice var matchFlags stringSlice - + flag.Var(&nameFlags, "name", "Match exact filename (can be specified multiple times)") flag.Var(&matchFlags, "match", "Match directory pattern (can be specified multiple times)") - + // Custom usage message flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [--name ]... [--match ]... \n\n", os.Args[0]) @@ -49,16 +49,16 @@ func main() { 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]) } - + 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 { @@ -66,30 +66,30 @@ func main() { 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 { @@ -97,7 +97,7 @@ func main() { } else { matcher = collector.NewCompositeMatcher(matchers) } - + // Create collector and collect files c := collector.New(matcher) files, err := c.Collect(sourceDir) @@ -108,10 +108,10 @@ func main() { } 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 { @@ -120,14 +120,14 @@ func main() { 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) -} \ No newline at end of file +}