Skip to content

Commit 07585d0

Browse files
authored
Fix argument concatenation with $ (#553)
1 parent 6fe7e51 commit 07585d0

File tree

3 files changed

+97
-8
lines changed

3 files changed

+97
-8
lines changed

docs/scripts.md

+20
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,26 @@ const example = await $`echo example`;
140140
await $`echo ${example}`;
141141
```
142142

143+
### Concatenation
144+
145+
```sh
146+
# Bash
147+
tmpDir="/tmp"
148+
mkdir "$tmpDir/filename"
149+
```
150+
151+
```js
152+
// zx
153+
const tmpDir = '/tmp'
154+
await $`mkdir ${tmpDir}/filename`;
155+
```
156+
157+
```js
158+
// Execa
159+
const tmpDir = '/tmp'
160+
await $`mkdir ${tmpDir}/filename`;
161+
```
162+
143163
### Parallel commands
144164

145165
```sh

lib/command.js

+32-8
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,45 @@ const parseExpression = expression => {
7676
throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`);
7777
};
7878

79-
const parseTemplate = (template, index, templates, expressions) => {
79+
const concatTokens = (tokens, nextTokens, isNew) => isNew || tokens.length === 0 || nextTokens.length === 0
80+
? [...tokens, ...nextTokens]
81+
: [
82+
...tokens.slice(0, -1),
83+
`${tokens[tokens.length - 1]}${nextTokens[0]}`,
84+
...nextTokens.slice(1),
85+
];
86+
87+
const parseTemplate = ({templates, expressions, tokens, index, template}) => {
8088
const templateString = template ?? templates.raw[index];
8189
const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean);
90+
const newTokens = concatTokens(
91+
tokens,
92+
templateTokens,
93+
templateString.startsWith(' '),
94+
);
8295

8396
if (index === expressions.length) {
84-
return templateTokens;
97+
return newTokens;
8598
}
8699

87100
const expression = expressions[index];
101+
const expressionTokens = Array.isArray(expression)
102+
? expression.map(expression => parseExpression(expression))
103+
: [parseExpression(expression)];
104+
return concatTokens(
105+
newTokens,
106+
expressionTokens,
107+
templateString.endsWith(' '),
108+
);
109+
};
110+
111+
export const parseTemplates = (templates, expressions) => {
112+
let tokens = [];
88113

89-
return Array.isArray(expression)
90-
? [...templateTokens, ...expression.map(expression => parseExpression(expression))]
91-
: [...templateTokens, parseExpression(expression)];
114+
for (const [index, template] of templates.entries()) {
115+
tokens = parseTemplate({templates, expressions, tokens, index, template});
116+
}
117+
118+
return tokens;
92119
};
93120

94-
export const parseTemplates = (templates, expressions) => templates.flatMap(
95-
(template, index) => parseTemplate(template, index, templates, expressions),
96-
);

test/command.js

+45
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ test('$ allows array interpolation', async t => {
113113
t.is(stdout, 'foo\nbar');
114114
});
115115

116+
test('$ allows empty array interpolation', async t => {
117+
const {stdout} = await $`echo.js foo ${[]} bar`;
118+
t.is(stdout, 'foo\nbar');
119+
});
120+
116121
test('$ allows execa return value interpolation', async t => {
117122
const foo = await $`echo.js foo`;
118123
const {stdout} = await $`echo.js ${foo} bar`;
@@ -172,6 +177,46 @@ test('$ handles invalid escape sequence', async t => {
172177
t.is(stdout, '\\u');
173178
});
174179

180+
test('$ can concatenate at the end of tokens', async t => {
181+
const {stdout} = await $`echo.js foo${'bar'}`;
182+
t.is(stdout, 'foobar');
183+
});
184+
185+
test('$ does not concatenate at the end of tokens with a space', async t => {
186+
const {stdout} = await $`echo.js foo ${'bar'}`;
187+
t.is(stdout, 'foo\nbar');
188+
});
189+
190+
test('$ can concatenate at the end of tokens followed by an array', async t => {
191+
const {stdout} = await $`echo.js foo${['bar', 'foo']}`;
192+
t.is(stdout, 'foobar\nfoo');
193+
});
194+
195+
test('$ can concatenate at the start of tokens', async t => {
196+
const {stdout} = await $`echo.js ${'foo'}bar`;
197+
t.is(stdout, 'foobar');
198+
});
199+
200+
test('$ does not concatenate at the start of tokens with a space', async t => {
201+
const {stdout} = await $`echo.js ${'foo'} bar`;
202+
t.is(stdout, 'foo\nbar');
203+
});
204+
205+
test('$ can concatenate at the start of tokens followed by an array', async t => {
206+
const {stdout} = await $`echo.js ${['foo', 'bar']}foo`;
207+
t.is(stdout, 'foo\nbarfoo');
208+
});
209+
210+
test('$ can concatenate at the start and end of tokens followed by an array', async t => {
211+
const {stdout} = await $`echo.js foo${['bar', 'foo']}bar`;
212+
t.is(stdout, 'foobar\nfoobar');
213+
});
214+
215+
test('$ can concatenate multiple tokens', async t => {
216+
const {stdout} = await $`echo.js ${'foo'}bar${'foo'}`;
217+
t.is(stdout, 'foobarfoo');
218+
});
219+
175220
test('$ allows escaping spaces in commands with interpolation', async t => {
176221
const {stdout} = await $`${'command with space.js'} foo bar`;
177222
t.is(stdout, 'foo\nbar');

0 commit comments

Comments
 (0)