Skip to content

Commit

Permalink
Merge pull request #551 from oclif/mdonnalley/preserve-quotes
Browse files Browse the repository at this point in the history
fix: preserve quotes in inputs to runCommand
  • Loading branch information
mdonnalley authored May 22, 2024
2 parents 72156cc + 2fad42a commit 31d0aba
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 15 deletions.
18 changes: 17 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ function makeLoadOptions(loadOpts?: Interfaces.LoadOptions): Interfaces.LoadOpti
return loadOpts ?? {root: findRoot()}
}

/**
* Split a string into an array of strings, preserving quoted substrings
*
* @example
* splitString('foo bar --name "foo"') // ['foo bar', '--name', 'foo']
* splitString('foo bar --name "foo bar"') // ['foo bar', '--name', 'foo bar']
* splitString('foo bar --name="foo bar"') // ['foo bar', '--name=foo bar']
* splitString('foo bar --name=foo bar') // ['foo bar', '--name=foo', 'bar']
*
* @param str input string
* @returns array of strings with quotes removed
*/
function splitString(str: string): string[] {
return (str.match(/(?:[^\s"]+|"[^"]*")+/g) ?? []).map((s) => s.replaceAll(/^"|"$|(?<==)"/g, ''))
}

/**
* Capture the stderr and stdout output of a function
* @param fn async function to run
Expand Down Expand Up @@ -138,7 +154,7 @@ export async function runCommand<T>(
captureOpts?: CaptureOptions,
): Promise<CaptureResult<T>> {
const loadOptions = makeLoadOptions(loadOpts)
const argsArray = (Array.isArray(args) ? args : [args]).join(' ').split(' ')
const argsArray = splitString((Array.isArray(args) ? args : [args]).join(' '))

const [id, ...rest] = argsArray
const finalArgs = id === '.' ? rest : argsArray
Expand Down
115 changes: 101 additions & 14 deletions test/run-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ describe('runCommand', () => {
expect(result?.name).to.equal('foo')
})

it('should handle single string', async () => {
const {result, stdout} = await runCommand<{name: string}>('foo:bar --name=foo', {root})
expect(stdout).to.equal('hello foo!\n')
expect(result?.name).to.equal('foo')
})

it('should handle expected exit codes', async () => {
const {error, stdout} = await runCommand(['exit', '--code=101'], {root})
expect(stdout).to.equal('exiting with code 101\n')
Expand All @@ -50,15 +44,108 @@ describe('runCommand', () => {
const {stdout} = await runCommand(['--help'])
expect(stdout).to.include('$ @oclif/test [COMMAND]')
})
})

describe('single command cli', () => {
// eslint-disable-next-line unicorn/prefer-module
const root = join(__dirname, 'fixtures/single')
describe('single command cli', () => {
// eslint-disable-next-line unicorn/prefer-module
const root = join(__dirname, 'fixtures/single')

it('should run a single command cli', async () => {
const {result, stdout} = await runCommand<{name: string}>(['.'], {root})
expect(stdout).to.equal('hello world!\n')
expect(result?.name).to.equal('world')
it('should run a single command cli', async () => {
const {result, stdout} = await runCommand<{name: string}>(['.'], {root})
expect(stdout).to.equal('hello world!\n')
expect(result?.name).to.equal('world')
})
})

const cases = [
{
description: 'should handle single string',
expected: 'foo',
input: 'foo%sbar --name foo',
},
{
description: 'should handle an array of strings',
expected: 'foo',
input: ['foo%sbar', '--name', 'foo'],
},
{
description: 'should handle a string with =',
expected: 'foo',
input: 'foo%sbar --name=foo',
},
{
description: 'should handle an array of strings with =',
expected: 'foo',
input: ['foo%sbar', '--name=foo'],
},
{
description: 'should handle a string with quotes',
expected: 'foo',
input: 'foo%sbar --name "foo"',
},
{
description: 'should handle an array of strings with quotes',
expected: 'foo',
input: ['foo%sbar', '--name', '"foo"'],
},
{
description: 'should handle a string with quotes and with =',
expected: 'foo',
input: 'foo%sbar --name="foo"',
},
{
description: 'should handle an array of strings with quotes and with =',
expected: 'foo',
input: ['foo%sbar', '--name="foo"'],
},
{
description: 'should handle a string with spaces in quotes',
expected: 'foo bar',
input: 'foo%sbar --name "foo bar"',
},
{
description: 'should handle an array of strings with spaces in quotes',
expected: 'foo bar',
input: ['foo%sbar', '--name', '"foo bar"'],
},
{
description: 'should handle a string with spaces in quotes and with =',
expected: 'foo bar',
input: 'foo%sbar --name="foo bar"',
},
{
description: 'should handle an array of strings with spaces in quotes and with =',
expected: 'foo bar',
input: ['foo%sbar', '--name="foo bar"'],
},
]

const makeTestCases = (separator: string) =>
cases.map(({description, expected, input}) => ({
description: description.replace('%s', separator),
expected,
input: Array.isArray(input) ? input.map((i) => i.replace('%s', separator)) : input.replace('%s', separator),
}))

describe('arg input (colon separator)', () => {
const testCases = makeTestCases(':')

for (const {description, expected, input} of testCases) {
it(description, async () => {
const {result, stdout} = await runCommand<{name: string}>(input, {root})
expect(stdout).to.equal(`hello ${expected}!\n`)
expect(result?.name).to.equal(expected)
})
}
})

describe('arg input (space separator)', () => {
const testCases = makeTestCases(' ')
for (const {description, expected, input} of testCases) {
it(description, async () => {
const {result, stdout} = await runCommand<{name: string}>(input, {root})
expect(stdout).to.equal(`hello ${expected}!\n`)
expect(result?.name).to.equal(expected)
})
}
})
})

0 comments on commit 31d0aba

Please sign in to comment.