1// Common utilities for interacting with `args` library. 2// These functions should be used by every command. 3import arg, { Spec } from 'arg'; 4import chalk from 'chalk'; 5 6import { replaceValue } from './array'; 7import * as Log from '../log'; 8 9/** 10 * Parse args and assert unknown options. 11 * 12 * @param schema the `args` schema for parsing the command line arguments. 13 * @param argv extra strings 14 * @returns processed args object. 15 */ 16export function assertWithOptionsArgs( 17 schema: arg.Spec, 18 options: arg.Options 19): arg.Result<arg.Spec> { 20 try { 21 return arg(schema, options); 22 } catch (error: any) { 23 // Ensure unknown options are handled the same way. 24 if (error.code === 'ARG_UNKNOWN_OPTION') { 25 Log.exit(error.message, 1); 26 } 27 // Otherwise rethrow the error. 28 throw error; 29 } 30} 31 32export function printHelp(info: string, usage: string, options: string, extra: string = ''): never { 33 Log.exit( 34 chalk` 35 {bold Info} 36 ${info} 37 38 {bold Usage} 39 {dim $} ${usage} 40 41 {bold Options} 42 ${options.split('\n').join('\n ')} 43` + extra, 44 0 45 ); 46} 47 48/** 49 * Enables the resolution of arguments that can either be a string or a boolean. 50 * 51 * @param args arguments that were passed to the command. 52 * @param rawMap raw map of arguments that are passed to the command. 53 * @param extraArgs extra arguments and aliases that should be resolved as string or boolean. 54 * @returns parsed arguments and project root. 55 */ 56export async function resolveStringOrBooleanArgsAsync( 57 args: string[], 58 rawMap: arg.Spec, 59 extraArgs: arg.Spec 60) { 61 const combined = { 62 ...rawMap, 63 ...extraArgs, 64 }; 65 // Assert any missing arguments 66 assertUnknownArgs(combined, args); 67 68 // Collapse aliases into fully qualified arguments. 69 args = collapseAliases(combined, args); 70 // Resolve all of the string or boolean arguments and the project root. 71 return _resolveStringOrBooleanArgs(extraArgs, args); 72} 73 74export function _resolveStringOrBooleanArgs(multiTypeArgs: Spec, args: string[]) { 75 // Default project root, if a custom one is defined then it will overwrite this. 76 let projectRoot: string = ''; 77 // The resolved arguments. 78 const settings: Record<string, string | true | undefined> = {}; 79 80 // Create a list of possible arguments, this will filter out aliases. 81 const possibleArgs = Object.entries(multiTypeArgs) 82 .filter(([, value]) => typeof value !== 'string') 83 .map(([key]) => key); 84 85 // Loop over arguments in reverse order so we can resolve if a value belongs to a flag. 86 for (let i = args.length - 1; i > -1; i--) { 87 const value = args[i]; 88 // At this point we should have converted all aliases to fully qualified arguments. 89 if (value.startsWith('--')) { 90 // If we ever find an argument then it must be a boolean because we are checking in reverse 91 // and removing arguments from the array if we find a string. 92 settings[value] = true; 93 } else { 94 // Get the previous argument in the array. 95 const nextValue = i > 0 ? args[i - 1] : null; 96 if (nextValue && possibleArgs.includes(nextValue)) { 97 settings[nextValue] = value; 98 i--; 99 } else if ( 100 // Prevent finding two values that are dangling 101 !projectRoot && 102 // If the last value is not a flag and it doesn't have a recognized flag before it (instead having a string value or nothing) 103 // then it must be the project root. 104 (i === args.length - 1 || i === 0) 105 ) { 106 projectRoot = value; 107 } else { 108 // This will asserts if two strings are passed in a row and not at the end of the line. 109 throw new Error(`Unknown argument: ${value}`); 110 } 111 } 112 } 113 114 return { 115 args: settings, 116 projectRoot, 117 }; 118} 119 120/** Convert all aliases to fully qualified flag names. */ 121export function collapseAliases(arg: Spec, args: string[]): string[] { 122 const aliasMap = getAliasTuples(arg); 123 124 for (const [arg, alias] of aliasMap) { 125 args = replaceValue(args, arg, alias); 126 } 127 128 // Assert if there are duplicate flags after we collapse the aliases. 129 assertDuplicateArgs(args, aliasMap); 130 return args; 131} 132 133/** Assert that the spec has unknown arguments. */ 134export function assertUnknownArgs(arg: Spec, args: string[]) { 135 const allowedArgs = Object.keys(arg); 136 const unknownArgs = args.filter((arg) => !allowedArgs.includes(arg) && arg.startsWith('-')); 137 if (unknownArgs.length > 0) { 138 throw new Error(`Unknown arguments: ${unknownArgs.join(', ')}`); 139 } 140} 141 142function getAliasTuples(arg: Spec): [string, string][] { 143 return Object.entries(arg).filter(([, value]) => typeof value === 'string') as [string, string][]; 144} 145 146/** Asserts that a duplicate flag has been used, this naively throws without knowing if an alias or flag were used as the duplicate. */ 147export function assertDuplicateArgs(args: string[], argNameAliasTuple: [string, string][]) { 148 for (const [argName, argNameAlias] of argNameAliasTuple) { 149 if (args.filter((a) => [argName, argNameAlias].includes(a)).length > 1) { 150 throw new Error(`Can only provide one instance of ${argName} or ${argNameAlias}`); 151 } 152 } 153} 154