Skip to content

Commit 389c475

Browse files
committed
feat: split variables with quotes support
1 parent a557684 commit 389c475

File tree

10 files changed

+146
-21
lines changed

10 files changed

+146
-21
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"dependencies": {
1111
"arg": "^5.0.2",
1212
"lodash-es": "^4.17.21",
13+
"split-string": "^6.1.0",
1314
"string-width": "^7.2.0",
1415
"wrap-ansi": "^9.0.0"
1516
},

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/compose/compose-factory.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@ export const composeFactory =
5656
<T extends Command>(command: T, _settings: Partial<Settings> = {}) => {
5757
assert.command(command)
5858

59-
const settings: Settings = defaults({ ..._settings }, { help: true, split: ':' })
59+
// TODO: validate settings
60+
const settings: Settings = defaults({ ..._settings }, {
61+
// help: true,
62+
quotes: ['"', "'"],
63+
separator: ':',
64+
} satisfies Settings)
6065

6166
const intents = listIntent(extract(command))
6267

6368
return async (userContext = {}): Promise<void> => {
6469
// TODO: assert user context
65-
6670
const context = defaultContext(presetContext, userContext)
6771

6872
try {

src/help/inputs/input-choice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const usageInputChoice = (
3030
choices.length === 1
3131
? choices[0]
3232
: `(${join(
33-
map(choices, (choice) => `${properties.settings.split}${choice}`),
33+
map(choices, (choice) => `${properties.settings.separator}${choice}`),
3434
HELP_SIGN_OR,
3535
)})`
3636
}${repeat ? `${HELP_SIGN_REPEAT}` : ''}`,

src/help/inputs/input-string.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const usageInputString = (
2626
? undefined
2727
: `${enclose(variables)}=${
2828
repeat
29-
? `<string>[${properties.settings.split}<string>]${HELP_SIGN_REPEAT}`
29+
? `<string>[${properties.settings.separator}<string>]${HELP_SIGN_REPEAT}`
3030
: '<string>'
3131
}`,
3232
}

src/input/string/reducer.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,25 @@ describe('input/string/reducer', () => {
7676
})
7777
})
7878

79+
it('...', async () => {
80+
const { cmd, spy } = withRepeat()
81+
82+
await cmd({ argv: [], env: {} })
83+
84+
assert.deepEqual(spy.mock.calls[0][0], {
85+
string: ['picture'],
86+
})
87+
88+
await cmd({
89+
argv: ['--str', '!', '--str', 'ABC'],
90+
env: { STRING: '"Hello:Hello":"World"' },
91+
})
92+
93+
assert.deepEqual(spy.mock.calls[1][0], {
94+
string: ['!', 'ABC', 'Hello:Hello', 'World'],
95+
})
96+
})
97+
7998
it('...', async () => {
8099
const { cmd, spy } = withoutRepeat()
81100

src/types.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* eslint-disable typescript/no-unsafe-function-type */
2-
31
import type { FluentInterface, Model, SYMBOL_LOG, SYMBOL_STATE } from '@escapace/fluent'
42
import type $ from '@escapace/typelevel'
53
import type { InputBoolean } from './input/boolean/types'
@@ -97,9 +95,8 @@ export type UnionMerge<T extends object> =
9795
: never
9896

9997
export interface Settings {
100-
// TODO: custom help function
101-
help: boolean | Function
102-
split: string
98+
quotes: string[]
99+
separator: string
103100
}
104101

105102
interface Console {

src/utilities/normalize.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { flatMap, map, split } from 'lodash-es'
1+
import { map } from 'lodash-es'
2+
import split from 'split-string'
23
import type { InputChoiceProperties } from '../input/choice/types'
34
import type { InputStringProperties } from '../input/string/types'
45
import {
@@ -7,15 +8,54 @@ import {
78
type GenericVariable,
89
InputType,
910
type NormalizedStringValue,
11+
type Settings,
1012
} from '../types'
13+
import { trimWhitespace, unquote } from './unquote'
14+
15+
function trimArrayWhitespace(array: string[]): string[] {
16+
// Remove excess whitespace and newline strings from the beginning
17+
while (array.length > 0 && /^\p{Z}*$/u.test(array[0])) {
18+
array.shift()
19+
}
20+
21+
// Remove excess whitespace and newline strings from the end
22+
while (array.length > 0 && /^\p{Z}*$/u.test(array[array.length - 1])) {
23+
array.pop()
24+
}
25+
26+
return array
27+
}
28+
29+
const normalizeVariableString = (
30+
value: string,
31+
options: {
32+
repeat: boolean
33+
} & Pick<Settings, 'quotes' | 'separator'>,
34+
): string[] => {
35+
const lines = trimArrayWhitespace(value.split(/\r?\n/))
36+
37+
if (lines.length > 1) {
38+
return [lines.join('\n')]
39+
}
40+
41+
const string = lines[0] ?? ''
42+
43+
if (!options.repeat) {
44+
return [unquote(trimWhitespace(string), options.quotes)]
45+
}
46+
47+
return split(string, { quotes: options.quotes, separator: options.separator }).map((value) =>
48+
unquote(value, options.quotes),
49+
)
50+
}
1151

1252
export function normalizeString(
1353
previousValues: DeNormalizedStringValue[],
1454
properties: InputChoiceProperties | InputStringProperties,
1555
): NormalizedStringValue[] {
1656
const { repeat } = properties.model.state
1757

18-
return flatMap(previousValues, (previousValue) => {
58+
return map(previousValues, (previousValue) => {
1959
if (previousValue.type === InputType.Option) {
2060
return repeat
2161
? map(
@@ -27,15 +67,18 @@ export function normalizeString(
2767
)
2868
: (previousValue as GenericOption<string>)
2969
} /* (previousValue.type === InputType.Variable) */ else {
30-
return repeat
31-
? map(
32-
split(previousValue.value, properties.settings.split),
33-
(value): GenericVariable<string> => ({
34-
...previousValue,
35-
value,
36-
}),
37-
)
38-
: previousValue
70+
const asd: Array<GenericVariable<string>> = normalizeVariableString(previousValue.value, {
71+
quotes: properties.settings.quotes,
72+
repeat,
73+
separator: properties.settings.separator,
74+
}).map(
75+
(value): GenericVariable<string> => ({
76+
...previousValue,
77+
value,
78+
}),
79+
)
80+
81+
return asd
3982
}
40-
})
83+
}).flat()
4184
}

src/utilities/unquote.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import assert from 'node:assert'
2+
import { describe, it } from 'vitest'
3+
import { unquote } from './unquote'
4+
5+
describe('unquote', () => {
6+
it('should remove matching quotes from a string', () => {
7+
const quotes = ['"', "'"]
8+
const input = '\'"hello"\''
9+
const result = unquote(input, quotes)
10+
assert.strictEqual(result, 'hello')
11+
})
12+
13+
it('should handle nested quotes correctly', () => {
14+
const quotes = ['"', "'"]
15+
const input = '"\'hello\'"'
16+
const result = unquote(input, quotes)
17+
assert.strictEqual(result, 'hello')
18+
})
19+
20+
it('should ignore leading and trailing whitespace', () => {
21+
const quotes = ['"', "'"]
22+
const input = " ' hello ' "
23+
const result = unquote(input, quotes)
24+
assert.strictEqual(result, 'hello')
25+
})
26+
27+
it('should return the input string if no quotes match', () => {
28+
const quotes = ['"', "'"]
29+
const input = 'no quotes'
30+
const result = unquote(input, quotes)
31+
assert.strictEqual(result, 'no quotes')
32+
})
33+
34+
it('should handle empty strings gracefully', () => {
35+
const quotes = ['"', "'"]
36+
const input = ''
37+
const result = unquote(input, quotes)
38+
assert.strictEqual(result, '')
39+
})
40+
})

src/utilities/unquote.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const trimWhitespace = (value: string): string => value.replace(/^\p{Z}+|\p{Z}+$/gu, '')
2+
3+
export function unquote(input: string, quotes: string[]): string {
4+
let result = trimWhitespace(input)
5+
6+
// eslint-disable-next-line typescript/no-loop-func
7+
while (quotes.some((quote) => result.startsWith(quote) && result.endsWith(quote))) {
8+
result = trimWhitespace(result.slice(1, -1))
9+
}
10+
11+
return result
12+
}

0 commit comments

Comments
 (0)