diff --git a/instruction.go b/instruction.go index 12d5029..9b9316c 100644 --- a/instruction.go +++ b/instruction.go @@ -508,7 +508,32 @@ func ExpandPattern(source, workdir, target string, filesOnly bool) (links []Link } // Normalize path to convert backslashes to forward slashes before pattern processing source = NormalizePath(source, workdir) + + if !strings.ContainsAny(source, "*?[{") { + info, statErr := os.Stat(source) + if statErr != nil { + if os.IsNotExist(statErr) { + LogInfo("Literal source %s does not exist, skipping", FormatSourcePath(source)) + return nil, nil + } + return nil, fmt.Errorf("failed to stat literal source %s: %w", source, statErr) + } + + if filesOnly && info.IsDir() { + LogInfo("Files-only mode: skipping directory %s", FormatSourcePath(source)) + return nil, nil + } + + return []LinkInstruction{ + { + Source: source, + Target: target, + }, + }, nil + } + static, pattern := doublestar.SplitPattern(source) + if static == "" || static == "." { static = workdir } @@ -520,8 +545,6 @@ func ExpandPattern(source, workdir, target string, filesOnly bool) (links []Link return nil, fmt.Errorf("error expanding pattern: %w", err) } - singleMatch := len(files) == 1 - for _, file := range files { fullPath := filepath.Join(static, file) @@ -531,18 +554,6 @@ func ExpandPattern(source, workdir, target string, filesOnly bool) (links []Link continue } - if singleMatch { - if info.IsDir() && filesOnly { - LogInfo("Files-only mode: skipping single matched directory %s", FormatSourcePath(fullPath)) - continue - } - links = append(links, LinkInstruction{ - Source: fullPath, - Target: target, - }) - continue - } - if filesOnly && info.IsDir() { LogInfo("Files-only mode: skipping directory %s", FormatSourcePath(fullPath)) continue diff --git a/instruction_test.go b/instruction_test.go index 644ef51..2f2d456 100644 --- a/instruction_test.go +++ b/instruction_test.go @@ -758,8 +758,8 @@ func TestGlobPatterns(t *testing.T) { expectedMappings := map[string]string{ "src/file1.txt": "dst/txt/file1.txt", "src/file2.txt": "dst/txt/file2.txt", - "src/file3.log": "dst/logs", // Single file - target is directory directly - "src/readme.md": "dst/docs", // Single file - target is directory directly + "src/file3.log": "dst/logs/file3.log", + "src/readme.md": "dst/docs/readme.md", } for sourceEnd, expectedTargetEnd := range expectedMappings { @@ -1469,7 +1469,7 @@ func TestDestinationPathMapping(t *testing.T) { instruction := instructions[0] // Verify using path endings assert.True(t, strings.HasSuffix(instruction.Source, "src/root.txt")) - assert.True(t, strings.HasSuffix(instruction.Target, "dst")) // Single file - target is directory directly + assert.True(t, strings.HasSuffix(instruction.Target, "dst/root.txt")) }) t.Run("src/foo/*.txt -> dst should map src/foo/foo.txt to dst/foo.txt", func(t *testing.T) { @@ -1486,7 +1486,7 @@ func TestDestinationPathMapping(t *testing.T) { instruction := instructions[0] // Verify using path endings assert.True(t, strings.HasSuffix(instruction.Source, "src/foo/foo.txt")) - assert.True(t, strings.HasSuffix(instruction.Target, "dst")) // Single file - target is directory directly + assert.True(t, strings.HasSuffix(instruction.Target, "dst/foo.txt")) }) t.Run("Complex nested pattern src/foo/**/bar/*.txt -> dst should preserve structure", func(t *testing.T) {