Skip to content

Commit

Permalink
Add arg group factories c.bigint and c.bigintArray
Browse files Browse the repository at this point in the history
Closes #203
  • Loading branch information
carnesen committed Jan 14, 2023
1 parent 30b431a commit 514e200
Show file tree
Hide file tree
Showing 30 changed files with 309 additions and 66 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16
18
2 changes: 1 addition & 1 deletion packages/cli-examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-examples/src/advanced-command-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -15,6 +16,7 @@ export const advancedCommandGroup = c.commandGroup({
echoWithColorCommand,
demoDoubleDashArgumentsCommand,
hidingCommandGroup,
multiplyIntegersCommand,
parseJsonCommand,
],
});
2 changes: 1 addition & 1 deletion packages/cli-examples/src/echo-command.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-examples/src/echo-hidden-command.ts
Original file line number Diff line number Diff line change
@@ -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: `
Expand Down
12 changes: 12 additions & 0 deletions packages/cli-examples/src/multiply-bigints-command.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
12 changes: 9 additions & 3 deletions packages/cli-examples/src/multiply-command.ts
100644 → 100755
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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();
}
19 changes: 0 additions & 19 deletions packages/cli-examples/src/multiply.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/cli-examples/src/throw-error-command.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
4 changes: 3 additions & 1 deletion packages/cli/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/__tests__/util.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = '<special>';
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');
});
});
Original file line number Diff line number Diff line change
@@ -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 = '<special>';
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([]);
});
});
Original file line number Diff line number Diff line change
@@ -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 = '<special>';
const optional = true;
Expand Down Expand Up @@ -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');
});
});
42 changes: 42 additions & 0 deletions packages/cli/src/arg-groups/c-cli-bigint-arg-group.ts
Original file line number Diff line number Diff line change
@@ -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<Optional extends boolean> =
CCliArgGroupOptions<Optional>;

export type CCliBigintArgGroupValue<Optional extends boolean> =
CCliConditionalValue<bigint, Optional>;

/** `number`-valued command-line argument group */
export class CCliBigintArgGroup<Optional extends boolean> extends CCliArgGroup<
CCliBigintArgGroupValue<Optional>,
Optional
> {
public parse(
args: CCliParseArgs<Optional>,
): CCliBigintArgGroupValue<Optional> {
if (!args) {
return this.undefinedAsValue();
}

this.assertSingleArg(args);

return convertToBigint(args[0]);
}

/** {@link CCliBigintArgGroup} factory function */
public static create<Optional extends boolean>(
options: CCliBigintArgGroupOptions<Optional> = {},
): CCliBigintArgGroup<Optional> {
return new CCliBigintArgGroup<Optional>({
placeholder: '<integer>',
...options,
});
}
}
41 changes: 41 additions & 0 deletions packages/cli/src/arg-groups/c-cli-bigint-array-arg-group.ts
Original file line number Diff line number Diff line change
@@ -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<Optional extends boolean = boolean> =
CCliArgGroupOptions<Optional>;

export type CCliNumberArrayArgGroupValue<Optional extends boolean> =
CCliConditionalValue<bigint[], Optional>;

/** `number[]`-valued argument group */
export class CCliBigintArrayArgGroup<
Optional extends boolean,
> extends CCliArgGroup<CCliNumberArrayArgGroupValue<Optional>, Optional> {
public parse(
args: CCliParseArgs<Optional>,
): CCliNumberArrayArgGroupValue<Optional> {
if (!args) {
return this.undefinedAsValue();
}

this.assertOneOrMoreArgs(args);

return args.map(convertToBigint);
}

/** {@link CCliBigintArrayArgGroup} factory function */
public static create<Optional extends boolean>(
options: CCliBigintArrayArgGroupOptions<Optional> = {},
): CCliBigintArrayArgGroup<Optional> {
return new CCliBigintArrayArgGroup<Optional>({
placeholder: '<integer0> [...]',
...options,
});
}
}
6 changes: 1 addition & 5 deletions packages/cli/src/arg-groups/c-cli-number-arg-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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} */
Expand All @@ -27,9 +26,6 @@ export class CCliNumberArgGroup<Optional extends boolean> extends CCliArgGroup<
}

this.assertSingleArg(args);
if (args.length === 0) {
throw new CCliUsageError(`Expected a ${this.options.placeholder}`);
}

return convertToNumber(args[0]);
}
Expand Down
Loading

0 comments on commit 514e200

Please sign in to comment.