From e0556c998e2ea1fb788c817e501b21dcd77af4ef Mon Sep 17 00:00:00 2001 From: David Sherret Date: Mon, 13 Jul 2026 12:40:24 -0400 Subject: [PATCH] fix: make cp -r with a trailing /. copy directory contents like GNU cp --- mod.test.ts | 37 +++++++++++++++++++++++++++++++++++++ src/commands/cpMv.ts | 22 ++++++++++++++++++---- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/mod.test.ts b/mod.test.ts index 71166ba..cce8b1c 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -2243,6 +2243,30 @@ Deno.test("cp test2", async () => { }); }); +Deno.test("cp -r with trailing dot copies directory contents", async () => { + await withTempDir(async (dir) => { + // https://github.com/denoland/deno_task_shell/issues/176 + await $`mkdir -p public/.well-known`; + dir.join("public/index.html").writeSync("test"); + dir.join("public/.well-known/security.txt").writeSync("test"); + + await $`mkdir dist`; + await $`cp -r public/. dist/`; + assert(!dir.join("dist/public").existsSync()); + assert(dir.join("dist/index.html").existsSync()); + assert(dir.join("dist/.well-known/security.txt").existsSync()); + + // non-existent destination + await $`cp -r public/. dist2`; + assert(dir.join("dist2/index.html").existsSync()); + + // "." as the entire source + await $`mkdir dist3 && cd public && cp -r . ../dist3`; + assert(dir.join("dist3/index.html").existsSync()); + assert(dir.join("dist3/.well-known/security.txt").existsSync()); + }); +}); + Deno.test("move test", async () => { await withTempDir(async (dir) => { const file1 = dir.join("file1.txt"); @@ -2276,6 +2300,19 @@ Deno.test("move test", async () => { assertEquals(await getStdErr($`mv`), "mv: missing operand\n"); assertStringIncludes(await getStdErr($`mv ${file1}`), "mv: missing destination file operand after"); + + // refuses to move '.' or '..' like GNU mv + const otherDir = dir.join("other"); + otherDir.mkdirSync(); + assertEquals( + await getStdErr($`mv dest/. other`), + "mv: cannot move 'dest/.': refusing to move '.' or '..'\n", + ); + assertEquals( + await getStdErr($`mv .. other`), + "mv: cannot move '..': refusing to move '.' or '..'\n", + ); + assert(destDir.join("file1.txt").existsSync()); }); }); diff --git a/src/commands/cpMv.ts b/src/commands/cpMv.ts index fcfa2fb..ada1eac 100644 --- a/src/commands/cpMv.ts +++ b/src/commands/cpMv.ts @@ -113,7 +113,15 @@ export async function parseMvArgs(cwd: string, args: string[]): Promise p?.isDirectory()) - ? calculateDestinationPath(destination, fromPath) + ? calculateDestinationPath(destination, fromArgs[0]) : destination; operations.push({ @@ -167,11 +175,17 @@ async function getCopyAndMoveOperations( } /** Calculates destination path - * destination should be a directory + * destination should be a directory and from should be + * the path as specified on the command line * example: * destination: /dir/a * from : /path/file * returns : /dir/a/file + * + * The basename is taken from the path as specified rather than + * the resolved path so that a trailing `.` resolves to the + * destination itself like GNU cp (ex. `cp -r public/. dist` + * copies the contents of `public` into `dist`). */ function calculateDestinationPath(destination: string, from: string) { return path.join(destination, path.basename(from));