Skip to content

Commit

Permalink
feat: add tabtab.uninstall()
Browse files Browse the repository at this point in the history
  • Loading branch information
mklabs committed Oct 6, 2018
1 parent 0b00ccd commit 23907cd
Show file tree
Hide file tree
Showing 11 changed files with 416 additions and 116 deletions.
4 changes: 3 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ const BASH_LOCATION = '~/.bashrc';
const FISH_LOCATION = '~/.config/fish/config.fish';
const ZSH_LOCATION = '~/.zshrc';
const COMPLETION_DIR = '~/.config/tabtab';
const TABTAB_SCRIPT_NAME = '__tabtab';

module.exports = {
BASH_LOCATION,
ZSH_LOCATION,
FISH_LOCATION,
COMPLETION_DIR
COMPLETION_DIR,
TABTAB_SCRIPT_NAME
};
1 change: 0 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const uninstall = async (options = { name: '' }) => {

return installer
.uninstall({ name })
.then(() => console.log('Uninstalled completion for %s package', name))
.catch(err => console.error('ERROR while uninstalling', err));
};

Expand Down
231 changes: 182 additions & 49 deletions lib/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ const path = require('path');
const untildify = require('untildify');
const { promisify } = require('es6-promisify');
const mkdirp = promisify(require('mkdirp'));
const { tabtabDebug, systemShell } = require('./utils');
const { tabtabDebug, systemShell, exists } = require('./utils');

const debug = tabtabDebug('tabtab:installer');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const unlink = promisify(fs.unlink);

const {
BASH_LOCATION,
FISH_LOCATION,
ZSH_LOCATION,
COMPLETION_DIR
COMPLETION_DIR,
TABTAB_SCRIPT_NAME
} = require('./constants');

/**
Expand All @@ -24,26 +27,77 @@ const {
const shellExtension = () => systemShell();

/**
* Helper to return the correct script template based on the SHELL script
* location
* Helper to return the correct script template based on the SHELL provided
*
* @param {String} location - Shell script location
* @returns The template script content
* @param {String} shell - Shell to base the check on, defaults to system shell.
* @returns The template script content, defaults to Bash for shell we don't know yet
*/
const scriptFromLocation = location => {
if (location === BASH_LOCATION) {
return path.join(__dirname, 'scripts/bash.sh');
}

if (location === FISH_LOCATION) {
const scriptFromShell = (shell = systemShell()) => {
if (shell === 'fish') {
return path.join(__dirname, 'scripts/fish.sh');
}

if (location === ZSH_LOCATION) {
if (shell === 'zsh') {
return path.join(__dirname, 'scripts/zsh.sh');
}

// For Bash and others
return path.join(__dirname, 'scripts/bash.sh');
};

/**
* Helper to return the expected location for SHELL config file, based on the
* provided shell value.
*
* @param {String} shell - Shell value to test against
* @returns {String} Either ~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish,
* untildified. Defaults to ~/.bashrc if provided SHELL is not valid.
*/
const locationFromShell = (shell = systemShell()) => {
if (shell === 'bash') return untildify(BASH_LOCATION);
if (shell === 'zsh') return untildify(ZSH_LOCATION);
if (shell === 'fish') return untildify(FISH_LOCATION);
return BASH_LOCATION;
};

/**
* Helper to return the source line to add depending on the SHELL provided or detected.
*
* If the provided SHELL is not known, it returns the source line for a Bash shell.
*
* @param {String} scriptname - The script to source
* @param {String} shell - Shell to base the check on, defaults to system
* shell.
*/
const sourceLineForShell = (scriptname, shell = systemShell()) => {
if (shell === 'fish') {
return `[ -f ${scriptname} ]; and . ${scriptname}; or true`;
}

if (shell === 'zsh') {
return `[[ -f ${scriptname} ]] && . ${scriptname} || true`;
}

// For Bash and others
return `[ -f ${scriptname} ] && . ${scriptname} || true`;
};

/**
* Helper to check if a filename is one of the SHELL config we expect
*
* @param {String} filename - Filename to check against
* @returns {Boolean} Either true or false
*/
const isInShellConfig = filename =>
[
BASH_LOCATION,
ZSH_LOCATION,
FISH_LOCATION,
untildify(BASH_LOCATION),
untildify(ZSH_LOCATION),
untildify(FISH_LOCATION)
].includes(filename);

/**
* Checks a given file for the existence of a specific line. Used to prevent
* adding multiple completion source to SHELL scripts.
Expand Down Expand Up @@ -78,10 +132,9 @@ const checkFilenameForLine = async (filename, line) => {
* @param {Object} options - Options with
* - filename: The file to modify
* - scriptname: The line to add sourcing this file
* - location: The SHELL script location
* - name: The package being configured
*/
const writeLineToFilename = ({ filename, scriptname, location, name }) => (
const writeLineToFilename = ({ filename, scriptname, name }) => (
resolve,
reject
) => {
Expand All @@ -97,22 +150,18 @@ const writeLineToFilename = ({ filename, scriptname, location, name }) => (
debug('Writing to shell configuration file (%s)', filename);
debug('scriptname:', scriptname);

stream.write(`\n# tabtab source for ${name} package`);
stream.write('\n# uninstall by removing these lines');

if (location === BASH_LOCATION) {
stream.write(`\n[ -f ${scriptname} ] && . ${scriptname} || true`);
} else if (location === FISH_LOCATION) {
debug('Addding fish line');
stream.write(`\n[ -f ${scriptname} ]; and . ${scriptname}; or true`);
} else if (location === ZSH_LOCATION) {
debug('Addding zsh line');
stream.write(`\n[[ -f ${scriptname} ]] && . ${scriptname} || true`);
const inShellConfig = isInShellConfig(filename);
if (inShellConfig) {
stream.write(`\n# tabtab source for packages`);
} else {
stream.write(`\n# tabtab source for ${name} package`);
}

stream.write('\n# uninstall by removing these lines');
stream.write(`\n${sourceLineForShell(scriptname)}`);
stream.end('\n');

console.log('Added tabtab source line in "%s" file', filename);
console.log('=> Added tabtab source line in "%s" file', filename);
})
.catch(err => {
console.error('mkdirp ERROR', err);
Expand All @@ -130,21 +179,23 @@ const writeLineToFilename = ({ filename, scriptname, location, name }) => (
* - name: The package configured for completion
*/
const writeToShellConfig = async ({ location, name }) => {
const scriptname = path.join(COMPLETION_DIR, `__tabtab.${shellExtension()}`);
const scriptname = path.join(
COMPLETION_DIR,
`${TABTAB_SCRIPT_NAME}.${shellExtension()}`
);

const filename = location;

// Check if SHELL script already has a line for tabtab
const existing = await checkFilenameForLine(filename, scriptname);
if (existing) {
return console.log('Tabtab line already exists in %s file', filename);
return console.log('=> Tabtab line already exists in %s file', filename);
}

return new Promise(
writeLineToFilename({
filename,
scriptname,
location,
name
})
);
Expand All @@ -156,43 +207,40 @@ const writeToShellConfig = async ({ location, name }) => {
* completion is added to this file.
*
* @param {Object} options - Options object with
* - location: The SHELL script location (~/.bashrc, ~/.zshrc or
* ~/.config/fish/config.fish)
* - name: The package configured for completion
*/
const writeToTabtabScript = async ({ name, location }) => {
const filename = path.join(COMPLETION_DIR, `__tabtab.${shellExtension()}`);
const writeToTabtabScript = async ({ name }) => {
const filename = path.join(
COMPLETION_DIR,
`${TABTAB_SCRIPT_NAME}.${shellExtension()}`
);

const scriptname = path.join(COMPLETION_DIR, `${name}.${shellExtension()}`);

// Check if tabtab completion file already has this line in it
const existing = await checkFilenameForLine(filename, scriptname);
if (existing) {
return console.log('Tabtab line already exists in %s file', filename);
return console.log('=> Tabtab line already exists in %s file', filename);
}

return new Promise(
writeLineToFilename({ filename, scriptname, location, name })
);
return new Promise(writeLineToFilename({ filename, scriptname, name }));
};

/**
* This writes a new completion script in the internal `~/.config/tabtab`
* directory. Depending on the SHELL used (and the location parameter), a
* different script is created for the given SHELL.
* directory. Depending on the SHELL used, a different script is created for
* the given SHELL.
*
* @param {Object} options - Options object with
* - name: The package configured for completion
* - completer: The binary that will act as the completer for `name` program
* - location: The SHELL script location (~/.bashrc, ~/.zshrc or
* ~/.config/fish/config.fish)
*/
const writeToCompletionScript = ({ name, completer, location }) => {
const writeToCompletionScript = ({ name, completer }) => {
const filename = untildify(
path.join(COMPLETION_DIR, `${name}.${shellExtension()}`)
);

const script = scriptFromLocation(location);
const script = scriptFromShell();
debug('Writing completion script to', filename);
debug('with', script);

Expand All @@ -209,6 +257,7 @@ const writeToCompletionScript = ({ name, completer, location }) => {
writeFile(filename, filecontent)
)
)
.then(() => console.log('=> Wrote completion script to %s file', filename))
.catch(err => console.error('ERROR:', err));
};

Expand Down Expand Up @@ -247,18 +296,79 @@ const install = async (options = { name: '', completer: '', location: '' }) => {
]).then(() => {
const { location, name } = options;
console.log(`
Tabtab source line added to ${location} for ${name} package.
=> Tabtab source line added to ${location} for ${name} package.
Make sure to reload your SHELL.
`);
});
};

/**
* Not yet implemented. Here the idea is to uninstall a given package
* completion from internal tabtab and / or the SHELL config.
* Removes the 3 relevant lines from provided filename, based on the package
* name passed in.
*
* @param {type} name - parameter description...
* @param {String} filename - The filename to operate on
* @param {String} name - The package name to look for
*/
const removeLinesFromFilename = async (filename, name) => {
/* eslint-disable no-unused-vars */
debug('Removing lines from %s file, looking for %s package', filename, name);
if (!(await exists(filename))) {
return debug('File %s does not exist', filename);
}

const filecontent = await readFile(filename, 'utf8');
const lines = filecontent.split(/\r?\n/);

const sourceLine = isInShellConfig(filename)
? `# tabtab source for packages`
: `# tabtab source for ${name} package`;

const hasLine = !!filecontent.match(`${sourceLine}`);
if (!hasLine) {
return debug('File %s does not include the line: %s', filename, sourceLine);
}

let lineIndex = -1;
const buffer = lines
// Build up the new buffer, removing the 3 lines following the sourceline
.map((line, index) => {
const match = line.match(sourceLine);
if (match) {
lineIndex = index;
} else if (lineIndex + 3 <= index) {
lineIndex = -1;
}

return lineIndex === -1 ? line : '';
})
// Remove any double empty lines from this file
.map((line, index, array) => {
const next = array[index + 1];
if (line === '' && next === '') {
return;
}

return line;
})
// Remove any undefined value from there
.filter(line => line !== undefined)
.join('\n')
.trim();

await writeFile(filename, buffer);
console.log('=> Removed tabtab source lines from %s file', filename);
};

/**
* Here the idea is to uninstall a given package completion from internal
* tabtab scripts and / or the SHELL config.
*
* It also removes the relevant scripts if no more completion are installed on
* the system.
*
* @param {Object} options - Options object with
* - name: The package name to look for
*/
const uninstall = async (options = { name: '' }) => {
debug('Uninstall with options', options);
Expand All @@ -271,9 +381,32 @@ const uninstall = async (options = { name: '' }) => {
const completionScript = untildify(
path.join(COMPLETION_DIR, `${name}.${shellExtension()}`)
);
debug('Uninstalling completionScript', completionScript);

// First, lets remove the completion script itself
if (await exists(completionScript)) {
await unlink(completionScript);
console.log('=> Removed completion script (%s)', completionScript);
}

// Then the lines in ~/.config/tabtab/__tabtab.shell
const tabtabScript = untildify(
path.join(COMPLETION_DIR, `${TABTAB_SCRIPT_NAME}.${shellExtension()}`)
);
await removeLinesFromFilename(tabtabScript, name);

// Then, check if __tabtab.shell is empty, if so remove the last source line in SHELL config
const isEmpty = (await readFile(tabtabScript, 'utf8')).trim() === '';
if (isEmpty) {
const shellScript = locationFromShell();
debug(
'File %s is empty. Removing source line from %s file',
tabtabScript,
shellScript
);
await removeLinesFromFilename(shellScript, name);
}

console.log('=> Uninstalled completion for %s package', name);
};

module.exports = {
Expand Down
Loading

0 comments on commit 23907cd

Please sign in to comment.