Git Product home page Git Product logo

sade's Introduction

sade CI licenses

Smooth (CLI) Operator ๐ŸŽถ

Sade is a small but powerful tool for building command-line interface (CLI) applications for Node.js that are fast, responsive, and helpful!

It enables default commands, git-like subcommands, option flags with aliases, default option values with type-casting, required-vs-optional argument handling, command validation, and automated help text generation!

Your app's UX will be as smooth as butter... just like Sade's voice. ๐Ÿ˜‰

Install

$ npm install --save sade

Usage

Input:

#!/usr/bin/env node

const sade = require('sade');

const prog = sade('my-cli');

prog
  .version('1.0.5')
  .option('--global, -g', 'An example global flag')
  .option('-c, --config', 'Provide path to custom config', 'foo.config.js');

prog
  .command('build <src> <dest>')
  .describe('Build the source directory. Expects an `index.js` entry file.')
  .option('-o, --output', 'Change the name of the output file', 'bundle.js')
  .example('build src build --global --config my-conf.js')
  .example('build app public -o main.js')
  .action((src, dest, opts) => {
    console.log(`> building from ${src} to ${dest}`);
    console.log('> these are extra opts', opts);
  });

prog.parse(process.argv);

Output:

$ my-cli --help

  Usage
    $ my-cli <command> [options]

  Available Commands
    build    Build the source directory.

  For more info, run any command with the `--help` flag
    $ my-cli build --help

  Options
    -v, --version    Displays current version
    -g, --global     An example global flag
    -c, --config     Provide path to custom config  (default foo.config.js)
    -h, --help       Displays this message


$ my-cli build --help

  Description
    Build the source directory.
    Expects an `index.js` entry file.

  Usage
    $ my-cli build <src> [options]

  Options
    -o, --output    Change the name of the output file  (default bundle.js)
    -g, --global    An example global flag
    -c, --config    Provide path to custom config  (default foo.config.js)
    -h, --help      Displays this message

  Examples
    $ my-cli build src build --global --config my-conf.js
    $ my-cli build app public -o main.js

Tips

  • Define your global/program-wide version, options, description, and/or examples first.
    Once you define a Command, you can't access the global-scope again.

  • Define all commands & options in the order that you want them to appear.
    Sade will not mutate or sort your CLI for you. Global options print before local options.

  • Required arguments without values will error & exit
    An Insufficient arguments! error will be displayed along with a help prompt.

  • Don't worry about manually displaying help~!
    Your help text is displayed automatically... including command-specific help text!

  • Automatic default/basic patterns
    Usage text will always append [options] & --help and --version are done for you.

  • Only define what you want to display!
    Help text sections (example, options, etc) will only display if you provide values.

Subcommands

Subcommands are defined & parsed like any other command! When defining their usage, everything up until the first argument ([foo] or <foo>) is interpreted as the command string.

They should be defined in the order that you want them to appear in your general --help output.

Lastly, it is not necessary to define the subcommand's "base" as an additional command. However, if you choose to do so, it's recommended that you define it first for better visibility.

const prog = sade('git');

// Not necessary for subcommands to work, but it's here anyway!
prog
  .command('remote')
  .describe('Manage set of tracked repositories')
  .action(opts => {
    console.log('~> Print current remotes...');
  });

prog
  .command('remote add <name> <url>', 'Demo...')
  .action((name, url, opts) => {
    console.log(`~> Adding a new remote (${name}) to ${url}`);
  });

prog
  .command('remote rename <old> <new>', 'Demo...')
  .action((old, nxt, opts) => {
    console.log(`~> Renaming from ${old} to ${nxt}~!`);
  });

Single Command Mode

In certain circumstances, you may only need sade for a single-command CLI application.

Note: Until v1.6.0, this made for an awkward pairing.

To enable this, you may make use of the isSingle argument. Doing so allows you to pass the program's entire usage text into the name argument.

With "Single Command Mode" enabled, your entire binary operates as one command. This means that any prog.command calls are disallowed & will instead throw an Error. Of course, you may still define a program version, a description, an example or two, and declare options. You are customizing the program's attributes as a whole.*

* This is true for multi-command applications, too, up until your first prog.command() call!

Example

Let's reconstruct sirv-cli, which is a single-command application that (optionally) accepts a directory from which to serve files. It also offers a slew of option flags:

sade('sirv [dir]', true)
  .version('1.0.0')
  .describe('Run a static file server')
  .example('public -qeim 31536000')
  .example('--port 8080 --etag')
  .example('my-app --dev')
  .option('-D, --dev', 'Enable "dev" mode')
  .option('-e, --etag', 'Enable "Etag" header')
  // There are a lot...
  .option('-H, --host', 'Hostname to bind', 'localhost')
  .option('-p, --port', 'Port to bind', 5000)
  .action((dir, opts) => {
    // Program handler
  })
  .parse(process.argv);

When sirv --help is run, the generated help text is trimmed, fully aware that there's only one command in this program:

  Description
    Run a static file server

  Usage
    $ sirv [dir] [options]

  Options
    -D, --dev        Enable "dev" mode
    -e, --etag       Enable "Etag" header
    -H, --host       Hostname to bind  (default localhost)
    -p, --port       Port to bind  (default 5000)
    -v, --version    Displays current version
    -h, --help       Displays this message

  Examples
    $ sirv public -qeim 31536000
    $ sirv --port 8080 --etag
    $ sirv my-app --dev

Command Aliases

Command aliases are alternative names (aliases) for a command. They are often used as shortcuts or as typo relief!

The aliased names do not appear in the general help text.
Instead, they only appear within the Command-specific help text under an "Aliases" section.

Limitations

  • You cannot assign aliases while in Single Command Mode
  • You cannot call prog.alias() before defining any Commands (via prog.commmand())
  • You, the developer, must keep track of which aliases have already been used and/or exist as Command names

Example

Let's reconstruct the npm install command as a Sade program:

sade('npm')
  // ...
  .command('install [package]', 'Install a package', {
    alias: ['i', 'add', 'isntall']
  })
  .option('-P, --save-prod', 'Package will appear in your dependencies.')
  .option('-D, --save-dev', 'Package will appear in your devDependencies.')
  .option('-O, --save-optional', 'Package will appear in your optionalDependencies')
  .option('-E, --save-exact', 'Save exact versions instead of using a semver range operator')
  // ...

When we run npm --help we'll see this general help text:

  Usage
    $ npm <command> [options]

  Available Commands
    install    Install a package

  For more info, run any command with the `--help` flag
    $ npm install --help

  Options
    -v, --version    Displays current version
    -h, --help       Displays this message

When we run npm install --help โ€” or the help flag with any of install's aliases โ€” we'll see this command-specific help text:

  Description
    Install a package

  Usage
    $ npm install [package] [options]

  Aliases
    $ npm i
    $ npm add
    $ npm isntall

  Options
    -P, --save-prod        Package will appear in your dependencies.
    -D, --save-dev         Package will appear in your devDependencies.
    -O, --save-optional    Package will appear in your optionalDependencies
    -E, --save-exact       Save exact versions instead of using a semver range operator
    -h, --help             Displays this message

API

sade(name, isSingle)

Returns: Program

Returns your chainable Sade instance, aka your Program.

name

Type: String
Required: true

The name of your Program / binary application.

isSingle

Type: Boolean
Default: name.includes(' ');

If your Program is meant to have only one command.
When true, this simplifies your generated --help output such that:

  • the "root-level help" is your only help text
  • the "root-level help" does not display an Available Commands section
  • the "root-level help" does not inject $ name <command> into the Usage section
  • the "root-level help" does not display For more info, run any command with the --help flag text

You may customize the Usage of your command by modifying the name argument directly.
Please read Single Command Mode for an example and more information.

Important: Whenever name includes a custom usage, then isSingle is automatically assumed and enforced!

prog.command(usage, desc, opts)

Create a new Command for your Program. This changes the current state of your Program.

All configuration methods (prog.describe, prog.action, etc) will apply to this Command until another Command has been created!

usage

Type: String

The usage pattern for your current Command. This will be included in the general or command-specific --help output.

Required arguments are wrapped with < and > characters; for example, <foo> and <bar>.

Optional arguments are wrapped with [ and ] characters; for example, [foo] and [bar].

All arguments are positionally important, which means they are passed to your current Command's handler function in the order that they were defined.

When optional arguments are defined but don't receive a value, their positionally-equivalent function parameter will be undefined.

Important: You must define & expect required arguments before optional arguments!

sade('foo')

  .command('greet <adjective> <noun>')
  .action((adjective, noun, opts) => {
    console.log(`Hello, ${adjective} ${noun}!`);
  })

  .command('drive <vehicle> [color] [speed]')
  .action((vehicle, color, speed, opts) => {
    let arr = ['Driving my'];
    arr.push(color ? `${color} ${vehicle}` : vehicle);
    speed && arr.push(`at ${speed}`);
    opts.yolo && arr.push('...YOLO!!');
    let str = arr.join(' ');
    console.log(str);
  });
$ foo greet beautiful person
# //=> Hello, beautiful person!

$ foo drive car
# //=> Driving my car

$ foo drive car red
# //=> Driving my red card

$ foo drive car blue 100mph --yolo
# //=> Driving my blue car at 100mph ...YOLO!!

desc

Type: String
Default: ''

The Command's description. The value is passed directly to prog.describe.

opts

Type: Object
Default: {}

opts.alias

Type: String|Array

Optionally define one or more aliases for the current Command.
When declared, the opts.alias value is passed directly to the prog.alias method.

// Program A is equivalent to Program B
// ---

const A = sade('bin')
  .command('build', 'My build command', { alias: 'b' })
  .command('watch', 'My watch command', { alias: ['w', 'dev'] });

const B = sade('bin')
  .command('build', 'My build command').alias('b')
  .command('watch', 'My watch command').alias('w', 'dev');
opts.default

Type: Boolean

Manually set/force the current Command to be the Program's default command. This ensures that the current Command will run if no command was specified.

Important: If you run your Program without a Command and without specifying a default command, your Program will exit with a No command specified error.

const prog = sade('greet');

prog.command('hello');
//=> only runs if :: `$ greet hello`

// $ greet
//=> error: No command specified.

prog.command('howdy', '', { default:true });
//=> runs as `$ greet` OR `$ greet howdy`

// $ greet
//=> runs 'howdy' handler

// $ greet foobar
//=> error: Invalid command

prog.describe(text)

Add a description to the current Command.

text

Type: String|Array

The description text for the current Command. This will be included in the general or command-specific --help output.

Internally, your description will be separated into an Array of sentences.

For general --help output, only the first sentence will be displayed. However, all sentences will be printed for command-specific --help text.

Note: Pass an Array if you don't want internal assumptions. However, the first item is always displayed in general help, so it's recommended to keep it short.

prog.alias(...names)

Define one or more aliases for the current Command.

Important: An error will be thrown if:
1) the program is in Single Command Mode; or
2) prog.alias is called before any prog.command.

names

Type: String

The list of alternative names (aliases) for the current Command.
For example, you may want to define shortcuts and/or common typos for the Command's full name.

Important: Sade does not check if the incoming names are already in use by other Commands or their aliases.
During conflicts, the Command with the same name is given priority, otherwise the first Command (according to Program order) with name as an alias is chosen.

The prog.alias() is append-only, so calling it multiple times within a Command context will keep all aliases, including those initially passed via opts.alias.

sade('bin')
  .command('hello <name>', 'Greet someone by their name', {
    alias: ['hey', 'yo']
  })
  .alias('hi', 'howdy')
  .alias('hola', 'oi');
//=> hello aliases: hey, yo, hi, howdy, hola, oi

prog.action(handler)

Attach a callback to the current Command.

handler

Type: Function

The function to run when the current Command is executed.

Its parameters are based (positionally) on your Command's usage definition.

All options, flags, and extra/unknown values are included as the last parameter.

Note: Optional arguments are also passed as parameters & may be undefined!

sade('foo')
  .command('cp <src> <dest>')
  .option('-f, --force', 'Overwrite without confirmation')
  .option('-c, --clone-dir', 'Copy files to additional directory')
  .option('-v, --verbose', 'Enable verbose output')
  .action((src, dest, opts) => {
    console.log(`Copying files from ${src} --> ${dest}`);
    opts.c && console.log(`ALSO copying files from ${src} --> ${opts['clone-dir']}`);
    console.log('My options:', opts);
  })

// $ foo cp original my-copy -v
//=> Copying files from original --> my-copy
//=> My options: { _:[], v:true, verbose:true }

// $ foo cp original my-copy --clone-dir my-backup
//=> Copying files from original --> my-copy
//=> ALSO copying files from original --> my-backup
//=> My options: { _:[], c:'my-backup', 'clone-dir':'my-backup' }

prog.example(str)

Add an example for the current Command.

str

Type: String

The example string to add. This will be included in the general or command-specific --help output.

Note: Your example's str will be prefixed with your Program's name.

prog.option(flags, desc, value)

Add an Option to the current Command.

flags

Type: String

The Option's flags, which may optionally include an alias.

You may use a comma (,) or a space ( ) to separate the flags.

Note: The short & long flags can be declared in any order. However, the alias will always be displayed first.

Important: If using hyphenated flag names, they will be accessible as declared within your action() handler!

prog.option('--global'); // no alias
prog.option('-g, --global'); // alias first, comma
prog.option('--global -g'); // alias last, space
// etc...

desc

Type: String

The description for the Option.

value

Type: String

The default value for the Option.

Flags and aliases, if parsed, are true by default. See mri for more info.

Note: You probably only want to define a default value if you're expecting a String or Number value type.

If you do pass a String or Number value type, your flag value will be casted to the same type. See mri#options.default for info~!

prog.version(str)

The --version and -v flags will automatically output the Program version.

str

Type: String
Default: 0.0.0

The new version number for your Program.

Note: Your Program version is 0.0.0 until you change it.

prog.parse(arr, opts)

Parse a set of CLI arguments.

arr

Type: Array

Your Program's process.argv input.

Important: Do not .slice(2)! Doing so will break parsing~!

opts

Type: Object
Default: {}

Additional process.argv parsing config. See mri's options for details.

Important: These values override any internal values!

prog
  .command('hello')
  .option('-f, --force', 'My flag');
//=> currently has alias pair: f <--> force

prog.parse(process.argv, {
  alias: {
    f: ['foo', 'fizz']
  },
  default: {
    abc: 123
  }
});
//=> ADDS alias pair: f <--> foo
//=> REMOVES alias pair: f <--> force
//=> ADDS alias pair: f <--> fizz
//=> ADDS default: abc -> 123 (number)

opts.unknown

Type: Function
Default: undefined

Callback to run when an unspecified option flag has been found. This is passed directly to mri.

Your handler will receive the unknown flag (string) as its only argument.
You may return a string, which will be used as a custom error message. Otherwise, a default message is displayed.

sade('sirv')
  .command('start [dir]')
  .parse(process.argv, {
    unknown: arg => `Custom error message: ${arg}`
  });

/*
$ sirv start --foobar

  ERROR
    Custom error message: --foobar

  Run `$ sirv --help` for more info.
*/

opts.lazy

Type: Boolean
Default: false

If true, Sade will not immediately execute the action handler. Instead, parse() will return an object of { name, args, handler } shape, wherein the name is the command name, args is all arguments that would be passed to the action handler, and handler is the function itself.

From this, you may choose when to run the handler function. You also have the option to further modify the args for any reason, if needed.

let { name, args, handler } = prog.parse(process.argv, { lazy:true });
console.log('> Received command: ', name);

// later on...
handler.apply(null, args);

prog.help(cmd)

Manually display the help text for a given command. If no command name is provided, the general/global help is printed.

Your general and command-specific help text is automatically attached to the --help and -h flags.

Note: You don't have to call this directly! It's automatically run when you bin --help

cmd

Type: String
Default: null

The name of the command for which to display help. Otherwise displays the general help.

License

MIT ยฉ Luke Edwards

sade's People

Contributors

agilgur5 avatar lukeed avatar marvinhagemeister avatar mrwest808 avatar ryanccn avatar vutran avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sade's Issues

Cannot lazy parse and support `help`/`version` at same time

sade/src/index.js

Lines 150 to 152 in 1cdf3e4

// show main help if relied on "default" for multi-cmd
if (argv.help) return this.help(!isSingle && !isVoid && name);
if (argv.version) return this._version();

The early return here will break the following:

const prog = sade('cool-cli', true).version('1.0.0').describe('Some cool CLI.')

const {args} = prog.parse(process.argv, {lazy: true})

if (args.something) {
  // do stuff
}

This is inconsistent with the documentation:

sade/readme.md

Lines 638 to 653 in 1cdf3e4

#### opts.lazy
Type: `Boolean`<br>
Default: `false`
If true, Sade will not immediately execute the `action` handler. Instead, `parse()` will return an object of `{ name, args, handler }` shape, wherein the `name` is the command name, `args` is all arguments that _would be_ passed to the action handler, and `handler` is the function itself.
From this, you may choose when to run the `handler` function. You also have the option to further modify the `args` for any reason, if needed.
```js
let { name, args, handler } = prog.parse(process.argv, { lazy:true });
console.log('> Received command: ', name);
// later on...
handler.apply(null, args);
```

One solution may be to return:

{
  args: [],
  name: '',
  handler: undefined,
  helpOrVersion: true // indicates the user passed one of the options
}

Types in Output of Options

I couldn't find any way to display/specify which types arguments take in. For instance, something like this would be helpful.

$ my-cli --help

  Options
    -c, --config     Provide path to custom config  (default foo.config.js) [Type: String]

Sade says it's in single mode when single mode is explicitly disabled

CleanShot 2021-12-17 at 10 41 41

My code:

const app = async () => {
  consola.log(tagline)
  const bus = new EventEmitter()

  try {
    const cli = sade('udd <command> [options]', false)

    // configure the cli options
    cli
      .version('0.1.0')
      .option('-p, --project', 'Google Cloud Project ID')
      .option('-y, --confirm', 'Non-iteractive, confirm all actions')

    rollback(cli, bus)

    cli.parse(process.argv)
  } catch (err) {
    // fail gracefully
    consola.error(err)
    process.exit(1)
  }
}

The result, which I also get if I leave isSingle undefined:

 ERROR  Disable "single" mode to add commands                                     10:31:25

  at Sade.command (node_modules/sade/lib/index.js:25:10)
  at rollback (src/lib/rollback.ts:14:6)
  at app (src/main.ts:21:19)
  at Object.<anonymous> (src/main.ts:31:1)
  at Module._compile (internal/modules/cjs/loader.js:1085:14)
  at Module.m._compile (node_modules/ts-node/src/index.ts:1371:23)
  at Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
  at Object.require.extensions.<computed> [as .ts] (node_modules/ts-node/src/index.ts:1374:12)
  at Module.load (internal/modules/cjs/loader.js:950:32)
  at Function.Module._load (internal/modules/cjs/loader.js:790:14)

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

This works, though:

cli.single = true

The option method does not add the aliases to `cmd.default` object

Heya.

I know that in that cmd or "command object" we have both .alias and .default properties. But it isn't easy to merged them both in one single object.

For example

cli
  .command('foo', 'abc de')
  .option('-f, --format', 'some formatting', 'bar')
  .option('--fix', 'barry dazzle', true)

so they looks like

alias: { f: [ 'format' ] }
default: { fix: true, format: 'bar' }

So, it isn't possible to detect what default has what aliases. Would it be better the keys of alias object to be the name format in that case and its value to be its aliases? I believe that defining aliases in minimist (and probably mri) can accept both variants.

I want to merge them in one final object that is the same as "argv", same as the parsed result of mri/minimist, except the _.

What about required thing for some option? Possible?

Didn't even tried, but it's not documented anyway, so this issue could be used for such PR.

I'm talking about somehting like

mycli cmd --registry <url> --token=<authToken>

In above, <url> is required for the registry option so it should throw with "Insufficient arguments for that option" or something like that. To be the same as when there is required args for some command.

As i looked over the source code before some days, i didn't noticed that it is possible, because of the simple parsing in $.parse utility, so.

[feature request] Alias for parent command should apply to subcommands

prog
  .command('foo')
  .describe('Top level command')
  .alias('f')
  .action(() => console.log('see `app foo --help`'))

prog
  .command('foo bar')
  .describe('A subcommand')
  .action(() => console.log('did a thing'))

prog
  .command('foo baz <a>')
  .describe('Another subcommand')
  .action((a) => console.log('did another thing', a))

Given the above, each of the following should work:

  • prog foo
  • prog foo bar
  • prog foo baz 1
  • prog f
  • prog f bar
  • prog f baz 1

Is it possible to use 'generic' value as the default command?

Let me try to explain.

I have this CLI:

my-cli something-else
my-cli -h

I want to be able to use what I want as the first argument. Something like this:

// Instead of:
.command('build <entry>')
// just:
.command('<entry>')

is that clear?

A more real example:

prog
    .command('<entry>')
    .describe('List your entries')
    .option('a, --add')
    .action((entry, opts) => {
      console.log('You said:')
      console.log(entry)
     })

I need to be able to run my-cli hello and not only using my-cli a hello

Tab-Completion for command arguments

Would be awesome if command arguments could be tab-completed. For example with the command prog.command('install '), you could specify some options for , so let's say Wordpress or Drupal, and when I'm typing out the command if I type Wo and press tab then it would autocomplete to Wordpress. Similar to Vorpal's autocomplete

Feature request: global/universal actions

These might better be defined as 'hooks'.

I spent a while trying to create my own prehook on actions, something that would get run against every action. Something like the following:

old_action = prog.action.bind(prog);
prog.action = function () {
    always_gets_called_on_every_action();
    return old_action.apply(prog, arguments)
}

The problem is there's still no way to get at the opts argument in this prog.action overwrite, which is what I need.

Use case: change the config file for every action (-c, --config) without manually baking it into every action.

I decided to stop wasting time on this and make hooks or something like that an actual feature request, or at least a point of conversation.

Thanks.

Parsed args (argv/opts) always to be the 1st param of action

Heya. Currently you always get the argv (what's called "opts" everywhere in readme - i don't like that) from mri as last parameter of the action function. It's pretty much not comfortable in anyway.

It not make sense to be last. It is the only guaranteed param that can exist, so the best position of is to be always first. Working with variable position arguments is not feeling good any way.

Adding option for that would work too.

Typescript errors after upgrade

After upgrading to the latest version of Sade, I'm getting the following type error on my actions:

Argument of type '(service: string, opts: Options) => Promise<void>' is not assignable to parameter of type 
'Handler<[service: string]>'.\n  Types of parameters 'opts' and 'args_1' are incompatible.\n    
Type 'Argv<Default>' has no properties in common with type 'Options'.

Pass Optional params thru to Action

Just like <required> patterns, should pass thru to action() as positionally-relevant parameters.

Unlike <required>, they shouldn't throw (#4) if they're empty; however, they should still pass a falsey value (null or undefined) to the function so User knows.

[Feature Request]: suggest matching commands if the user mistypes

Say we've the following CLI definition:-

my-cli <command> [options]

init - Initialize
serve - serve the project
generate - scaffold out a new project

-V, version
-h, help information

If we're to go with commander.js, tj/commander.js#1015 (comment) would make it possible. While, yargs is shipped with a dedicated recommendCommands() method as part of the API. It would be great if the feature was implemented here with sade as well.

Sade parses past `--`

If an optional argument is defined sade will parse after --

import sade from 'sade'
sade('prog')
  .command('build [config]').action(console.log)
  .parse(['node', 'cli.js', 'build', '--', 'abc', 'xyz'])

Results in "abc", {_: ["xyz"]}

But I'd expect it to parse undefined, {_: ["abc', "xyz"]}

expose the `error`utility (in src/utils.js)

The error utility is used internally for cases like this:

$ node index.js --abc

  ERROR
    Invalid option: --abc

  Run `$ my-cli-app --help` for more info.

https://github.com/lukeed/sade/blob/master/src/utils.js#L79-L84

But even if all the options are accepted by sade, they might not be valid (example: 2 options might conflict with each other). The user is responsible for these app-specific validations.

It would then be useful if sade could provide a prog.error(message).

Thanks for considering this.

UPDATE: I've made a fork for personal use where this feature is implemented. Is there any interest in a PR?

paulovieira@cd26309

UPDATE 2: below is the sade template that I'm using for my cli applications. Notice the call to the new prog.error utility (after the call to validateArgs), which would output this:

$ node cli.js --param1=aaa

  ERROR
    something is wrong

  Run `$ my-cli --help` for more info.
#!/usr/bin/env node

let Sade = require('sade');

// 1 - setup sade 

let isSingleCommand = true;
let prog = Sade('my-cli', isSingleCommand);

prog
  .example('--param1=123')
  .example('--param2=abc')

  .option('--param1', 'Help for param1')
  .option('--param2', 'Help for param2')

  .action(({ param1, param2, _ }) => {

    // do stuff...
  });

// 2 - lazy parse + validation

let parseOptions = {
  lazy: true,
  unknown: arg => `Unknown option: ${arg}`
}

let { name, args, handler } = prog.parse(process.argv, parseOptions);
let { isValid, message } = validateArgs(args);

// 3 - proceed to the handler added in .action() or abort with a user-friendly message

if (isValid) { 
  handler.apply(null, args);
}
else {
  prog.error(prog.bin, message);
}

function validateArgs(options) {
  
  // return { isValid: true };
  return { isValid: false, message: 'something is wrong' };
}

Unhandled promise rejections

Sade is great, thanks!

I noticed that if my async handlers error out, then node exits with an UnhandledPromiseRejectionWarning but doesn't print the error message or stack trace or such. I think it'd be convenient if sade would catch these by default and print something, or give me an easy way to define an error handler (aside from manually wrapping each action).

Feat: command default value

Allow passing default value to a command.

Currently you can only pass opts.default: true to make the command default to the whole CLI.

Problem comes from the

if (opts.default) this.default = cmd;

Use case:

sade
  .command('run [file]', 'Run your file', {
    default: 'src/index.js',
    // default: true,
    alias: ['r'],
  })
  .example('run')
  .example('run file.js')
  .action((file, argv) => {
    console.log(file); // currently `undefined`
  });

Disallow unknown options

It would be nice if there's a way to disallow unknown options. For example, mycommand --silent should throw an error with:

sade("mycommand", true).option("-v, --verbose", "desc", false);

A hack/workaround I wrote is:

  for (const key of Object.keys(opts)) {
    if (!cli.tree.__all__.alias.hasOwnProperty(key) && key !== "_") {
      const unknownOpt = key.length === 1 ? `-${key}` : `--${key}`;
      throw new Error(`Unknown option: ${unknownOpt}`);
    }
  }

Rename the `this.name` to something other

Because I need to do some weird stuff like this

const programName = this.name;
delete this.name;

// where `this` is the `prog` which is the "instance" of calling sade()
const handler = Object.assign(taskObj.handler, this);

// ! restore, because the help() needs it
this.name = programName;

return handler;

The case. I override a bit the .action method, for few reasons. And in this method I have above code, because I need to return the handler function and I where I use it I need also access to the "instance".

And in general, just "name" isn't that good. Probably this.programName makes more sense.

Allow to define custom version function

What?

As far as I can see to define CLI version you do this

import sade from 'sade'
const CLI = sade('latitude')
CLI.version('1.2.3')

Then that shows

latitude 1.2.3

It's fine but our CLI install a nodejs server that we call the app. We would like to show both versions. CLI and app versions. Something like this:
image

So far we manage to hack sade by overriding _version private function

CLI['_version'] = versionCommand

But this is not ideal. I think it would be nice if .version method allows a function

CLI.version(versionCommand)

I'm open to make this change if you consider is a good idea.

Default command does not change help script

If I define two commands and mark one default, the returned help text via cli.js --help shows the arguments as if no command was called even though I can supply an argument.

repro.js:

const sade = require("sade");

const cli = sade("test")
  .command("something <a>", undefined, { default: true })
  .action((a) => console.log(a))
  .command("somethingelse <b>")
  .action((b) => console.log(b));

cli.parse(process.argv);
$ node dist/repro.js --help

  Usage
    $ test <command> [options]

  Available Commands
    something        
    somethingelse    

  For more info, run any command with the `--help` flag
    $ test something --help
    $ test somethingelse --help

  Options
    -v, --version    Displays current version
    -h, --help       Displays this message
$ node dist/repro.js

  ERROR
    Insufficient arguments!

  Run `$ test something --help` for more info.
$ node dist/repro.js something --help

  Usage
    $ test something <a> [options]

  Options
    -h, --help    Displays this message
$ node dist/repro.js something

  ERROR
    Insufficient arguments!

  Run `$ test something --help` for more info.

[feature request] "Cold" parse method

I'd like to be able to parse argv just to get parsed options without actually executing registered commands. IMHO it's slightly unintuitive API that parse method also executes stuff, I'd argue that it should only parse and nothing else, but ofc that would be a breaking change, so I'm merely asking if there is a possibility to add a "cold" version of this method?

I can work on it if we figure out what it should be called :p

reference https://github.com/developit/microbundle/pull/62/files#r163778889

RFC: Command Aliases

Command aliases are (generally) an abbreviated form of the command. They're typically geared for lazy typers and/or power users ๐Ÿ˜‰

The most common example that many of us are probably familiar with is npm install, which is also accessible via npm i. All options and arguments still carry over identically.

Here's how it would/could be added in a Sade program:

sade('npm')
  .command('install [package]', 'Install a package', { alias: 'i' })
  .option('-P, --save-prod', 'Package will appear in your dependencies.')
  .option('-D, --save-dev', 'Package will appear in your devDependencies.')
  .option('-O, --save-optional', 'Package will appear in your optionalDependencies')
  .option('-E, --save-exact', 'Save exact versions instead of using a semver range operator')
  .action(handler);

When running npm --help, you'll see this:

  Usage
    $ npm <command> [options]

  Available Commands
    install    Install a package

  For more info, run any command with the `--help` flag
    $ npm install --help

  Options
    -v, --version    Displays current version
    -h, --help       Displays this message

Note: There's no mention of aliases here. This output is unchanged

And when running npm install --help you'd see this:

  Description
    Install a package

  Usage
    $ npm install [package] [options]

  Aliases
    $ npm i

  Options
    -P, --save-prod        Package will appear in your dependencies.
    -D, --save-dev         Package will appear in your devDependencies.
    -O, --save-optional    Package will appear in your optionalDependencies
    -E, --save-exact       Save exact versions instead of using a semver range operator
    -h, --help             Displays this message

Of course, running npm install and npm i are synonymous, which also means that running npm i -h would also print the above output.


And now, some options and voting ๐Ÿ˜„

1. Do we even want this?



2. Should multiple aliases be allowed?


Back to the npm install example โ€“ they actually have 3 aliases: i, isntall, add

For the same Sade program, this would mean the alias option accepts an array:

prog.command('install [package]', '...', { alias: ['i', 'add', 'isntall'] })
  Description
    Install a package

  Usage
    $ npm install [package] [options]

  Aliases
    $ npm i
    $ npm add
    $ npm isntall

  Options
    -P, --save-prod        Package will appear in your dependencies.
    -D, --save-dev         Package will appear in your devDependencies.
    -O, --save-optional    Package will appear in your optionalDependencies
    -E, --save-exact       Save exact versions instead of using a semver range operator
    -h, --help             Displays this message

3. Should aliases be added through a method?


Personally, a new .alias() method would only make sense if we allow multiple aliases. Otherwise it'd be awkward and a no-go from me, I think.

Another drawback is that this would be the only API method that requires declaration after a .command() usage. Unlike .option(), you can't declare a "global alias" since that means nothing.

A final drawback is that a command's aliases is declared across two methods, whereas an option's alias is declared in one fell swoop. Keeping command aliases within the command() options will keep it "in one fell swoop"

// Proposed:
prog.command('install [package]', '...', { 
  alias: ['i', 'add', 'isntall']
})

// Alternative:
prog
  .command('install [package]', '...')
  .alias('i', 'add', 'isntall')

Thanks for dropping by!

PS: Votes are anonymous ๐Ÿ‘

Aliases with multiple characters break things

foo.js

const prog = require('sade')('prog')

prog
  .command('foo', 'do foo thing')
  .option('--foobar -fb', 'add foobar thing')

prog.parse(process.argv)
node foo.js foo --help 

  Description
    do foo thing

  Usage
    $ prog foo [options]

  Options
    -foobar, --fb    add foobar thing
    -h, --help       Displays this message

Notice that what should be --foobar is -foobar and what should be -fb is --fb.

Additionally:

const prog = require('sade')('prog')

prog
  .command('foo', 'do foo thing')
  .option('--foo-bar -f', 'add foobar thing')

prog.parse(process.argv)
node foo.js foo --help

  Description
    do foo thing

  Usage
    $ prog foo [options]

  Options
    -f, --foobar    add foobar thing
    -h, --help      Displays this message

Notice that what should be --foo-bar is --foobar.

Hyphenated Flags are Truncated

Flag names that contain hyphens lose their inner-hyphens during parse.

For example --foo-bar is converted to foobar but should be foo-bar.

Relevant code: https://github.com/lukeed/sade/blob/master/lib/utils.js#L83

Initially, my thoughts are that foo-bar is the correct way to store it. Dealing with & remembering that keys can be auto-converted to camelcase has proven to be annoying in past projects... plus it's simpler to just drop the leading hyphens.


Taken from #8

TypeError: Cannot read property '0' of undefined

When there is no description for a command and you trigger mycli -h.

More descriptive error would be good. Another way can be to just set some default description like "there is no description for this command".

How to pack/distribute?

Running your example code, but getting:

โžœ  wrklog node index.js
> building from [object Object] to undefined
> these are extra opts undefined
const sade = require('sade')
const prog = sade('wrk')

prog
  .version('1.0.5')
  .option('--global, -g', 'An example global flag')
  .option('-c, --config', 'Provide path to custom config', 'foo.config.js')

prog
  .command('build <src> <dest>')
  .describe('Build the source directory. Expects an `index.js` entry file.')
  .option('-o, --output', 'Change the name of the output file', 'bundle.js')
  .example('build src build --global --config my-conf.js')
  .example('build app public -o main.js')
  .action((src, dest, opts) => {
    console.log(`> building from ${src} to ${dest}`)
    console.log('> these are extra opts', opts)
  })

prog.parse(process.argv)

Support promises as handlers

return opts.lazy ? { args, name, handler } : handler.apply(null, args);

With the above, the following will not return an error:

prog.command('new <val>').action(async val => {
  if (val === 'foo') throw Error('swallowed by the ether')
  console.log('yay')
})

First argument is ignored when ran inside a packaged Electron application

When using sade in an Electron app, the first command line argument is ignored, but only when the Electron app has been built and packaged.

This happens because process.argv array content is different in this case. Normally the first element of the array is the NodeJS executable, the second element is the script filename, and only the third element is a relevant CLI argument. This is why the first two elements are always skipped.

['node.exe', 'script.js', 'first', 'second', 'third']

However, a packaged Electron app has everything inside the executable, so the relevant CLI arguments start already from the second element of prcess.argv:

['my_electron_app.exe', 'first', 'second', 'third']

Because sade always skips the first two elements, one significant argument gets ignored in this case.

Possible solutions:

  • Provide option to set the offset to a value other than 2. Alternatively provide a boolean option which sets offset to 1 rather than 2.
  • Auto-detect packaged Electron app. require('electron').app.isPackaged is not really viable, since it doesn't work in non-Electron environments. Yargs uses process.versions.electron && !process.defaultApp to detect argv indices.
  • Workaround in user code. Let the user detect Electron environment (E.g. with that isPackaged), and insert a dummy element at the beginning of process.argv before passing it to Sade#parse().

TypeError: prog.help is not a function

How can I call prog.help()? Here's a shortened example

#!/usr/bin/env node
'use strict';
const sade = require('sade');
const ghat = require('./lib');
const pkg = require('./package.json');

const prog = sade(pkg.name + ' <source>', true)
	.version(pkg.version)
	.describe(pkg.description)
	.action(async (source, options) => {
		try {
			await ghat(source, options);
		} catch (error) {
			if (error instanceof ghat.InputError) {
				console.error('โŒ', error.message);
				prog.help();
			} else {
				throw error;
			}
		}
	})
	.parse(process.argv);

That throws with

TypeError: prog.help is not a function

Apparently prog is a Promise, but I can't handle it at all:

try {
	  console.log(1);
	  await prog
	  console.log(2);
} catch (error) {
	  console.log(3);
	  console.log(error)
}

This will output just 1, exiting successfully without further errors.

Node 15.


Full code: https://github.com/fregante/ghat/blob/ef83253245f6ca40f86349440e64d56123943264/bin.js#L36

You can also try running it directly to see the original error: npx ghat lukeed/sade/404

Default help

Given:

const prog = sade('greet');
 
prog.command('hello');
//=> only runs if :: `$ greet hello`
 
// $ greet
//=> error: No command specified.
 
prog.command('howdy', '', { default:true });
//=> runs as `$ greet` OR `$ greet howdy`
 
// $ greet
//=> runs 'howdy' handler
 
// $ greet foobar
//=> error: Invalid command

I would like to be able to specify that greet -h be equivalent to greet howdy -h.

I like the simple API of sade for describing CLI options. But almost every time I turn to this module for parsing such options, it's not for an interface that requires commands. I just need to supply options on the application itself. Defining a default command, albeit unintuitive, is an easy to way to do that. But it makes it difficult to get to the real "help" output.

[feature request] Command `--help` should show available subcommands

prog
  .command('foo')
  .describe('Top level command')
  .action(() => console.log('see `app foo --help`'))

prog
  .command('foo bar')
  .describe('A subcommand')
  .action(() => console.log('did a thing'))

Use case: the foo command doesn't do anything and requires usage of subcommands. As it is, doing app foo --help does not display any possible subcommands under the "Usage" section. Instead, one has to do app --help to see a potentially long list of available commands and subcommands. Ideally, app --help would show the foo command but not the foo bar command so that one would have to do app foo --help to see the available subcommands.

Side request: foo could do something, but it'd just be an alias of a required subcommand.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.