diff --git a/docs/scripts.md b/docs/scripts.md index a0af2d59b..8158b1d47 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -140,6 +140,26 @@ const example = await $`echo example`; await $`echo ${example}`; ``` +### Concatenation + +```sh +# Bash +tmpDir="/tmp" +mkdir "$tmpDir/filename" +``` + +```js +// zx +const tmpDir = '/tmp' +await $`mkdir ${tmpDir}/filename`; +``` + +```js +// Execa +const tmpDir = '/tmp' +await $`mkdir ${tmpDir}/filename`; +``` + ### Parallel commands ```sh diff --git a/lib/command.js b/lib/command.js index b6d2e5ef5..7ae9c2bc7 100644 --- a/lib/command.js +++ b/lib/command.js @@ -76,21 +76,45 @@ const parseExpression = expression => { throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`); }; -const parseTemplate = (template, index, templates, expressions) => { +const concatTokens = (tokens, nextTokens, isNew) => isNew || tokens.length === 0 || nextTokens.length === 0 + ? [...tokens, ...nextTokens] + : [ + ...tokens.slice(0, -1), + `${tokens[tokens.length - 1]}${nextTokens[0]}`, + ...nextTokens.slice(1), + ]; + +const parseTemplate = ({templates, expressions, tokens, index, template}) => { const templateString = template ?? templates.raw[index]; const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean); + const newTokens = concatTokens( + tokens, + templateTokens, + templateString.startsWith(' '), + ); if (index === expressions.length) { - return templateTokens; + return newTokens; } const expression = expressions[index]; + const expressionTokens = Array.isArray(expression) + ? expression.map(expression => parseExpression(expression)) + : [parseExpression(expression)]; + return concatTokens( + newTokens, + expressionTokens, + templateString.endsWith(' '), + ); +}; + +export const parseTemplates = (templates, expressions) => { + let tokens = []; - return Array.isArray(expression) - ? [...templateTokens, ...expression.map(expression => parseExpression(expression))] - : [...templateTokens, parseExpression(expression)]; + for (const [index, template] of templates.entries()) { + tokens = parseTemplate({templates, expressions, tokens, index, template}); + } + + return tokens; }; -export const parseTemplates = (templates, expressions) => templates.flatMap( - (template, index) => parseTemplate(template, index, templates, expressions), -); diff --git a/test/command.js b/test/command.js index affcc58db..e6f92f50c 100644 --- a/test/command.js +++ b/test/command.js @@ -112,6 +112,11 @@ test('$ allows array interpolation', async t => { t.is(stdout, 'foo\nbar'); }); +test('$ allows empty array interpolation', async t => { + const {stdout} = await $`echo.js foo ${[]} bar`; + t.is(stdout, 'foo\nbar'); +}); + test('$ allows execa return value interpolation', async t => { const foo = await $`echo.js foo`; const {stdout} = await $`echo.js ${foo} bar`; @@ -171,6 +176,46 @@ test('$ handles invalid escape sequence', async t => { t.is(stdout, '\\u'); }); +test('$ can concatenate at the end of tokens', async t => { + const {stdout} = await $`echo.js foo${'bar'}`; + t.is(stdout, 'foobar'); +}); + +test('$ does not concatenate at the end of tokens with a space', async t => { + const {stdout} = await $`echo.js foo ${'bar'}`; + t.is(stdout, 'foo\nbar'); +}); + +test('$ can concatenate at the end of tokens followed by an array', async t => { + const {stdout} = await $`echo.js foo${['bar', 'foo']}`; + t.is(stdout, 'foobar\nfoo'); +}); + +test('$ can concatenate at the start of tokens', async t => { + const {stdout} = await $`echo.js ${'foo'}bar`; + t.is(stdout, 'foobar'); +}); + +test('$ does not concatenate at the start of tokens with a space', async t => { + const {stdout} = await $`echo.js ${'foo'} bar`; + t.is(stdout, 'foo\nbar'); +}); + +test('$ can concatenate at the start of tokens followed by an array', async t => { + const {stdout} = await $`echo.js ${['foo', 'bar']}foo`; + t.is(stdout, 'foo\nbarfoo'); +}); + +test('$ can concatenate at the start and end of tokens followed by an array', async t => { + const {stdout} = await $`echo.js foo${['bar', 'foo']}bar`; + t.is(stdout, 'foobar\nfoobar'); +}); + +test('$ can concatenate multiple tokens', async t => { + const {stdout} = await $`echo.js ${'foo'}bar${'foo'}`; + t.is(stdout, 'foobarfoo'); +}); + test('$ allows escaping spaces in commands with interpolation', async t => { const {stdout} = await $`${'command with space.js'} foo bar`; t.is(stdout, 'foo\nbar');