diff --git a/bin/index.spec.ts b/bin/index.spec.ts index 4536a9d1..87061b60 100644 --- a/bin/index.spec.ts +++ b/bin/index.spec.ts @@ -146,6 +146,15 @@ describe('exiting conditions', () => { expect(exit.code).toBe(0); }); + it('strips outer CLI wrapper quotes before running a command', async () => { + const child = run('"echo foo"'); + const lines = await child.getLogLines(); + const exit = await child.exit; + + expect(lines).toContainEqual(expect.stringContaining('foo')); + expect(exit.code).toBe(0); + }); + it('is of failure by default when one of the command fails', async () => { const exit = await run('"echo foo" "exit 1"').exit; diff --git a/bin/index.ts b/bin/index.ts index 19ba5d28..672ae1b9 100755 --- a/bin/index.ts +++ b/bin/index.ts @@ -8,6 +8,7 @@ import { assertDeprecated } from '../lib/assert.js'; import * as defaults from '../lib/defaults.js'; import { concurrently } from '../lib/index.js'; import { castArray, splitOutsideParens } from '../lib/utils.js'; +import { normalizeCliCommand } from './normalize-cli-command.js'; import { readPackageJson } from './read-package-json.js'; const version = String(readPackageJson().version); @@ -238,7 +239,7 @@ if (!commands.length) { concurrently( commands.map((command, index) => ({ - command: String(command), + command: normalizeCliCommand(String(command)), name: names[index], })), { diff --git a/bin/normalize-cli-command.spec.ts b/bin/normalize-cli-command.spec.ts new file mode 100644 index 00000000..aa53407b --- /dev/null +++ b/bin/normalize-cli-command.spec.ts @@ -0,0 +1,52 @@ +import { expect, it } from 'vitest'; + +import { normalizeCliCommand } from './normalize-cli-command.js'; + +it('strips outer CLI wrapper double quotes', () => { + expect(normalizeCliCommand('"echo foo"')).toBe('echo foo'); +}); + +it('strips outer CLI wrapper single quotes', () => { + expect(normalizeCliCommand("'echo foo'")).toBe('echo foo'); +}); + +it('strips quotes around a single wrapped token', () => { + expect(normalizeCliCommand('"echo"')).toBe('echo'); + expect(normalizeCliCommand("'echo'")).toBe('echo'); +}); + +it('preserves quotes in well-formed shell commands', () => { + expect(normalizeCliCommand('"/usr/local/bin/mytool" --flag "some value"')).toBe( + '"/usr/local/bin/mytool" --flag "some value"', + ); +}); + +it('preserves well-formed shell commands with multiple quote sets', () => { + expect( + normalizeCliCommand('"/usr/local/bin/mytool" --flag "some value" --other "last arg"'), + ).toBe('"/usr/local/bin/mytool" --flag "some value" --other "last arg"'); +}); + +it('preserves single quotes in well-formed shell commands', () => { + expect(normalizeCliCommand("'printf' '%s %s' foo bar")).toBe("'printf' '%s %s' foo bar"); +}); + +it('returns unquoted input unchanged', () => { + expect(normalizeCliCommand('echo foo')).toBe('echo foo'); +}); + +it('returns an empty string unchanged', () => { + expect(normalizeCliCommand('')).toBe(''); +}); + +it('leaves ambiguous input unchanged', () => { + expect(normalizeCliCommand('"echo foo')).toBe('"echo foo'); +}); + +it('leaves input with an unclosed single quote unchanged', () => { + expect(normalizeCliCommand("echo foo'")).toBe("echo foo'"); +}); + +it('leaves input with mismatched quote types unchanged', () => { + expect(normalizeCliCommand('"echo foo\'')).toBe('"echo foo\''); +}); diff --git a/bin/normalize-cli-command.ts b/bin/normalize-cli-command.ts new file mode 100644 index 00000000..d3cea9e8 --- /dev/null +++ b/bin/normalize-cli-command.ts @@ -0,0 +1,18 @@ +export function normalizeCliCommand(command: string): string { + if (command.length < 2) { + return command; + } + + const quote = command[0]; + const last = command[command.length - 1]; + if ((quote !== '"' && quote !== "'") || last !== quote) { + return command; + } + + const inner = command.slice(1, -1); + if (inner.includes(quote)) { + return command; + } + + return inner; +} diff --git a/lib/concurrently.spec.ts b/lib/concurrently.spec.ts index 621cc1ba..cd237b40 100644 --- a/lib/concurrently.spec.ts +++ b/lib/concurrently.spec.ts @@ -136,13 +136,28 @@ it('does not spawn further commands on abort signal aborted', () => { expect(spawn).toHaveBeenCalledTimes(1); }); -it('runs controllers with the commands', () => { - create(['echo', '"echo wrapped"']); +it('preserves quotes in well-formed shell commands in the library API', () => { + create(['"/usr/local/bin/mytool" --flag "some value"']); controllers.forEach((controller) => { expect(controller.handle).toHaveBeenCalledWith([ - expect.objectContaining({ command: 'echo', index: 0 }), - expect.objectContaining({ command: 'echo wrapped', index: 1 }), + expect.objectContaining({ + command: '"/usr/local/bin/mytool" --flag "some value"', + index: 0, + }), + ]); + }); +}); + +it('passes commands with multiple quote sets through unchanged in the library API', () => { + create(['"/usr/local/bin/mytool" --flag "some value" --other "last arg"']); + + controllers.forEach((controller) => { + expect(controller.handle).toHaveBeenCalledWith([ + expect.objectContaining({ + command: '"/usr/local/bin/mytool" --flag "some value" --other "last arg"', + index: 0, + }), ]); }); }); diff --git a/lib/concurrently.ts b/lib/concurrently.ts index c7b3ee38..e33c8fee 100644 --- a/lib/concurrently.ts +++ b/lib/concurrently.ts @@ -17,7 +17,6 @@ import { CommandParser } from './command-parser/command-parser.js'; import { ExpandArguments } from './command-parser/expand-arguments.js'; import { ExpandShortcut } from './command-parser/expand-shortcut.js'; import { ExpandWildcard } from './command-parser/expand-wildcard.js'; -import { StripQuotes } from './command-parser/strip-quotes.js'; import { CompletionListener, SuccessCondition } from './completion-listener.js'; import { FlowController } from './flow-control/flow-controller.js'; import { Logger } from './logger.js'; @@ -170,11 +169,7 @@ export function concurrently( const prefixColorSelector = new PrefixColorSelector(options.prefixColors || []); - const commandParsers: CommandParser[] = [ - new StripQuotes(), - new ExpandShortcut(), - new ExpandWildcard(), - ]; + const commandParsers: CommandParser[] = [new ExpandShortcut(), new ExpandWildcard()]; if (options.additionalArguments) { commandParsers.push(new ExpandArguments(options.additionalArguments));