Skip to content

Commit 434cf45

Browse files
authored
feat(core): allow {args} to be fully interpolated in run-commands (#31824)
## Current Behavior Currently, there is no way for a target using `run-commands` to define where in the command args are attached. This is problematic in some tooling cases where args positional location matters ## Expected Behavior Placing `{args}` into the command should allow for interpolation of any and all args provided. Therefore commands can be written such as `docker run {args} imageRef`
1 parent 1f56ead commit 434cf45

File tree

2 files changed

+79
-18
lines changed

2 files changed

+79
-18
lines changed

packages/nx/src/executors/run-commands/run-commands.impl.spec.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ function normalize(p: string) {
1111
return p.startsWith('/private') ? p.substring(8) : p;
1212
}
1313

14-
function readFile(f: string) {
15-
return readFileSync(f).toString().replace(/\s/g, '');
14+
function readFile(
15+
f: string,
16+
{ preserveWhitespace }: { preserveWhitespace: boolean } = {
17+
preserveWhitespace: false,
18+
}
19+
) {
20+
const fileContents = readFileSync(f).toString();
21+
return preserveWhitespace ? fileContents : fileContents.replace(/\s/g, '');
1622
}
1723

1824
describe('Run Commands', () => {
@@ -222,6 +228,21 @@ describe('Run Commands', () => {
222228
}
223229
);
224230

231+
it('should interpolate {args} to contain all provided args', async () => {
232+
const f = fileSync().name;
233+
const result = await runCommands(
234+
{
235+
command: `echo {args} >> ${f}`,
236+
__unparsed__: [`--publish 8080:80`, `--expose 80`],
237+
},
238+
context
239+
);
240+
expect(result).toEqual(expect.objectContaining({ success: true }));
241+
expect(readFile(f, { preserveWhitespace: true }).trim()).toEqual(
242+
`--publish 8080:80 --expose 80`
243+
);
244+
});
245+
225246
it('should run commands serially', async () => {
226247
const f = fileSync().name;
227248
let result = await runCommands(

packages/nx/src/executors/run-commands/run-commands.impl.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -231,23 +231,28 @@ export function interpolateArgsIntoCommand(
231231
>,
232232
forwardAllArgs: boolean
233233
): string {
234+
if (command.indexOf('{args.') > -1 && command.indexOf('{args}') > -1) {
235+
throw new Error(
236+
'Command should not contain both {args} and {args.*} values. Please choose one to use.'
237+
);
238+
}
234239
if (command.indexOf('{args.') > -1) {
235240
const regex = /{args\.([^}]+)}/g;
236241
return command.replace(regex, (_, group: string) =>
237242
opts.parsedArgs[group] !== undefined ? opts.parsedArgs[group] : ''
238243
);
244+
} else if (command.indexOf('{args}') > -1) {
245+
const regex = /{args}/g;
246+
const args = [
247+
...unknownOptionsToArgsArray(opts),
248+
...unparsedOptionsToArgsArray(opts),
249+
];
250+
const argsString = `${args.join(' ')} ${opts.args ?? ''}`;
251+
return command.replace(regex, argsString);
239252
} else if (forwardAllArgs) {
240253
let args = '';
241254
if (Object.keys(opts.unknownOptions ?? {}).length > 0) {
242-
const unknownOptionsArgs = Object.keys(opts.unknownOptions)
243-
.filter(
244-
(k) =>
245-
typeof opts.unknownOptions[k] !== 'object' &&
246-
opts.parsedArgs[k] === opts.unknownOptions[k]
247-
)
248-
.map((k) => `--${k}=${opts.unknownOptions[k]}`)
249-
.map(wrapArgIntoQuotesIfNeeded)
250-
.join(' ');
255+
const unknownOptionsArgs = unknownOptionsToArgsArray(opts).join(' ');
251256
if (unknownOptionsArgs) {
252257
args += ` ${unknownOptionsArgs}`;
253258
}
@@ -256,14 +261,9 @@ export function interpolateArgsIntoCommand(
256261
args += ` ${opts.args}`;
257262
}
258263
if (opts.__unparsed__?.length > 0) {
259-
const filteredParsedOptions = filterPropKeysFromUnParsedOptions(
260-
opts.__unparsed__,
261-
opts.parsedArgs
262-
);
264+
const filteredParsedOptions = unparsedOptionsToArgsArray(opts);
263265
if (filteredParsedOptions.length > 0) {
264-
args += ` ${filteredParsedOptions
265-
.map(wrapArgIntoQuotesIfNeeded)
266-
.join(' ')}`;
266+
args += ` ${filteredParsedOptions.join(' ')}`;
267267
}
268268
}
269269
return `${command}${args}`;
@@ -272,6 +272,46 @@ export function interpolateArgsIntoCommand(
272272
}
273273
}
274274

275+
function unknownOptionsToArgsArray(
276+
opts: Pick<
277+
NormalizedRunCommandsOptions,
278+
| 'args'
279+
| 'parsedArgs'
280+
| '__unparsed__'
281+
| 'unknownOptions'
282+
| 'unparsedCommandArgs'
283+
>
284+
) {
285+
return Object.keys(opts.unknownOptions ?? {})
286+
.filter(
287+
(k) =>
288+
typeof opts.unknownOptions[k] !== 'object' &&
289+
opts.parsedArgs[k] === opts.unknownOptions[k]
290+
)
291+
.map((k) => `--${k}=${opts.unknownOptions[k]}`)
292+
.map(wrapArgIntoQuotesIfNeeded);
293+
}
294+
295+
function unparsedOptionsToArgsArray(
296+
opts: Pick<
297+
NormalizedRunCommandsOptions,
298+
| 'args'
299+
| 'parsedArgs'
300+
| '__unparsed__'
301+
| 'unknownOptions'
302+
| 'unparsedCommandArgs'
303+
>
304+
) {
305+
const filteredParsedOptions = filterPropKeysFromUnParsedOptions(
306+
opts.__unparsed__,
307+
opts.parsedArgs
308+
);
309+
if (filteredParsedOptions.length > 0) {
310+
return filteredParsedOptions.map(wrapArgIntoQuotesIfNeeded);
311+
}
312+
return [];
313+
}
314+
275315
function parseArgs(
276316
unparsedCommandArgs: { [k: string]: string },
277317
unknownOptions: { [k: string]: string },

0 commit comments

Comments
 (0)