args-json
Version:
Zero-dependency typed command-line argument parser
155 lines (118 loc) • 4.32 kB
text/typescript
export type ArgMap = Record<string, string>;
function toCamelCase(x: string): string {
let s = x.replace(/^[-_.\s~+]|[-_.\s~+]$/g, '');
if (!/[-_.\s~+]/.test(s))
return s.slice(0, 1).toLowerCase() + s.slice(1);
return s.toLowerCase().replace(/[-_.\s~+](\S)/g, (_, match) => match.toUpperCase());
}
function toKey(x: string | undefined): string | undefined {
if (x) {
if (x.startsWith('--') && x.length > 2)
return toCamelCase(x.slice(2));
if (x.startsWith('-') && x.length === 2)
return toCamelCase(x.slice(1));
}
}
function split(x: string): string[] {
let words: string[] = [], word = '';
let hasOpenSingleQuote = false;
let hasOpenDoubleQuote = false;
for (let i = 0; i < x.length; i++) {
let c = x[i];
if (/^\s/.test(c) && !hasOpenSingleQuote && !hasOpenDoubleQuote) {
if (word) words.push(word);
word = '';
continue;
}
if (c === '\'' && x[i - 1] !== '\\')
hasOpenSingleQuote = !hasOpenSingleQuote;
if (c === '"' && x[i - 1] !== '\\')
hasOpenDoubleQuote = !hasOpenDoubleQuote;
word += c;
}
if (word)
words.push(word);
return words;
}
function getDefaultInput(): string[] {
// eslint-disable-next-line no-constant-binary-expression -- for non-node environments
return typeof process === undefined ? [] : process.argv;
}
export function parseArgs<T extends Record<string, unknown> = Record<string, unknown>>(
map?: ArgMap,
): T;
export function parseArgs<T extends Record<string, unknown> = Record<string, unknown>>(
input?: string | string[],
map?: ArgMap,
): T;
export function parseArgs<T extends Record<string, unknown> = Record<string, unknown>>(
input?: string | string[] | ArgMap,
map?: ArgMap,
): T {
let normalizedInput: string[];
let normalizedMap: ArgMap | undefined;
if (input === undefined)
normalizedInput = getDefaultInput();
else if (typeof input === 'string')
normalizedInput = split(input);
else if (Array.isArray(input))
normalizedInput = input.map(x => String(x));
else if (input !== null && typeof input === 'object') {
normalizedInput = getDefaultInput();
normalizedMap = input;
}
else normalizedInput = [];
normalizedInput = normalizedInput
.map(item => {
let normalizedItem = item.trim();
let k = normalizedItem.indexOf('=');
if (k === -1)
return normalizedItem;
return [
normalizedItem.slice(0, k),
normalizedItem.slice(k + 1),
];
})
.flat();
if (map)
normalizedMap = map;
let key = '';
let parsedArgs: Record<string, unknown> = {};
for (let rawValue of normalizedInput) {
rawValue = rawValue.trim();
if (rawValue.startsWith('"') && rawValue.endsWith('"'))
rawValue = rawValue.slice(1, -1);
else if (rawValue.startsWith('\'') && rawValue.endsWith('\''))
rawValue = rawValue.slice(1, -1);
let parsedKey = toKey(rawValue);
if (parsedKey !== undefined) {
let nextKey = normalizedMap?.[parsedKey] ?? parsedKey;
if (key && nextKey !== key && parsedArgs[key] === undefined)
parsedArgs[key] = true;
key = nextKey;
continue;
}
let parsedValue;
if (rawValue) {
try {
parsedValue = JSON.parse(rawValue);
}
catch {
parsedValue = rawValue;
}
}
else parsedValue = true;
let prevValue = parsedArgs[key];
let value;
if (prevValue === undefined)
value = parsedValue;
else if (Array.isArray(prevValue))
value = [...prevValue, parsedValue];
else
value = [prevValue, parsedValue];
parsedArgs[key] = value;
}
if (key && parsedArgs[key] === undefined)
parsedArgs[key] = true;
return parsedArgs as T;
}