Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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());
});
});

Expand Down
22 changes: 18 additions & 4 deletions src/commands/cpMv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,15 @@ export async function parseMvArgs(cwd: string, args: string[]): Promise<MoveFlag
if (paths.length === 0) throw Error("missing operand");
else if (paths.length === 1) throw Error(`missing destination file operand after '${paths[0]}'`);

return { operations: await getCopyAndMoveOperations(cwd, paths) };
const operations = await getCopyAndMoveOperations(cwd, paths);
for (const operation of operations) {
// matches GNU mv, which errors renaming these (ex. `mv public/. dist`)
const basename = path.basename(operation.from.specified);
if (basename === "." || basename === "..") {
throw Error(`cannot move '${operation.from.specified}': refusing to move '.' or '..'`);
}
}
return { operations };
}

async function getCopyAndMoveOperations(
Expand All @@ -131,7 +139,7 @@ async function getCopyAndMoveOperations(
}
for (const from of fromArgs) {
const fromPath = resolvePath(cwd, from);
const toPath = path.join(destination, path.basename(fromPath));
const toPath = calculateDestinationPath(destination, from);
operations.push(
{
from: {
Expand All @@ -149,7 +157,7 @@ async function getCopyAndMoveOperations(
const fromPath = resolvePath(cwd, fromArgs[0]);

const toPath = await safeLstat(destination).then((p) => p?.isDirectory())
? calculateDestinationPath(destination, fromPath)
? calculateDestinationPath(destination, fromArgs[0])
: destination;

operations.push({
Expand All @@ -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));
Expand Down
Loading