From 514e200aa34c928d2236ef4842ace2821faab782 Mon Sep 17 00:00:00 2001 From: Chris Arnesen Date: Sat, 14 Jan 2023 13:56:15 -0600 Subject: [PATCH] Add arg group factories `c.bigint` and `c.bigintArray` Closes #203 --- .github/workflows/test.yml | 2 +- .nvmrc | 2 +- packages/cli-examples/package.json | 2 +- .../src/advanced-command-group.ts | 2 + packages/cli-examples/src/echo-command.ts | 2 +- .../cli-examples/src/echo-hidden-command.ts | 2 +- .../src/multiply-bigints-command.ts | 12 ++++ packages/cli-examples/src/multiply-command.ts | 12 +++- packages/cli-examples/src/multiply.ts | 19 ------- .../cli-examples/src/throw-error-command.ts | 2 +- packages/cli/changelog.md | 6 +- packages/cli/readme.md | 4 +- packages/cli/src/__tests__/util.test.ts | 2 +- .../__tests__/c-cli-bigint-arg-group.test.ts | 56 +++++++++++++++++++ .../c-cli-bigint-array-arg-group.test.ts | 53 ++++++++++++++++++ .../c-cli-string-choice-arg-group.test.ts | 13 ++++- .../src/arg-groups/c-cli-bigint-arg-group.ts | 42 ++++++++++++++ .../c-cli-bigint-array-arg-group.ts | 41 ++++++++++++++ .../src/arg-groups/c-cli-number-arg-group.ts | 6 +- .../c-cli-number-array-arg-group.ts | 2 +- packages/cli/src/c-cli-arg-group.ts | 7 ++- packages/cli/src/c-cli-conditional-value.ts | 2 + packages/cli/src/{c.ts => c-functions.ts} | 8 +++ packages/cli/src/c-namespace.ts | 2 + packages/cli/src/convert-to-bigint.ts | 19 +++++++ .../cli/src/{util.ts => convert-to-number.ts} | 4 -- packages/cli/src/index.ts | 2 +- packages/cli/src/usage-for-command.ts | 2 +- packages/cli/src/wrap-in-square-brackets.ts | 3 + packages/cli/usage.md | 44 +++++++++------ 30 files changed, 309 insertions(+), 66 deletions(-) create mode 100755 packages/cli-examples/src/multiply-bigints-command.ts mode change 100644 => 100755 packages/cli-examples/src/multiply-command.ts delete mode 100755 packages/cli-examples/src/multiply.ts create mode 100644 packages/cli/src/arg-groups/__tests__/c-cli-bigint-arg-group.test.ts create mode 100644 packages/cli/src/arg-groups/__tests__/c-cli-bigint-array-arg-group.test.ts create mode 100644 packages/cli/src/arg-groups/c-cli-bigint-arg-group.ts create mode 100644 packages/cli/src/arg-groups/c-cli-bigint-array-arg-group.ts rename packages/cli/src/{c.ts => c-functions.ts} (82%) create mode 100644 packages/cli/src/c-namespace.ts create mode 100644 packages/cli/src/convert-to-bigint.ts rename packages/cli/src/{util.ts => convert-to-number.ts} (78%) create mode 100644 packages/cli/src/wrap-in-square-brackets.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8e4608..1daeb1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [ '18', '16', '14' ] + node-version: [ '19', '18', '16', '14' ] name: Node.js ${{ matrix.node-version }} steps: - name: Checkout source diff --git a/.nvmrc b/.nvmrc index b6a7d89..3c03207 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 +18 diff --git a/packages/cli-examples/package.json b/packages/cli-examples/package.json index 8b826c0..5e9eeea 100644 --- a/packages/cli-examples/package.json +++ b/packages/cli-examples/package.json @@ -19,7 +19,7 @@ "lint": "eslint --ext .ts src/", "lint:fix": "npm run lint -- --fix", "release": "carnesen-dev release --semverBump", - "test": "npm run lint && npm run unit-test && npm run build:clean && node lib/cli echo ok && ts-node src/multiply 2 3 4 && npm pack", + "test": "npm run lint && npm run unit-test && npm run build:clean && node lib/cli echo ok && ts-node src/multiply-command 2 3 4 && npm pack", "type-check": "npm run build -- --noEmit", "type-check:watch": "npm run type-check -- --watch", "unit-test": "jest src", diff --git a/packages/cli-examples/src/advanced-command-group.ts b/packages/cli-examples/src/advanced-command-group.ts index d1b2b9e..8a1165c 100644 --- a/packages/cli-examples/src/advanced-command-group.ts +++ b/packages/cli-examples/src/advanced-command-group.ts @@ -3,6 +3,7 @@ import { hidingCommandGroup } from './hiding-command-group'; import { demoDoubleDashArgumentsCommand } from './demo-double-dash-arguments-command'; import { parseJsonCommand } from './parse-json-command'; import { echoWithColorCommand } from './echo-with-color-command'; +import { multiplyIntegersCommand } from './multiply-bigints-command'; export const advancedCommandGroup = c.commandGroup({ name: 'advanced', @@ -15,6 +16,7 @@ export const advancedCommandGroup = c.commandGroup({ echoWithColorCommand, demoDoubleDashArgumentsCommand, hidingCommandGroup, + multiplyIntegersCommand, parseJsonCommand, ], }); diff --git a/packages/cli-examples/src/echo-command.ts b/packages/cli-examples/src/echo-command.ts index c210836..c1ac7e6 100644 --- a/packages/cli-examples/src/echo-command.ts +++ b/packages/cli-examples/src/echo-command.ts @@ -1,6 +1,6 @@ import { c } from '@carnesen/cli'; -/** A CliCommand that prints to the terminal like the `echo` shell command */ +/** A CLI command that prints to the terminal like the `echo` shell command */ export const echoCommand = c.command({ name: 'echo', description: 'Prints the provided arguments to the terminal', diff --git a/packages/cli-examples/src/echo-hidden-command.ts b/packages/cli-examples/src/echo-hidden-command.ts index 9f1532a..c394204 100644 --- a/packages/cli-examples/src/echo-hidden-command.ts +++ b/packages/cli-examples/src/echo-hidden-command.ts @@ -1,6 +1,6 @@ import { c } from '@carnesen/cli'; -/** A "hidden" CliCommand that otherwise behaves like the normal "echo" command */ +/** A "hidden" CLI command that otherwise behaves like the normal "echo" */ export const echoHiddenCommand = c.command({ name: 'echo-hidden', description: ` diff --git a/packages/cli-examples/src/multiply-bigints-command.ts b/packages/cli-examples/src/multiply-bigints-command.ts new file mode 100755 index 0000000..1617ca1 --- /dev/null +++ b/packages/cli-examples/src/multiply-bigints-command.ts @@ -0,0 +1,12 @@ +/** src/multiply.ts */ +import { c } from '@carnesen/cli'; + +/** A command for multiplying large integers as JavaScript {@link BigInt}s */ +export const multiplyIntegersCommand = c.command({ + name: 'multiply-integers', + description: 'Multiply integers and print the result', + positionalArgGroup: c.bigintArray(), + action({ positionalValue: bigints }) { + return bigints.reduce((a, b) => a * b, 1n); + }, +}); diff --git a/packages/cli-examples/src/multiply-command.ts b/packages/cli-examples/src/multiply-command.ts old mode 100644 new mode 100755 index 83cefa5..83ffccd --- a/packages/cli-examples/src/multiply-command.ts +++ b/packages/cli-examples/src/multiply-command.ts @@ -1,8 +1,8 @@ +/** src/multiply-command.ts */ import { c } from '@carnesen/cli'; -/** - * A CliCommand for multiplying numbers - */ +/** A command for multiplying numbers, the one and only command for this CLI, + * exported for unit testing */ export const multiplyCommand = c.command({ name: 'multiply', description: 'Multiply numbers and print the result', @@ -11,3 +11,9 @@ export const multiplyCommand = c.command({ return numbers.reduce((a, b) => a * b, 1); }, }); + +if (require.main === module) { + // This module is the entrypoint for this Node.js process + const cli = c.cli(multiplyCommand); + cli.run(); +} diff --git a/packages/cli-examples/src/multiply.ts b/packages/cli-examples/src/multiply.ts deleted file mode 100755 index f997bc0..0000000 --- a/packages/cli-examples/src/multiply.ts +++ /dev/null @@ -1,19 +0,0 @@ -// src/multiply.ts -import { c } from '@carnesen/cli'; - -const multiplyCommand = c.command({ - name: 'multiply', - description: 'Multiply numbers and print the result', - positionalArgGroup: c.numberArray(), - action({ positionalValue: numbers }) { - return numbers.reduce((a, b) => a * b, 1); - }, -}); - -// Export for unit testing -export const cli = c.cli(multiplyCommand); - -// If this module is the entrypoint for this Node.js process -if (require.main === module) { - cli.run(); -} diff --git a/packages/cli-examples/src/throw-error-command.ts b/packages/cli-examples/src/throw-error-command.ts index 90aeb2a..4b1b4bb 100644 --- a/packages/cli-examples/src/throw-error-command.ts +++ b/packages/cli-examples/src/throw-error-command.ts @@ -1,7 +1,7 @@ import { c, CCliTerseError, CCliUsageError } from '@carnesen/cli'; /** - * A CliCommand for demonstrating how errors are displayed + * CLI command for demonstrating how errors are displayed * */ export const throwErrorCommand = c.command({ name: 'throw-error', diff --git a/packages/cli/changelog.md b/packages/cli/changelog.md index ecdedb3..3bfe282 100644 --- a/packages/cli/changelog.md +++ b/packages/cli/changelog.md @@ -2,11 +2,9 @@ ## Upcoming -## carnesen-cli-0.8.1 (2022-09-11) - +- Feature: Add arg group factories `c.bigint` and `c.bigintArray` - -## carnesen-cli-0.8.0 (2022-09-11) +## carnesen-cli-0.8.1 (2022-09-11) This is a significant release with breaking changes. It refactors all the **@carnesen/cli** abstractions as TypeScript/ECMAScript `class`es. Earlier implementations of this library favored a pattern of factory functions producing a plain object. We adopt a new namespace/branding convention "CCli" for the new `class`es. Compared to the old namespace (just "Cli"), the extra "C" (for "carnesen"!) makes the exported symbols more distinctive and easily auto-imported in your IDE. For most use cases however you won't need to use the "CCli" symbols because this release provides a new convenient API for defining CLI's, the `c` namespace object. For example: diff --git a/packages/cli/readme.md b/packages/cli/readme.md index aa07d62..f789c1f 100644 --- a/packages/cli/readme.md +++ b/packages/cli/readme.md @@ -20,7 +20,9 @@ and is known to work with Node.js 12+ and all modern web browsers. - **Hidden commands** Add "hidden=true" to any argument/command/group, and we'll hide it in the automatic usage docs. We use this feature for easter eggs and internal-only/beta commands. -- **Automatic autocomplete** (Coming soon!): [Autocomplete](https://en.wikipedia.org/wiki/Autocomplete) supercharges a CLI. We've implemented automatic autocomplete in [the live examples](https://cli.carnesen.com) and plan to [add this as a feature to the core library](https://github.com/carnesen/cli/issues/32) in a future release. +- **Automatic autocomplete** (Coming some day!): [Autocomplete](https://en.wikipedia.org/wiki/Autocomplete) supercharges a CLI. We've implemented automatic autocomplete in [the live examples](https://cli.carnesen.com) and plan to [add this as a feature to the core library](https://github.com/carnesen/cli/issues/32) in a future release. + +- **REPL** (Coming some day!): The [live examples](https://cli.carnesen.com) are [implemented](https://github.com/carnesen/cli/blob/30b431a4505e0bd2a28e4db9dfd4e50ba5a935b0/packages/cli-website/src/cli-repl.ts) as custom [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop). In a future release we'll lift that code into the framework to make it just as easy to build a REPL as it is to build a CLI. ## Stability diff --git a/packages/cli/src/__tests__/util.test.ts b/packages/cli/src/__tests__/util.test.ts index 5acbc52..7e1b733 100644 --- a/packages/cli/src/__tests__/util.test.ts +++ b/packages/cli/src/__tests__/util.test.ts @@ -1,6 +1,6 @@ import { runAndCatchSync } from '@carnesen/run-and-catch'; import { CCliUsageError } from '../c-cli-usage-error'; -import { convertToNumber } from '../util'; +import { convertToNumber } from '../convert-to-number'; describe(convertToNumber.name, () => { it('converts the provided string to a number', () => { diff --git a/packages/cli/src/arg-groups/__tests__/c-cli-bigint-arg-group.test.ts b/packages/cli/src/arg-groups/__tests__/c-cli-bigint-arg-group.test.ts new file mode 100644 index 0000000..8d43d3e --- /dev/null +++ b/packages/cli/src/arg-groups/__tests__/c-cli-bigint-arg-group.test.ts @@ -0,0 +1,56 @@ +import { runAndCatch } from '@carnesen/run-and-catch'; +import { CCliUsageError } from '../../c-cli-usage-error'; +import { CCliBigintArgGroup } from '../c-cli-bigint-arg-group'; + +const description = 'foo bar baz'; +const hidden = true; +const placeholder = ''; +const optional = true; + +const argGroup = CCliBigintArgGroup.create({ + optional, + description, + hidden, + placeholder, +}); + +describe(CCliBigintArgGroup.name, () => { + it('returns `undefined` if args is `undefined` and no defaultValue has been provided', () => { + expect(argGroup.parse(undefined)).toBe(undefined); + }); + + it('parse returns the zeroth element of args', () => { + expect(argGroup.parse(['1'])).toBe(1n); + }); + + it('throws UsageError "expected just one" if args has more than one element', async () => { + const exception = await runAndCatch(argGroup.parse, ['0', '1']); + expect(exception).toBeInstanceOf(CCliUsageError); + expect(exception.message).toMatch(/expected a single/i); + expect(exception.message).toMatch(placeholder); + }); + + it('throws UsageError "expected a" if args is an empty array', async () => { + const exception = await runAndCatch(argGroup.parse, []); + expect(exception).toBeInstanceOf(CCliUsageError); + expect(exception.message).toMatch(/expected a/i); + expect(exception.message).toMatch(placeholder); + }); + + it('attaches config properties', () => { + expect(argGroup.description).toBe(description); + expect(argGroup.hidden).toBe(hidden); + expect(argGroup.placeholder).toBe(placeholder); + expect(argGroup.optional).toBe(optional); + }); + + it('config is optional', () => { + CCliBigintArgGroup.create(); + }); + + it('throws UsageError "expected a" if args is an empty array', async () => { + const exception = await runAndCatch(argGroup.parse, ['1n']); + expect(exception).toBeInstanceOf(CCliUsageError); + expect(exception.message).toBe('"1n" is not an integer'); + }); +}); diff --git a/packages/cli/src/arg-groups/__tests__/c-cli-bigint-array-arg-group.test.ts b/packages/cli/src/arg-groups/__tests__/c-cli-bigint-array-arg-group.test.ts new file mode 100644 index 0000000..3187a3e --- /dev/null +++ b/packages/cli/src/arg-groups/__tests__/c-cli-bigint-array-arg-group.test.ts @@ -0,0 +1,53 @@ +import { runAndCatch } from '@carnesen/run-and-catch'; +import { CCliUsageError } from '../../c-cli-usage-error'; +import { CCliBigintArrayArgGroup } from '../c-cli-bigint-array-arg-group'; + +const description = 'foo bar baz'; +const hidden = true; +const placeholder = ''; +const optional = true; + +const argGroup = CCliBigintArrayArgGroup.create({ + description, + hidden, + placeholder, + optional, +}); + +describe(CCliBigintArrayArgGroup.name, () => { + it('parse returns is args converted to numbers', () => { + const parsed = argGroup.parse(['0', '1', '2']); + if (!parsed) { + throw new Error('Expected parsed to return a value'); + } + expect(parsed[0] === 0n).toBe(true); + expect(parsed[1] === 1n).toBe(true); + expect(parsed[2] === 2n).toBe(true); + }); + + it('parse returns `undefined` if args is', () => { + expect(argGroup.parse(undefined)).toBe(undefined); + }); + + it('parse throws USAGE error "expected one or more" if args is an empty array', async () => { + const exception = await runAndCatch(argGroup.parse, []); + expect(exception).toBeInstanceOf(CCliUsageError); + expect(exception.message).toMatch(/expected one or more/i); + expect(exception.message).toMatch(placeholder); + }); + + it('attaches config properties', () => { + expect(argGroup.description).toBe(description); + expect(argGroup.hidden).toBe(hidden); + expect(argGroup.placeholder).toBe(placeholder); + expect(argGroup.optional).toBe(optional); + }); + + it('config is optional', () => { + CCliBigintArrayArgGroup.create(); + }); + + it('has a _suggest method always returning []', () => { + expect(argGroup._suggest([])).toEqual([]); + }); +}); diff --git a/packages/cli/src/arg-groups/__tests__/c-cli-string-choice-arg-group.test.ts b/packages/cli/src/arg-groups/__tests__/c-cli-string-choice-arg-group.test.ts index 3147fd0..230b407 100644 --- a/packages/cli/src/arg-groups/__tests__/c-cli-string-choice-arg-group.test.ts +++ b/packages/cli/src/arg-groups/__tests__/c-cli-string-choice-arg-group.test.ts @@ -1,8 +1,10 @@ import { runAndCatch } from '@carnesen/run-and-catch'; +import { renderCCliDescription } from '../../c-cli-description'; +import { CCliNoopColor } from '../../c-cli-noop-color'; import { CCliUsageError } from '../../c-cli-usage-error'; import { CCliStringChoiceArgGroup } from '../c-cli-string-choice-arg-group'; -const description = 'foo bar baz'; +const description = 'my special string choice'; const hidden = true; const placeholder = ''; const optional = true; @@ -51,4 +53,13 @@ describe(CCliStringChoiceArgGroup.name, () => { expect(await argGroup._suggest!([])).toEqual(['foo', 'bar']); expect(await argGroup._suggest!(['foo'])).toEqual([]); }); + + it('includes the choices in the rendered arg group description', () => { + const rendered = renderCCliDescription(argGroup.description, { + color: CCliNoopColor.create(), + }); + + expect(rendered).toMatch('Choices'); + expect(rendered).toMatch('foo'); + }); }); diff --git a/packages/cli/src/arg-groups/c-cli-bigint-arg-group.ts b/packages/cli/src/arg-groups/c-cli-bigint-arg-group.ts new file mode 100644 index 0000000..ae2aead --- /dev/null +++ b/packages/cli/src/arg-groups/c-cli-bigint-arg-group.ts @@ -0,0 +1,42 @@ +import { + CCliArgGroup, + CCliArgGroupOptions, + CCliParseArgs, +} from '../c-cli-arg-group'; +import { CCliConditionalValue } from '../c-cli-conditional-value'; +import { convertToBigint } from '../convert-to-bigint'; + +/** Options for {@link CCliBigintArgGroup} */ +export type CCliBigintArgGroupOptions = + CCliArgGroupOptions; + +export type CCliBigintArgGroupValue = + CCliConditionalValue; + +/** `number`-valued command-line argument group */ +export class CCliBigintArgGroup extends CCliArgGroup< + CCliBigintArgGroupValue, + Optional +> { + public parse( + args: CCliParseArgs, + ): CCliBigintArgGroupValue { + if (!args) { + return this.undefinedAsValue(); + } + + this.assertSingleArg(args); + + return convertToBigint(args[0]); + } + + /** {@link CCliBigintArgGroup} factory function */ + public static create( + options: CCliBigintArgGroupOptions = {}, + ): CCliBigintArgGroup { + return new CCliBigintArgGroup({ + placeholder: '', + ...options, + }); + } +} diff --git a/packages/cli/src/arg-groups/c-cli-bigint-array-arg-group.ts b/packages/cli/src/arg-groups/c-cli-bigint-array-arg-group.ts new file mode 100644 index 0000000..74cd880 --- /dev/null +++ b/packages/cli/src/arg-groups/c-cli-bigint-array-arg-group.ts @@ -0,0 +1,41 @@ +import { + CCliArgGroup, + CCliArgGroupOptions, + CCliParseArgs, +} from '../c-cli-arg-group'; +import { CCliConditionalValue } from '../c-cli-conditional-value'; +import { convertToBigint } from '../convert-to-bigint'; + +/** Options for {@link CCliBigintArrayArgGroup} a.k.a `ccli.bigintArray` */ +export type CCliBigintArrayArgGroupOptions = + CCliArgGroupOptions; + +export type CCliNumberArrayArgGroupValue = + CCliConditionalValue; + +/** `number[]`-valued argument group */ +export class CCliBigintArrayArgGroup< + Optional extends boolean, +> extends CCliArgGroup, Optional> { + public parse( + args: CCliParseArgs, + ): CCliNumberArrayArgGroupValue { + if (!args) { + return this.undefinedAsValue(); + } + + this.assertOneOrMoreArgs(args); + + return args.map(convertToBigint); + } + + /** {@link CCliBigintArrayArgGroup} factory function */ + public static create( + options: CCliBigintArrayArgGroupOptions = {}, + ): CCliBigintArrayArgGroup { + return new CCliBigintArrayArgGroup({ + placeholder: ' [...]', + ...options, + }); + } +} diff --git a/packages/cli/src/arg-groups/c-cli-number-arg-group.ts b/packages/cli/src/arg-groups/c-cli-number-arg-group.ts index 1c21290..c4be041 100644 --- a/packages/cli/src/arg-groups/c-cli-number-arg-group.ts +++ b/packages/cli/src/arg-groups/c-cli-number-arg-group.ts @@ -3,8 +3,7 @@ import { CCliArgGroupOptions, CCliParseArgs, } from '../c-cli-arg-group'; -import { convertToNumber } from '../util'; -import { CCliUsageError } from '../c-cli-usage-error'; +import { convertToNumber } from '../convert-to-number'; import { CCliConditionalValue } from '../c-cli-conditional-value'; /** Options for {@link CCliNumberArgGroup} */ @@ -27,9 +26,6 @@ export class CCliNumberArgGroup extends CCliArgGroup< } this.assertSingleArg(args); - if (args.length === 0) { - throw new CCliUsageError(`Expected a ${this.options.placeholder}`); - } return convertToNumber(args[0]); } diff --git a/packages/cli/src/arg-groups/c-cli-number-array-arg-group.ts b/packages/cli/src/arg-groups/c-cli-number-array-arg-group.ts index e542330..6273064 100644 --- a/packages/cli/src/arg-groups/c-cli-number-array-arg-group.ts +++ b/packages/cli/src/arg-groups/c-cli-number-array-arg-group.ts @@ -4,7 +4,7 @@ import { CCliParseArgs, } from '../c-cli-arg-group'; import { CCliConditionalValue } from '../c-cli-conditional-value'; -import { convertToNumber } from '../util'; +import { convertToNumber } from '../convert-to-number'; /** Options for {@link CCliNumberArrayArgGroup} a.k.a `ccli.numberArray` */ export type CCliNumberArrayArgGroupOptions = diff --git a/packages/cli/src/c-cli-arg-group.ts b/packages/cli/src/c-cli-arg-group.ts index 4c70078..2c336b5 100644 --- a/packages/cli/src/c-cli-arg-group.ts +++ b/packages/cli/src/c-cli-arg-group.ts @@ -38,9 +38,10 @@ export type InferParsedValueFromCCliArgGroup = ? NonNullable : never; -/** A group of adjacent command-line arguments - * @typeParam Value Type of the value returned by {@link CCliArgGroup.parse} - * @typeParam Optional If `true`, the type of `args` passed to +/** A group of consecutive command-line arguments parsed together into a single + * well-typed value + * @param Value Type of the value returned by {@link CCliArgGroup.parse} + * @param Optional If `true`, the type of `args` passed to * {@link CCliArgGroup.parse} includes `undefined`. */ export abstract class CCliArgGroup< Value = unknown, diff --git a/packages/cli/src/c-cli-conditional-value.ts b/packages/cli/src/c-cli-conditional-value.ts index c45b26d..d5a1d29 100644 --- a/packages/cli/src/c-cli-conditional-value.ts +++ b/packages/cli/src/c-cli-conditional-value.ts @@ -1,3 +1,5 @@ +/** Generic type helper for adding `| undefined` to the parsed value type when + * an argument group is optional */ export type CCliConditionalValue< Value, Optional extends boolean, diff --git a/packages/cli/src/c.ts b/packages/cli/src/c-functions.ts similarity index 82% rename from packages/cli/src/c.ts rename to packages/cli/src/c-functions.ts index f4283af..c78d69a 100644 --- a/packages/cli/src/c.ts +++ b/packages/cli/src/c-functions.ts @@ -1,6 +1,8 @@ /** The exports of this module are exported in the main index as the `c` * namespace object */ +import { CCliBigintArgGroup } from './arg-groups/c-cli-bigint-arg-group'; +import { CCliBigintArrayArgGroup } from './arg-groups/c-cli-bigint-array-arg-group'; import { CCliFlagArgGroup } from './arg-groups/c-cli-flag-arg-group'; import { CCliJsonArgGroup } from './arg-groups/c-cli-json-arg-group'; import { CCliNumberArgGroup } from './arg-groups/c-cli-number-arg-group'; @@ -25,6 +27,12 @@ export const commandGroup = CCliCommandGroup.create; // Argument group factories // +/** {@link CCliBigintArgGroup} factory function ({@link CCliBigintArgGroup.create}) */ +export const bigint = CCliBigintArgGroup.create; + +/** {@link CCliBigintArrayArgGroup} factory function ({@link CCliBigintArrayArgGroup.create}) */ +export const bigintArray = CCliBigintArrayArgGroup.create; + /** {@link CCliFlagArgGroup} factory function ({@link CCliFlagArgGroup.create}) */ export const flag = CCliFlagArgGroup.create; diff --git a/packages/cli/src/c-namespace.ts b/packages/cli/src/c-namespace.ts new file mode 100644 index 0000000..7144c09 --- /dev/null +++ b/packages/cli/src/c-namespace.ts @@ -0,0 +1,2 @@ +// The `c` namespace object is a compact, convenient API +export * as c from './c-functions'; diff --git a/packages/cli/src/convert-to-bigint.ts b/packages/cli/src/convert-to-bigint.ts new file mode 100644 index 0000000..af03351 --- /dev/null +++ b/packages/cli/src/convert-to-bigint.ts @@ -0,0 +1,19 @@ +import { CCliUsageError } from './c-cli-usage-error'; + +export function convertToBigint(rawValue: string): bigint { + let value: bigint | undefined; + if (rawValue.length > 0) { + try { + value = BigInt(rawValue); + } catch (exception) { + if (!(exception instanceof SyntaxError)) { + throw exception; + } + } + } + if (typeof value === 'undefined') { + throw new CCliUsageError(`"${rawValue}" is not an integer`); + } + + return value; +} diff --git a/packages/cli/src/util.ts b/packages/cli/src/convert-to-number.ts similarity index 78% rename from packages/cli/src/util.ts rename to packages/cli/src/convert-to-number.ts index 74852db..8c8ae33 100644 --- a/packages/cli/src/util.ts +++ b/packages/cli/src/convert-to-number.ts @@ -1,9 +1,5 @@ import { CCliUsageError } from './c-cli-usage-error'; -export function wrapInSquareBrackets(str: string): string { - return `[${str}]`; -} - export function convertToNumber(rawValue: string): number { let value = NaN; if (rawValue.length > 0) { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d41c270..5e745e7 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,7 +1,7 @@ /** This module is the main entrypoint for the **@carnesen/cli** package */ // The `c` namespace object is a compact, convenient API -export * as c from './c'; +export * from './c-namespace'; // The full API export * from './c-cli-command'; diff --git a/packages/cli/src/usage-for-command.ts b/packages/cli/src/usage-for-command.ts index 7e871fa..6bad8da 100644 --- a/packages/cli/src/usage-for-command.ts +++ b/packages/cli/src/usage-for-command.ts @@ -1,4 +1,4 @@ -import { wrapInSquareBrackets } from './util'; +import { wrapInSquareBrackets } from './wrap-in-square-brackets'; import { reWrapText } from './re-wrap-text'; import { TwoColumnTable, TwoColumnTableRow } from './two-column-table'; import { diff --git a/packages/cli/src/wrap-in-square-brackets.ts b/packages/cli/src/wrap-in-square-brackets.ts new file mode 100644 index 0000000..cb9152e --- /dev/null +++ b/packages/cli/src/wrap-in-square-brackets.ts @@ -0,0 +1,3 @@ +export function wrapInSquareBrackets(str: string): string { + return `[${str}]`; +} diff --git a/packages/cli/usage.md b/packages/cli/usage.md index e56a064..4bb3190 100644 --- a/packages/cli/usage.md +++ b/packages/cli/usage.md @@ -10,8 +10,11 @@ Here is a TypeScript Node.js CLI that does some basic arithmetic: ```typescript // src/multiply.ts + +// The "c" export is a compact API sufficient for most CLIs import { c } from '@carnesen/cli'; +// This CLI has just one command const multiplyCommand = c.command({ name: 'multiply', description: 'Multiply numbers and print the result', @@ -24,8 +27,8 @@ const multiplyCommand = c.command({ // Export for unit testing export const cli = c.cli(multiplyCommand); -// If this module is the entrypoint for this Node.js process if (require.main === module) { + // This module is the entrypoint for this Node.js process cli.run(); } ``` @@ -59,7 +62,7 @@ Only `` is required. This section of the documentation describes each o ### Command -A command defines an action `function` or `async function` together with its command-line arguments. For example, in `cloud users list`, the command is `list`. Some commands don't have any arguments. For example: +A command defines an `action` function together with its command-line arguments. For example, in `cloud users list`, the command is `list`. Some commands don't have any arguments. For example: ```typescript import { c } from '@carnesen/cli'; @@ -67,17 +70,17 @@ import { c } from '@carnesen/cli'; export const listUsersCommand = c.command({ name: 'list', async action() { - // Fetch all users + // Code here to fetch and return all users ... } }) ``` [[`c.command`]] is an alias for [[`CCliCommand.create`]], a factory function that returns a [[`CCliCommand`]]. -Most commands define arguments through the `positionalArgGroup`, `namedArgGroups`, and/or `doubleDashArgGroup` properties as described below. +Most commands declare arguments through the `positionalArgGroup`, `namedArgGroups`, and/or `doubleDashArgGroup` properties as described below. ### Command group -You can use command groups to organize the commands in your CLI. For example, in `cloud users list`, `cloud` and `users` are command groups and `list` is a command. Command groups are optional. Organize your CLI to suit your needs and taste: +You can use command groups to organize the commands in your CLI. For example, in `cloud users list`, `cloud` and `users` are command groups. Command groups are optional. Organize your CLI to suit your needs and taste: - `list-cloud-users`: No command groups - `cloud list-users`: A single command group @@ -100,6 +103,10 @@ export const rootCommandGroup = c.commandGroup({ [[`c.commandGroup`]] is an alias for [[`CCliCommandGroup.create`]], a factory function that returns a [[`CCliCommandGroup`]]. +### Argument group + +An _argument group_ declares a collection of zero or more consecutive command-line arguments parsed together as a single well-typed value. In the example "multiply 1 2 3", the `c.numberArray` arg group receives strings from the command-line `["1", "2", "3"]` and parses a `number[]` value `[1, 2, 3]`. Argument groups require at least one argument unless they're configured with `optional: true` in which case zero arguments is OK and the parsed value type includes `| undefined`. The `@carnesen/cli` package provides argument group factories for many parsed value types (`bigint`, `bigint[]`, `boolean`, `number`, `number[]`, `string`, `string[]`, `unknown` (JSON)), but you can also define your own custom argument group types by extending `CCliArgGroup`. + ### Positional arguments A command's `positionalArgGroup` receives all the command-line arguments after the command but before the first argument that starts with `--`. For example, in `cloud users delete carl karen --force`, the positional arguments are `carl` and `karen`. The argument group's parsed value is the `positionalValue` property of the action input: @@ -111,18 +118,19 @@ export const deleteCommand = c.command({ name: 'delete', positionalArgGroup: c.stringArray(), async action({ positionalValue: usernames }) { - // The CliStringArrayArgGroup parser returns an array of strings e.g. - // ["carl", "karen"] - // Delete the users ... + // ^^ `usernames` is typed `string[]` + // + // If we had provided `{optional: true}` to `c.stringArray()`, + // `usernames` would be typed `string[] | undefined` + // + // Code here to delete the users ... } }) ``` -By default, the [[`CCliStringArrayArgGroup`]] parser throws a [[`CCliUsageError`]] when no argument is provided. If `optional == true` and no argument is provided, `positionalValue` is `undefined`. - ### Named arguments -A command's `namedArgGroups` receives all the command-line arguments of the form `--name value`. The parsed values are passed into the command's action as a property called `namedValues` of shape `{ name: , ... }`. For example, in `cloud users list` let's add a command-line flag to filter out inactive users: +A command's `namedArgGroups` declares argument groups that receive arguments preceded by a named argument group separator `--name`. For example in `cloud users delete karen --force`, `--force` is a named argument group separator with no subsequent arguments. The `force` named argument group receives `undefined` (no argument provided). The parsed values are passed into the command's `action` as a property `namedValues` of shape `{ : , ... }`. Here's an example adding a command-line flag to `cloud users list`: ```typescript import { c } from '@carnesen/cli'; @@ -130,18 +138,22 @@ import { c } from '@carnesen/cli'; export const listCommand = c.command({ name: 'list', namedArgGroups: { - active: c.flag(), + active: c.flag({ + description: "If passed, only list active users", + }), }, async action({ namedValues: { active } }) { - // CliFlagArgGroup returns false unless --active was passed - // Fetch the users ... + // `active` is true` if --active was passed + // or `false` otherwise + // + // Code here to fetch the users ... } }) ``` ### Double-dash arguments -All command-line arguments after a lone `--` are passed to the command's `doubleDashArgGroup`. After the lone `--`, things like `--name` aren't interpreted as argument group separators. This is particularly useful for passing arguments through to another CLI like `do -- git --version`. The argument group's return value is the `doubleDashValue` property of the action's input: +All command-line arguments after a lone argument `--` (the double-dash argument group separator argument) are passed to the command's `doubleDashArgGroup`. After the lone `--`, things like `--name` aren't interpreted as named argument group separators. This is particularly useful for passing arguments through to another program. Suppose we make a CLI for executing commands on a remote system. We might try a command line like `exec-remote git --version`. The problem there is that `@carnesen/cli` interprets `--version` as a named argument group separator. Instead we want to treat it just as a string that gets passed to the `git` command on the remote system. Inspired by [npm run-script](https://docs.npmjs.com/cli/v9/commands/npm-run-script), we'll instead have the user do `exec-remote -- git --version`. The `doubleDashArgGroup`'s `action` receives the parsed double-dash arguments as `doubleDashValue`: ```typescript import { c } from '@carnesen/cli'; @@ -150,7 +162,7 @@ export const doCommand = c.command({ name: 'do', doubleDashArgGroup: c.stringArray(), async action({ doubleDashValue: args }) { - // Do stuff ... + // Code here to do stuff ... } }) ```