Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/01-basic-flags/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
2 changes: 1 addition & 1 deletion examples/02-error-handling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
2 changes: 1 addition & 1 deletion examples/03-simple-commands/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
2 changes: 1 addition & 1 deletion examples/04-package-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
2 changes: 1 addition & 1 deletion examples/05-application-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
2 changes: 1 addition & 1 deletion examples/06-builtin-commands/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
2 changes: 1 addition & 1 deletion examples/07-prompting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
10 changes: 2 additions & 8 deletions examples/07-prompting/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -25,9 +19,9 @@ class UsernamePromptResolver extends Resolver {
})
}

async keyExists (key: string): Promise<boolean> {
async keyExists (key: string, userDidPassArg: boolean): Promise<boolean> {
// We only care about resolving our username argument
return key === 'username'
return key === 'username' && userDidPassArg
}

async resolveKey (): Promise<string> {
Expand Down
8 changes: 4 additions & 4 deletions src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ export class Args<TArgTypes extends DefaultArgTypes = DefaultArgTypes> {
* @param inherit - Whether to inherit arguments from this configuration into the parser
* @returns this
*/
public command<TName extends string, TCommand extends Command> (
[name, ...aliases]: [`${TName}`, ...string[]],
command: TCommand,
public command (
[name, ...aliases]: [string, ...string[]],
command: Command,
inherit = false
): Args<TArgTypes> {
if (this._state.commands.has(name)) {
Expand Down Expand Up @@ -409,7 +409,7 @@ export class Args<TArgTypes extends DefaultArgTypes = DefaultArgTypes> {
* @returns The result of the parse
*/
public async parseToResult (argString: string | string[], executeCommands = false): Promise<Result<ParseSuccess<TArgTypes>, 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)

Expand Down
19 changes: 16 additions & 3 deletions src/builder/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
5 changes: 4 additions & 1 deletion src/builder/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +22,8 @@ export abstract class Command {
...opts.parserOpts
}
}

this.log = this.opts.parserOpts.logger
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/builder/default-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { StoredParserOpts } from '../opts'
import { Resolver } from './resolver'

export class EnvironmentResolver extends Resolver {
async keyExists (key: string, opts: StoredParserOpts): Promise<boolean> {
async keyExists (key: string, _: boolean, opts: StoredParserOpts): Promise<boolean> {
const envKey = `${opts.environmentPrefix}_${key.toUpperCase()}`
const platform = currentPlatform()
return platform.getEnv(envKey) !== undefined
Expand Down
3 changes: 2 additions & 1 deletion src/builder/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>
abstract keyExists (key: string, userDidPassArg: boolean, opts: StoredParserOpts): Promise<boolean>
/**
* Resolve the provided key to its string value.
*
Expand Down
2 changes: 1 addition & 1 deletion src/internal/parse/coerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions src/internal/parse/schematic-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ 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')])

// Test our resolvers to see if any of them have a value, so we know whether to reject below
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
}
}
Expand Down Expand Up @@ -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
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/internal/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/util/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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}>)`
Expand Down
13 changes: 8 additions & 5 deletions src/util/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface Stringifiable { toString: () => string }
type LoggingFunction<T> = (...args: Stringifiable[]) => T

const LEVEL_TO_CONSOLE: Record<LogLevel, () => (...args: unknown[]) => void> = {
internal: () => console.trace,
trace: () => console.trace,
debug: () => console.debug,
info: () => console.log,
Expand All @@ -11,7 +12,8 @@ const LEVEL_TO_CONSOLE: Record<LogLevel, () => (...args: unknown[]) => void> = {
}

const LEVEL_TO_NUMBER: Record<LogLevel, number> = {
trace: 0,
internal: 0,
trace: 1,
debug: 10,
info: 20,
warn: 30,
Expand All @@ -22,13 +24,14 @@ const LEVEL_TO_NUMBER: Record<LogLevel, number> = {
/**
* 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)
Expand All @@ -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()
Expand Down
13 changes: 7 additions & 6 deletions test/parsing/utils.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
}
}
Expand Down
8 changes: 4 additions & 4 deletions test/schema/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <', () => {
Expand All @@ -57,23 +57,23 @@ 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', () => {
const parser = new Args(parserOpts)

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', () => {
const parser = new Args(parserOpts)

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', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe('Help generation utils', () => {
expect(util.generateHelp(parser)).toMatchInlineSnapshot(`
"program-name - program description

Usage: program-name [--flag-f<string>] [--opt-multi-o<string...>] (--opt-req-r<string...>) (--enum-e<a | b | c>) (--long <number>) [--long-optional <number>] <POSITIONALREQ> [<POSITIONAL>] <POSMULTI...>
Usage: program-name [--flag | -f <string>] [--opt-multi | -o <string...>] (--opt-req | -r <string...>) (--enum | -e <a | b | c>) (--long <number>) [--long-optional <number>] <POSITIONALREQ> [<POSITIONAL>] <POSMULTI...>

Commands:
program-name [help, nohelp] (--cmd-arg <string>)
Expand Down