oclif-plugin-completion
Version:
oclif plugin for generating shell completions
418 lines (360 loc) • 13.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getInstructionsForBash = exports.generateCompletionAliasScriptForBash = exports.generateCompletionScriptForBash = void 0;
const template_1 = require("./template");
const getBootstrap = (rootCommandName) => {
const bootstrapTemplate = template_1.template `
__${'bin'}_debug()
{
if [[ -n \${BASH_COMP_DEBUG_FILE} ]]; then
echo "$*" >> "\${BASH_COMP_DEBUG_FILE}"
fi
}
# Homebrew on Macs have version 1.3 of bash-completion which doesn't include
# _init_completion. This is a very minimal version of that function.
__${'bin'}_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$@" cur prev words cword
}
__${'bin'}_index_of_word()
{
local w word=$1
shift
index=0
for w in "$@"; do
[[ $w = "$word" ]] && return
index=$((index+1))
done
index=-1
}
__${'bin'}_contains_word()
{
local w word=$1; shift
for w in "$@"; do
[[ $w = "$word" ]] && return
done
return 1
}
__${'bin'}_filter_flag()
{
local flag
for inserted_flag in "\${inserted_flags[@]}"; do
for i in "\${!COMPREPLY[@]}"; do
flag="\${COMPREPLY[i]%%=*}"
if [[ $flag = $inserted_flag ]]; then
if ! __${'bin'}_contains_word $flag "\${multi_flags[@]}"; then
__${'bin'}_debug "\${FUNCNAME[0]}: \${COMPREPLY[i]}"
unset 'COMPREPLY[i]'
fi
fi
done
done
COMPREPLY=("\${COMPREPLY[@]}")
}
__${'bin'}_handle_reply()
{
local comp
__${'bin'}_debug "\${FUNCNAME[0]}: c is $c words[c] is \${words[c]} cur is $cur"
case $cur in
-*)
if [[ $(type -t compopt) = "builtin" ]]; then
compopt -o nospace
fi
local allflags=("\${flags[*]}")
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "\${allflags[*]}" -- "$cur")
if [[ $(type -t compopt) = "builtin" ]]; then
[[ "\${COMPREPLY[0]}" == *= ]] || compopt +o nospace
fi
__${'bin'}_filter_flag
# complete after --flag=abc
if [[ $cur == *=* ]]; then
COMPREPLY=()
if [[ $(type -t compopt) = "builtin" ]]; then
compopt +o nospace
fi
local index flag
flag="\${cur%%=*}"
__${'bin'}_index_of_word "$flag" "\${option_flags[@]}"
__${'bin'}_debug "\${FUNCNAME[0]}: flag is $flag index is $index"
if [[ \${index} -ge 0 ]]; then
cur="\${cur#*=}"
local option_flag_handler="\${option_flag_handlers[\${index}]}"
$option_flag_handler
fi
fi
return
;;
esac
local index
__${'bin'}_index_of_word "$prev" "\${option_flags[@]}"
__${'bin'}_debug "\${FUNCNAME[0]}: flag is $flag index is $index"
if [[ \${index} -ge 0 ]]; then
local option_flag_handler="\${option_flag_handlers[\${index}]}"
$option_flag_handler
return
fi
local completions
completions=("\${commands[@]}")
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "\${completions[*]}" -- "$cur")
__${'bin'}_filter_flag
# available in bash-completion >= 2, not always present on macOS
if declare -F __ltrim_colon_completions >/dev/null; then
__ltrim_colon_completions "$cur"
fi
# If there is only 1 completion and it is a flag with an = it will be completed
# but we don't want a space after the =
if [[ "\${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "\${COMPREPLY[0]}" == --*= ]]; then
compopt -o nospace
fi
}
__${'bin'}_handle_flag()
{
__${'bin'}_debug "\${FUNCNAME[0]}: c is $c words[c] is \${words[c]}"
# skip the argument to option flag without =
if [[ \${words[c]} != *"="* ]] && __${'bin'}_contains_word "\${words[c]}" "\${option_flags[@]}"; then
__${'bin'}_debug "\${FUNCNAME[0]}: found a flag \${words[c]}, skip the next argument"
c=$((c+1))
# if we are looking for a flags value, don't show commands
if [[ $c -eq $cword ]]; then
commands=()
fi
fi
c=$((c+1))
}
__${'bin'}_handle_command()
{
__${'bin'}_debug "\${FUNCNAME[0]}: c is $c words[c] is \${words[c]}"
local next_command
if [[ -n \${last_command} ]]; then
next_command="_\${last_command}_\${words[c]//:/__}"
else
if [[ $c -eq 0 ]]; then
next_command="_${'bin'}"
else
next_command="_\${words[c]//:/__}"
fi
fi
c=$((c+1))
__${'bin'}_debug "\${FUNCNAME[0]}: looking for \${next_command}"
declare -F "$next_command" >/dev/null && $next_command
}
__${'bin'}_handle_word()
{
__${'bin'}_debug "\${FUNCNAME[0]}: c is $c words[c] is \${words[c]}"
if [[ -z "\${BASH_VERSION}" || "\${BASH_VERSINFO[0]}" -gt 3 ]]; then
if __${'bin'}_contains_word "\${words[c]}" "\${command_aliases[@]}"; then
__${'bin'}_debug "\${FUNCNAME[0]}: words[c] is \${words[c]} -> \${command_by_alias[$\{words[c]}]}"
words[c]=\${command_by_alias[$\{words[c]}]}
fi
fi
if [[ $c -ge $cword ]]; then
__${'bin'}_handle_reply
__${'bin'}_debug "\${FUNCNAME[0]}: COMPREPLY is \${COMPREPLY[@]}"
return
fi
if [[ "\${words[c]}" == -* ]]; then
__${'bin'}_handle_flag
elif __${'bin'}_contains_word "\${words[c]}" "\${commands[@]}"; then
__${'bin'}_handle_command
elif __${'bin'}_contains_word "\${words[c]}" "\${command_aliases[@]}"; then
__${'bin'}_handle_command
elif [[ $c -eq 0 ]]; then
__${'bin'}_handle_command
else
c=$((c+1))
fi
__${'bin'}_handle_word
}
`;
return bootstrapTemplate({ bin: rootCommandName });
};
const getInit = (bin, aliases) => {
const initFunctionTemplate = template_1.template `
__${'bin'}_init()
{
__${'bin'}_debug ""
local cur prev words cword
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -n ":" -n "=" || return
else
__${'bin'}_init_completion -n ":" -n "=" || return
fi
__${'bin'}_debug "\${FUNCNAME[0]}: words is \${words[@]}"
local c=0
local last_command
local commands=("${'bin'}")
local command_aliases=()
declare -A command_by_alias 2>/dev/null || :
local args=()
local flags=()
local multi_flags=()
local option_flags=()
local option_flag_handlers=()
local inserted_flags=()
__${'bin'}_handle_word
}
`;
const initTemplate = template_1.template `
if [[ $(type -t compopt) = "builtin" ]]; then
${'initForBuiltin'}
else
${'init'}
fi
`;
const commandNames = [bin, ...aliases];
const parts = [
initFunctionTemplate({ bin }).trim(),
initTemplate({
init: commandNames
.map((name) => {
return template_1.template `complete -o default -o nospace -F __${'bin'}_init ${'name'}`({ bin, name });
})
.join('\n '),
initForBuiltin: commandNames
.map((name) => {
return template_1.template `complete -o default -F __${'bin'}_init ${'name'}`({
bin,
name,
});
})
.join('\n '),
}).trim(),
];
return parts.join('\n'.repeat(2));
};
function generateCompletionScriptForBash({ bin, aliases, commands, }) {
const scriptParts = [];
scriptParts.push(getBootstrap(bin));
commands.forEach((command) => {
let commandName = [bin, command.id].join(' ');
commandName = commandName === null || commandName === void 0 ? void 0 : commandName.replace(/ /g, '_');
commandName = commandName === null || commandName === void 0 ? void 0 : commandName.replace(/:/g, '__');
const parts = [];
for (const [name, flag] of Object.entries(command.flags)) {
if (flag.type === 'option' && flag.options) {
parts.push(`_${commandName}___flag_options--${name}()`);
parts.push(`{`);
parts.push(` local options=()`);
for (const option of flag.options) {
parts.push(` options+=("${option}")`);
}
parts.push(` COMPREPLY=( $( compgen -W "\${options[*]}" -- "$cur" ) )`);
parts.push(`}`);
}
}
parts.push(`_${commandName}()`);
parts.push(`{`);
parts.push(` last_command=${commandName}`);
parts.push(` commands=()`);
parts.push(` command_aliases=()`);
parts.push(` args=()`);
parts.push(` flags=()`);
parts.push(` flag_aliases=()`);
parts.push(` multi_flags=()`);
parts.push(` option_flags=()`);
parts.push(` option_flag_handlers=()`);
parts.push(` required_flags=()`);
parts.push(` inserted_flags=()`);
for (const arg of command.args) {
if (arg.hidden) {
continue;
}
}
for (const [name, flag] of Object.entries(command.flags)) {
if (flag.hidden) {
continue;
}
parts.push(` flags+=("--${name}")`);
if (flag.char) {
parts.push(` flags+=("-${flag.char}")`);
}
if (flag.type === 'option') {
// TODO: need upstream fix. `flag.multiple` property does not exist
// Upstream PR: https://github.com/oclif/config/pull/113
// @ts-expect-error
if (flag.multiple || !flag.multiple) {
// Until the Upstream PR is merged, everything is multiple 🤷♂️
parts.push(` multi_flags+=("--${name}")`);
if (flag.char) {
parts.push(` multi_flags+=("-${flag.char}")`);
}
}
if (flag.options) {
parts.push(` option_flags+=("--${name}")`);
parts.push(` option_flag_handlers+=("_${commandName}___flag_options--${name}")`);
if (flag.char) {
parts.push(` option_flags+=("-${flag.char}")`);
parts.push(` option_flag_handlers+=("_${commandName}___flag_options--${name}")`);
}
}
}
if (flag.required) {
parts.push(` required_flags+=("--${name}")`);
if (flag.char) {
parts.push(` required_flags+=("-${flag.char}")`);
}
}
}
parts.push(`}`);
scriptParts.push(parts.join(`\n`));
});
const rootCommandParts = [];
rootCommandParts.push(`_${bin}()`);
rootCommandParts.push(`{`);
rootCommandParts.push(` commands=()`);
for (const command of commands) {
if (command.hidden) {
continue;
}
rootCommandParts.push(` commands+=("${command.id}")`);
if (command.aliases.length > 0) {
for (const alias of command.aliases) {
rootCommandParts.push(` command_aliases+=("${alias}")`);
}
rootCommandParts.push(` if [[ -z "\${BASH_VERSION}" || "\${BASH_VERSINFO[0]}" -gt 3 ]]; then`);
for (const alias of command.aliases) {
rootCommandParts.push(` command_by_alias[${alias}]=${command.id}`);
}
rootCommandParts.push(` else`);
for (const alias of command.aliases) {
rootCommandParts.push(` command+=("${alias}")`);
}
rootCommandParts.push(` fi`);
}
}
rootCommandParts.push(` last_command=${bin}`);
rootCommandParts.push(`}`);
scriptParts.push(rootCommandParts.join(`\n`));
scriptParts.push(getInit(bin, aliases));
return scriptParts.join('\n'.repeat(2));
}
exports.generateCompletionScriptForBash = generateCompletionScriptForBash;
function generateCompletionAliasScriptForBash({ bin, }) {
const scriptParts = [];
scriptParts.push(`_xfunc ${bin} __${bin}_init`);
return scriptParts.join('\n'.repeat(2));
}
exports.generateCompletionAliasScriptForBash = generateCompletionAliasScriptForBash;
function getInstructionsForBash({ bin, shell, aliases, }) {
const scriptName = (name) => name;
const lines = [
`Make sure you have the "bash-completion" package installed on your system.`,
``,
`Running the following command will generate the completion script for ${shell} shell:`,
``,
` $ ${bin} completion:generate --shell=${shell} > ${scriptName(bin)}`,
];
lines.push(``, `You need to put that "${scriptName(bin)}" file in one of the following directories (depending on your system):`, ``, ` - $XDG_DATA_HOME/bash-completion/completions`, ` - ~/.local/share/bash-completion/completions`, ` - /usr/local/share/bash-completion/completions`, ` - /usr/share/bash-completion/completions`, ``, `Usually this should work:`, ``, ` $ ${bin} completion:generate --shell=${shell} | tee ~/.local/share/bash-completion/completions/${scriptName(bin)}`);
if (aliases.length > 0) {
const plural = aliases.length > 1;
lines.push(``, `Also, '${bin}' provides ${plural ? 'these' : 'the'} ${plural ? 'aliases' : 'alias'}: '${aliases.join("', '")}'. You can generate completion ${plural ? 'scripts' : 'script'} for ${plural ? 'those' : 'that'} using the "completion:generate:alias" command. For example:`, ``, ` $ ${bin} completion:generate:alias --shell=${shell} ${aliases[0]} | tee ~/.local/share/bash-completion/completions/${scriptName(aliases[0])}`);
}
lines.push(``, `For more info, visit: https://www.npmjs.com/package/oclif-plugin-completion#${shell}`, ``, `Enjoy!`);
return lines;
}
exports.getInstructionsForBash = getInstructionsForBash;