Skip to content

Commit 134c1a8

Browse files
Fixed Trailing Slashes in cp command + added unit tests (#3921)
Fixes: #2835 ## Changes Fixed handling of trailing directory separators in the `cp` command. When a target path ends with `/` or `\`, the command now validates that the directory exists before copying. If the directory doesn't exist, it returns a clear error message instead of failing later. Added a helper function `hasTrailingDirSeparator` to detect trailing directory separators for both Unix-style (`/`) and Windows-style (`\`) paths. ## Why Previously, the `cp` command didn't properly handle trailing directory separators. When users specified a target path like `target/dir/` (expecting it to be treated as a directory), the command would not validate that the directory exists first, leading to confusing error messages later in the copy process. This change ensures that trailing directory separators are explicitly treated as indicating a directory target, and validates that the directory exists upfront. ## Tests Added unit tests in `integration/cmd/fs/cp_test.go`: - `TestFsCpFileToNonExistentDir`: Tests copying files to both existing and non-existent directories with trailing slashes - `TestFsCpFileToNonExistentDirWindowsPaths`: Windows-specific tests for trailing backslashes (`\`) and forward slashes (`/`) on Windows The tests verify that: - Copying to an existing directory with a trailing separator succeeds - Copying to a non-existent directory with a trailing separator returns an appropriate error message - Both Unix-style (`/`) and Windows-style (`\`) separators are handled correctly --------- Co-authored-by: Andrew Nester <[email protected]>
1 parent 05a5e2a commit 134c1a8

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

cmd/fs/cp.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"errors"
66
"fmt"
77
"io/fs"
8+
"os"
89
"path"
910
"path/filepath"
11+
"strings"
1012

1113
"github.com/databricks/cli/cmd/root"
1214
"github.com/databricks/cli/libs/cmdio"
@@ -126,6 +128,16 @@ func (c *copy) emitFileCopiedEvent(sourcePath, targetPath string) error {
126128
return cmdio.RenderWithTemplate(c.ctx, event, "", template)
127129
}
128130

131+
// hasTrailingDirSeparator checks if a path ends with a directory separator.
132+
func hasTrailingDirSeparator(path string) bool {
133+
return strings.HasSuffix(path, string(os.PathSeparator))
134+
}
135+
136+
// trimTrailingDirSeparators removes all trailing directory separators from a path.
137+
func trimTrailingDirSeparators(path string) string {
138+
return strings.TrimRight(path, string(os.PathSeparator))
139+
}
140+
129141
func newCpCommand() *cobra.Command {
130142
cmd := &cobra.Command{
131143
Use: "cp SOURCE_PATH TARGET_PATH",
@@ -190,6 +202,11 @@ func newCpCommand() *cobra.Command {
190202
return c.cpDirToDir(sourcePath, targetPath)
191203
}
192204

205+
// If target path has a trailing separator, trim it and let case 2 handle it
206+
if hasTrailingDirSeparator(fullTargetPath) {
207+
targetPath = trimTrailingDirSeparators(targetPath)
208+
}
209+
193210
// case 2: source path is a file, and target path is a directory. In this case
194211
// we copy the file to inside the directory
195212
if targetInfo, err := targetFiler.Stat(ctx, targetPath); err == nil && targetInfo.IsDir() {

integration/cmd/fs/cp_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,68 @@ func TestFsCpSourceIsDirectoryButTargetIsFile(t *testing.T) {
374374
})
375375
}
376376
}
377+
378+
func TestFsCpFileToNonExistentDir(t *testing.T) {
379+
t.Parallel()
380+
381+
for _, testCase := range copyTests() {
382+
t.Run(testCase.name, func(t *testing.T) {
383+
t.Parallel()
384+
385+
ctx := context.Background()
386+
sourceFiler, sourceDir := testCase.setupSource(t)
387+
targetFiler, targetDir := testCase.setupTarget(t)
388+
setupSourceFile(t, ctx, sourceFiler)
389+
390+
// Create a directory in the target
391+
err := targetFiler.Mkdir(ctx, "existingdir")
392+
require.NoError(t, err)
393+
394+
// Copy file to existing directory with trailing slash - should succeed
395+
existingDir := path.Join(targetDir, "existingdir") + "/"
396+
testcli.RequireSuccessfulRun(t, ctx, "fs", "cp", path.Join(sourceDir, "foo.txt"), existingDir)
397+
assertTargetFile(t, ctx, targetFiler, "existingdir/foo.txt")
398+
399+
// Try to copy file to a non-existent directory with trailing slash - should fail
400+
nonExistentDir := path.Join(targetDir, "nonexistent", "mydir") + "/"
401+
_, _, err = testcli.RequireErrorRun(t, ctx, "fs", "cp", path.Join(sourceDir, "foo.txt"), nonExistentDir)
402+
assert.Error(t, err)
403+
assert.Contains(t, err.Error(), "directory")
404+
assert.Contains(t, err.Error(), "no such directory")
405+
})
406+
}
407+
}
408+
409+
func TestFsCpFileToNonExistentDirWindowsPaths(t *testing.T) {
410+
if runtime.GOOS != "windows" {
411+
t.Skip("Skipping test on non-windows OS")
412+
}
413+
414+
ctx := context.Background()
415+
sourceFiler, sourceDir := setupLocalFiler(t)
416+
targetFiler, targetDir := setupLocalFiler(t)
417+
setupSourceFile(t, ctx, sourceFiler)
418+
419+
// Create a directory in the target
420+
err := targetFiler.Mkdir(ctx, "existingdir")
421+
require.NoError(t, err)
422+
423+
// Copy file to existing directory with trailing backslash (Windows style) - should succeed
424+
windowsExistingDir := filepath.Join(filepath.FromSlash(targetDir), "existingdir") + "\\"
425+
testcli.RequireSuccessfulRun(t, ctx, "fs", "cp", filepath.Join(filepath.FromSlash(sourceDir), "foo.txt"), windowsExistingDir)
426+
assertTargetFile(t, ctx, targetFiler, "existingdir/foo.txt")
427+
428+
// Try to copy file to a non-existent directory with trailing backslash - should fail
429+
windowsNonExistentDir := filepath.Join(filepath.FromSlash(targetDir), "nonexistent", "mydir") + "\\"
430+
_, _, err = testcli.RequireErrorRun(t, ctx, "fs", "cp", filepath.Join(filepath.FromSlash(sourceDir), "foo.txt"), windowsNonExistentDir)
431+
assert.Error(t, err)
432+
assert.Contains(t, err.Error(), "directory")
433+
assert.Contains(t, err.Error(), "no such directory")
434+
435+
// Also test with forward slash on Windows (should also work)
436+
forwardSlashDir := path.Join(targetDir, "nonexistent2", "mydir2") + "/"
437+
_, _, err = testcli.RequireErrorRun(t, ctx, "fs", "cp", path.Join(sourceDir, "foo.txt"), forwardSlashDir)
438+
assert.Error(t, err)
439+
assert.Contains(t, err.Error(), "directory")
440+
assert.Contains(t, err.Error(), "no such directory")
441+
}

0 commit comments

Comments
 (0)