lukeed / sade Goto Github PK
View Code? Open in Web Editor NEWSmooth (CLI) Operator ๐ถ
License: MIT License
Smooth (CLI) Operator ๐ถ
License: MIT License
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
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]
I currently just need to tweak few things and override the .action
method.
Line 169 in 52909ec
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')
})
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
Here is a cli I am working with ๐
For every command, I am adding, sade
creates an additional line showing how to use the --help
flag. When more commands are added, this creates a longer help message.
Please add the ability to disable this section. For a cli with a lot of commands, terminal spaces is not wasted.
Ooops, one more issue :D
I may PR soon.
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
Should exit & display help text for usage information.
Following the technique presented in https://www.sensedeep.com/blog/posts/2021/how-to-create-single-source-npm-module.html, a library can have both cjs & mjs outputs. I'll submit a PR using this technique but am not attached to using Typescript, as long as there is an mjs & cjs build.
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.
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"]}
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:
offset
to a value other than 2
. Alternatively provide a boolean option which sets offset
to 1
rather than 2
.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.isPackaged
), and insert a dummy element at the beginning of process.argv
before passing it to Sade#parse()
.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
.
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.
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.
This seems odd to me. If yarn
and npm
are anything to go by, the --version
flag should only print the version number. ๐
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.
Lines 150 to 152 in 1cdf3e4
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:
Lines 638 to 653 in 1cdf3e4
One solution may be to return:
{
args: [],
name: '',
handler: undefined,
helpOrVersion: true // indicates the user passed one of the 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}`);
}
}
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?
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' };
}
Eg; git remote add|remove
, up team login|switch|etc
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
SvelteKit needs -h
for host, but currently can't use it because sade is hardcoding it to --help
(sveltejs/kit#1432)
I'm used to -h
for host. E.g. that's what the mysql
and psql
command-line clients do. They use -?
for help
After upgrading to the latest version of Sade, I'm getting the following type error on my action
s:
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'.
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
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`
});
If one specifies --custom-option
, it would be nice if you could access it as args.customOption
instead of args["custom-option"]
.
Is this something you'd like to support?
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 _
.
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
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.
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.
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.
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".
what would you say makes sade the right way?
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:
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.
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.
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).
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)
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.
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 ๐
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
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 ๐
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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.