-
-
Notifications
You must be signed in to change notification settings - Fork 236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new --matrix option to multiply commands #526
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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([]); | ||
}); | ||
}); |
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[][]) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't seem like it's read outside of the constructor, so for now let's not make it a field
Suggested change
|
||||||
this.matrices = matrices; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line isn't necessary, as the
Suggested change
|
||||||
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)) { | ||||||
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should an error be thrown if the placeholder isn't a number? |
||||||
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[][], | ||||||
); | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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. | ||
|
@@ -179,6 +185,10 @@ export function concurrently( | |
new ExpandWildcard(), | ||
]; | ||
|
||
if (options.matrices?.length) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like matrices have preference over passthrough arguments using the same It needs to be called out in the help and in the docs. |
||
commandParsers.push(new ExpandMatrices(options.matrices)); | ||
} | ||
|
||
if (options.additionalArguments) { | ||
commandParsers.push(new ExpandArguments(options.additionalArguments)); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please update this comment? It looks like it's assuming some context from elsewhere. |
||
*/ | ||
matrices?: readonly string[][]; | ||
}; | ||
|
||
export function concurrently( | ||
|
@@ -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, | ||
}); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the
_
is to signal that the field is private - if there isn't another field with the same name (such as a getter), let's not add that prefix.It can also be made
readonly
: