diff --git a/ADVANCED_README.md b/ADVANCED_README.md index 002a112a..83a45f1e 100644 --- a/ADVANCED_README.md +++ b/ADVANCED_README.md @@ -12,6 +12,7 @@ - [Options](#a987) - [Schema](#schema) - [How to update PM2?](#update-pm2) +- [PM2 auto-completion](#auto-completion) - [Allow PM2 to bind apps on port 80/443 without root](#authbind-pm2) ### Features @@ -265,6 +266,27 @@ Then update the in-memory PM2 : $ pm2 update ``` + +## PM2 auto-completion + +Append pm2 completion script to your .bashrc or .zshrc file: + +```bash +$ pm2 completion install +``` + +Add pm2 completion to your current session: + +```bash +$ . <(pm2 completion) +``` + +Manually append completion script to your ~/.bashrc or ~/.zshrc file: + +```bash +$ pm2 completion >> ~/.bashrc +``` + ## Allow PM2 to bind applications on ports 80/443 without root diff --git a/bin/pm2 b/bin/pm2 index 93e662c7..0a98dbd2 100755 --- a/bin/pm2 +++ b/bin/pm2 @@ -16,6 +16,7 @@ var CLI = require('../lib/CLI'); var cst = require('../constants.js'); var pkg = require('../package.json'); var platform = require('os').platform(); +var tabtab = require('../lib/completion.js'); CLI.pm2Init(); @@ -115,6 +116,32 @@ function beginCommandProcessing() { commander.parse(process.argv); } +function checkCompletion(){ + return tabtab.complete('pm2', function(err, data) { + if(err || !data) return; + if(/^--\w?/.test(data.last)) return tabtab.log(commander.options.map(function (data) { + return data.long; + }), data); + if(/^-\w?/.test(data.last)) return tabtab.log(commander.options.map(function (data) { + return data.short; + }), data); + // array containing commands after which process name should be listed + var cmdProcess = ['stop', 'restart', 'scale', 'reload', 'gracefulReload', 'delete', 'reset', 'pull', 'forward', 'backward']; + if(cmdProcess.indexOf(data.prev) > -1) { + CLI.connect(function() { + CLI.list(function(err, list){ + tabtab.log(list.map(function(el){ return el.name}), data); + CLI.disconnect(); + }); + }); + } + else if (data.prev == 'pm2') + tabtab.log(commander.commands.map(function (data) { + return data._name; + }), data); + }); +}; + if (process.argv.indexOf('--no-daemon') > -1) { // // Start daemon if it does not exist @@ -132,10 +159,17 @@ if (process.argv.indexOf('--no-daemon') > -1) { }); }); } -else +else { Satan.start(false, function() { - beginCommandProcessing(); + if (process.argv.slice(2)[0] === 'completion') { + checkCompletion(); + CLI.disconnect(); + } + else + beginCommandProcessing(); }); +} + // diff --git a/lib/completion.js b/lib/completion.js new file mode 100644 index 00000000..58ca5fdc --- /dev/null +++ b/lib/completion.js @@ -0,0 +1,225 @@ +var fs = require('fs'), + pth = require('path'), + exec = require('child_process').exec; + +// hacked from node-tabtab 0.0.4 https://github.com/mklabs/node-tabtab.git +// Itself based on npm completion by @isaac + +exports.complete = function complete(name, completer, cb) { + + // cb not there, assume callback is completer and + // the completer is the executable itself + if(!cb) { + cb = completer; + completer = name; + } + + var env = parseEnv(); + + // if not a complete command, return here. + if(!env.complete) return cb(); + + // if install cmd, add complete script to either ~/.bashrc or ~/.zshrc + if(env.install) return install(name, completer, function(err, state) { + console.log(state || err.message); + if(err) return cb(err); + cb(null, null, state); + }); + + // if install cmd, add complete script to either ~/.bashrc or ~/.zshrc + if(env.uninstall) return uninstall(name, completer, function(err, state) { + console.log(state || err.message); + if(err) return cb(err); + cb(null, null, state); + }); + + // if the COMP_* are not in the env, then dump the install script. + if(!env.words || !env.point || !env.line) return script(name, completer, function(err, content) { + if(err) return cb(err); + process.stdout.write(content, function (n) { cb(null, null, content); }); + process.stdout.on("error", function (er) { + // Darwin is a real dick sometimes. + // + // This is necessary because the "source" or "." program in + // bash on OS X closes its file argument before reading + // from it, meaning that you get exactly 1 write, which will + // work most of the time, and will always raise an EPIPE. + // + // Really, one should not be tossing away EPIPE errors, or any + // errors, so casually. But, without this, `. <(npm completion)` + // can never ever work on OS X. + // -- isaacs + // https://github.com/isaacs/npm/blob/master/lib/completion.js#L162 + if (er.errno === "EPIPE") er = null + cb(er, null, content); + }); + cb(null, null, content); + }); + + var partial = env.line.substr(0, env.point), + last = env.line.split(' ').slice(-1).join(''), + lastPartial = partial.split(' ').slice(-1).join(''), + prev = env.line.split(' ').slice(0, -1).slice(-1)[0]; + + cb(null, { + line: env.line, + words: env.words, + point: env.point, + partial: partial, + last: last, + prev: prev, + lastPartial: lastPartial + }); +}; + +// simple helper function to know if the script is run +// in the context of a completion command. Also mapping the +// special ` completion` cmd. +exports.isComplete = function isComplete() { + var env = parseEnv(); + return env.complete || (env.words && env.point && env.line); +}; + +exports.parseOut = function parseOut(str) { + var shorts = str.match(/\s-\w+/g); + var longs = str.match(/\s--\w+/g); + + return { + shorts: shorts.map(trim).map(cleanPrefix), + longs: longs.map(trim).map(cleanPrefix) + }; +}; + +// specific to cake case +exports.parseTasks = function(str, prefix, reg) { + var tasks = str.match(reg || new RegExp('^' + prefix + '\\s[^#]+', 'gm')) || []; + return tasks.map(trim).map(function(s) { + return s.replace(prefix + ' ', ''); + }); +}; + +exports.log = function log(arr, o, prefix) { + prefix = prefix || ''; + arr = Array.isArray(arr) ? arr : [arr]; + arr.filter(abbrev(o)).forEach(function(v) { + console.log(prefix + v); + }); +} + +function trim (s) { + return s.trim(); +} + +function cleanPrefix(s) { + return s.replace(/-/g, ''); +} + +function abbrev(o) { return function(it) { + return new RegExp('^' + o.last.replace(/^--?/g, '')).test(it); +}} + +// output the completion.sh script to the console for install instructions. +// This is actually a 'template' where the package name is used to setup +// the completion on the right command, and properly name the bash/zsh functions. +function script(name, completer, cb) { + var p = pth.join(__dirname, 'completion.sh'); + + fs.readFile(p, 'utf8', function (er, d) { + if (er) return cb(er); + cb(null, d); + }); +} + +function install(name, completer, cb) { + var markerIn = '###-begin-' + name + '-completion-###', + markerOut = '###-end-' + name + '-completion-###'; + + var rc, scriptOutput; + + readRc(completer, function(err, file) { + if(err) return cb(err); + + var part = file.split(markerIn)[1]; + if(part) { + return cb(null, ' ✗ ' + completer + ' has been already installed. Do nothing.'); + } + + rc = file; + next(); + }); + + script(name, completer, function(err, file) { + scriptOutput = file; + next(); + }); + + function next() { + if(!rc || !scriptOutput) return; + + writeRc(rc + scriptOutput, function(err) { + if(err) return cb(err); + return cb(null, ' ✓ ' + completer + ' installed.'); + }); + } +} + +function uninstall(name, completer, cb) { + var markerIn = '\n\n###-begin-' + name + '-completion-###', + markerOut = '###-end-' + name + '-completion-###\n'; + + readRc(completer, function(err, file) { + if(err) return cb(err); + + var part = file.split(markerIn)[1]; + if(!part) { + return cb(null, ' ✗ ' + completer + ' has been already uninstalled. Do nothing.'); + } + + part = markerIn + part.split(markerOut)[0] + markerOut; + writeRc(file.replace(part, ''), function(err) { + if(err) return cb(err); + return cb(null, ' ✓ ' + completer + ' uninstalled.'); + }); + }); +} + +function readRc(completer, cb) { + var file = '.' + process.env.SHELL.match(/\/bin\/(\w+)/)[1] + 'rc', + filepath = pth.join(process.env.HOME, file); + fs.lstat(filepath, function (err, stats) { + if(err) return cb(new Error("No " + file + " file. You'll have to run instead: " + completer + " completion >> ~/" + file)); + fs.readFile(filepath, 'utf8', cb); + }); +} + +function writeRc(content, cb) { + var file = '.' + process.env.SHELL.match(/\/bin\/(\w+)/)[1] + 'rc', + filepath = pth.join(process.env.HOME, file); + fs.lstat(filepath, function (err, stats) { + if(err) return cb(new Error("No " + file + " file. You'll have to run instead: " + completer + " completion >> ~/" + file)); + fs.writeFile(filepath, content, cb); + }); +} + +function installed (marker, completer, cb) { + readRc(completer, function(err, file) { + if(err) return cb(err); + var installed = file.match(marker); + return cb(!!installed); + }); +} + +function parseEnv() { + var args = process.argv.slice(2), + complete = args[0] === 'completion'; + + return { + args: args, + complete: complete, + install: complete && args[1] === 'install', + uninstall: complete && args[1] === 'uninstall', + words: +process.env.COMP_CWORD, + point: +process.env.COMP_POINT, + line: process.env.COMP_LINE + } +}; diff --git a/lib/completion.sh b/lib/completion.sh new file mode 100644 index 00000000..c71df32b --- /dev/null +++ b/lib/completion.sh @@ -0,0 +1,40 @@ +###-begin-pm2-completion-### +### credits to for the completion file model +# +# Installation: pm2 completion >> ~/.bashrc (or ~/.zshrc) +# + +COMP_WORDBREAKS=${COMP_WORDBREAKS/=/} +COMP_WORDBREAKS=${COMP_WORDBREAKS/@/} +export COMP_WORDBREAKS + +if type complete &>/dev/null; then + _pm2_completion () { + local si="$IFS" + IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \ + COMP_LINE="$COMP_LINE" \ + COMP_POINT="$COMP_POINT" \ + pm2 completion -- "${COMP_WORDS[@]}" \ + 2>/dev/null)) || return $? + IFS="$si" + } + complete -o default -F _pm2_completion pm2 +elif type compctl &>/dev/null; then + _pm2_completion () { + local cword line point words si + read -Ac words + read -cn cword + let cword-=1 + read -l line + read -ln point + si="$IFS" + IFS=$'\n' reply=($(COMP_CWORD="$cword" \ + COMP_LINE="$line" \ + COMP_POINT="$point" \ + pm2 completion -- "${words[@]}" \ + 2>/dev/null)) || return $? + IFS="$si" + } + compctl -K _pm2_completion pm2 +fi +###-end-pm2-completion-###