From 12aa04d1b5f88f3f3334b8c733abd34411d2c414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=B7=9D=E6=8B=93=E9=A6=AC?= Date: Fri, 17 Apr 2026 18:45:22 +0900 Subject: [PATCH 1/4] test: add CLI command normalization specs --- bin/normalize-cli-command.spec.ts | 44 +++++++++++++++++++++++++++++++ bin/normalize-cli-command.ts | 7 +++++ 2 files changed, 51 insertions(+) create mode 100644 bin/normalize-cli-command.spec.ts create mode 100644 bin/normalize-cli-command.ts diff --git a/bin/normalize-cli-command.spec.ts b/bin/normalize-cli-command.spec.ts new file mode 100644 index 00000000..7ec64657 --- /dev/null +++ b/bin/normalize-cli-command.spec.ts @@ -0,0 +1,44 @@ +import { expect, it } from 'vitest'; + +import { normalizeCliCommand } from './normalize-cli-command.js'; + +it('CLIラッパーとして付いた外側のクォートを外す', () => { + expect(normalizeCliCommand('"echo foo"')).toBe('echo foo'); +}); + +it('CLIラッパーとして付いた外側のシングルクォートを外す', () => { + expect(normalizeCliCommand("'echo foo'")).toBe('echo foo'); +}); + +it('単一トークン全体を包むクォートは外す', () => { + expect(normalizeCliCommand('"echo"')).toBe('echo'); + expect(normalizeCliCommand("'echo'")).toBe('echo'); +}); + +it('正しいシェルコマンド内のクォートは保持する', () => { + expect(normalizeCliCommand('"/usr/local/bin/mytool" --flag "some value"')).toBe( + '"/usr/local/bin/mytool" --flag "some value"', + ); +}); + +it('複数のクォートセットを含む正しいシェルコマンドは保持する', () => { + expect( + normalizeCliCommand('"/usr/local/bin/mytool" --flag "some value" --other "last arg"'), + ).toBe('"/usr/local/bin/mytool" --flag "some value" --other "last arg"'); +}); + +it('正しいシェルコマンド内のシングルクォートは保持する', () => { + expect(normalizeCliCommand("'printf' '%s %s' foo bar")).toBe("'printf' '%s %s' foo bar"); +}); + +it('クォートされていない入力はそのまま返す', () => { + expect(normalizeCliCommand('echo foo')).toBe('echo foo'); +}); + +it('空文字はそのまま返す', () => { + expect(normalizeCliCommand('')).toBe(''); +}); + +it('判定が曖昧な入力はそのままにする', () => { + 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..45907246 --- /dev/null +++ b/bin/normalize-cli-command.ts @@ -0,0 +1,7 @@ +export function normalizeCliCommand(command: string): string { + if (/^".+?"$/.test(command) || /^'.+?'$/.test(command)) { + return command.slice(1, command.length - 1); + } + + return command; +} From 12e086209ef701dd41404e599e1c1965af38df99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=B7=9D=E6=8B=93=E9=A6=AC?= Date: Fri, 17 Apr 2026 21:32:45 +0900 Subject: [PATCH 2/4] refactor: stop stripping quotes in concurrently library --- bin/normalize-cli-command.spec.ts | 8 ++++++++ lib/concurrently.spec.ts | 23 +++++++++++++++++++---- lib/concurrently.ts | 7 +------ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bin/normalize-cli-command.spec.ts b/bin/normalize-cli-command.spec.ts index 7ec64657..41a76a52 100644 --- a/bin/normalize-cli-command.spec.ts +++ b/bin/normalize-cli-command.spec.ts @@ -42,3 +42,11 @@ it('空文字はそのまま返す', () => { it('判定が曖昧な入力はそのままにする', () => { expect(normalizeCliCommand('"echo foo')).toBe('"echo foo'); }); + +it('閉じていないシングルクォートを含む入力はそのままにする', () => { + expect(normalizeCliCommand("echo foo'")).toBe("echo foo'"); +}); + +it('開始と終了のクォート種類が一致しない入力はそのままにする', () => { + expect(normalizeCliCommand('"echo foo\'')).toBe('"echo foo\''); +}); diff --git a/lib/concurrently.spec.ts b/lib/concurrently.spec.ts index 621cc1ba..681fb14c 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('ライブラリ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('ライブラリ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)); From 203caae3df88291fc601daef8725e2634cd8f1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=B7=9D=E6=8B=93=E9=A6=AC?= Date: Fri, 17 Apr 2026 22:35:04 +0900 Subject: [PATCH 3/4] fix: move quote normalization to CLI entrypoint --- bin/index.spec.ts | 9 +++++++++ bin/index.ts | 3 ++- bin/normalize-cli-command.ts | 17 ++++++++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/bin/index.spec.ts b/bin/index.spec.ts index 4536a9d1..fe6fe1a2 100644 --- a/bin/index.spec.ts +++ b/bin/index.spec.ts @@ -146,6 +146,15 @@ describe('exiting conditions', () => { expect(exit.code).toBe(0); }); + it('CLIラッパーとして付いた外側のクォートを外して実行する', 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 f8220167..08084db2 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 } 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.ts b/bin/normalize-cli-command.ts index 45907246..d3cea9e8 100644 --- a/bin/normalize-cli-command.ts +++ b/bin/normalize-cli-command.ts @@ -1,7 +1,18 @@ export function normalizeCliCommand(command: string): string { - if (/^".+?"$/.test(command) || /^'.+?'$/.test(command)) { - return command.slice(1, command.length - 1); + if (command.length < 2) { + return command; } - 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; } From 4213f6689793833ed3140381d500f6d343999f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=B7=9D=E6=8B=93=E9=A6=AC?= Date: Fri, 17 Apr 2026 22:43:22 +0900 Subject: [PATCH 4/4] test: rename new specs to English --- bin/index.spec.ts | 2 +- bin/normalize-cli-command.spec.ts | 22 +++++++++++----------- lib/concurrently.spec.ts | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bin/index.spec.ts b/bin/index.spec.ts index fe6fe1a2..87061b60 100644 --- a/bin/index.spec.ts +++ b/bin/index.spec.ts @@ -146,7 +146,7 @@ describe('exiting conditions', () => { expect(exit.code).toBe(0); }); - it('CLIラッパーとして付いた外側のクォートを外して実行する', async () => { + 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; diff --git a/bin/normalize-cli-command.spec.ts b/bin/normalize-cli-command.spec.ts index 41a76a52..aa53407b 100644 --- a/bin/normalize-cli-command.spec.ts +++ b/bin/normalize-cli-command.spec.ts @@ -2,51 +2,51 @@ import { expect, it } from 'vitest'; import { normalizeCliCommand } from './normalize-cli-command.js'; -it('CLIラッパーとして付いた外側のクォートを外す', () => { +it('strips outer CLI wrapper double quotes', () => { expect(normalizeCliCommand('"echo foo"')).toBe('echo foo'); }); -it('CLIラッパーとして付いた外側のシングルクォートを外す', () => { +it('strips outer CLI wrapper single quotes', () => { expect(normalizeCliCommand("'echo foo'")).toBe('echo foo'); }); -it('単一トークン全体を包むクォートは外す', () => { +it('strips quotes around a single wrapped token', () => { expect(normalizeCliCommand('"echo"')).toBe('echo'); expect(normalizeCliCommand("'echo'")).toBe('echo'); }); -it('正しいシェルコマンド内のクォートは保持する', () => { +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('複数のクォートセットを含む正しいシェルコマンドは保持する', () => { +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('正しいシェルコマンド内のシングルクォートは保持する', () => { +it('preserves single quotes in well-formed shell commands', () => { expect(normalizeCliCommand("'printf' '%s %s' foo bar")).toBe("'printf' '%s %s' foo bar"); }); -it('クォートされていない入力はそのまま返す', () => { +it('returns unquoted input unchanged', () => { expect(normalizeCliCommand('echo foo')).toBe('echo foo'); }); -it('空文字はそのまま返す', () => { +it('returns an empty string unchanged', () => { expect(normalizeCliCommand('')).toBe(''); }); -it('判定が曖昧な入力はそのままにする', () => { +it('leaves ambiguous input unchanged', () => { expect(normalizeCliCommand('"echo foo')).toBe('"echo foo'); }); -it('閉じていないシングルクォートを含む入力はそのままにする', () => { +it('leaves input with an unclosed single quote unchanged', () => { expect(normalizeCliCommand("echo foo'")).toBe("echo foo'"); }); -it('開始と終了のクォート種類が一致しない入力はそのままにする', () => { +it('leaves input with mismatched quote types unchanged', () => { expect(normalizeCliCommand('"echo foo\'')).toBe('"echo foo\''); }); diff --git a/lib/concurrently.spec.ts b/lib/concurrently.spec.ts index 681fb14c..cd237b40 100644 --- a/lib/concurrently.spec.ts +++ b/lib/concurrently.spec.ts @@ -136,7 +136,7 @@ it('does not spawn further commands on abort signal aborted', () => { expect(spawn).toHaveBeenCalledTimes(1); }); -it('ライブラリAPIでは正しいシェルコマンド内のクォートを保持する', () => { +it('preserves quotes in well-formed shell commands in the library API', () => { create(['"/usr/local/bin/mytool" --flag "some value"']); controllers.forEach((controller) => { @@ -149,7 +149,7 @@ it('ライブラリAPIでは正しいシェルコマンド内のクォートを }); }); -it('ライブラリAPIでは複数のクォートセットを含むコマンドをそのまま渡す', () => { +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) => {