Skip to content

Commit

Permalink
Fix argument concatenation with $ (#553)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Mar 14, 2023
1 parent 6fe7e51 commit 07585d0
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 8 deletions.
20 changes: 20 additions & 0 deletions docs/scripts.md
Expand Up @@ -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
Expand Down
40 changes: 32 additions & 8 deletions lib/command.js
Expand Up @@ -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),
);
45 changes: 45 additions & 0 deletions test/command.js
Expand Up @@ -113,6 +113,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`;
Expand Down Expand Up @@ -172,6 +177,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');
Expand Down

0 comments on commit 07585d0

Please sign in to comment.