diff --git a/mod.test.ts b/mod.test.ts index 74cfd2b..21ee04f 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -1265,8 +1265,11 @@ Deno.test("copy test", async () => { "cp: target 'non-existent' is not a directory\n", ); - assertEquals(await getStdErr($`cp "" ""`), "cp: missing file operand\n"); - assertStringIncludes(await getStdErr($`cp ${file1} ""`), "cp: missing destination file operand after"); + assertEquals(await getStdErr($`cp`), "cp: missing file operand\n"); + assertStringIncludes(await getStdErr($`cp ${file1}`), "cp: missing destination file operand after"); + + assertEquals(await getStdErr($`cp`), "cp: missing file operand\n"); + assertStringIncludes(await getStdErr($`cp ${file1}`), "cp: missing destination file operand after"); // recursive test destDir.join("sub_dir").mkdirSync(); @@ -1331,8 +1334,8 @@ Deno.test("move test", async () => { "mv: target 'non-existent' is not a directory\n", ); - assertEquals(await getStdErr($`mv "" ""`), "mv: missing operand\n"); - assertStringIncludes(await getStdErr($`mv ${file1} ""`), "mv: missing destination file operand after"); + assertEquals(await getStdErr($`mv`), "mv: missing operand\n"); + assertStringIncludes(await getStdErr($`mv ${file1}`), "mv: missing destination file operand after"); }); }); @@ -1648,6 +1651,11 @@ Deno.test("ensure KillSignalController readme example works", async () => { assert(endTime - startTime < 1000); }); +Deno.test("should support empty quoted string", async () => { + const output = await $`echo '' test ''`.text(); + assertEquals(output, " test "); +}); + function ensurePromiseNotResolved(promise: Promise) { return new Promise((resolve, reject) => { promise.then(() => reject(new Error("Promise was resolved"))); diff --git a/src/shell.ts b/src/shell.ts index 1eb52c5..45977e3 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -838,10 +838,11 @@ async function evaluateWord(word: Word, context: Context) { return result.join(" "); } -async function evaluateWordParts(wordParts: WordPart[], context: Context) { +async function evaluateWordParts(wordParts: WordPart[], context: Context, quoted = false) { // not implemented mostly, and copying from deno_task_shell const result: string[] = []; let currentText = ""; + let hasQuoted = false; for (const stringPart of wordParts) { let evaluationResult: string | undefined = undefined; switch (stringPart.kind) { @@ -852,8 +853,9 @@ async function evaluateWordParts(wordParts: WordPart[], context: Context) { evaluationResult = context.getVar(stringPart.value); // value is name break; case "quoted": { - const text = (await evaluateWordParts(stringPart.value, context)).join(" "); + const text = (await evaluateWordParts(stringPart.value, context, true)).join(""); currentText += text; + hasQuoted = true; continue; } case "command": @@ -862,26 +864,30 @@ async function evaluateWordParts(wordParts: WordPart[], context: Context) { } if (evaluationResult != null) { - const parts = evaluationResult.split(" ") - .map((t) => t.trim()) - .filter((t) => t.length > 0); - if (parts.length > 0) { - // append the first part to the current text - currentText += parts[0]; - - // store the current text - result.push(currentText); - - // store all the rest of the parts - result.push(...parts.slice(1)); - - // use the last part as the current text so it maybe - // gets appended to in the future - currentText = result.pop()!; + if (quoted) { + currentText += evaluationResult; + } else { + const parts = evaluationResult.split(" ") + .map((t) => t.trim()) + .filter((t) => t.length > 0); + if (parts.length > 0) { + // append the first part to the current text + currentText += parts[0]; + + // store the current text + result.push(currentText); + + // store all the rest of the parts + result.push(...parts.slice(1)); + + // use the last part as the current text so it maybe + // gets appended to in the future + currentText = result.pop()!; + } } } } - if (currentText.length !== 0) { + if (hasQuoted || currentText.length !== 0) { result.push(currentText); } return result;