Git Product home page Git Product logo

sade's Issues

[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

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]

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')
})

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

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

[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.

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"]}

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().

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.

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.

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.

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.

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
}

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}`);
    }
  }

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' };
}

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

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'.

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

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`
  });

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 _.

[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

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.

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.

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.

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".

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 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.

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).

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)

[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.

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 ๐Ÿ‘

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

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.