Skip to content

Commit f656f5d

Browse files
committed
Add new --matrix option to multiply commands
1 parent 7f3efb2 commit f656f5d

File tree

5 files changed

+202
-0
lines changed

5 files changed

+202
-0
lines changed

bin/concurrently.ts

+8
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ const program = yargs(hideBin(process.argv))
9393
type: 'boolean',
9494
default: defaults.timings,
9595
},
96+
matrix: {
97+
describe:
98+
'Run many commands as a matrix using space-separated parameters. ' +
99+
'E.g. concurrently --matrix "a b c" --matrix "1 2 3" "echo {1}{2}"',
100+
type: 'string',
101+
array: true,
102+
},
96103
'passthrough-arguments': {
97104
alias: 'P',
98105
describe:
@@ -253,6 +260,7 @@ concurrently(
253260
timestampFormat: args.timestampFormat,
254261
timings: args.timings,
255262
teardown: args.teardown,
263+
matrices: args.matrix?.map((matrix) => matrix.split(' ')),
256264
additionalArguments: args.passthroughArguments ? additionalArguments : undefined,
257265
},
258266
).result.then(
+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { CommandInfo } from '../command';
2+
import { combinations, ExpandMatrices } from './expand-matrices';
3+
4+
const createCommandInfo = (command: string): CommandInfo => ({
5+
command,
6+
name: '',
7+
});
8+
9+
describe('ExpandMatrices', () => {
10+
it('should replace placeholders with matrix values', () => {
11+
const matrices = [
12+
['a', 'b'],
13+
['1', '2'],
14+
];
15+
const expandMatrices = new ExpandMatrices(matrices);
16+
const commandInfo = createCommandInfo('echo {1} and {2}');
17+
18+
const result = expandMatrices.parse(commandInfo);
19+
20+
expect(result).toEqual([
21+
{ command: 'echo a and 1', name: '' },
22+
{ command: 'echo a and 2', name: '' },
23+
{ command: 'echo b and 1', name: '' },
24+
{ command: 'echo b and 2', name: '' },
25+
]);
26+
});
27+
28+
it('should handle escaped placeholders', () => {
29+
const matrices = [['a', 'b']];
30+
const expandMatrices = new ExpandMatrices(matrices);
31+
const commandInfo = createCommandInfo('echo \\{1} and {1}');
32+
33+
const result = expandMatrices.parse(commandInfo);
34+
35+
expect(result).toEqual([
36+
{ command: 'echo {1} and a', name: '' },
37+
{ command: 'echo {1} and b', name: '' },
38+
]);
39+
});
40+
41+
it('should replace placeholders with empty string if index is out of bounds', () => {
42+
const matrices = [['a']];
43+
const expandMatrices = new ExpandMatrices(matrices);
44+
const commandInfo = createCommandInfo('echo {2}');
45+
46+
const result = expandMatrices.parse(commandInfo);
47+
48+
expect(result).toEqual([{ command: 'echo ', name: '' }]);
49+
});
50+
});
51+
52+
describe('combinations', () => {
53+
it('should return all possible combinations of the given dimensions', () => {
54+
const dimensions = [
55+
['a', 'b'],
56+
['1', '2'],
57+
];
58+
59+
const result = combinations(dimensions);
60+
61+
expect(result).toEqual([
62+
['a', '1'],
63+
['a', '2'],
64+
['b', '1'],
65+
['b', '2'],
66+
]);
67+
});
68+
69+
it('should handle single dimension', () => {
70+
const dimensions = [['a', 'b']];
71+
72+
const result = combinations(dimensions);
73+
74+
expect(result).toEqual([['a'], ['b']]);
75+
});
76+
77+
it('should handle empty dimensions', () => {
78+
const dimensions: string[][] = [];
79+
80+
const result = combinations(dimensions);
81+
82+
expect(result).toEqual([[]]);
83+
});
84+
85+
it('should handle dimensions with empty arrays', () => {
86+
const dimensions = [['a', 'b'], []];
87+
88+
const result = combinations(dimensions);
89+
90+
expect(result).toEqual([]);
91+
});
92+
93+
it('should handle dimensions with multiple empty arrays', () => {
94+
const dimensions = [[], []];
95+
96+
const result = combinations(dimensions);
97+
98+
expect(result).toEqual([]);
99+
});
100+
101+
it('should handle dimensions with some empty arrays', () => {
102+
const dimensions = [['a', 'b'], [], ['x', 'y']];
103+
104+
const result = combinations(dimensions);
105+
106+
expect(result).toEqual([]);
107+
});
108+
109+
it('should handle dimensions with all empty arrays', () => {
110+
const dimensions = [[], [], []];
111+
112+
const result = combinations(dimensions);
113+
114+
expect(result).toEqual([]);
115+
});
116+
});

src/command-parser/expand-matrices.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { quote } from 'shell-quote';
2+
3+
import { CommandInfo } from '../command';
4+
import { CommandParser } from './command-parser';
5+
6+
/**
7+
* Replace placeholders with new commands for each combination of matrices.
8+
*/
9+
export class ExpandMatrices implements CommandParser {
10+
private _bindings: string[][];
11+
12+
constructor(private readonly matrices: readonly string[][]) {
13+
this.matrices = matrices;
14+
this._bindings = combinations(matrices);
15+
}
16+
17+
parse(commandInfo: CommandInfo) {
18+
return this._bindings.map((binding) => this.replacePlaceholders(commandInfo, binding));
19+
}
20+
21+
private replacePlaceholders(commandInfo: CommandInfo, binding: string[]): CommandInfo {
22+
const command = commandInfo.command.replace(
23+
/\\?\{([0-9]*)?\}/g,
24+
(match, placeholderTarget) => {
25+
// Don't replace the placeholder if it is escaped by a backslash.
26+
if (match.startsWith('\\')) {
27+
return match.slice(1);
28+
}
29+
30+
let index = 0;
31+
if (placeholderTarget && !isNaN(placeholderTarget)) {
32+
index = parseInt(placeholderTarget, 10) - 1;
33+
}
34+
35+
// Replace numeric placeholder if value exists in additional arguments.
36+
if (index < binding.length) {
37+
return quote([binding[index]]);
38+
}
39+
40+
// Replace placeholder with empty string
41+
// if value doesn't exist in additional arguments.
42+
return '';
43+
},
44+
);
45+
46+
return { ...commandInfo, command };
47+
}
48+
}
49+
50+
/**
51+
* Returns all possible combinations of the given dimensions.
52+
*/
53+
export function combinations(dimensions: readonly string[][]): string[][] {
54+
return dimensions.reduce(
55+
(acc, dimension) => {
56+
return acc.flatMap((accItem) =>
57+
dimension.map((dimensionItem) => accItem.concat(dimensionItem)),
58+
);
59+
},
60+
[[]] as string[][],
61+
);
62+
}

src/concurrently.ts

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from './command';
1515
import { CommandParser } from './command-parser/command-parser';
1616
import { ExpandArguments } from './command-parser/expand-arguments';
17+
import { ExpandMatrices } from './command-parser/expand-matrices';
1718
import { ExpandShortcut } from './command-parser/expand-shortcut';
1819
import { ExpandWildcard } from './command-parser/expand-wildcard';
1920
import { StripQuotes } from './command-parser/strip-quotes';
@@ -147,6 +148,11 @@ export type ConcurrentlyOptions = {
147148
*/
148149
killSignal?: string;
149150

151+
/**
152+
* Specify variables which will spawn multiple commands.
153+
*/
154+
matrices?: readonly string[][];
155+
150156
/**
151157
* List of additional arguments passed that will get replaced in each command.
152158
* If not defined, no argument replacing will happen.
@@ -179,6 +185,10 @@ export function concurrently(
179185
new ExpandWildcard(),
180186
];
181187

188+
if (options.matrices?.length) {
189+
commandParsers.push(new ExpandMatrices(options.matrices));
190+
}
191+
182192
if (options.additionalArguments) {
183193
commandParsers.push(new ExpandArguments(options.additionalArguments));
184194
}

src/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ export type ConcurrentlyOptions = Omit<BaseConcurrentlyOptions, 'abortSignal' |
103103
* If not defined, no argument replacing will happen.
104104
*/
105105
additionalArguments?: string[];
106+
107+
/**
108+
* This command should be run multiple times, for each of the provided matrices.
109+
*/
110+
matrices?: readonly string[][];
106111
};
107112

108113
export function concurrently(
@@ -171,6 +176,7 @@ export function concurrently(
171176
new Teardown({ logger, spawn: options.spawn, commands: options.teardown || [] }),
172177
],
173178
prefixColors: options.prefixColors || [],
179+
matrices: options.matrices,
174180
additionalArguments: options.additionalArguments,
175181
});
176182
}

0 commit comments

Comments
 (0)