@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
237 lines (192 loc) • 7.58 kB
JavaScript
const path = require('node:path');
const cds = require('../../bin/cds');
const { bold } = require('../../lib/util/term');
module.exports = Object.assign(completion, {
handleCompletion,
options: ['--shell', '--curr', '--line', '--prev'],
shortcuts: ['-s'],
flags: ['--add', '--profile', '--remove'],
help: `
# SYNOPSIS
*cds completion* <options>
Adds or removes shell completion for cds commands. Currently supported shells:
Bash, fish, Git Bash, Zsh, and PowerShell.
Depending on the operating system and the shell type, the following files will be changed:
Linux, WSL
Bash: ~/.bashrc
fish: ~/.config/fish/config.fish
Zsh: ~/.zshrc
macOS
Bash: ~/.bash_profile
fish: ~/.config/fish/config.fish
Zsh: ~/.zshrc
Windows
PowerShell: $PROFILE
Git Bash: ~/.bash_profile
This command also acts as an internal completion handler for the shell completion scripts.
# OPTIONS
*--add*
Add shell completion for cds commands (see examples).
*--remove*
Remove shell completion for cds commands (see examples).
*--shell* <bash | fish | gitbash | ps | zsh>
Force the shell completion code to be added to the correct shell profile file.
bash: macOS, Linux, WSL
fish: macOS, Linux, WSL
zsh: macOS, Linux, WSL
gitbash: Git Bash for Windows
ps: PowerShell
# EXAMPLES
*cds* completion --add # Add shell completion for cds commands
*cds* completion --remove # Remove shell completion for cds commands
*cds* completion --add --shell bash # Add shell completion for cds commands and enforce using Bash shell
# ALIASES
*cds* add completion # Add shell completion for cds commands
*cds* add completion --shell bash # Add shell completion for cds commands and enforce using Bash shell
`});
function getScriptFile(shell) {
switch (shell) {
case 'bash':
case 'gitbash':
return 'cds-bash.sh';
case 'fish':
return 'cds-fish.fish';
case 'ps':
return 'cds-ps.ps1';
case 'zsh':
return 'cds-zsh.sh';
case '':
case undefined:
case null:
throw 'Internal error: option --shell is missing';
default: {
const completionSetup = require('../../lib/init/template/completion/completionSetup');
throw `Internal error: unsupported shell: ${shell}, supported shells are ${completionSetup.supportedShells.join(', ')}`;
}
}
}
async function completion(_argv, options) {
if (options.profile) {
const scriptFile = getScriptFile(options.shell);
return console.log(path.join(__dirname, 'scripts', scriptFile));
}
const completionSetup = require('../../lib/init/template/completion/completionSetup');
if (options.add || options.remove) {
await completionSetup.setup(options.add, options);
return;
}
// workaround to parse complex clis containing -- as part of value,
// e.g. cds completion --line "cds add --"
const localArgv = process.argv.slice(3);
const localOptions = {}
for (let i = 0; i < localArgv.length; i++) {
localOptions[localArgv[i].replaceAll('-', '')] = localArgv[++i];
}
if (localOptions.curr === undefined || localOptions.prev === undefined || localOptions.line === undefined) {
throw `Some information is missing. Please run ${bold('cds help completion')} for details on how to call this command.`;
}
const shell = localOptions.shell;
if (!completionSetup.supportedShells.includes(shell)) {
throw `internal Error: no completion handler found for shell type: ${shell}, supported shells are ${completionSetup.supportedShells.join(', ')}`;
}
const currentWord = localOptions.curr?.trim().replace(/^'|'$/g, '');
const previousWord = localOptions.prev?.trim().replace(/^'|'$/g, '');
const line = localOptions.line?.trim().replace(/^'|'$/g, '');
const segments = getSegments(line);
const cdsCmd = segments[1] || '';
const completions = await getCompletions(cdsCmd, previousWord, currentWord, segments);
reply(completions);
}
async function handleCompletion(currentWord, previousWord, argv) {
const allOptionsFlags = ['--add', '--remove', '--shell']
.filter(e => !argv.includes(e));
if (currentWord?.startsWith('-')) {
return allOptionsFlags;
}
if (previousWord === '--shell') {
const { supportedShells } = require('../../lib/init/template/completion/completionSetup');
return supportedShells;
}
return allOptionsFlags;
}
function reply(values) {
if (!Array.isArray(values) || values.length === 0) {
return;
}
values = values.filter(e => !!(typeof e === 'string' && e.trim()));
values = [...new Set(values)];
values.sort((a, b) => {
if (a.startsWith('-') && !b.startsWith('-')) { return 1; }
if (!a.startsWith('-') && b.startsWith('-')) { return -1; }
return a.localeCompare(b);
});
console.log(values.join('\n'));
}
function getSegments(line) {
const cliSplit = /'(.*?)'|"(.*?)"|(\\,|\\ |[^, ])+/gs;
const result = line.match(cliSplit);
return result.map(s =>
s.trim()
.replace(/(\\ )/g, ' ')
.replace(/^["']|["']$/g, '')
);
}
function getCdsCmd(cdsCmd) {
cdsCmd = cds.Shortcuts?.[cdsCmd] ?? cdsCmd;
return cds.load(cdsCmd);
}
function getAllOptions(cdsCmdObj, segments) {
return [
...cdsCmdObj.options ?? [],
...cdsCmdObj.flags?.filter(o => !segments.includes(o)) ?? []
];
}
function getUtil() {
const completionFs = require('./completionFs');
return {
cds,
completionFs
}
}
async function getCompletions(cdsCmd, previousWord, currentWord, segments) {
const _filter = (completions, currentWord) => {
return completions.filter(token => token?.startsWith(currentWord));
}
const _getCompletions = async () => {
if (previousWord === 'cds') {
if (!currentWord) {
return cds.cmds;
} else {
const completions = _filter(cds.cmds, currentWord);
if (completions.length > 0) {
return completions;
}
cdsCmd = 'compile';
previousWord = 'compile';
}
}
try {
const cdsCmdObj = getCdsCmd(cdsCmd);
if (typeof cdsCmdObj.handleCompletion === 'function') {
return await cdsCmdObj.handleCompletion(currentWord, previousWord, segments, getUtil());
}
if (currentWord?.startsWith('-')) {
return getAllOptions(cdsCmdObj, segments);
}
const completionFs = require('./completionFs');
const all = await completionFs.readdir(currentWord);
if (!(cdsCmdObj.options?.includes(previousWord) || cdsCmdObj.shortcuts?.includes(previousWord))) {
const allOptions = getAllOptions(cdsCmdObj, segments);
all.push(...allOptions);
}
return all;
} catch {
return [];
}
}
let completions = await _getCompletions();
if (currentWord) {
completions = _filter(completions, currentWord);
}
return completions;
}