├── .gitattributes ├── examples └── tabtab-test-complete │ ├── .gitignore │ ├── package.json │ ├── package-lock.json │ ├── readme.md │ └── index.js ├── .eslintrc.json ├── test ├── fixtures │ └── tabtab-install.js ├── basic.js ├── parse-env.js ├── tabtab-install.js ├── utils │ └── index.js ├── log.js └── installer.js ├── .gitignore ├── lib ├── utils │ ├── index.js │ ├── systemShell.js │ ├── exists.js │ └── tabtabDebug.js ├── constants.js ├── scripts │ ├── zsh.sh │ ├── fish.sh │ └── bash.sh ├── prompt.js ├── index.js └── installer.js ├── api ├── prompt.js.md ├── index.js.md └── installer.js.md ├── .travis.yml ├── LICENSE ├── package.json ├── readme.md └── CHANGELOG.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "mklabs", 3 | 4 | "rules": { 5 | "no-param-reassign": "off", 6 | "no-console": "off", 7 | "consistent-return": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/tabtab-install.js: -------------------------------------------------------------------------------- 1 | const tabtab = require('../..'); 2 | 3 | (async () => { 4 | await tabtab.install({ 5 | name: 'foo', 6 | completer: 'foo-complete' 7 | }); 8 | })(); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .completions/tt 2 | .tern-port 3 | src/ 4 | .completions/ 5 | node_modules/ 6 | .nyc_output/ 7 | 8 | note.txt 9 | quick-test.js 10 | coverage/ 11 | tabtab/ 12 | test/tabtab.log 13 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | const tabtabDebug = require('./tabtabDebug'); 2 | const systemShell = require('./systemShell'); 3 | const exists = require('./exists'); 4 | 5 | module.exports = { 6 | systemShell, 7 | tabtabDebug, 8 | exists 9 | }; 10 | -------------------------------------------------------------------------------- /api/prompt.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## prompt() 4 | Asks user about SHELL and desired location. 5 | 6 | It is too difficult to check spawned SHELL, the user has to use chsh before 7 | it is reflected in process.env.SHELL 8 | 9 | **Kind**: global function 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | node_js: 5 | - 10 6 | - 8 7 | - 7 8 | 9 | cache: 10 | directories: 11 | - node_modules 12 | 13 | notifications: 14 | email: false 15 | 16 | before_install: 17 | - npm install -g npm@latest 18 | 19 | after_success: 20 | - npm run coverage 21 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | const BASH_LOCATION = '~/.bashrc'; 2 | const FISH_LOCATION = '~/.config/fish/config.fish'; 3 | const ZSH_LOCATION = '~/.zshrc'; 4 | const COMPLETION_DIR = '~/.config/tabtab'; 5 | const TABTAB_SCRIPT_NAME = '__tabtab'; 6 | 7 | module.exports = { 8 | BASH_LOCATION, 9 | ZSH_LOCATION, 10 | FISH_LOCATION, 11 | COMPLETION_DIR, 12 | TABTAB_SCRIPT_NAME 13 | }; 14 | -------------------------------------------------------------------------------- /lib/utils/systemShell.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility to figure out the shell used on the system. 3 | * 4 | * Sadly, we can't use `echo $0` in node, maybe with more work. So we rely on 5 | * process.env.SHELL. 6 | * 7 | * TODO: More work on this, namely to detect Git bash on Windows (bash.exe) 8 | */ 9 | const systemShell = () => (process.env.SHELL || '').split('/').slice(-1)[0]; 10 | 11 | module.exports = systemShell; 12 | -------------------------------------------------------------------------------- /lib/utils/exists.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const untildify = require('untildify'); 3 | const { promisify } = require('es6-promisify'); 4 | 5 | const readFile = promisify(fs.readFile); 6 | 7 | module.exports = async file => { 8 | let fileExists; 9 | try { 10 | await readFile(untildify(file)); 11 | fileExists = true; 12 | } catch (err) { 13 | fileExists = false; 14 | } 15 | 16 | return fileExists; 17 | }; 18 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabtab-test-complete", 3 | "version": "1.0.0", 4 | "bin": { 5 | "tabtab-test": "index.js" 6 | }, 7 | "description": "Basic test package for tabtab completion", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "debug": "^4.0.1", 15 | "minimist": "^1.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/scripts/zsh.sh: -------------------------------------------------------------------------------- 1 | ###-begin-{pkgname}-completion-### 2 | if type compdef &>/dev/null; then 3 | _{pkgname}_completion () { 4 | local reply 5 | local si=$IFS 6 | 7 | IFS=$'\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" {completer} completion -- "${words[@]}")) 8 | IFS=$si 9 | 10 | _describe 'values' reply 11 | } 12 | compdef _{pkgname}_completion {pkgname} 13 | fi 14 | ###-end-{pkgname}-completion-### 15 | -------------------------------------------------------------------------------- /lib/scripts/fish.sh: -------------------------------------------------------------------------------- 1 | ###-begin-{pkgname}-completion-### 2 | function _{pkgname}_completion 3 | set cmd (commandline -o) 4 | set cursor (commandline -C) 5 | set words (node -pe "'$cmd'.split(' ').length") 6 | 7 | set completions (eval env DEBUG=\"" \"" COMP_CWORD=\""$words\"" COMP_LINE=\""$cmd \"" COMP_POINT=\""$cursor\"" {completer} completion -- $cmd) 8 | 9 | for completion in $completions 10 | echo -e $completion 11 | end 12 | end 13 | 14 | complete -f -d '{pkgname}' -c {pkgname} -a "(eval _{pkgname}_completion)" 15 | ###-end-{pkgname}-completion-### 16 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const tabtab = require('..'); 2 | const assert = require('assert'); 3 | 4 | describe('tabtab', () => { 5 | it('tabtab.shell()', () => { 6 | let shell = tabtab.shell(); 7 | assert.equal(shell, 'bash'); 8 | 9 | const previousShell = process.env.SHELL; 10 | process.env.SHELL = '/bin/bash'; 11 | shell = tabtab.shell(); 12 | assert.equal(shell, 'bash'); 13 | 14 | process.env.SHELL = '/usr/bin/zsh'; 15 | shell = tabtab.shell(); 16 | assert.equal(shell, 'zsh'); 17 | 18 | process.env.SHELL = '/usr/bin/fish'; 19 | shell = tabtab.shell(); 20 | assert.equal(shell, 'fish'); 21 | 22 | process.env.SHELL = previousShell; 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/parse-env.js: -------------------------------------------------------------------------------- 1 | const tabtab = require('..'); 2 | const assert = require('assert'); 3 | 4 | describe('tabtab.parseEnv()', () => { 5 | it('parseEnv with COMP stuff', () => { 6 | assert.equal(typeof tabtab.parseEnv, 'function'); 7 | 8 | const result = tabtab.parseEnv( 9 | Object.assign({}, process.env, { 10 | COMP_CWORD: 3, 11 | COMP_LINE: 'foo bar baz', 12 | COMP_POINT: 11 13 | }) 14 | ); 15 | 16 | assert.deepEqual(result, { 17 | complete: true, 18 | words: 3, 19 | point: 11, 20 | line: 'foo bar baz', 21 | partial: 'foo bar baz', 22 | last: 'baz', 23 | lastPartial: 'baz', 24 | prev: 'bar' 25 | }); 26 | }); 27 | 28 | it('parseEnv without COMP stuff', () => { 29 | const result = tabtab.parseEnv(Object.assign({}, process.env)); 30 | assert.equal(result.complete, false); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /lib/scripts/bash.sh: -------------------------------------------------------------------------------- 1 | ###-begin-{pkgname}-completion-### 2 | if type complete &>/dev/null; then 3 | _{pkgname}_completion () { 4 | local words cword 5 | if type _get_comp_words_by_ref &>/dev/null; then 6 | _get_comp_words_by_ref -n = -n @ -n : -w words -i cword 7 | else 8 | cword="$COMP_CWORD" 9 | words=("${COMP_WORDS[@]}") 10 | fi 11 | 12 | local si="$IFS" 13 | IFS=$'\n' COMPREPLY=($(COMP_CWORD="$cword" \ 14 | COMP_LINE="$COMP_LINE" \ 15 | COMP_POINT="$COMP_POINT" \ 16 | {completer} completion -- "${words[@]}" \ 17 | 2>/dev/null)) || return $? 18 | IFS="$si" 19 | if type __ltrim_colon_completions &>/dev/null; then 20 | __ltrim_colon_completions "${words[cword]}" 21 | fi 22 | } 23 | complete -o default -F _{pkgname}_completion {pkgname} 24 | fi 25 | ###-end-{pkgname}-completion-### 26 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabtab-test-complete", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "debug": { 8 | "version": "4.0.1", 9 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.0.1.tgz", 10 | "integrity": "sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw==", 11 | "requires": { 12 | "ms": "^2.1.1" 13 | } 14 | }, 15 | "minimist": { 16 | "version": "1.2.0", 17 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 18 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 19 | }, 20 | "ms": { 21 | "version": "2.1.1", 22 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 23 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/utils/tabtabDebug.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | 4 | /** 5 | * If TABTAB_DEBUG env is set, make it so that debug statements are also log to 6 | * TABTAB_DEBUG file provided. 7 | */ 8 | const tabtabDebug = name => { 9 | /* eslint-disable global-require */ 10 | let debug = require('debug')(name); 11 | 12 | if (process.env.TABTAB_DEBUG) { 13 | const file = process.env.TABTAB_DEBUG; 14 | const stream = fs.createWriteStream(file, { 15 | flags: 'a+' 16 | }); 17 | 18 | const log = (...args) => { 19 | args = args.map(arg => { 20 | if (typeof arg === 'string') return arg; 21 | return JSON.stringify(arg); 22 | }); 23 | 24 | const str = `${util.format(...args)}\n`; 25 | stream.write(str); 26 | }; 27 | 28 | if (process.env.COMP_LINE) { 29 | debug = log; 30 | } else { 31 | debug.log = log; 32 | } 33 | } 34 | 35 | return debug; 36 | }; 37 | 38 | module.exports = tabtabDebug; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT 2011-2018 License (MIT) 2 | 3 | Copyright (c) Mickael Daniel 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/readme.md: -------------------------------------------------------------------------------- 1 | # tabtab-test-complete 2 | 3 | A simple package to test out tabtab against real completions. 4 | 5 | To install, simply run `npm link` 6 | 7 | npm link 8 | 9 | It'll install the following binary system-wide: 10 | 11 | - tabtab-test: The actual binary being completed 12 | 13 | ## Shell notes 14 | 15 | To test against **bash**, make sure to have `$SHELL` set to either `bash` or `/bin/bash` or similar. 16 | 17 | To test against **zsh**, make sure to have zsh installed, and then, if you use bash 18 | as your standard SHELL, type `zsh`. It'll spawn a new zsh session. Within this, 19 | run `SHELL=zsh` to set the environment accordingly so that tabtab understands 20 | the current shell used is actually zsh. 21 | 22 | Similarly, to test against **fish**, make sure to have fish installed, and then 23 | the same steps to reproduce. This time, make sure to type `fish` and run `set 24 | SHELL fish`. This is required for tabtab to understand the shell being used is 25 | actually fish. 26 | 27 | Those steps are not required if testing against your system shell (possibly using `chsh`). 28 | 29 | ## Completion install 30 | 31 | In this example package, simply run: 32 | 33 | tabtab-test install-completion 34 | 35 | You'll need to do this for each and every shell you're testing against. Follow 36 | the `Shell notes` described above for details. 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "mklabs", 3 | "name": "tabtab", 4 | "description": "tab completion helpers, for node cli programs. Inspired by npm completion.", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib/**/*.{js,sh}" 8 | ], 9 | "scripts": { 10 | "test": "mkdirp ~/.config/tabtab && DEBUG='tabtab*' c8 mocha --timeout 5000", 11 | "posttest": "npm run eslint", 12 | "mocha": "DEBUG='tabtab*' mocha --timeout 5000", 13 | "coverage": "c8 report --reporter=text-lcov | coveralls", 14 | "coverage-html": "npm run mocha && c8 report --reporter=html && serve coverage", 15 | "eslint": "eslint lib/ test/", 16 | "watch": "npm-watch", 17 | "readme": "remark readme.md --use toc --output", 18 | "changelog": "auto-changelog -p", 19 | "api": "for file in `echo index.js installer.js prompt.js`; do jsdoc2md lib/$file > api/$file.md; done", 20 | "docs": "npm run api && npm run readme && npm run changelog" 21 | }, 22 | "watch": { 23 | "test": "{lib,test}/**/*.js" 24 | }, 25 | "devDependencies": { 26 | "auto-changelog": "^1.8.0", 27 | "c8": "^3.2.0", 28 | "coveralls": "^3.0.2", 29 | "eslint-config-mklabs": "^1.0.9", 30 | "inquirer-test": "^2.0.1", 31 | "jsdoc-to-markdown": "^4.0.1", 32 | "mocha": "^5.2.0", 33 | "npm-watch": "^0.4.0", 34 | "remark-cli": "^5.0.0", 35 | "remark-toc": "^5.0.0", 36 | "serve": "^10.0.2" 37 | }, 38 | "license": "MIT", 39 | "keywords": [ 40 | "terminal", 41 | "tab", 42 | "unix", 43 | "console", 44 | "complete", 45 | "completion" 46 | ], 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/mklabs/tabtab.git" 50 | }, 51 | "dependencies": { 52 | "debug": "^4.0.1", 53 | "es6-promisify": "^6.0.0", 54 | "inquirer": "^6.0.0", 55 | "minimist": "^1.2.0", 56 | "mkdirp": "^0.5.1", 57 | "untildify": "^3.0.3" 58 | }, 59 | "auto-changelog": { 60 | "template": "keepachangelog", 61 | "unreleased": true, 62 | "commitLimit": false, 63 | "ignoreCommitPattern": "changelog|readme|^test" 64 | }, 65 | "version": "3.0.3" 66 | } 67 | -------------------------------------------------------------------------------- /lib/prompt.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const path = require('path'); 3 | const debug = require('./utils/tabtabDebug')('tabtab:prompt'); 4 | 5 | /** 6 | * Asks user about SHELL and desired location. 7 | * 8 | * It is too difficult to check spawned SHELL, the user has to use chsh before 9 | * it is reflected in process.env.SHELL 10 | */ 11 | const prompt = () => { 12 | const ask = inquirer.createPromptModule(); 13 | 14 | const questions = [ 15 | { 16 | type: 'list', 17 | name: 'shell', 18 | message: 'Which Shell do you use ?', 19 | choices: ['bash', 'zsh', 'fish'], 20 | default: 'bash' 21 | } 22 | ]; 23 | 24 | const locations = { 25 | bash: '~/.bashrc', 26 | zsh: '~/.zshrc', 27 | fish: '~/.config/fish/config.fish' 28 | }; 29 | 30 | const finalAnswers = {}; 31 | 32 | return ask(questions) 33 | .then(answers => { 34 | const { shell } = answers; 35 | debug('answers', shell); 36 | 37 | const location = locations[shell]; 38 | debug(`Will install completion to ${location}`); 39 | 40 | Object.assign(finalAnswers, { location, shell }); 41 | return location; 42 | }) 43 | .then(location => 44 | ask({ 45 | type: 'confirm', 46 | name: 'locationOK', 47 | message: `We will install completion to ${location}, is it ok ?` 48 | }) 49 | ) 50 | .then(answers => { 51 | const { locationOK } = answers; 52 | if (locationOK) { 53 | debug('location is ok, return', finalAnswers); 54 | return finalAnswers; 55 | } 56 | 57 | // otherwise, ask for specific **absolute** path 58 | return ask({ 59 | name: 'userLocation', 60 | message: 'Which path then ? Must be absolute.', 61 | validate: input => { 62 | debug('Validating input', input); 63 | return path.isAbsolute(input); 64 | } 65 | }).then(lastAnswer => { 66 | const { userLocation } = lastAnswer; 67 | console.log(`Very well, we will install using ${userLocation}`); 68 | Object.assign(finalAnswers, { location: userLocation }); 69 | 70 | return finalAnswers; 71 | }); 72 | }); 73 | }; 74 | 75 | module.exports = prompt; 76 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const opts = require('minimist')(process.argv.slice(2), { 4 | string: ['foo', 'bar'], 5 | boolean: ['help', 'version', 'loglevel'] 6 | }); 7 | 8 | const tabtab = require('../..'); 9 | 10 | const args = opts._; 11 | 12 | const completion = env => { 13 | if (!env.complete) return; 14 | 15 | if (env.prev === 'someCommand') { 16 | return tabtab.log(['is', 'this', 'the', 'real', 'life']); 17 | } 18 | 19 | if (env.prev === 'anotherOne') { 20 | return tabtab.log(['is', 'this', 'just', 'fantasy']); 21 | } 22 | 23 | if (env.prev === '--loglevel') { 24 | return tabtab.log(['error', 'warn', 'info', 'notice', 'verbose']); 25 | } 26 | 27 | return tabtab.log([ 28 | '--help', 29 | '--version', 30 | '--loglevel', 31 | 'foo', 32 | 'bar', 33 | 'install-completion', 34 | 'uninstall-completion', 35 | 'completion', 36 | 'someCommand:someCommand is a some kind of command with a description', 37 | { 38 | name: 'someOtherCommand:hey', 39 | description: 'You must add a description for items with ":" in them' 40 | }, 41 | 'anotherOne' 42 | ]); 43 | }; 44 | 45 | const init = async () => { 46 | const cmd = args[0]; 47 | 48 | if (opts.help) { 49 | return console.log('Output help here'); 50 | } 51 | 52 | if (opts.version) { 53 | return console.log('Output version here'); 54 | } 55 | 56 | if (opts.loglevel) { 57 | return console.log('Output version here'); 58 | } 59 | 60 | if (cmd === 'foo') { 61 | return console.log('foobar'); 62 | } 63 | 64 | if (cmd === 'bar') { 65 | return console.log('barbar'); 66 | } 67 | 68 | if (cmd === 'someCommand') { 69 | return console.log('is this the real life ?'); 70 | } 71 | 72 | if (cmd === 'anotherOne') { 73 | return console.log('is this just fantasy ?'); 74 | } 75 | 76 | if (cmd === 'install-completion') { 77 | // Here we install for the program `tabtab-test` (this file), with 78 | // completer being the same program. Sometimes, you want to complete 79 | // another program that's where the `completer` option might come handy. 80 | await tabtab 81 | .install({ 82 | name: 'tabtab-test', 83 | completer: 'tabtab-test' 84 | }) 85 | .catch(err => console.error('INSTALL ERROR', err)); 86 | 87 | return; 88 | } 89 | 90 | if (cmd === 'uninstall-completion') { 91 | // Here we uninstall for the program `tabtab-test` (this file). 92 | await tabtab 93 | .uninstall({ 94 | name: 'tabtab-test' 95 | }) 96 | .catch(err => console.error('UNINSTALL ERROR', err)); 97 | 98 | return; 99 | } 100 | 101 | if (cmd === 'completion') { 102 | const env = tabtab.parseEnv(process.env); 103 | return completion(env); 104 | } 105 | }; 106 | 107 | init(); 108 | -------------------------------------------------------------------------------- /test/tabtab-install.js: -------------------------------------------------------------------------------- 1 | const tabtab = require('..'); 2 | const assert = require('assert'); 3 | const run = require('inquirer-test'); 4 | const debug = require('debug')('tabtab:test:install'); 5 | const untildify = require('untildify'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | const { promisify } = require('es6-promisify'); 9 | const { COMPLETION_DIR } = require('../lib/constants'); 10 | const { rejects, setupSuiteForInstall } = require('./utils'); 11 | 12 | const readFile = promisify(fs.readFile); 13 | 14 | // For node 7 / 8 15 | assert.rejects = rejects; 16 | 17 | // inquirer-test needs a little bit more time, or my setup 18 | const TIMEOUT = 500; 19 | const { ENTER } = run; 20 | 21 | describe('tabtab.install()', () => { 22 | it('is a function', () => { 23 | assert.equal(typeof tabtab.install, 'function'); 24 | }); 25 | 26 | it('rejects on missing options', async () => { 27 | await assert.rejects(async () => tabtab.install(), TypeError); 28 | }); 29 | 30 | it('rejects on missing name options', async () => { 31 | await assert.rejects( 32 | async () => tabtab.install({}), 33 | /options\.name is required/ 34 | ); 35 | }); 36 | 37 | it('rejects on missing completer options', async () => { 38 | await assert.rejects( 39 | async () => tabtab.install({ name: 'foo' }), 40 | /options\.completer is required/ 41 | ); 42 | }); 43 | 44 | describe('tabtab.install() on ~/.bashrc', () => { 45 | setupSuiteForInstall(); 46 | 47 | it('asks about shell (bash) with custom location', () => { 48 | const cliPath = path.join(__dirname, 'fixtures/tabtab-install.js'); 49 | 50 | return run( 51 | [cliPath], 52 | [ENTER, 'n', ENTER, '/tmp/foo', ENTER], 53 | TIMEOUT 54 | ).then(result => { 55 | debug('Test result', result); 56 | 57 | assert.ok(/Which Shell do you use \? bash/.test(result)); 58 | assert.ok( 59 | /We will install completion to ~\/\.bashrc, is it ok \?/.test(result) 60 | ); 61 | assert.ok(/Which path then \? Must be absolute/.test(result)); 62 | assert.ok(/Very well, we will install using \/tmp\/foo/.test(result)); 63 | }); 64 | }); 65 | 66 | it('asks about shell (bash) with default location', () => { 67 | const cliPath = path.join(__dirname, 'fixtures/tabtab-install.js'); 68 | 69 | return run([cliPath], [ENTER, ENTER], TIMEOUT) 70 | .then(result => { 71 | debug('Test result', result); 72 | 73 | assert.ok(/Which Shell do you use \? bash/.test(result)); 74 | assert.ok( 75 | /install completion to ~\/\.bashrc, is it ok \? Yes/.test(result) 76 | ); 77 | }) 78 | .then(() => readFile(untildify('~/.bashrc'), 'utf8')) 79 | .then(filecontent => { 80 | assert.ok(/tabtab source for packages/.test(filecontent)); 81 | assert.ok(/uninstall by removing these lines/.test(filecontent)); 82 | assert.ok( 83 | filecontent.match(`. ${path.join(COMPLETION_DIR, '__tabtab.bash')}`) 84 | ); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const untildify = require('untildify'); 4 | const { promisify } = require('es6-promisify'); 5 | const { COMPLETION_DIR, TABTAB_SCRIPT_NAME } = require('../../lib/constants'); 6 | 7 | const { exists } = require('../../lib/utils'); 8 | 9 | const writeFile = promisify(fs.writeFile); 10 | const readFile = promisify(fs.readFile); 11 | 12 | /** 13 | * Returns both { exists, content } 14 | * 15 | * @param {filename} filename - The file to check and read 16 | */ 17 | const readIfExists = async filename => { 18 | /* eslint-disable no-return-await */ 19 | const filepath = untildify(filename); 20 | const fileExists = await exists(filepath); 21 | const content = fileExists ? await readFile(filepath, 'utf8') : ''; 22 | 23 | return { 24 | exists: fileExists, 25 | content 26 | }; 27 | }; 28 | 29 | const afterWrites = (prevBashrc, prevScript) => async () => { 30 | const bashrc = untildify('~/.bashrc'); 31 | const tabtabScript = untildify( 32 | path.join(COMPLETION_DIR, `${TABTAB_SCRIPT_NAME}.bash`) 33 | ); 34 | 35 | await writeFile(bashrc, prevBashrc); 36 | await writeFile(tabtabScript, prevScript); 37 | }; 38 | 39 | /** This simply setup a suite with after hook for tabtab.install. 40 | * 41 | * Defaults to afterEach, pass in true to make it so that it uses "after" 42 | * instead. 43 | * 44 | * @param {Boolean} shouldUseAfter - True to use after instead of afterEach 45 | */ 46 | const setupSuiteForInstall = async (shouldUseAfter = false) => { 47 | const files = {}; 48 | const hook = shouldUseAfter ? after : afterEach; 49 | const tabtabScript = path.join(COMPLETION_DIR, `${TABTAB_SCRIPT_NAME}.bash`); 50 | 51 | before(async () => { 52 | const { exists: bashrcExists, content: bashrcContent } = await readIfExists( 53 | '~/.bashrc' 54 | ); 55 | 56 | const { 57 | exists: tabtabScriptExists, 58 | content: tabtabScriptContent 59 | } = await readIfExists(tabtabScript); 60 | 61 | files.bashrcExists = bashrcExists; 62 | files.bashrcContent = bashrcContent; 63 | files.tabtabScriptExists = tabtabScriptExists; 64 | files.tabtabScriptContent = tabtabScriptContent; 65 | }); 66 | 67 | hook(async () => { 68 | const { 69 | bashrcExists, 70 | bashrcContent, 71 | tabtabScriptExists, 72 | tabtabScriptContent 73 | } = files; 74 | 75 | if (bashrcExists) { 76 | await writeFile(untildify('~/.bashrc'), bashrcContent); 77 | } 78 | 79 | if (tabtabScriptExists) { 80 | await writeFile(untildify(tabtabScript), tabtabScriptContent); 81 | } 82 | }); 83 | }; 84 | 85 | // For node 7 / 8 86 | const rejects = async (promise, error, message = '') => { 87 | let toThrow; 88 | await promise().catch(err => { 89 | if (error instanceof RegExp) { 90 | const ok = error.test(err.message); 91 | if (!ok) { 92 | toThrow = new Error( 93 | `AssertionError: ${error} is not validated 94 | ${message}` 95 | ); 96 | } 97 | } else { 98 | const ok = err instanceof error; 99 | if (!ok) { 100 | toThrow = new Error( 101 | `AssertionError: ${err.name} is not an instanceof ${error.name} 102 | ${message}` 103 | ); 104 | } 105 | } 106 | }); 107 | 108 | if (toThrow) { 109 | throw toThrow; 110 | } 111 | }; 112 | 113 | module.exports = { 114 | readIfExists, 115 | rejects, 116 | afterWrites, 117 | setupSuiteForInstall 118 | }; 119 | -------------------------------------------------------------------------------- /test/log.js: -------------------------------------------------------------------------------- 1 | const tabtab = require('..'); 2 | const assert = require('assert'); 3 | 4 | describe('tabtab.log', () => { 5 | it('tabtab.log throws an Error in case args is not an Array', () => { 6 | assert.throws(() => { 7 | tabtab.log('foo', 'bar'); 8 | }, /^Error: log: Invalid arguments, must be an array$/); 9 | }); 10 | 11 | const logTestHelper = items => { 12 | const logs = []; 13 | const { log } = console; 14 | console.log = data => logs.push(data); 15 | tabtab.log(items); 16 | console.log = log; 17 | return logs; 18 | }; 19 | 20 | it('tabtab.log logs item to the console', () => { 21 | assert.equal(typeof tabtab.log, 'function'); 22 | 23 | const logs = logTestHelper(['--foo', '--bar']); 24 | 25 | assert.equal(logs.length, 2); 26 | assert.deepStrictEqual(logs, ['--foo', '--bar']); 27 | }); 28 | 29 | it('tabtab.log accepts { name, description }', () => { 30 | const logs = logTestHelper([ 31 | { name: '--foo', description: 'Foo options' }, 32 | { name: '--bar', description: 'Bar option' } 33 | ]); 34 | 35 | assert.equal(logs.length, 2); 36 | assert.deepStrictEqual(logs, ['--foo', '--bar']); 37 | }); 38 | 39 | it('tabtab.log normalize String and Objects', () => { 40 | const logs = logTestHelper([ 41 | { name: '--foo', description: 'Foo options' }, 42 | { name: '--bar', description: 'Bar option' }, 43 | 'foobar' 44 | ]); 45 | 46 | assert.equal(logs.length, 3); 47 | assert.deepStrictEqual(logs, ['--foo', '--bar', 'foobar']); 48 | }); 49 | 50 | it('tabtab.log normalize String and Objects, with description stripped out on Bash', () => { 51 | const shell = process.env.SHELL; 52 | process.env.SHELL = '/bin/bash'; 53 | const logs = logTestHelper([ 54 | { name: '--foo', description: 'Foo options' }, 55 | { name: '--bar', description: 'Bar option' }, 56 | 'foobar', 57 | 'barfoo:barfoo is not foobar' 58 | ]); 59 | 60 | assert.equal(logs.length, 4); 61 | assert.deepStrictEqual(logs, ['--foo', '--bar', 'foobar', 'barfoo']); 62 | process.env.SHELL = shell; 63 | }); 64 | 65 | it('tabtab.log with description NOT stripped out on Zsh', () => { 66 | const shell = process.env.SHELL; 67 | process.env.SHELL = '/usr/bin/zsh'; 68 | const logs = logTestHelper([ 69 | { name: '--foo', description: 'Foo option' }, 70 | { name: '--bar', description: 'Bar option' }, 71 | 'foobar', 72 | 'barfoo:barfoo is not foobar' 73 | ]); 74 | 75 | assert.equal(logs.length, 4); 76 | assert.deepStrictEqual(logs, [ 77 | '--foo:Foo option', 78 | '--bar:Bar option', 79 | 'foobar', 80 | 'barfoo:barfoo is not foobar' 81 | ]); 82 | process.env.SHELL = shell; 83 | }); 84 | 85 | it('tabtab.log with description NOT stripped out on fish', () => { 86 | const shell = process.env.SHELL; 87 | process.env.SHELL = '/usr/bin/fish'; 88 | const logs = logTestHelper([ 89 | { name: '--foo', description: 'Foo option' }, 90 | { name: '--bar', description: 'Bar option' }, 91 | 'foobar', 92 | 'barfoo:barfoo is not foobar' 93 | ]); 94 | 95 | assert.equal(logs.length, 4); 96 | assert.deepStrictEqual(logs, [ 97 | '--foo\tFoo option', 98 | '--bar\tBar option', 99 | 'foobar', 100 | 'barfoo\tbarfoo is not foobar' 101 | ]); 102 | process.env.SHELL = shell; 103 | }); 104 | 105 | it('tabtab.log could use {name, description} for completions with ":" in them', () => { 106 | const shell = process.env.SHELL; 107 | process.env.SHELL = '/usr/bin/zsh'; 108 | const logs = logTestHelper([ 109 | { name: '--foo:bar', description: 'Foo option' }, 110 | { name: '--bar:foo', description: 'Bar option' }, 111 | 'foobar', 112 | 'barfoo:barfoo is not foobar' 113 | ]); 114 | 115 | assert.equal(logs.length, 4); 116 | assert.deepStrictEqual(logs, [ 117 | '--foo\\:bar:Foo option', 118 | '--bar\\:foo:Bar option', 119 | 'foobar', 120 | 'barfoo:barfoo is not foobar' 121 | ]); 122 | process.env.SHELL = shell; 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /test/installer.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const untildify = require('untildify'); 5 | const { promisify } = require('es6-promisify'); 6 | const { 7 | install, 8 | uninstall, 9 | writeToShellConfig, 10 | writeToCompletionScript 11 | } = require('../lib/installer'); 12 | const { COMPLETION_DIR, TABTAB_SCRIPT_NAME } = require('../lib/constants'); 13 | const { rejects, setupSuiteForInstall } = require('./utils'); 14 | 15 | // For node 7 / 8 16 | assert.rejects = rejects; 17 | 18 | const readFile = promisify(fs.readFile); 19 | const writeFile = promisify(fs.writeFile); 20 | 21 | describe('installer', () => { 22 | it('has install / uninstall functions', () => { 23 | assert.equal(typeof install, 'function'); 24 | assert.equal(typeof uninstall, 'function'); 25 | }); 26 | 27 | it('install rejects on missing options', async () => { 28 | await assert.rejects(async () => install(), /options.name is required/); 29 | await assert.rejects( 30 | async () => install({ name: 'foo ' }), 31 | /options.completer is required/ 32 | ); 33 | 34 | await assert.rejects( 35 | async () => install({ name: 'foo ', completer: 'foo-complete' }), 36 | /options.location is required/ 37 | ); 38 | }); 39 | 40 | it('uninstall rejects on missing options', async () => { 41 | await assert.rejects( 42 | async () => uninstall(), 43 | /Unable to uninstall if options.name is missing/, 44 | 'Uninstall should throw the expected message when name is missing' 45 | ); 46 | }); 47 | 48 | it('has writeToShellConfig / writeToCompletionScript functions', () => { 49 | assert.equal(typeof writeToShellConfig, 'function'); 50 | assert.equal(typeof writeToCompletionScript, 'function'); 51 | }); 52 | 53 | describe('installer on ~/.bashrc', () => { 54 | setupSuiteForInstall(true); 55 | 56 | before(async () => { 57 | // Make sure __tabtab.bash starts with empty content, it'll be restored by setupSuiteForInstall 58 | await writeFile( 59 | untildify(path.join(COMPLETION_DIR, `${TABTAB_SCRIPT_NAME}.bash`)), 60 | '' 61 | ); 62 | }); 63 | 64 | it('installs the necessary line into ~/.bashrc', () => 65 | install({ 66 | name: 'foo', 67 | completer: 'foo-complete', 68 | location: '~/.bashrc' 69 | }) 70 | .then(() => readFile(untildify('~/.bashrc'), 'utf8')) 71 | .then(filecontent => { 72 | assert.ok(/tabtab source for packages/.test(filecontent)); 73 | assert.ok(/uninstall by removing these lines/.test(filecontent)); 74 | assert.ok( 75 | filecontent.match(`. ${path.join(COMPLETION_DIR, '__tabtab.bash')}`) 76 | ); 77 | }) 78 | .then(() => 79 | readFile( 80 | untildify(path.join(COMPLETION_DIR, '__tabtab.bash')), 81 | 'utf8' 82 | ) 83 | ) 84 | .then(filecontent => { 85 | assert.ok(/tabtab source for foo/.test(filecontent)); 86 | assert.ok( 87 | filecontent.match(`. ${path.join(COMPLETION_DIR, 'foo.bash')}`) 88 | ); 89 | })); 90 | 91 | it('uninstalls the necessary line from ~/.bashrc and completion scripts', () => 92 | uninstall({ 93 | name: 'foo' 94 | }) 95 | .then(() => readFile(untildify('~/.bashrc'), 'utf8')) 96 | .then(filecontent => { 97 | assert.ok(!/tabtab source for packages/.test(filecontent)); 98 | assert.ok(!/uninstall by removing these lines/.test(filecontent)); 99 | assert.ok( 100 | !filecontent.match( 101 | `. ${path.join(COMPLETION_DIR, '__tabtab.bash')}` 102 | ) 103 | ); 104 | }) 105 | .then(() => 106 | readFile( 107 | untildify(path.join(COMPLETION_DIR, '__tabtab.bash')), 108 | 'utf8' 109 | ) 110 | ) 111 | .then(filecontent => { 112 | assert.ok(!/tabtab source for foo/.test(filecontent)); 113 | assert.ok( 114 | !filecontent.match(`. ${path.join(COMPLETION_DIR, 'foo.bash')}`) 115 | ); 116 | })); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /api/index.js.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | 3 |
Install and enable completion on user system. It'll ask for:
6 |Public: Main utility to extract information from command line arguments and 13 | Environment variables, namely COMP args in "plumbing" mode.
14 |options - The options hash as parsed by minimist, plus an env property 15 | representing user environment (default: { env: process.env }) 16 | :_ - The arguments Array parsed by minimist (positional arguments) 17 | :env - The environment Object that holds COMP args (default: process.env)
18 |Examples
19 |const env = tabtab.parseEnv(); 20 | // env: 21 | // complete A Boolean indicating whether we act in "plumbing mode" or not 22 | // words The Number of words in the completed line 23 | // point A Number indicating cursor position 24 | // line The String input line 25 | // partial The String part of line preceding cursor position 26 | // last The last String word of the line 27 | // lastPartial The last word String of partial 28 | // prev The String word preceding last
29 |Returns the data env object.
30 |Helper to normalize String and Objects with { name, description } when logging out.
33 |Main logging utility to pass completion items.
36 |This is simply an helper to log to stdout with each item separated by a new 37 | line.
38 |Bash needs in addition to filter out the args for the completion to work 39 | (zsh, fish don't need this).
40 |Object | to use with namely `name` and `completer` |
56 |
57 |
58 |
59 | ## parseEnv()
60 | Public: Main utility to extract information from command line arguments and
61 | Environment variables, namely COMP args in "plumbing" mode.
62 |
63 | options - The options hash as parsed by minimist, plus an env property
64 | representing user environment (default: { env: process.env })
65 | :_ - The arguments Array parsed by minimist (positional arguments)
66 | :env - The environment Object that holds COMP args (default: process.env)
67 |
68 | Examples
69 |
70 | const env = tabtab.parseEnv();
71 | // env:
72 | // complete A Boolean indicating whether we act in "plumbing mode" or not
73 | // words The Number of words in the completed line
74 | // point A Number indicating cursor position
75 | // line The String input line
76 | // partial The String part of line preceding cursor position
77 | // last The last String word of the line
78 | // lastPartial The last word String of partial
79 | // prev The String word preceding last
80 |
81 | Returns the data env object.
82 |
83 | **Kind**: global function
84 |
85 |
86 | ## completionItem(item)
87 | Helper to normalize String and Objects with { name, description } when logging out.
88 |
89 | **Kind**: global function
90 |
91 | | Param | Type | Description |
92 | | --- | --- | --- |
93 | | item | String \| Object | Item to normalize |
94 |
95 |
96 |
97 | ## log(Arguments)
98 | Main logging utility to pass completion items.
99 |
100 | This is simply an helper to log to stdout with each item separated by a new
101 | line.
102 |
103 | Bash needs in addition to filter out the args for the completion to work
104 | (zsh, fish don't need this).
105 |
106 | **Kind**: global function
107 |
108 | | Param | Type | Description |
109 | | --- | --- | --- |
110 | | Arguments | Array | to log, Strings or Objects with name and description property. |
111 |
112 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | const prompt = require('./prompt');
2 | const installer = require('./installer');
3 | const { tabtabDebug, systemShell } = require('./utils');
4 |
5 | // If TABTAB_DEBUG env is set, make it so that debug statements are also log to
6 | // TABTAB_DEBUG file provided.
7 | const debug = tabtabDebug('tabtab');
8 |
9 | /**
10 | * Install and enable completion on user system. It'll ask for:
11 | *
12 | * - SHELL (bash, zsh or fish)
13 | * - Path to shell script (with sensible defaults)
14 | *
15 | * @param {Object} Options to use with namely `name` and `completer`
16 | *
17 | */
18 | const install = async (options = { name: '', completer: '' }) => {
19 | const { name, completer } = options;
20 | if (!name) throw new TypeError('options.name is required');
21 | if (!completer) throw new TypeError('options.completer is required');
22 |
23 | return prompt().then(({ location }) =>
24 | installer.install({
25 | name,
26 | completer,
27 | location
28 | })
29 | );
30 | };
31 |
32 | const uninstall = async (options = { name: '' }) => {
33 | const { name } = options;
34 | if (!name) throw new TypeError('options.name is required');
35 |
36 | return installer
37 | .uninstall({ name })
38 | .catch(err => console.error('ERROR while uninstalling', err));
39 | };
40 |
41 | /**
42 | * Public: Main utility to extract information from command line arguments and
43 | * Environment variables, namely COMP args in "plumbing" mode.
44 | *
45 | * options - The options hash as parsed by minimist, plus an env property
46 | * representing user environment (default: { env: process.env })
47 | * :_ - The arguments Array parsed by minimist (positional arguments)
48 | * :env - The environment Object that holds COMP args (default: process.env)
49 | *
50 | * Examples
51 | *
52 | * const env = tabtab.parseEnv();
53 | * // env:
54 | * // complete A Boolean indicating whether we act in "plumbing mode" or not
55 | * // words The Number of words in the completed line
56 | * // point A Number indicating cursor position
57 | * // line The String input line
58 | * // partial The String part of line preceding cursor position
59 | * // last The last String word of the line
60 | * // lastPartial The last word String of partial
61 | * // prev The String word preceding last
62 | *
63 | * Returns the data env object.
64 | */
65 | const parseEnv = env => {
66 | if (!env) {
67 | throw new Error('parseEnv: You must pass in an environment object.');
68 | }
69 |
70 | debug(
71 | 'Parsing env. CWORD: %s, COMP_POINT: %s, COMP_LINE: %s',
72 | env.COMP_CWORD,
73 | env.COMP_POINT,
74 | env.COMP_LINE
75 | );
76 |
77 | let cword = Number(env.COMP_CWORD);
78 | let point = Number(env.COMP_POINT);
79 | const line = env.COMP_LINE || '';
80 |
81 | if (Number.isNaN(cword)) cword = 0;
82 | if (Number.isNaN(point)) point = 0;
83 |
84 | const partial = line.slice(0, point);
85 |
86 | const parts = line.split(' ');
87 | const prev = parts.slice(0, -1).slice(-1)[0];
88 |
89 | const last = parts.slice(-1).join('');
90 | const lastPartial = partial
91 | .split(' ')
92 | .slice(-1)
93 | .join('');
94 |
95 | let complete = true;
96 | if (!env.COMP_CWORD || !env.COMP_POINT || !env.COMP_LINE) {
97 | complete = false;
98 | }
99 |
100 | return {
101 | complete,
102 | words: cword,
103 | point,
104 | line,
105 | partial,
106 | last,
107 | lastPartial,
108 | prev
109 | };
110 | };
111 |
112 | /**
113 | * Helper to normalize String and Objects with { name, description } when logging out.
114 | *
115 | * @param {String|Object} item - Item to normalize
116 | */
117 | const completionItem = item => {
118 | debug('completion item', item);
119 |
120 | if (item.name || item.description) return item;
121 | const shell = systemShell();
122 |
123 | let name = item;
124 | let description = '';
125 | const matching = /^(.*?)(\\)?:(.*)$/.exec(item);
126 | if (matching) {
127 | [, name, , description] = matching;
128 | }
129 |
130 | if (shell === 'zsh' && /\\/.test(item)) {
131 | name += '\\';
132 | }
133 |
134 | return {
135 | name,
136 | description
137 | };
138 | };
139 |
140 | /**
141 | * Main logging utility to pass completion items.
142 | *
143 | * This is simply an helper to log to stdout with each item separated by a new
144 | * line.
145 | *
146 | * Bash needs in addition to filter out the args for the completion to work
147 | * (zsh, fish don't need this).
148 | *
149 | * @param {Array} Arguments to log, Strings or Objects with name and
150 | * description property.
151 | */
152 | const log = args => {
153 | const shell = systemShell();
154 |
155 | if (!Array.isArray(args)) {
156 | throw new Error('log: Invalid arguments, must be an array');
157 | }
158 |
159 | // Normalize arguments if there are some Objects { name, description } in them.
160 | args = args.map(completionItem).map(item => {
161 | const { name, description } = item;
162 | let str = name;
163 | if (shell === 'zsh' && description) {
164 | str = `${name.replace(/:/g, '\\:')}:${description}`;
165 | } else if (shell === 'fish' && description) {
166 | str = `${name}\t${description}`;
167 | }
168 |
169 | return str;
170 | });
171 |
172 | if (shell === 'bash') {
173 | const env = parseEnv(process.env);
174 | args = args.filter(arg => arg.indexOf(env.last) === 0);
175 | }
176 |
177 | for (const arg of args) {
178 | console.log(`${arg}`);
179 | }
180 | };
181 |
182 | module.exports = {
183 | shell: systemShell,
184 | install,
185 | uninstall,
186 | parseEnv,
187 | log
188 | };
189 |
--------------------------------------------------------------------------------
/api/installer.js.md:
--------------------------------------------------------------------------------
1 | ## Functions
2 |
3 | Little helper to return the correct file extension based on the SHELL value.
6 |Helper to return the correct script template based on the SHELL provided
9 |StringHelper to return the expected location for SHELL config file, based on the 12 | provided shell value.
13 |Helper to return the source line to add depending on the SHELL provided or detected.
16 |If the provided SHELL is not known, it returns the source line for a Bash shell.
17 |BooleanHelper to check if a filename is one of the SHELL config we expect
20 |BooleanChecks a given file for the existence of a specific line. Used to prevent 23 | adding multiple completion source to SHELL scripts.
24 |Opens a file for modification adding a new source line for the given
27 | SHELL. Used for both SHELL script and tabtab internal one.
Writes to SHELL config file adding a new line, but only one, to the SHELL 31 | config script. This enables tabtab to work for the given SHELL.
32 |Writes to tabtab internal script that acts as a frontend router for the 35 | completion mechanism, in the internal ~/.config/tabtab directory. Every 36 | completion is added to this file.
37 |This writes a new completion script in the internal ~/.config/tabtab
40 | directory. Depending on the SHELL used, a different script is created for
41 | the given SHELL.
Top level install method. Does three things:
45 |Removes the 3 relevant lines from provided filename, based on the package 53 | name passed in.
54 |Here the idea is to uninstall a given package completion from internal 57 | tabtab scripts and / or the SHELL config.
58 |It also removes the relevant scripts if no more completion are installed on 59 | the system.
60 |String | Shell to base the check on, defaults to system shell. |
81 |
82 |
83 |
84 | ## locationFromShell(shell) ⇒ String
85 | Helper to return the expected location for SHELL config file, based on the
86 | provided shell value.
87 |
88 | **Kind**: global function
89 | **Returns**: String - Either ~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish,
90 | untildified. Defaults to ~/.bashrc if provided SHELL is not valid.
91 |
92 | | Param | Type | Description |
93 | | --- | --- | --- |
94 | | shell | String | Shell value to test against |
95 |
96 |
97 |
98 | ## sourceLineForShell(scriptname, shell)
99 | Helper to return the source line to add depending on the SHELL provided or detected.
100 |
101 | If the provided SHELL is not known, it returns the source line for a Bash shell.
102 |
103 | **Kind**: global function
104 |
105 | | Param | Type | Description |
106 | | --- | --- | --- |
107 | | scriptname | String | The script to source |
108 | | shell | String | Shell to base the check on, defaults to system shell. |
109 |
110 |
111 |
112 | ## isInShellConfig(filename) ⇒ Boolean
113 | Helper to check if a filename is one of the SHELL config we expect
114 |
115 | **Kind**: global function
116 | **Returns**: Boolean - Either true or false
117 |
118 | | Param | Type | Description |
119 | | --- | --- | --- |
120 | | filename | String | Filename to check against |
121 |
122 |
123 |
124 | ## checkFilenameForLine(filename, line) ⇒ Boolean
125 | Checks a given file for the existence of a specific line. Used to prevent
126 | adding multiple completion source to SHELL scripts.
127 |
128 | **Kind**: global function
129 | **Returns**: Boolean - true or false, false if the line is not present.
130 |
131 | | Param | Type | Description |
132 | | --- | --- | --- |
133 | | filename | String | The filename to check against |
134 | | line | String | The line to look for |
135 |
136 |
137 |
138 | ## writeLineToFilename(options)
139 | Opens a file for modification adding a new `source` line for the given
140 | SHELL. Used for both SHELL script and tabtab internal one.
141 |
142 | **Kind**: global function
143 |
144 | | Param | Type | Description |
145 | | --- | --- | --- |
146 | | options | Object | Options with - filename: The file to modify - scriptname: The line to add sourcing this file - name: The package being configured |
147 |
148 |
149 |
150 | ## writeToShellConfig(options)
151 | Writes to SHELL config file adding a new line, but only one, to the SHELL
152 | config script. This enables tabtab to work for the given SHELL.
153 |
154 | **Kind**: global function
155 |
156 | | Param | Type | Description |
157 | | --- | --- | --- |
158 | | options | Object | Options object with - location: The SHELL script location (~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish) - name: The package configured for completion |
159 |
160 |
161 |
162 | ## writeToTabtabScript(options)
163 | Writes to tabtab internal script that acts as a frontend router for the
164 | completion mechanism, in the internal ~/.config/tabtab directory. Every
165 | completion is added to this file.
166 |
167 | **Kind**: global function
168 |
169 | | Param | Type | Description |
170 | | --- | --- | --- |
171 | | options | Object | Options object with - name: The package configured for completion |
172 |
173 |
174 |
175 | ## writeToCompletionScript(options)
176 | This writes a new completion script in the internal `~/.config/tabtab`
177 | directory. Depending on the SHELL used, a different script is created for
178 | the given SHELL.
179 |
180 | **Kind**: global function
181 |
182 | | Param | Type | Description |
183 | | --- | --- | --- |
184 | | options | Object | Options object with - name: The package configured for completion - completer: The binary that will act as the completer for `name` program |
185 |
186 |
187 |
188 | ## install(options)
189 | Top level install method. Does three things:
190 |
191 | - Writes to SHELL config file, adding a new line to tabtab internal script.
192 | - Creates or edit tabtab internal script
193 | - Creates the actual completion script for this package.
194 |
195 | **Kind**: global function
196 |
197 | | Param | Type | Description |
198 | | --- | --- | --- |
199 | | options | Object | Options object with - name: The program name to complete - completer: The actual program or binary that will act as the completer for `name` program. Can be the same. - location: The SHELL script config location (~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish) |
200 |
201 |
202 |
203 | ## removeLinesFromFilename(filename, name)
204 | Removes the 3 relevant lines from provided filename, based on the package
205 | name passed in.
206 |
207 | **Kind**: global function
208 |
209 | | Param | Type | Description |
210 | | --- | --- | --- |
211 | | filename | String | The filename to operate on |
212 | | name | String | The package name to look for |
213 |
214 |
215 |
216 | ## uninstall(options)
217 | Here the idea is to uninstall a given package completion from internal
218 | tabtab scripts and / or the SHELL config.
219 |
220 | It also removes the relevant scripts if no more completion are installed on
221 | the system.
222 |
223 | **Kind**: global function
224 |
225 | | Param | Type | Description |
226 | | --- | --- | --- |
227 | | options | Object | Options object with - name: The package name to look for |
228 |
229 |
--------------------------------------------------------------------------------
/lib/installer.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const untildify = require('untildify');
4 | const { promisify } = require('es6-promisify');
5 | const mkdirp = promisify(require('mkdirp'));
6 | const { tabtabDebug, systemShell, exists } = require('./utils');
7 |
8 | const debug = tabtabDebug('tabtab:installer');
9 |
10 | const readFile = promisify(fs.readFile);
11 | const writeFile = promisify(fs.writeFile);
12 | const unlink = promisify(fs.unlink);
13 |
14 | const {
15 | BASH_LOCATION,
16 | FISH_LOCATION,
17 | ZSH_LOCATION,
18 | COMPLETION_DIR,
19 | TABTAB_SCRIPT_NAME
20 | } = require('./constants');
21 |
22 | /**
23 | * Little helper to return the correct file extension based on the SHELL value.
24 | *
25 | * @returns The correct file extension for the given SHELL script location
26 | */
27 | const shellExtension = () => systemShell();
28 |
29 | /**
30 | * Helper to return the correct script template based on the SHELL provided
31 | *
32 | * @param {String} shell - Shell to base the check on, defaults to system shell.
33 | * @returns The template script content, defaults to Bash for shell we don't know yet
34 | */
35 | const scriptFromShell = (shell = systemShell()) => {
36 | if (shell === 'fish') {
37 | return path.join(__dirname, 'scripts/fish.sh');
38 | }
39 |
40 | if (shell === 'zsh') {
41 | return path.join(__dirname, 'scripts/zsh.sh');
42 | }
43 |
44 | // For Bash and others
45 | return path.join(__dirname, 'scripts/bash.sh');
46 | };
47 |
48 | /**
49 | * Helper to return the expected location for SHELL config file, based on the
50 | * provided shell value.
51 | *
52 | * @param {String} shell - Shell value to test against
53 | * @returns {String} Either ~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish,
54 | * untildified. Defaults to ~/.bashrc if provided SHELL is not valid.
55 | */
56 | const locationFromShell = (shell = systemShell()) => {
57 | if (shell === 'bash') return untildify(BASH_LOCATION);
58 | if (shell === 'zsh') return untildify(ZSH_LOCATION);
59 | if (shell === 'fish') return untildify(FISH_LOCATION);
60 | return BASH_LOCATION;
61 | };
62 |
63 | /**
64 | * Helper to return the source line to add depending on the SHELL provided or detected.
65 | *
66 | * If the provided SHELL is not known, it returns the source line for a Bash shell.
67 | *
68 | * @param {String} scriptname - The script to source
69 | * @param {String} shell - Shell to base the check on, defaults to system
70 | * shell.
71 | */
72 | const sourceLineForShell = (scriptname, shell = systemShell()) => {
73 | if (shell === 'fish') {
74 | return `[ -f ${scriptname} ]; and . ${scriptname}; or true`;
75 | }
76 |
77 | if (shell === 'zsh') {
78 | return `[[ -f ${scriptname} ]] && . ${scriptname} || true`;
79 | }
80 |
81 | // For Bash and others
82 | return `[ -f ${scriptname} ] && . ${scriptname} || true`;
83 | };
84 |
85 | /**
86 | * Helper to check if a filename is one of the SHELL config we expect
87 | *
88 | * @param {String} filename - Filename to check against
89 | * @returns {Boolean} Either true or false
90 | */
91 | const isInShellConfig = filename =>
92 | [
93 | BASH_LOCATION,
94 | ZSH_LOCATION,
95 | FISH_LOCATION,
96 | untildify(BASH_LOCATION),
97 | untildify(ZSH_LOCATION),
98 | untildify(FISH_LOCATION)
99 | ].includes(filename);
100 |
101 | /**
102 | * Checks a given file for the existence of a specific line. Used to prevent
103 | * adding multiple completion source to SHELL scripts.
104 | *
105 | * @param {String} filename - The filename to check against
106 | * @param {String} line - The line to look for
107 | * @returns {Boolean} true or false, false if the line is not present.
108 | */
109 | const checkFilenameForLine = async (filename, line) => {
110 | debug('Check filename (%s) for "%s"', filename, line);
111 |
112 | let filecontent = '';
113 | try {
114 | filecontent = await readFile(untildify(filename), 'utf8');
115 | } catch (err) {
116 | if (err.code !== 'ENOENT') {
117 | return console.error(
118 | 'Got an error while trying to read from %s file',
119 | filename,
120 | err
121 | );
122 | }
123 | }
124 |
125 | return !!filecontent.match(`${line}`);
126 | };
127 |
128 | /**
129 | * Opens a file for modification adding a new `source` line for the given
130 | * SHELL. Used for both SHELL script and tabtab internal one.
131 | *
132 | * @param {Object} options - Options with
133 | * - filename: The file to modify
134 | * - scriptname: The line to add sourcing this file
135 | * - name: The package being configured
136 | */
137 | const writeLineToFilename = ({ filename, scriptname, name }) => (
138 | resolve,
139 | reject
140 | ) => {
141 | const filepath = untildify(filename);
142 |
143 | debug('Creating directory for %s file', filepath);
144 | mkdirp(path.dirname(filepath))
145 | .then(() => {
146 | const stream = fs.createWriteStream(filepath, { flags: 'a' });
147 | stream.on('error', reject);
148 | stream.on('finish', () => resolve());
149 |
150 | debug('Writing to shell configuration file (%s)', filename);
151 | debug('scriptname:', scriptname);
152 |
153 | const inShellConfig = isInShellConfig(filename);
154 | if (inShellConfig) {
155 | stream.write(`\n# tabtab source for packages`);
156 | } else {
157 | stream.write(`\n# tabtab source for ${name} package`);
158 | }
159 |
160 | stream.write('\n# uninstall by removing these lines');
161 | stream.write(`\n${sourceLineForShell(scriptname)}`);
162 | stream.end('\n');
163 |
164 | console.log('=> Added tabtab source line in "%s" file', filename);
165 | })
166 | .catch(err => {
167 | console.error('mkdirp ERROR', err);
168 | reject(err);
169 | });
170 | };
171 |
172 | /**
173 | * Writes to SHELL config file adding a new line, but only one, to the SHELL
174 | * config script. This enables tabtab to work for the given SHELL.
175 | *
176 | * @param {Object} options - Options object with
177 | * - location: The SHELL script location (~/.bashrc, ~/.zshrc or
178 | * ~/.config/fish/config.fish)
179 | * - name: The package configured for completion
180 | */
181 | const writeToShellConfig = async ({ location, name }) => {
182 | const scriptname = path.join(
183 | COMPLETION_DIR,
184 | `${TABTAB_SCRIPT_NAME}.${shellExtension()}`
185 | );
186 |
187 | const filename = location;
188 |
189 | // Check if SHELL script already has a line for tabtab
190 | const existing = await checkFilenameForLine(filename, scriptname);
191 | if (existing) {
192 | return console.log('=> Tabtab line already exists in %s file', filename);
193 | }
194 |
195 | return new Promise(
196 | writeLineToFilename({
197 | filename,
198 | scriptname,
199 | name
200 | })
201 | );
202 | };
203 |
204 | /**
205 | * Writes to tabtab internal script that acts as a frontend router for the
206 | * completion mechanism, in the internal ~/.config/tabtab directory. Every
207 | * completion is added to this file.
208 | *
209 | * @param {Object} options - Options object with
210 | * - name: The package configured for completion
211 | */
212 | const writeToTabtabScript = async ({ name }) => {
213 | const filename = path.join(
214 | COMPLETION_DIR,
215 | `${TABTAB_SCRIPT_NAME}.${shellExtension()}`
216 | );
217 |
218 | const scriptname = path.join(COMPLETION_DIR, `${name}.${shellExtension()}`);
219 |
220 | // Check if tabtab completion file already has this line in it
221 | const existing = await checkFilenameForLine(filename, scriptname);
222 | if (existing) {
223 | return console.log('=> Tabtab line already exists in %s file', filename);
224 | }
225 |
226 | return new Promise(writeLineToFilename({ filename, scriptname, name }));
227 | };
228 |
229 | /**
230 | * This writes a new completion script in the internal `~/.config/tabtab`
231 | * directory. Depending on the SHELL used, a different script is created for
232 | * the given SHELL.
233 | *
234 | * @param {Object} options - Options object with
235 | * - name: The package configured for completion
236 | * - completer: The binary that will act as the completer for `name` program
237 | */
238 | const writeToCompletionScript = ({ name, completer }) => {
239 | const filename = untildify(
240 | path.join(COMPLETION_DIR, `${name}.${shellExtension()}`)
241 | );
242 |
243 | const script = scriptFromShell();
244 | debug('Writing completion script to', filename);
245 | debug('with', script);
246 |
247 | return readFile(script, 'utf8')
248 | .then(filecontent =>
249 | filecontent
250 | .replace(/\{pkgname\}/g, name)
251 | .replace(/{completer}/g, completer)
252 | // on Bash on windows, we need to make sure to remove any \r
253 | .replace(/\r?\n/g, '\n')
254 | )
255 | .then(filecontent =>
256 | mkdirp(path.dirname(filename)).then(() =>
257 | writeFile(filename, filecontent)
258 | )
259 | )
260 | .then(() => console.log('=> Wrote completion script to %s file', filename))
261 | .catch(err => console.error('ERROR:', err));
262 | };
263 |
264 | /**
265 | * Top level install method. Does three things:
266 | *
267 | * - Writes to SHELL config file, adding a new line to tabtab internal script.
268 | * - Creates or edit tabtab internal script
269 | * - Creates the actual completion script for this package.
270 | *
271 | * @param {Object} options - Options object with
272 | * - name: The program name to complete
273 | * - completer: The actual program or binary that will act as the completer
274 | * for `name` program. Can be the same.
275 | * - location: The SHELL script config location (~/.bashrc, ~/.zshrc or
276 | * ~/.config/fish/config.fish)
277 | */
278 | const install = async (options = { name: '', completer: '', location: '' }) => {
279 | debug('Install with options', options);
280 | if (!options.name) {
281 | throw new Error('options.name is required');
282 | }
283 |
284 | if (!options.completer) {
285 | throw new Error('options.completer is required');
286 | }
287 |
288 | if (!options.location) {
289 | throw new Error('options.location is required');
290 | }
291 |
292 | await Promise.all([
293 | writeToShellConfig(options),
294 | writeToTabtabScript(options),
295 | writeToCompletionScript(options)
296 | ]).then(() => {
297 | const { location, name } = options;
298 | console.log(`
299 | => Tabtab source line added to ${location} for ${name} package.
300 |
301 | Make sure to reload your SHELL.
302 | `);
303 | });
304 | };
305 |
306 | /**
307 | * Removes the 3 relevant lines from provided filename, based on the package
308 | * name passed in.
309 | *
310 | * @param {String} filename - The filename to operate on
311 | * @param {String} name - The package name to look for
312 | */
313 | const removeLinesFromFilename = async (filename, name) => {
314 | /* eslint-disable no-unused-vars */
315 | debug('Removing lines from %s file, looking for %s package', filename, name);
316 | if (!(await exists(filename))) {
317 | return debug('File %s does not exist', filename);
318 | }
319 |
320 | const filecontent = await readFile(filename, 'utf8');
321 | const lines = filecontent.split(/\r?\n/);
322 |
323 | const sourceLine = isInShellConfig(filename)
324 | ? `# tabtab source for packages`
325 | : `# tabtab source for ${name} package`;
326 |
327 | const hasLine = !!filecontent.match(`${sourceLine}`);
328 | if (!hasLine) {
329 | return debug('File %s does not include the line: %s', filename, sourceLine);
330 | }
331 |
332 | let lineIndex = -1;
333 | const buffer = lines
334 | // Build up the new buffer, removing the 3 lines following the sourceline
335 | .map((line, index) => {
336 | const match = line.match(sourceLine);
337 | if (match) {
338 | lineIndex = index;
339 | } else if (lineIndex + 3 <= index) {
340 | lineIndex = -1;
341 | }
342 |
343 | return lineIndex === -1 ? line : '';
344 | })
345 | // Remove any double empty lines from this file
346 | .map((line, index, array) => {
347 | const next = array[index + 1];
348 | if (line === '' && next === '') {
349 | return;
350 | }
351 |
352 | return line;
353 | })
354 | // Remove any undefined value from there
355 | .filter(line => line !== undefined)
356 | .join('\n')
357 | .trim();
358 |
359 | await writeFile(filename, buffer);
360 | console.log('=> Removed tabtab source lines from %s file', filename);
361 | };
362 |
363 | /**
364 | * Here the idea is to uninstall a given package completion from internal
365 | * tabtab scripts and / or the SHELL config.
366 | *
367 | * It also removes the relevant scripts if no more completion are installed on
368 | * the system.
369 | *
370 | * @param {Object} options - Options object with
371 | * - name: The package name to look for
372 | */
373 | const uninstall = async (options = { name: '' }) => {
374 | debug('Uninstall with options', options);
375 | const { name } = options;
376 |
377 | if (!name) {
378 | throw new Error('Unable to uninstall if options.name is missing');
379 | }
380 |
381 | const completionScript = untildify(
382 | path.join(COMPLETION_DIR, `${name}.${shellExtension()}`)
383 | );
384 |
385 | // First, lets remove the completion script itself
386 | if (await exists(completionScript)) {
387 | await unlink(completionScript);
388 | console.log('=> Removed completion script (%s)', completionScript);
389 | }
390 |
391 | // Then the lines in ~/.config/tabtab/__tabtab.shell
392 | const tabtabScript = untildify(
393 | path.join(COMPLETION_DIR, `${TABTAB_SCRIPT_NAME}.${shellExtension()}`)
394 | );
395 | await removeLinesFromFilename(tabtabScript, name);
396 |
397 | // Then, check if __tabtab.shell is empty, if so remove the last source line in SHELL config
398 | const isEmpty = (await readFile(tabtabScript, 'utf8')).trim() === '';
399 | if (isEmpty) {
400 | const shellScript = locationFromShell();
401 | debug(
402 | 'File %s is empty. Removing source line from %s file',
403 | tabtabScript,
404 | shellScript
405 | );
406 | await removeLinesFromFilename(shellScript, name);
407 | }
408 |
409 | console.log('=> Uninstalled completion for %s package', name);
410 | };
411 |
412 | module.exports = {
413 | install,
414 | uninstall,
415 | checkFilenameForLine,
416 | writeToShellConfig,
417 | writeToTabtabScript,
418 | writeToCompletionScript,
419 | writeLineToFilename
420 | };
421 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # tabtab
2 |
3 | [](https://travis-ci.org/mklabs/tabtab)
4 | [](https://coveralls.io/github/mklabs/tabtab?branch=3.0.0)
5 |
6 | A node package to do some custom command line `