diff --git a/collector/matcher.go b/collector/matcher.go index 779cef0..d063d72 100644 --- a/collector/matcher.go +++ b/collector/matcher.go @@ -116,4 +116,24 @@ func (m *PatternMatcher) dirMatchesPattern(dirPath string) (bool, error) { } return true, nil +} + +// CompositeMatcher combines multiple matchers with OR logic +type CompositeMatcher struct { + matchers []Matcher +} + +// NewCompositeMatcher creates a matcher that combines multiple matchers +func NewCompositeMatcher(matchers []Matcher) *CompositeMatcher { + return &CompositeMatcher{matchers: matchers} +} + +// ShouldInclude returns true if ANY of the matchers match the file +func (m *CompositeMatcher) ShouldInclude(path string, info os.FileInfo) bool { + for _, matcher := range m.matchers { + if matcher.ShouldInclude(path, info) { + return true + } + } + return false } \ No newline at end of file diff --git a/main.go b/main.go index c21f09f..fb36195 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/atomaka/collect/archiver" "github.com/atomaka/collect/collector" @@ -17,27 +18,43 @@ const ( 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 - nameFlag := flag.String("name", "", "Match exact filename") - matchFlag := flag.String("match", "", "Match directory pattern") + // 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]) + fmt.Fprintf(os.Stderr, "Usage: %s [--name ]... [--match ]... \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]) } flag.Parse() // Validate flags - if (*nameFlag == "" && *matchFlag == "") || (*nameFlag != "" && *matchFlag != "") { - fmt.Fprintf(os.Stderr, "Error: Exactly one of --name or --match must be specified\n\n") + 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) } @@ -60,12 +77,25 @@ func main() { os.Exit(exitInvalidArgs) } - // Create matcher + // 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 *nameFlag != "" { - matcher = collector.NewNameMatcher(*nameFlag) + if len(matchers) == 1 { + matcher = matchers[0] } else { - matcher = collector.NewPatternMatcher(*matchFlag) + matcher = collector.NewCompositeMatcher(matchers) } // Create collector and collect files diff --git a/test.sh b/test.sh index e98e322..2e91b1a 100755 --- a/test.sh +++ b/test.sh @@ -118,9 +118,9 @@ run_test "no files found error" \ run_test "invalid arguments - no flags" \ "./collect test/ output.zip" 3 -# Test 6: Invalid arguments - both flags -run_test "invalid arguments - both flags" \ - "./collect --name .mise.toml --match 'aet-*' test/ output.zip" 3 +# Test 6: Invalid arguments - no positional args +run_test "invalid arguments - no positional args" \ + "./collect --name .mise.toml" 3 # Test 7: Invalid arguments - missing output file run_test "invalid arguments - missing output file" \ @@ -144,6 +144,28 @@ run_test "name matching finds files in pattern dirs too" \ "./collect --name .mise.toml test/ test-name-all.tgz" verify_archive_contents "test-name-all.tgz" ".mise.toml subdir/.mise.toml subdir/aet-bin/.mise.toml" +# Test 12: Multiple name flags +echo "readme" > test/README.md +echo "makefile" > test/Makefile +echo "another makefile" > test/subdir/Makefile +run_test "multiple name flags" \ + "./collect --name .mise.toml --name README.md --name Makefile test/ test-multi-name.tgz" +verify_archive_contents "test-multi-name.tgz" ".mise.toml Makefile README.md subdir/.mise.toml subdir/Makefile subdir/aet-bin/.mise.toml" + +# Test 13: Multiple match flags +mkdir -p test/prefix-one test/prefix-two test/other-prefix +echo "file1" > test/prefix-one/data.txt +echo "file2" > test/prefix-two/config.yml +echo "file3" > test/other-prefix/info.log +run_test "multiple match flags" \ + "./collect --match 'prefix-*' --match 'aet-*' test/ test-multi-match.tgz" +verify_archive_contents "test-multi-match.tgz" "deep/nested/aet-tools/deep.sh prefix-one/data.txt prefix-two/config.yml subdir/aet-bin/.mise.toml subdir/aet-bin/tool subdir/aet-config/settings.conf" + +# Test 14: Combined name and match flags +run_test "combined name and match flags" \ + "./collect --name .mise.toml --match 'aet-*' --name README.md test/ test-combined.tgz" +verify_archive_contents "test-combined.tgz" ".mise.toml README.md deep/nested/aet-tools/deep.sh subdir/.mise.toml subdir/aet-bin/.mise.toml subdir/aet-bin/tool subdir/aet-config/settings.conf" + # Clean up echo -e "\nCleaning up..." rm -rf test test-*.tgz test-*.zip collect /tmp/test_output.txt