Skip to content

Commit

Permalink
Add new --matrix option to multiply commands
Browse files Browse the repository at this point in the history
  • Loading branch information
cdrini committed Jan 16, 2025
1 parent 7f3efb2 commit f656f5d
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 0 deletions.
8 changes: 8 additions & 0 deletions bin/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ const program = yargs(hideBin(process.argv))
type: 'boolean',
default: defaults.timings,
},
matrix: {
describe:
'Run many commands as a matrix using space-separated parameters. ' +
'E.g. concurrently --matrix "a b c" --matrix "1 2 3" "echo {1}{2}"',
type: 'string',
array: true,
},
'passthrough-arguments': {
alias: 'P',
describe:
Expand Down Expand Up @@ -253,6 +260,7 @@ concurrently(
timestampFormat: args.timestampFormat,
timings: args.timings,
teardown: args.teardown,
matrices: args.matrix?.map((matrix) => matrix.split(' ')),
additionalArguments: args.passthroughArguments ? additionalArguments : undefined,
},
).result.then(
Expand Down
116 changes: 116 additions & 0 deletions src/command-parser/expand-matrices.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { CommandInfo } from '../command';
import { combinations, ExpandMatrices } from './expand-matrices';

const createCommandInfo = (command: string): CommandInfo => ({
command,
name: '',
});

describe('ExpandMatrices', () => {
it('should replace placeholders with matrix values', () => {
const matrices = [
['a', 'b'],
['1', '2'],
];
const expandMatrices = new ExpandMatrices(matrices);
const commandInfo = createCommandInfo('echo {1} and {2}');

const result = expandMatrices.parse(commandInfo);

expect(result).toEqual([
{ command: 'echo a and 1', name: '' },
{ command: 'echo a and 2', name: '' },
{ command: 'echo b and 1', name: '' },
{ command: 'echo b and 2', name: '' },
]);
});

it('should handle escaped placeholders', () => {
const matrices = [['a', 'b']];
const expandMatrices = new ExpandMatrices(matrices);
const commandInfo = createCommandInfo('echo \\{1} and {1}');

const result = expandMatrices.parse(commandInfo);

expect(result).toEqual([
{ command: 'echo {1} and a', name: '' },
{ command: 'echo {1} and b', name: '' },
]);
});

it('should replace placeholders with empty string if index is out of bounds', () => {
const matrices = [['a']];
const expandMatrices = new ExpandMatrices(matrices);
const commandInfo = createCommandInfo('echo {2}');

const result = expandMatrices.parse(commandInfo);

expect(result).toEqual([{ command: 'echo ', name: '' }]);
});
});

describe('combinations', () => {
it('should return all possible combinations of the given dimensions', () => {
const dimensions = [
['a', 'b'],
['1', '2'],
];

const result = combinations(dimensions);

expect(result).toEqual([
['a', '1'],
['a', '2'],
['b', '1'],
['b', '2'],
]);
});

it('should handle single dimension', () => {
const dimensions = [['a', 'b']];

const result = combinations(dimensions);

expect(result).toEqual([['a'], ['b']]);
});

it('should handle empty dimensions', () => {
const dimensions: string[][] = [];

const result = combinations(dimensions);

expect(result).toEqual([[]]);
});

it('should handle dimensions with empty arrays', () => {
const dimensions = [['a', 'b'], []];

const result = combinations(dimensions);

expect(result).toEqual([]);
});

it('should handle dimensions with multiple empty arrays', () => {
const dimensions = [[], []];

const result = combinations(dimensions);

expect(result).toEqual([]);
});

it('should handle dimensions with some empty arrays', () => {
const dimensions = [['a', 'b'], [], ['x', 'y']];

const result = combinations(dimensions);

expect(result).toEqual([]);
});

it('should handle dimensions with all empty arrays', () => {
const dimensions = [[], [], []];

const result = combinations(dimensions);

expect(result).toEqual([]);
});
});
62 changes: 62 additions & 0 deletions src/command-parser/expand-matrices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { quote } from 'shell-quote';

import { CommandInfo } from '../command';
import { CommandParser } from './command-parser';

/**
* Replace placeholders with new commands for each combination of matrices.
*/
export class ExpandMatrices implements CommandParser {
private _bindings: string[][];

constructor(private readonly matrices: readonly string[][]) {
this.matrices = matrices;
this._bindings = combinations(matrices);
}

parse(commandInfo: CommandInfo) {
return this._bindings.map((binding) => this.replacePlaceholders(commandInfo, binding));
}

private replacePlaceholders(commandInfo: CommandInfo, binding: string[]): CommandInfo {
const command = commandInfo.command.replace(
/\\?\{([0-9]*)?\}/g,
(match, placeholderTarget) => {
// Don't replace the placeholder if it is escaped by a backslash.
if (match.startsWith('\\')) {
return match.slice(1);
}

let index = 0;
if (placeholderTarget && !isNaN(placeholderTarget)) {
index = parseInt(placeholderTarget, 10) - 1;
}

// Replace numeric placeholder if value exists in additional arguments.
if (index < binding.length) {
return quote([binding[index]]);
}

// Replace placeholder with empty string
// if value doesn't exist in additional arguments.
return '';
},
);

return { ...commandInfo, command };
}
}

/**
* Returns all possible combinations of the given dimensions.
*/
export function combinations(dimensions: readonly string[][]): string[][] {
return dimensions.reduce(
(acc, dimension) => {
return acc.flatMap((accItem) =>
dimension.map((dimensionItem) => accItem.concat(dimensionItem)),
);
},
[[]] as string[][],
);
}
10 changes: 10 additions & 0 deletions src/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from './command';
import { CommandParser } from './command-parser/command-parser';
import { ExpandArguments } from './command-parser/expand-arguments';
import { ExpandMatrices } from './command-parser/expand-matrices';
import { ExpandShortcut } from './command-parser/expand-shortcut';
import { ExpandWildcard } from './command-parser/expand-wildcard';
import { StripQuotes } from './command-parser/strip-quotes';
Expand Down Expand Up @@ -147,6 +148,11 @@ export type ConcurrentlyOptions = {
*/
killSignal?: string;

/**
* Specify variables which will spawn multiple commands.
*/
matrices?: readonly string[][];

/**
* List of additional arguments passed that will get replaced in each command.
* If not defined, no argument replacing will happen.
Expand Down Expand Up @@ -179,6 +185,10 @@ export function concurrently(
new ExpandWildcard(),
];

if (options.matrices?.length) {
commandParsers.push(new ExpandMatrices(options.matrices));
}

if (options.additionalArguments) {
commandParsers.push(new ExpandArguments(options.additionalArguments));
}
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export type ConcurrentlyOptions = Omit<BaseConcurrentlyOptions, 'abortSignal' |
* If not defined, no argument replacing will happen.
*/
additionalArguments?: string[];

/**
* This command should be run multiple times, for each of the provided matrices.
*/
matrices?: readonly string[][];
};

export function concurrently(
Expand Down Expand Up @@ -171,6 +176,7 @@ export function concurrently(
new Teardown({ logger, spawn: options.spawn, commands: options.teardown || [] }),
],
prefixColors: options.prefixColors || [],
matrices: options.matrices,
additionalArguments: options.additionalArguments,
});
}
Expand Down

0 comments on commit f656f5d

Please sign in to comment.