diff --git a/examples/01-basic-flags/package.json b/examples/01-basic-flags/package.json index f90fff6..7d78c5b 100644 --- a/examples/01-basic-flags/package.json +++ b/examples/01-basic-flags/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/02-error-handling/package.json b/examples/02-error-handling/package.json index 9771a41..4f2ea31 100644 --- a/examples/02-error-handling/package.json +++ b/examples/02-error-handling/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/03-simple-commands/package.json b/examples/03-simple-commands/package.json index ebd4a96..399a17d 100644 --- a/examples/03-simple-commands/package.json +++ b/examples/03-simple-commands/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/04-package-manager/package.json b/examples/04-package-manager/package.json index ca31683..3a67355 100644 --- a/examples/04-package-manager/package.json +++ b/examples/04-package-manager/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/05-application-config/package.json b/examples/05-application-config/package.json index 75c806f..8a322b3 100644 --- a/examples/05-application-config/package.json +++ b/examples/05-application-config/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/06-builtin-commands/package.json b/examples/06-builtin-commands/package.json index 4ab779f..3057b32 100644 --- a/examples/06-builtin-commands/package.json +++ b/examples/06-builtin-commands/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/07-prompting/package.json b/examples/07-prompting/package.json index e48c98e..a557da1 100644 --- a/examples/07-prompting/package.json +++ b/examples/07-prompting/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "npm run build && node lib/index.js", + "start": "node --enable-source-maps lib/index.js", "build": "tsc" }, "keywords": [], diff --git a/examples/07-prompting/src/index.ts b/examples/07-prompting/src/index.ts index e89bca2..d505d8e 100644 --- a/examples/07-prompting/src/index.ts +++ b/examples/07-prompting/src/index.ts @@ -9,12 +9,6 @@ export const parserOpts: ParserOpts = { programVersion: 'v1' } -// Provide a custom resolver for the username key. -// This does have the downside that it will *always* try and resolve the key -// whether the user provides the flag or not. -// -// If this distinction matters, use an Argument and override the `resolveDefault` method -// to control the behaviour dependant on specificity class UsernamePromptResolver extends Resolver { private readonly rl: readline.Interface constructor (id: string) { @@ -25,9 +19,9 @@ class UsernamePromptResolver extends Resolver { }) } - async keyExists (key: string): Promise { + async keyExists (key: string, userDidPassArg: boolean): Promise { // We only care about resolving our username argument - return key === 'username' + return key === 'username' && userDidPassArg } async resolveKey (): Promise { diff --git a/src/args.ts b/src/args.ts index 3dcf104..4705a7f 100644 --- a/src/args.ts +++ b/src/args.ts @@ -116,9 +116,9 @@ export class Args { * @param inherit - Whether to inherit arguments from this configuration into the parser * @returns this */ - public command ( - [name, ...aliases]: [`${TName}`, ...string[]], - command: TCommand, + public command ( + [name, ...aliases]: [string, ...string[]], + command: Command, inherit = false ): Args { if (this._state.commands.has(name)) { @@ -409,7 +409,7 @@ export class Args { * @returns The result of the parse */ public async parseToResult (argString: string | string[], executeCommands = false): Promise, ParseError | CoercionError[] | CommandError>> { - this.opts.logger.debug(`Beginning parse of input '${argString}'`) + this.opts.logger.internal(`Beginning parse of input '${argString}'`) const tokenResult = tokenise(Array.isArray(argString) ? argString.join(' ') : argString) diff --git a/src/builder/builtin.ts b/src/builder/builtin.ts index 70e8fc6..65f4d92 100644 --- a/src/builder/builtin.ts +++ b/src/builder/builtin.ts @@ -58,8 +58,21 @@ export abstract class Builtin { * @returns The generated help string */ public helpInfo (): string { - return `${this.commandTriggers.map(cmd => `${cmd} <...args>`).join(', ')} | ${this.argumentTriggers.map(arg => `--${arg}`).join(', ')}` + const commands = this.commandTriggers.map(cmd => `${cmd} <...args>`).join(', ') + const args = this.argumentTriggers.map(arg => `--${arg}`).join(', ') + + if (commands && args) { + return `${commands} | ${args}` + } + + if (commands) { + return commands + } + + if (args) { + return args + } + + return `${this.constructor.name} | no triggers` } } - -export type BuiltinType = 'help' | 'completion' | 'version' | 'fallback' diff --git a/src/builder/command.ts b/src/builder/command.ts index 7aeba97..3b78af4 100644 --- a/src/builder/command.ts +++ b/src/builder/command.ts @@ -2,13 +2,14 @@ import { Args, DefaultArgTypes } from '../args' import { CommandError } from '../error' import { InternalCommand } from '../internal/parse/types' import { CommandOpts, StoredCommandOpts, defaultCommandOpts, defaultParserOpts } from '../opts' -import { ArgType } from '../util' +import { ArgType, Logger } from '../util' /** * Base class for all commands, including subcommands. Any user implemented command must extend from this class. */ export abstract class Command { public readonly opts: StoredCommandOpts + protected readonly log: Logger constructor ( opts: CommandOpts @@ -21,6 +22,8 @@ export abstract class Command { ...opts.parserOpts } } + + this.log = this.opts.parserOpts.logger } /** diff --git a/src/builder/default-resolvers.ts b/src/builder/default-resolvers.ts index e80ee7e..2568e63 100644 --- a/src/builder/default-resolvers.ts +++ b/src/builder/default-resolvers.ts @@ -3,7 +3,7 @@ import { StoredParserOpts } from '../opts' import { Resolver } from './resolver' export class EnvironmentResolver extends Resolver { - async keyExists (key: string, opts: StoredParserOpts): Promise { + async keyExists (key: string, _: boolean, opts: StoredParserOpts): Promise { const envKey = `${opts.environmentPrefix}_${key.toUpperCase()}` const platform = currentPlatform() return platform.getEnv(envKey) !== undefined diff --git a/src/builder/resolver.ts b/src/builder/resolver.ts index 559ca7a..87fd1d3 100644 --- a/src/builder/resolver.ts +++ b/src/builder/resolver.ts @@ -13,9 +13,10 @@ export abstract class Resolver { /** * Determine whether this resolver can resolve the provided key. * @param key - The key to check + * @param userDidPassArg - Whether the user provided an argument or not * @param opts - The parser opts */ - abstract keyExists (key: string, opts: StoredParserOpts): Promise + abstract keyExists (key: string, userDidPassArg: boolean, opts: StoredParserOpts): Promise /** * Resolve the provided key to its string value. * diff --git a/src/internal/parse/coerce.ts b/src/internal/parse/coerce.ts index b66a733..24c6ea6 100644 --- a/src/internal/parse/coerce.ts +++ b/src/internal/parse/coerce.ts @@ -90,7 +90,7 @@ async function resolveArgumentDefault ( const key = argument.type === 'flag' ? argument.longFlag : argument.key for (const resolver of resolvers) { - if (await resolver.keyExists(key, opts)) { + if (await resolver.keyExists(key, false, opts)) { const value = await resolver.resolveKey(key, opts) if (!value) { diff --git a/src/internal/parse/schematic-validation.ts b/src/internal/parse/schematic-validation.ts index ad9889f..d6884fe 100644 --- a/src/internal/parse/schematic-validation.ts +++ b/src/internal/parse/schematic-validation.ts @@ -37,6 +37,8 @@ export async function validateFlagSchematically ( } } + const userDidProvideArgs = (foundFlags ?? []).length > 0 + let { resolveDefault, optional, dependencies, conflicts, exclusive, requiredUnlessPresent } = argument.inner._state const [specifiedDefault, unspecifiedDefault] = await Promise.all([resolveDefault('specified'), resolveDefault('unspecified')]) @@ -44,7 +46,7 @@ export async function validateFlagSchematically ( let resolversHaveValue = false for (const resolver of resolvers) { - if (await resolver.keyExists(argument.longFlag, opts)) { + if (await resolver.keyExists(argument.longFlag, userDidProvideArgs, opts)) { resolversHaveValue = true } } @@ -110,7 +112,7 @@ export async function validatePositionalSchematically ( let resolversHaveValue = false for (const middleware of resolvers) { - if (await middleware.keyExists(argument.key, opts)) { + if (await middleware.keyExists(argument.key, foundFlag !== undefined, opts)) { resolversHaveValue = true } } diff --git a/src/internal/util.ts b/src/internal/util.ts index e742a06..581e715 100644 --- a/src/internal/util.ts +++ b/src/internal/util.ts @@ -17,11 +17,11 @@ export function getAliasDenotion (alias: FlagAlias): string { } } -const flagValidationRegex = /-+(?:[a-z]+)/ +const flagValidationRegex = /-+(?:[a-zA-Z]+)/ export function internaliseFlagString (flag: string): ['long' | 'short', string] { if (!flagValidationRegex.test(flag)) { - throw new SchemaError(`flags must match '--abcdef...' or '-abcdef' got '${flag}'`) + throw new SchemaError(`flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '${flag}'`) } // Long flag diff --git a/src/util/help.ts b/src/util/help.ts index 70043b2..f113a91 100644 --- a/src/util/help.ts +++ b/src/util/help.ts @@ -21,9 +21,9 @@ export function generateHelp (parser: Args<{}>): string { if (value.aliases.length) { if (isMultiType) { - return `[--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}...>]` + return `[--${value.longFlag} | ${value.aliases.map(getAliasDenotion).join(' | ')} <${value.inner.type}...>]` } - return `[--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}>]` + return `[--${value.longFlag} | ${value.aliases.map(getAliasDenotion).join(' | ')} <${value.inner.type}>]` } return `[--${value.longFlag} <${value.inner.type}>]` } else { @@ -36,9 +36,9 @@ export function generateHelp (parser: Args<{}>): string { if (value.aliases.length) { if (isMultiType) { - return `(--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}...>)` + return `(--${value.longFlag} | ${value.aliases.map(getAliasDenotion).join(' | ')} <${value.inner.type}...>)` } - return `(--${value.longFlag}${value.aliases.map(getAliasDenotion).join(' | ')}<${value.inner.type}>)` + return `(--${value.longFlag} | ${value.aliases.map(getAliasDenotion).join(' | ')} <${value.inner.type}>)` } return `(--${value.longFlag} <${value.inner.type}>)` diff --git a/src/util/logging.ts b/src/util/logging.ts index 619bec1..40c5f74 100644 --- a/src/util/logging.ts +++ b/src/util/logging.ts @@ -2,6 +2,7 @@ interface Stringifiable { toString: () => string } type LoggingFunction = (...args: Stringifiable[]) => T const LEVEL_TO_CONSOLE: Record (...args: unknown[]) => void> = { + internal: () => console.trace, trace: () => console.trace, debug: () => console.debug, info: () => console.log, @@ -11,7 +12,8 @@ const LEVEL_TO_CONSOLE: Record (...args: unknown[]) => void> = { } const LEVEL_TO_NUMBER: Record = { - trace: 0, + internal: 0, + trace: 1, debug: 10, info: 20, warn: 30, @@ -22,13 +24,14 @@ const LEVEL_TO_NUMBER: Record = { /** * The levels which a {@link Logger} can operate at. */ -export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' +export type LogLevel = 'internal' | 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' /** * The logging class used internally to (configurably) inform users about library behaviour. * This is a thin wrapper around the {@link console}, and should generally be set to something above 'info' in production. */ export class Logger { + internal = this.makeLevelFunc('internal', false) trace = this.makeLevelFunc('trace', false) debug = this.makeLevelFunc('debug', false) info = this.makeLevelFunc('info', false) @@ -51,12 +54,12 @@ export class Logger { const ourLevel = LEVEL_TO_NUMBER[this.level] const targetLevel = LEVEL_TO_NUMBER[level] - if (ourLevel >= targetLevel) { + if (ourLevel > targetLevel) { return } - const fn = LEVEL_TO_CONSOLE[this.level]() - fn(`[${this.name}]`, new Date().toISOString(), ':', ...args) + const fn = LEVEL_TO_CONSOLE[level]() + fn(`[${level.toUpperCase()}]`.padEnd(7), `[${this.name}]`, new Date().toISOString(), ':', ...args) if (exit) { process.exit() diff --git a/test/parsing/utils.ts b/test/parsing/utils.ts index ab946f6..3874939 100644 --- a/test/parsing/utils.ts +++ b/test/parsing/utils.ts @@ -1,6 +1,6 @@ import assert from 'assert' -import { ArgsState, MinimalArgument, StoredParserOpts, defaultCommandOpts } from '../../src' +import { ArgsState, Command, MinimalArgument, StoredParserOpts, defaultCommandOpts, defaultParserOpts } from '../../src' import { CoercedArguments, coerce } from '../../src/internal/parse/coerce' import { tokenise } from '../../src/internal/parse/lexer' import { ParsedArguments, parse } from '../../src/internal/parse/parser' @@ -20,17 +20,18 @@ export function makeInternalCommand ( aliases: aliases ?? [], isBase: true, inner: { + log: defaultParserOpts.logger, _subcommands: subcommands ?? {}, - args: p => p, + args: (p: any) => p, opts: { description: description ?? `${name} command description`, parserOpts: opts, ...defaultCommandOpts }, - run: p => p, - runner: p => p, - subcommand: p => ({} as any) - }, + run: (p: any) => p, + runner: (p: any) => p, + subcommand: (p: any) => ({} as any) + } as unknown as Command, parser: ({} as any) } } diff --git a/test/schema/validation.test.ts b/test/schema/validation.test.ts index a914d8c..e90b20e 100644 --- a/test/schema/validation.test.ts +++ b/test/schema/validation.test.ts @@ -30,7 +30,7 @@ describe('Schema validation', () => { expect(() => { // @ts-expect-error we are testing runtime validation, for JS users, or people who dont like playing by the rules parser.arg(['-1'], a.string()) - }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '-1'"`) + }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '-1'"`) }) it('rejects positionals not prefixed by <', () => { @@ -57,7 +57,7 @@ describe('Schema validation', () => { expect(() => { // @ts-expect-error we are testing runtime validation, for JS users, or people who dont like playing by the rules parser.arg(['--flag', '1'], a.string()) - }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '1'"`) + }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '1'"`) }) it('rejects long flags that do not have a valid ID', () => { @@ -65,7 +65,7 @@ describe('Schema validation', () => { expect(() => { parser.arg(['--1'], a.string()) - }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '--1'"`) + }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '--1'"`) }) it('rejects short flags that do not have a valid ID', () => { @@ -73,7 +73,7 @@ describe('Schema validation', () => { expect(() => { parser.arg(['--flag', '-1'], a.string()) - }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdef...' or '-abcdef' got '-1'"`) + }).toThrowErrorMatchingInlineSnapshot(`"flags must match '--abcdefABCDEF' or '-abcdefABCDEF' got '-1'"`) }) it('rejects duplicate long flags', () => { diff --git a/test/util.test.ts b/test/util.test.ts index d4166ff..89541f4 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -135,7 +135,7 @@ describe('Help generation utils', () => { expect(util.generateHelp(parser)).toMatchInlineSnapshot(` "program-name - program description -Usage: program-name [--flag-f] [--opt-multi-o] (--opt-req-r) (--enum-e) (--long ) [--long-optional ] [] +Usage: program-name [--flag | -f ] [--opt-multi | -o ] (--opt-req | -r ) (--enum | -e ) (--long ) [--long-optional ] [] Commands: program-name [help, nohelp] (--cmd-arg )