From 40de35db9e85ee1db8e983006ca5b9a6f3bd0daf Mon Sep 17 00:00:00 2001 From: Aymeric Lavit d'Hautefort Date: Tue, 30 Jun 2015 16:59:08 +0200 Subject: [PATCH 1/2] Added autocompletion based on tabtab in /lib --- bin/pm2 | 38 +++++++- lib/completion.js | 228 ++++++++++++++++++++++++++++++++++++++++++++++ lib/completion.sh | 42 +++++++++ 3 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 lib/completion.js create mode 100644 lib/completion.sh diff --git a/bin/pm2 b/bin/pm2 index efc33892..b7456e64 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(); @@ -114,6 +115,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 @@ -131,10 +158,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..4e236dac --- /dev/null +++ b/lib/completion.js @@ -0,0 +1,228 @@ +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); + d = d + .replace(/\{pkgname\}/g, name) + .replace(/{completer}/g, completer); + 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..6afea1d0 --- /dev/null +++ b/lib/completion.sh @@ -0,0 +1,42 @@ +###-begin-{pkgname}-completion-### +### credits to npm and node-tabtab, this file is coming directly from isaacs/npm repo +# +# npm command completion script +# +# Installation: {completer} completion >> ~/.bashrc (or ~/.zshrc) +# + +COMP_WORDBREAKS=${COMP_WORDBREAKS/=/} +COMP_WORDBREAKS=${COMP_WORDBREAKS/@/} +export COMP_WORDBREAKS + +if type complete &>/dev/null; then + _{pkgname}_completion () { + local si="$IFS" + IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \ + COMP_LINE="$COMP_LINE" \ + COMP_POINT="$COMP_POINT" \ + {completer} completion -- "${COMP_WORDS[@]}" \ + 2>/dev/null)) || return $? + IFS="$si" + } + complete -o default -F _{pkgname}_completion {pkgname} +elif type compctl &>/dev/null; then + _{pkgname}_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" \ + {completer} completion -- "${words[@]}" \ + 2>/dev/null)) || return $? + IFS="$si" + } + compctl -K _{pkgname}_completion + -f + {pkgname} +fi +###-end-{pkgname}-completion-### From 84157e55a1829a6a5785dc18da9cc82b885623fe Mon Sep 17 00:00:00 2001 From: Aymeric Lavit d'Hautefort Date: Tue, 30 Jun 2015 17:57:48 +0200 Subject: [PATCH 2/2] updated ADVANCED_README --- ADVANCED_README.md | 22 ++++++++++++++++++++++ lib/completion.js | 3 --- lib/completion.sh | 22 ++++++++++------------ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/ADVANCED_README.md b/ADVANCED_README.md index 260facb9..7522568e 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/lib/completion.js b/lib/completion.js index 4e236dac..58ca5fdc 100644 --- a/lib/completion.js +++ b/lib/completion.js @@ -126,9 +126,6 @@ function script(name, completer, cb) { fs.readFile(p, 'utf8', function (er, d) { if (er) return cb(er); - d = d - .replace(/\{pkgname\}/g, name) - .replace(/{completer}/g, completer); cb(null, d); }); } diff --git a/lib/completion.sh b/lib/completion.sh index 6afea1d0..c71df32b 100644 --- a/lib/completion.sh +++ b/lib/completion.sh @@ -1,9 +1,7 @@ -###-begin-{pkgname}-completion-### -### credits to npm and node-tabtab, this file is coming directly from isaacs/npm repo +###-begin-pm2-completion-### +### credits to for the completion file model # -# npm command completion script -# -# Installation: {completer} completion >> ~/.bashrc (or ~/.zshrc) +# Installation: pm2 completion >> ~/.bashrc (or ~/.zshrc) # COMP_WORDBREAKS=${COMP_WORDBREAKS/=/} @@ -11,18 +9,18 @@ COMP_WORDBREAKS=${COMP_WORDBREAKS/@/} export COMP_WORDBREAKS if type complete &>/dev/null; then - _{pkgname}_completion () { + _pm2_completion () { local si="$IFS" IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \ COMP_LINE="$COMP_LINE" \ COMP_POINT="$COMP_POINT" \ - {completer} completion -- "${COMP_WORDS[@]}" \ + pm2 completion -- "${COMP_WORDS[@]}" \ 2>/dev/null)) || return $? IFS="$si" } - complete -o default -F _{pkgname}_completion {pkgname} + complete -o default -F _pm2_completion pm2 elif type compctl &>/dev/null; then - _{pkgname}_completion () { + _pm2_completion () { local cword line point words si read -Ac words read -cn cword @@ -33,10 +31,10 @@ elif type compctl &>/dev/null; then IFS=$'\n' reply=($(COMP_CWORD="$cword" \ COMP_LINE="$line" \ COMP_POINT="$point" \ - {completer} completion -- "${words[@]}" \ + pm2 completion -- "${words[@]}" \ 2>/dev/null)) || return $? IFS="$si" } - compctl -K _{pkgname}_completion + -f + {pkgname} + compctl -K _pm2_completion pm2 fi -###-end-{pkgname}-completion-### +###-end-pm2-completion-###