Yargs documenting a flag - node.js

I was trying to figure out how to document and alias a param that takes no values with yargs
What I want to do is alias -c to --compile and be able to document --compile. If --compile
script sources -c
I was expecting it to be something like this
var argv = require('yargs')
.usage('Usage: $0 <input> [options]')
.example('$0 src/**.js -c', 'Generate a build')
.demand(1)
.boolean('compile')
.alias('compile', ['c'])
.nargs('c', 1)
.describe('compile', 'Whether to compile the results')
.version(function() {
return require('../package').version;
})
.argv;
However, calling script sources -c will generate an error
TypeError: Cannot read property 'newAliases' of undefined
at Object.self.help (/home/gyeates/code/lodash.modularize/node_modules/yargs/lib/usage.js:135:45)
at Object.self.showHelp (/home/gyeates/code/lodash.modularize/node_modules/yargs/lib/usage.js:211:29)
at Object.Argv.self.showHelp (/home/gyeates/code/lodash.modularize/node_modules/yargs/index.js:303:15)
at Object.self.fail (/home/gyeates/code/lodash.modularize/node_modules/yargs/lib/usage.js:37:39)

Get rid of nargs('c', 1). That method specifies the number of arguments that should be consumed after a key, in this case 1. We don't want the key to take any values.
var argv = require('yargs')
.usage('Usage: $0 <input> [options]')
.example('$0 src/**.js -c', 'Generate a build')
.demand(1)
.boolean('compile')
.alias('compile', ['c'])
.describe('compile', 'Whether to compile the results')
.version(function() {
return require('../package').version;
})
.argv;
More information on yargs methods can be found here.

Related

How to get yargs auto-complete working, when using --experimental-specifier-resolution=node

My objective is to write a CLI in Typescript/node.js, that uses --experimental-specifier-resolution=node, written in yargs with support for autocompletion.
To make this work, I use this entry.sh file, thanks to this helpful SO anwswer (and the bin: {eddy: "./entry.sh"} options in package.json points to this file)
#!/usr/bin/env bash
full_path=$(realpath $0)
dir_path=$(dirname $full_path)
script_path="$dir_path/dist/src/cli/entry.js"
# Path is made thanks to: https://code-maven.com/bash-shell-relative-path
# Combined with knowledge from: https://stackoverflow.com/questions/68111434/how-to-run-node-js-cli-with-experimental-specifier-resolution-node
/usr/bin/env node --experimental-specifier-resolution=node $script_path "$#"
This works great, and I can use the CLI. However, autocompletion does not work. According to yargs I should be able to get autocompletion by outputting the result from ./entry.sh completion to the ~/.bashrc profile. However this does not seem to work.
Output from ./entry.sh completion:
###-begin-entry.js-completions-###
#
# yargs command completion script
#
# Installation: ./dist/src/cli/entry.js completion >> ~/.bashrc
# or ./dist/src/cli/entry.js completion >> ~/.bash_profile on OSX.
#
_entry.js_yargs_completions()
{
local cur_word args type_list
cur_word="${COMP_WORDS[COMP_CWORD]}"
args=("${COMP_WORDS[#]}")
# ask yargs to generate completions.
type_list=$(./dist/src/cli/entry.js --get-yargs-completions "${args[#]}")
COMPREPLY=( $(compgen -W "${type_list}" -- ${cur_word}) )
# if no match was found, fall back to filename completion
if [ ${#COMPREPLY[#]} -eq 0 ]; then
COMPREPLY=()
fi
return 0
}
complete -o default -F _entry.js_yargs_completions entry.js
###-end-entry.js-completions-###
I tried modifying the completion output, but I don't really understand bash - just yet 😅
Update
Working on a reproducible example (WIP).
Repo is here.
Currently one of the big differences is that npm link does not work the same in the 2 different environments. It's only in the repo where I'm trying to reproduce that /usr/local/share/npm-global/bin/ is actually updated. Currently trying to investigate this.
You can try specifying the scriptName in your entry.js file to the name of your wrapper script. This may force generation of completion name using it. I haven't tried it but looking at the source code of yargs, it looks like the $0 parameter can be altered using scriptName, which in turn will affect how the completion-generation function generate the completion code:
In yargs-factor.ts:
scriptName(scriptName: string): YargsInstance {
this.customScriptName = true;
this.$0 = scriptName;
return this;
}
In completion.ts:
generateCompletionScript($0: string, cmd: string): string {
let script = this.zshShell
? templates.completionZshTemplate
: templates.completionShTemplate;
const name = this.shim.path.basename($0);
// add ./ to applications not yet installed as bin.
if ($0.match(/\.js$/)) $0 = `./${$0}`;
script = script.replace(/{{app_name}}/g, name);
script = script.replace(/{{completion_command}}/g, cmd);
return script.replace(/{{app_path}}/g, $0);
}
Also I'm not sure how the "bin" configuration works but maybe because of scriptName you'd no longer need a wrapper.
Make sure the version of yargs you use supports this.
Also as a side note I thought about suggesting to modify the generated completion script directly but besides being hackish that might also still lead to the script name being unrecognized during completion. Anyhow I just looked at the right approach first.
The modified version would like this:
_entry.sh_yargs_completions()
{
local cur_word args type_list
cur_word="${COMP_WORDS[COMP_CWORD]}"
args=("${COMP_WORDS[#]}")
# ask yargs to generate completions.
type_list=$(/path/to/entry.sh --get-yargs-completions "${args[#]}")
COMPREPLY=( $(compgen -W "${type_list}" -- ${cur_word}) )
# if no match was found, fall back to filename completion
if [ ${#COMPREPLY[#]} -eq 0 ]; then
COMPREPLY=()
fi
return 0
}
complete -o default -F _entry.sh_yargs_completions entry.sh
Another note: If the script name needs to be dynamic based on the name of its caller, you can make it identifiable through an environment variable, so in entry.sh you can declare it like this:
export ENTRY_JS_SCRIPT_NAME=entry.sh
node ...
Then somewhere in entry.js, you can access the variable name through this:
process.env.ENTRY_JS_SCRIPT_NAME
Maybe even just specify $0 or ${0##*/} whatever works:
export ENTRY_JS_SCRIPT_NAME=$0
Thanks, everyone. The solution I ended up with, was 2 fold:
I added a scriptName to the yargs config
In the .sh file "wrapping", I used which node to probably set the --experimental-specifier-resolution=node flags.
test-cli.js
#!/usr/bin/env node
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import { someOtherModule } from './some-other-module';
someOtherModule();
yargs(hideBin(process.argv))
.command('curl <url>', 'fetch the contents of the URL', () => {}, (argv) => {
console.info(argv)
})
.command('curlAgain <url>', 'fetch the contents of the URL', () => {}, (argv) => {
console.info(argv)
})
.demandCommand(1)
.help()
.completion()
.scriptName('eddy') // <== Added thanks to konsolebox
.parse()
test-cli.sh
#!/usr/bin/env bash
full_path="$(realpath "$0")"
dir_path="$(dirname $full_path)"
script_path="$dir_path/test-cli.js"
node_path="$(which node)" # <== Makes it work on github codespaces 😅
$node_path --experimental-specifier-resolution=node $script_path "$#"
package.json
{
"name": "lets-reproduce",
"type": "module",
"dependencies": {
"yargs": "^17.3.1"
},
"bin": {
"eddy": "./test-cli.sh"
}
}
Steps to install autocompletion:
run npm link
run eddy completion >> ~/.bashrc
source ~/.bashrc
profit 😅🔥

Jest cannot test commander help function

With jest I'm not able to test commander module functions that result in process exit.
For example, if I pass the --help option or an invalid parameter like -x (see below) process.exit or process.stdout.write are not called as they should looking at the commander sources.
import {Command} from "commander";
let mockExit: jest.SpyInstance;
let mockStdout: jest.SpyInstance;
beforeAll(() => {
mockExit = jest.spyOn(process, "exit").mockImplementation();
mockStdout = jest.spyOn(process.stdout, "write").mockImplementation();
});
afterAll(() => {
mockExit.mockRestore();
mockStdout.mockRestore();
});
test("Ask for help", () => {
// Setup
const save = JSON.parse(JSON.stringify(process.argv));
process.argv = ["--help"]; // Same setting it to "-x"
const program = new Command();
program
.option("-v, --verbose [level]", "verbose level")
.parse(process.argv);
expect(mockExit).toBeCalled();
// expect(mockStdout).toBeCalled();
// Cleanup
process.argv = save;
});
What is strange is that, from the behavior of other tests, process.argv is not restored after this one.
Tests are in typescript and passed through ts-jest.
Any ideas?
Thanks!
I suggest you use .exitOverride(), which is the approach Commander uses in its own tests. This means early "termination" is via a throw rather than exit.
https://github.com/tj/commander.js#override-exit-handling
The first problem though (from comments) is the arguments. Commander expects the parse arguments follow the conventions of node with argv[0] is the application and argv[1] is the script being run, with user parameters after that.
So instead of:
argsToParse = ["--help"];
something like:
argsToParse = ['node", "dummy.js", "--help"];
(No need to modify process.argv as such.)

How to retrieve command-line args which was passed during building [custom build system] in typescript

I know how to retrieve command-line args in JS by using following manner,
`Config.getTestArgs = () => {
try {
return global.commandLineArgs.args["test-args"];
}
catch (e) {
logger.error(`Error reading test - args from command line: ${ e } `);
return null;
}
};`
When i use the same way in Typescript, i get an error Cannot find module- global
If i pass my input like
`--build --test-args TestArument1`
getTestArgs should return TestArgument1 as output.
Consider we have our own build system which uses nodeJs and Typescript. Which nodeJS dependencies should i need to consider?
In Typescript and Node.js in general, there's a few ways to retrieve command line arguments. You can either use the built-in process.argv property, which returns an array containing the command line arguments passed when the Node.js process was launched. Since the first two arguments will almost always be node and path/to/script.js, it is generally used as process.argv.slice(2).
Example:
node script.js --build --test-args TestArgument1
script.js
console.log(process.argv.slice(2)) // [ '--build', '--test-args', 'TestArgument1' ]
The other, arguably better, way is to use a tried and tested library to parse your command line arguments. Popular options include:
Minimist: For minimal argument parsing.
Commander.js: Most adopted module for argument parsing.
Meow: Lighter alternative to Commander.js
Yargs: More sophisticated argument parsing (heavy).
Vorpal.js: Mature / interactive command-line applications with argument parsing.
For your case Minimist would probably the best solution.
node script.js --build --test-args TestArgument1 would look like this:
const argv: minimist.ParsedArgs = require('minimist')(process.argv.slice(2));
console.dir(argv);
/*
{
_: [ 'build' ],
test-args: 'TestArgument1'
}
*/

How to set nodejs interpreter arguments from javascript source

I have someScript.js which requires some node options for correct running. E.g. (let's suppose we have node 0.12.7):
node --harmony someScript.js
I wish to run it without --harmony option and somehow set it inside the script.
I tried to change process.execArgv:
process.execArgv.push('--harmony');
function getGenerator() {
return function *() {
yield 1;
};
}
var gen = getGenerator();
Since NodeJS is interpreter it could allow such settings change (even on the fly). But, NodeJS seems to ignore process.execArgv changes.
Also I tried v8.setFlagsFromString:
Interesting, all NodeJs versions which needed --harmony to support generators do not contain v8 module.
So, I made experiments with NodeJS 4.1.1
var v8 = require('v8');
var vm = require('vm');
v8.setFlagsFromString('--harmony');
var str1 =
'function test1(a, ...args) {' +
' console.log(args);' +
'};';
var str2 =
'function test2(a, ...args) {' +
' console.log(args);' +
'};';
eval(str1);
test1('a', 'b', 'c');
vm.runInThisContext(str2);
test2('a', 'b', 'c');
Unfortunately, v8.setFlagsFromString('--harmony') did not help, even for eval() and vm.runInThisContext().
I wish to avoid creating some wrapping script.
So, is there some other way to set nodejs arguments from javascript source?
If one will have such someScript.js:
#!/bin/sh
":" //# comment; exec /usr/bin/env node --harmony "$0" "$#"
function test(a, ...args) {
console.log(args);
}
test('a', 'b', 'c');
Then "sh someScript.js" command will use interpreter and arguments specified in the someScript.js.
If one will mark someScript.js executable (chmod u+x someScript.js). Then ./someScript.js command also will work.
This article was helpful:
http://sambal.org/2014/02/passing-options-node-shebang-line/
Also if you use Windows EOL there will be a bug with '\r' adding to last script argument. If there are no arguments there will be one ('\r').
UPDATE:
Some shells allow to use
#! /usr/bin/env node --harmony
instead of
#!/bin/sh
":" //# comment; exec /usr/bin/env node --harmony "$0" "$#"

How to support help command-line argument with required arguments in CliBuilder

I am using the CliBuilder to parse command-line arguments for a Groovy script. Among the arguments I have defined, I have one which is mandatory. Is there a way to support a -h,--help argument which would print the command usage without the annoying error message about missing arguments?
For example, running the following Groovy script with only the -h argument:
def cli = new CliBuilder (usage:'test', stopAtNonOption:false)
cli.r (longOpt:'required', required:true, 'Required argument.')
cli.h (longOpt:'help', 'Prints this message')
def options = cli.parse (args)
will generate the output below when it gets to the def options = cli.parse (args) line, and will automatically stop the script execution:
error: Missing required option: r
usage: test
-h,--help Prints this message
-r,--required Required argument.
I would like to display only the usage when the -h or --help argument is specified, without having to drop the required:true option for my required arguments. Is this possible?
I may not have understood the question properly, but is
cli.usage()
what you are looking for?
You can do something like below to avoid it:
def options
//or ['-h', '--help'].intersect(args?.toList())
if('-h' in args || '--help' in args) {
cli.usage()
} else {
options = cli.parse (args)
}

Resources