1
0
Fork 0

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
This commit is contained in:
Andrew Tomaka 2025-06-12 22:01:45 -04:00
parent 1265f9fb07
commit bde7aeed90
Signed by: atomaka
GPG key ID: 61209BF70A5B18BE
7 changed files with 104 additions and 72 deletions

32
CLAUDE.md Normal file
View file

@ -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

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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 ""
}
}

View file

@ -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
}
}

42
main.go
View file

@ -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 <filename>]... [--match <pattern>]... <source-dir> <output-archive>\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)
}
}