Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to specify executable file name #999

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .travis.yml
@@ -1,6 +1,5 @@
language: node_js
node_js:
- "6"
- "8"
- "10"
- "12"
Expand Down
3 changes: 3 additions & 0 deletions Readme.md
Expand Up @@ -330,6 +330,7 @@ Configuration options can be passed with the call to `.command()`. Specifying `t

When `.command()` is invoked with a description argument, this tells commander that you're going to use separate executables for sub-commands, much like `git(1)` and other popular tools.
Commander will search the executables in the directory of the entry script (like `./examples/pm`) with the name `program-subcommand`, like `pm-install`, `pm-search`.
You can specify a custom name with the `executableFile` configuration option.

You handle the options for an executable (sub)command in the executable, and don't declare them at the top-level.

Expand All @@ -341,11 +342,13 @@ program
.version('0.1.0')
.command('install [name]', 'install one or more packages')
.command('search [query]', 'search with optional query')
.command('update', 'update installed packages', {executableFile: 'myUpdateSubCommand'})
.command('list', 'list packages installed', {isDefault: true})
.parse(process.argv);
```

Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified.
Specifying a name with `executableFile` will override the default constructed name.

If the program is designed to be installed globally, make sure the executables have proper modes, like `755`.

Expand Down
60 changes: 41 additions & 19 deletions index.js
Expand Up @@ -157,6 +157,7 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts
cmd._helpDescription = this._helpDescription;
cmd._helpShortFlag = this._helpShortFlag;
cmd._helpLongFlag = this._helpLongFlag;
cmd._executableFile = opts.executableFile; // Custom name for executable file
this.commands.push(cmd);
cmd.parseExpectedArgs(args);
cmd.parent = this;
Expand Down Expand Up @@ -458,26 +459,39 @@ Command.prototype.parse = function(argv) {
var result = this.parseArgs(this.args, parsed.unknown);

// executable sub-commands
// (Debugging note for future: args[0] is not right if an action has been called)
var name = result.args[0];
var subCommand = null;

var aliasCommand = null;
// check alias of sub commands
// Look for subcommand
if (name) {
aliasCommand = this.commands.filter(function(command) {
subCommand = this.commands.find(function(command) {
return command._name === name;
});
}

// Look for alias
if (!subCommand && name) {
subCommand = this.commands.find(function(command) {
return command.alias() === name;
})[0];
});
if (subCommand) {
name = subCommand._name;
args[0] = name;
}
}

// Look for default subcommand
if (!subCommand && this.defaultExecutable) {
name = this.defaultExecutable;
args.unshift(name);
subCommand = this.commands.find(function(command) {
return command._name === name;
});
}

if (this._execs[name] && typeof this._execs[name] !== 'function') {
return this.executeSubCommand(argv, args, parsed.unknown);
} else if (aliasCommand) {
// is alias of a subCommand
args[0] = aliasCommand._name;
return this.executeSubCommand(argv, args, parsed.unknown);
} else if (this.defaultExecutable) {
// use the default subcommand
args.unshift(this.defaultExecutable);
return this.executeSubCommand(argv, args, parsed.unknown);
return this.executeSubCommand(argv, args, parsed.unknown, subCommand ? subCommand._executableFile : undefined);
}

return result;
Expand All @@ -489,10 +503,11 @@ Command.prototype.parse = function(argv) {
* @param {Array} argv
* @param {Array} args
* @param {Array} unknown
* @param {String} specifySubcommand
* @api private
*/

Command.prototype.executeSubCommand = function(argv, args, unknown) {
Command.prototype.executeSubCommand = function(argv, args, unknown, executableFile) {
args = args.concat(unknown);

if (!args.length) this.help();
Expand All @@ -504,24 +519,31 @@ Command.prototype.executeSubCommand = function(argv, args, unknown) {
args[1] = this._helpLongFlag;
}

var isExplicitJS = false; // Whether to use node to launch "executable"

// executable
var f = argv[1];
// name of the subcommand, link `pm-install`
var bin = basename(f, path.extname(f)) + '-' + args[0];
var pm = argv[1];
// name of the subcommand, like `pm-install`
var bin = basename(pm, path.extname(pm)) + '-' + args[0];
if (executableFile != null) {
bin = executableFile;
// Check for same extensions as we scan for below so get consistent launch behaviour.
var executableExt = path.extname(executableFile);
isExplicitJS = executableExt === '.js' || executableExt === '.ts' || executableExt === '.mjs';
}

// In case of globally installed, get the base dir where executable
// subcommand file should be located at
var baseDir;

var resolvedLink = fs.realpathSync(f);
var resolvedLink = fs.realpathSync(pm);

baseDir = dirname(resolvedLink);

// prefer local `./<bin>` to bin in the $PATH
var localBin = path.join(baseDir, bin);

// whether bin file is a js script with explicit `.js` or `.ts` extension
var isExplicitJS = false;
if (exists(localBin + '.js')) {
bin = localBin + '.js';
isExplicitJS = true;
Expand Down