Skip to content

Commit c9c193b

Browse files
committed
feat: custom separators for variadic flags
Signed-off-by: Michael Molisani <[email protected]>
1 parent 60f760d commit c9c193b

File tree

6 files changed

+358
-29
lines changed

6 files changed

+358
-29
lines changed

Diff for: docs/docs/features/argument-parsing/flags.mdx

+2
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ import DefaultFlagCode from "./examples/default-flag.txt";
151151

152152
A flag can be variadic when the type it represents is an array of values. In this case, the flag can be specified multiple times and each value is then parsed individually and added to a single array. If the type of a flag is an array it must be set as variadic.
153153

154+
If the `variadic` config property is set to a string, Stricli will use that as a separator and split each input string. This is useful for cases where the input string is a single value that contains multiple values separated by a specific character (like a comma).
155+
154156
import VariadicFlagCode from "./examples/variadic-flag.txt";
155157

156158
<StricliPlayground filename="variadic-flag" rootExport="root" appName="run" defaultInput="--id 5 -i 10 -i 15">

Diff for: packages/core/src/parameter/flag/types.ts

+22-6
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export interface BaseEnumFlagParameter<T extends string> extends BaseFlagParamet
120120
readonly default?: T;
121121
readonly optional?: boolean;
122122
readonly hidden?: boolean;
123-
readonly variadic?: boolean;
123+
readonly variadic?: boolean | string;
124124
}
125125

126126
interface RequiredEnumFlagParameter<T extends string> extends BaseEnumFlagParameter<T> {
@@ -169,8 +169,12 @@ interface OptionalVariadicEnumFlagParameter<T extends string> extends BaseEnumFl
169169
readonly hidden?: false;
170170
/**
171171
* Parameter extends array and must be set as variadic.
172+
* Also supports using an arbitrary string as a separator for individual inputs.
173+
* For example, `variadic: ","` will scan `--flag a,b,c` as `["a", "b", "c"]`.
174+
* If no separator is provided, the default behavior is to parse the input as a single string.
175+
* The separator cannot be the empty string or contain any whitespace.
172176
*/
173-
readonly variadic: true;
177+
readonly variadic: true | string;
174178
}
175179

176180
interface RequiredVariadicEnumFlagParameter<T extends string> extends BaseEnumFlagParameter<T> {
@@ -189,8 +193,12 @@ interface RequiredVariadicEnumFlagParameter<T extends string> extends BaseEnumFl
189193
readonly hidden?: false;
190194
/**
191195
* Parameter extends array and must be set as variadic.
196+
* Also supports using an arbitrary string as a separator for individual inputs.
197+
* For example, `variadic: ","` will scan `--flag a,b,c` as `["a", "b", "c"]`.
198+
* If no separator is provided, the default behavior is to parse the input as a single string.
199+
* The separator cannot be the empty string or contain any whitespace.
192200
*/
193-
readonly variadic: true;
201+
readonly variadic: true | string;
194202
}
195203

196204
export interface BaseParsedFlagParameter<T, CONTEXT extends CommandContext>
@@ -209,7 +217,7 @@ export interface BaseParsedFlagParameter<T, CONTEXT extends CommandContext>
209217
*/
210218
readonly inferEmpty?: boolean;
211219
readonly optional?: boolean;
212-
readonly variadic?: boolean;
220+
readonly variadic?: boolean | string;
213221
readonly hidden?: boolean;
214222
}
215223

@@ -271,8 +279,12 @@ interface OptionalVariadicParsedFlagParameter<T, CONTEXT extends CommandContext>
271279
readonly optional: true;
272280
/**
273281
* Parameter extends array and must be set as variadic.
282+
* Also supports using an arbitrary string as a separator for individual inputs.
283+
* For example, `variadic: ","` will scan `--flag a,b,c` as `["a", "b", "c"]`.
284+
* If no separator is provided, the default behavior is to parse the input as a single string.
285+
* The separator cannot be the empty string or contain any whitespace.
274286
*/
275-
readonly variadic: true;
287+
readonly variadic: true | string;
276288
/**
277289
* Default values are not supported for variadic parameters.
278290
*/
@@ -288,8 +300,12 @@ interface RequiredVariadicParsedFlagParameter<T, CONTEXT extends CommandContext>
288300
readonly optional?: false;
289301
/**
290302
* Parameter extends array and must be set as variadic.
303+
* Also supports using an arbitrary string as a separator for individual inputs.
304+
* For example, `variadic: ","` will scan `--flag a,b,c` as `["a", "b", "c"]`.
305+
* If no separator is provided, the default behavior is to parse the input as a single string.
306+
* The separator cannot be the empty string or contain any whitespace.
291307
*/
292-
readonly variadic: true;
308+
readonly variadic: true | string;
293309
/**
294310
* Default values are not supported for variadic parameters.
295311
*/

Diff for: packages/core/src/parameter/scanner.ts

+40-23
Original file line numberDiff line numberDiff line change
@@ -632,16 +632,21 @@ function isVariadicFlag<CONTEXT extends CommandContext>(flag: FlagParameter<CONT
632632
function storeInput<CONTEXT extends CommandContext>(
633633
flagInputs: Map<InternalFlagName, ArgumentInputs>,
634634
scannerCaseStyle: ScannerCaseStyle,
635-
namedFlag: NamedFlag<CONTEXT, FlagParameter<CONTEXT>>,
635+
[internalFlagName, flag]: NamedFlag<CONTEXT, FlagParameter<CONTEXT>>,
636636
input: string,
637637
) {
638-
const inputs = flagInputs.get(namedFlag[0]) ?? [];
639-
if (inputs.length > 0 && !isVariadicFlag(namedFlag[1])) {
640-
const externalFlagName = asExternal(namedFlag[0], scannerCaseStyle);
638+
const inputs = flagInputs.get(internalFlagName) ?? [];
639+
if (inputs.length > 0 && !isVariadicFlag(flag)) {
640+
const externalFlagName = asExternal(internalFlagName, scannerCaseStyle);
641641
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
642642
throw new UnexpectedFlagError(externalFlagName, inputs[0]!, input);
643643
}
644-
flagInputs.set(namedFlag[0], [...inputs, input]);
644+
if ("variadic" in flag && typeof flag.variadic === "string") {
645+
const multipleInputs = input.split(flag.variadic) as unknown as ArgumentInputs;
646+
flagInputs.set(internalFlagName, [...inputs, ...multipleInputs]);
647+
} else {
648+
flagInputs.set(internalFlagName, [...inputs, input]);
649+
}
645650
}
646651

647652
function isFlagSatisfiedByInputs(
@@ -858,24 +863,7 @@ export function buildArgumentScanner<FLAGS extends BaseFlags, ARGS extends BaseA
858863
},
859864
proposeCompletions: async ({ partial, completionConfig, text, context, includeVersionFlag }) => {
860865
if (activeFlag) {
861-
const flag = activeFlag[1];
862-
let values: readonly string[];
863-
if (flag.kind === "enum") {
864-
values = flag.values;
865-
} else if (flag.proposeCompletions) {
866-
values = await flag.proposeCompletions.call(context, partial);
867-
} else {
868-
values = [];
869-
}
870-
return values
871-
.map<ArgumentCompletion>((value) => {
872-
return {
873-
kind: "argument:value",
874-
completion: value,
875-
brief: flag.brief,
876-
};
877-
})
878-
.filter(({ completion }) => completion.startsWith(partial));
866+
return proposeFlagCompletionsForPartialInput<CONTEXT>(activeFlag[1], context, partial);
879867
}
880868
const completions: ArgumentCompletion[] = [];
881869
if (!treatInputsAsArguments) {
@@ -1016,6 +1004,35 @@ export function buildArgumentScanner<FLAGS extends BaseFlags, ARGS extends BaseA
10161004
};
10171005
}
10181006

1007+
async function proposeFlagCompletionsForPartialInput<CONTEXT extends CommandContext>(
1008+
flag: FlagParserExpectingInput<CONTEXT>,
1009+
context: CONTEXT,
1010+
partial: string,
1011+
) {
1012+
if (typeof flag.variadic === "string") {
1013+
if (partial.endsWith(flag.variadic)) {
1014+
return proposeFlagCompletionsForPartialInput(flag, context, "");
1015+
}
1016+
}
1017+
let values: readonly string[];
1018+
if (flag.kind === "enum") {
1019+
values = flag.values;
1020+
} else if (flag.proposeCompletions) {
1021+
values = await flag.proposeCompletions.call(context, partial);
1022+
} else {
1023+
values = [];
1024+
}
1025+
return values
1026+
.map<ArgumentCompletion>((value) => {
1027+
return {
1028+
kind: "argument:value",
1029+
completion: value,
1030+
brief: flag.brief,
1031+
};
1032+
})
1033+
.filter(({ completion }) => completion.startsWith(partial));
1034+
}
1035+
10191036
/**
10201037
* @internal
10211038
*/

Diff for: packages/core/src/routing/command/builder.ts

+18
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ function checkForNegationCollisions(flags: Record<string, FlagParameter<CommandC
7474
}
7575
}
7676

77+
function checkForInvalidVariadicSeparators(flags: Record<string, FlagParameter<CommandContext>>): void {
78+
for (const [internalFlagName, flag] of Object.entries(flags)) {
79+
if ("variadic" in flag && typeof flag.variadic === "string") {
80+
if (flag.variadic.length < 1) {
81+
throw new InternalError(
82+
`Unable to use "" as variadic separator for --${internalFlagName} as it is empty`,
83+
);
84+
}
85+
if (/\s/.test(flag.variadic)) {
86+
throw new InternalError(
87+
`Unable to use "${flag.variadic}" as variadic separator for --${internalFlagName} as it contains whitespace`,
88+
);
89+
}
90+
}
91+
}
92+
}
93+
7794
/**
7895
* Build command from loader or local function as action with associated parameters and documentation.
7996
*/
@@ -89,6 +106,7 @@ export function buildCommand<
89106
checkForReservedFlags(flags, ["help", "helpAll", "help-all"]);
90107
checkForReservedAliases(aliases, ["h", "H"]);
91108
checkForNegationCollisions(flags);
109+
checkForInvalidVariadicSeparators(flags);
92110
let loader: CommandFunctionLoader<BaseFlags, BaseArgs, CONTEXT>;
93111
if ("func" in builderArgs) {
94112
loader = async () => builderArgs.func as CommandFunction<BaseFlags, BaseArgs, CONTEXT>;

0 commit comments

Comments
 (0)