0.13.0 ā€¢ Published 1 year ago

cmd-ts v0.13.0

Weekly downloads
460
License
MIT
Repository
-
Last release
1 year ago

cmd-ts

šŸ’» A type-driven command line argument parser, with awesome error reporting šŸ¤¤

Not all command line arguments are strings, but for some reason, our CLI parsers force us to use strings everywhere. šŸ¤” cmd-ts is a fully-fledged command line argument parser, influenced by Rust's clap and structopt:

šŸ¤© Awesome autocomplete, awesome safeness

šŸŽ­ Decode your own custom types from strings with logic and context-aware error handling

šŸŒ² Nested subcommands, composable API

Basic usage

import { command, run, string, number, positional, option } from 'cmd-ts';

const cmd = command({
  name: 'my-command',
  description: 'print something to the screen',
  version: '1.0.0',
  args: {
    number: positional({ type: number, displayName: 'num' }),
    message: option({
      long: 'greeting',
      type: string,
    }),
  },
  handler: (args) => {
    args.message; // string
    args.number; // number
    console.log(args);
  },
});

run(cmd, process.argv.slice(2));

command(arguments)

Creates a CLI command.

Decoding custom types from strings

Not all command line arguments are strings. You sometimes want integers, UUIDs, file paths, directories, globs...

Note: this section describes the ReadStream type, implemented in ./src/example/test-types.ts

Let's say we're about to write a cat clone. We want to accept a file to read into stdout. A simple example would be something like:

// my-app.ts

import { command, run, positional, string } from 'cmd-ts';

const app = command({
  /// name: ...,
  args: {
    file: positional({ type: string, displayName: 'file' }),
  },
  handler: ({ file }) => {
    // read the file to the screen
    fs.createReadStream(file).pipe(stdout);
  },
});

// parse arguments
run(app, process.argv.slice(2));

That works okay. But we can do better. In which ways?

  • Error handling is out of the command line argument parser context, and in userland, making things less consistent and pretty.
  • It shows we lack composability and encapsulation ā€” and we miss a way to distribute shared "command line" behavior.

What if we had a way to get a Stream out of the parser, instead of a plain string? This is where cmd-ts gets its power from, custom type decoding:

// ReadStream.ts

import { Type } from 'cmd-ts';
import fs from 'fs';

// Type<string, Stream> reads as "A type from `string` to `Stream`"
const ReadStream: Type<string, Stream> = {
  async from(str) {
    if (!fs.existsSync(str)) {
      // Here is our error handling!
      throw new Error('File not found');
    }

    return fs.createReadStream(str);
  },
};

Now we can use (and share) this type and always get a Stream, instead of carrying the implementation detail around:

// my-app.ts

import { command, run, positional } from 'cmd-ts';

const app = command({
  // name: ...,
  args: {
    stream: positional({ type: ReadStream, displayName: 'file' }),
  },
  handler: ({ stream }) => stream.pipe(process.stdout),
});

// parse arguments
run(app, process.argv.slice(2));

Encapsulating runtime behaviour and safe type conversions can help us with awesome user experience:

  • We can throw an error when the file is not found
  • We can try to parse the string as a URI and check if the protocol is HTTP, if so - make an HTTP request and return the body stream
  • We can see if the string is -, and when it happens, return process.stdin like many Unix applications

And the best thing about it ā€” everything is encapsulated to an easily tested type definition, which can be easily shared and reused. Take a look at io-ts-types, for instance, which has types like DateFromISOString, NumberFromString and more, which is something we can totally do.

Inspiration

This project was previously called clio-ts, because it was based on io-ts. This is no longer the case, because I want to reduce the dependency count and mental overhead. I might have a function to migrate types between the two.

@infinitebrahmanuniverse/nolb-cmdwechaty@everything-registry/sub-chunk-1343@pp-finder/clicaveautask-graph-processortask-graph-protocoltablemark-cliwaka-pmwebdatacommonswechaty_tempwechaty-labwechaty-tokenworkspace-envshacl2tsremasteredserve-hpubthe-real-doctestinfluence-asset-export@s21toolkit/cli@s21toolkit/lintminauthgraphql-rss-parserimg-catgoogle-photos-migratejtz-time-tracker-utilskavok8testk8test-cli-logicgloblist-packerlphistory2pp-finderprint-openapipkl-typescriptpatch-semantic-release-npm-for-msrstalwart-ascii-fontsureksidecarspa-clienttrinoor_genver@api-ts/openapi-generator@antribute/backend-core@begit/cli@basemaps/sprites@binsee/sidecar@bisonai/orakl-cli@cogeotiff/cli@deptdash/cdk-webappyamas@flycode-org/tsfind@gerrit0/broken-link-checker@l2beat/discovery@lo1tuma/parallel-typescript@lunasec/cli@msessa/find-s3-objects-by-acl@mat3ra/standata@nebrazkp/upa@nirtamir-cli/core@nirtamir-cli/utils@nirtamir2/cli@nk11/ts-json-yaml@era-ci/core@eyeseetea/d2-logger@juzi/wechaty@journeyapps/ts-browser-compat@ignisda/athalar@pkl-community/pkl-typescript@qlite/cli@ryanwolhuter/ui-utils@s21toolkit/introspector@s21toolkit/shared@salesway/pgts@salesway/docker-tools@ryanke/violet@stavalfi/ci@phryneas/process-profile-sourcemaps@packtory/cli@p-lang/plang@sv-oss/changelog-autotagger@sv-oss/find-s3-objects-by-acl@solid-cli/core@solid-cli/utils@tahini/nc@sirhall/shared_worker@strwatcher/suipta@replit/river-codegen@crx-tools/create-app@crx-tools/scripts@csllc/iot-utilities@d-fischer/crowd@cotar/cli@bencemanyoki/borm@bencemanyoki/initp@bemedev/cli-test@bemedev/tsd-cli@thomasgormley/dev-cli@thomasskk/pg-migrateamtoolaijinkela-wechatybuttercup-to-1password
0.13.0

1 year ago

0.12.1

2 years ago

0.12.0

2 years ago

0.11.0

3 years ago

0.10.2

3 years ago

0.10.1

3 years ago

0.10.0

3 years ago

0.9.0

3 years ago

0.8.0

3 years ago

0.7.0

3 years ago

0.6.9

4 years ago

0.6.8

4 years ago

0.6.7

4 years ago

0.6.6

4 years ago

0.6.5

4 years ago

0.6.4

5 years ago

0.6.3

5 years ago

0.6.2

5 years ago

0.6.1

5 years ago

0.6.0

5 years ago

0.5.0

5 years ago

0.5.1

5 years ago

0.4.1

5 years ago

0.4.0

5 years ago

0.3.2

5 years ago

0.3.1

5 years ago

0.3.0

5 years ago

0.3.0-beta-2

5 years ago