From ba60a4207bf61c79c4c68c4ae894ae39eb2812c1 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sat, 15 Jan 2022 11:18:05 +0900 Subject: [PATCH] lib/cli: Re-implement "omb" and its completion for Bash --- lib/cli.bash | 269 +++++++++++++++++++++++++++++++++++---------------- lib/utils.sh | 68 +++++++++++-- 2 files changed, 244 insertions(+), 93 deletions(-) diff --git a/lib/cli.bash b/lib/cli.bash index d4202e2c0..ad377a43f 100644 --- a/lib/cli.bash +++ b/lib/cli.bash @@ -1,108 +1,207 @@ #!/usr/bin/env bash -function omb { - [[ $# -gt 0 ]] || { - _omb::help - return 1 - } +_omb_module_require lib:utils - local command="$1" - shift +function _omb_cmd_help { + echo 'Not yet implemented' +} +function _omb_cmd_changelog { + echo 'Not yet implemented' +} +function _omb_cmd_plugin { + echo 'Not yet implemented' +} +function _omb_cmd_pull { + echo 'Not yet implemented' +} +function _omb_cmd_reload { + echo 'Not yet implemented' +} +function _omb_cmd_theme { + echo 'Not yet implemented' +} +function _omb_cmd_update { + echo 'Not yet implemented' +} +function _omb_cmd_version { + echo 'Not yet implemented' +} + +function omb { + if (($# == 0)); then + _omb_cmd_help + return 2 + fi # Subcommand functions start with _ so that they don't # appear as completion entries when looking for `omb` - (( $+functions[_omb::$command] )) || { - _omb::help - return 1 - } + if ! _omb_util_function_exists "_omb_cmd_$1"; then + _omb_cmd_help + return 2 + fi + + _omb_cmd_"$@" +} + - _omb::$command "$@" +_omb_module_require lib:utils + +_omb_lib_cli__init_shopt= +_omb_util_get_shopt -v _omb_lib_cli__init_shopt extglob +shopt -s extglob + +function _comp_cmd_omb__describe { + eval "set -- $1 \"\${$2[@]}\"" + local type=$1; shift + local word desc words iword=0 + for word; do + desc="($type) ${word#*:}" # unused + word=${word%%:*} + words[iword++]=$word + done + + local -a filtered + _omb_util_split_lines filtered "$(compgen -W '"${words[@]}"' -- "${COMP_WORDS[COMP_CWORD]}")" + COMPREPLY+=("${filtered[@]}") } -function _omb { - local -a cmds subcmds - cmds=( - 'changelog:Print the changelog' - 'help:Usage information' - 'plugin:Manage plugins' - 'pr:Manage Oh My Bash Pull Requests' - 'reload:Reload the current bash session' - 'theme:Manage themes' - 'update:Update Oh My Bash' - 'version:Show the version' - ) - - if (( CURRENT == 2 )); then - _describe 'command' cmds - elif (( CURRENT == 3 )); then - case "$words[2]" in - changelog) local -a refs - refs=("${(@f)$(cd "$OSH"; command git for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)}") - _describe 'command' refs ;; - plugin) subcmds=( +function _comp_cmd_omb__get_available_plugins { + available_plugins=() + + local -a plugin_files + _omb_util_glob_expand plugin_files '{"$OSH","$OSH_CUSTOM"}/plugins/*/{_*,*.plugin.{bash,sh}}' + + local plugin + for plugin in "${plugin_files[@]##*/}"; do + case $plugin in + *.plugin.bash) plugin=${plugin%.plugin.bash} ;; + *.plugin.sh) plugin=${plugin%.plugin.sh} ;; + *) plugin=${plugin#_} ;; + esac + + _omb_util_array_contains available_plugins "$plugin" || + available_plugins+=("$plugin") + done +} + +function _comp_cmd_omb__get_available_themes { + available_themes=() + + local -a theme_files + _omb_util_glob_expand theme_files '{"$OSH","$OSH_CUSTOM"}/themes/*/{_*,*.theme.{bash,sh}}' + + local theme + for theme in "${theme_files[@]##*/}"; do + case $theme in + *.theme.bash) theme=${theme%.theme.bash} ;; + *.theme.sh) theme=${theme%.theme.sh} ;; + *) theme=${theme#_} ;; + esac + + _omb_util_array_contains available_themes "$theme" || + available_themes+=("$theme") + done +} + +## @fn _comp_cmd_omb__get_valid_plugins type +function _comp_cmd_omb__get_valid_plugins { + if [[ $1 == disable ]]; then + # if command is "disable", only offer already enabled plugins + valid_plugins=("${plugins[@]}") + else + local -a available_plugins + _comp_cmd_omb__get_available_plugins + valid_plugins=("${available_plugins[@]}") + + # if command is "enable", remove already enabled plugins + if [[ ${COMP_WORDS[2]} == enable ]]; then + _omb_util_array_remove valid_plugins "${plugins[@]}" + fi + fi +} + +function _comp_cmd_omb { + local shopt + _omb_util_get_shopt extglob + shopt -s extglob + + if ((COMP_CWORD == 1)); then + local -a cmds=( + 'changelog:Print the changelog' + 'help:Usage information' + 'plugin:Manage plugins' + 'pr:Manage Oh My Bash Pull Requests' + 'reload:Reload the current bash session' + 'theme:Manage themes' + 'update:Update Oh My Bash' + 'version:Show the version' + ) + _comp_cmd_omb__describe 'command' cmds + elif ((COMP_CWORD ==2)); then + case "${COMP_WORDS[1]}" in + changelog) + local -a refs + _omb_util_split_lines refs "$(command git -C "$OSH" for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)" + _comp_cmd_omb__describe 'command' refs ;; + plugin) + local -a subcmds=( 'disable:Disable plugin(s)' 'enable:Enable plugin(s)' 'info:Get plugin information' 'list:List plugins' 'load:Load plugin(s)' ) - _describe 'command' subcmds ;; - pr) subcmds=('clean:Delete all Pull Request branches' 'test:Test a Pull Request') - _describe 'command' subcmds ;; - theme) subcmds=('list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme') - _describe 'command' subcmds ;; + _comp_cmd_omb__describe 'command' subcmds ;; + pr) + local -a subcmds=( + 'clean:Delete all Pull Request branches' + 'test:Test a Pull Request' + ) + _comp_cmd_omb__describe 'command' subcmds ;; + theme) + local -a subcmds=( + 'list:List themes' + 'set:Set a theme in your .zshrc file' + 'use:Load a theme' + ) + _comp_cmd_omb__describe 'command' subcmds ;; esac - elif (( CURRENT == 4 )); then - case "${words[2]}::${words[3]}" in - plugin::(disable|enable|load)) - local -aU valid_plugins - - if [[ "${words[3]}" = disable ]]; then - # if command is "disable", only offer already enabled plugins - valid_plugins=($plugins) - else - valid_plugins=("$OSH"/plugins/*/{_*,*.plugin.zsh}(.N:h:t) "$OSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(.N:h:t)) - # if command is "enable", remove already enabled plugins - [[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins}) - fi - - _describe 'plugin' valid_plugins ;; - plugin::info) - local -aU plugins - plugins=("$OSH"/plugins/*/{_*,*.plugin.zsh}(.N:h:t) "$OSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(.N:h:t)) - _describe 'plugin' plugins ;; - theme::(set|use)) - local -aU themes - themes=("$OSH"/themes/*.zsh-theme(.N:t:r) "$OSH_CUSTOM"/**/*.zsh-theme(.N:r:gs:"$OSH_CUSTOM"/themes/:::gs:"$OSH_CUSTOM"/:::)) - _describe 'theme' themes ;; + elif ((COMP_CWORD == 3)); then + case "${COMP_WORDS[1]}::${COMP_WORDS[2]}" in + plugin::@(disable|enable|load)) + local -a valid_plugins + _comp_cmd_omb__get_valid_plugins "${COMP_WORDS[2]}" + _comp_cmd_omb__describe 'plugin' valid_plugins ;; + plugin::info) + local -a available_plugins + _comp_cmd_omb__get_available_plugins + _comp_cmd_omb__describe 'plugin' available_plugins ;; + theme::@(set|use)) + local -a available_themes + _comp_cmd_omb__get_available_themes + _comp_cmd_omb__describe 'theme' available_themes ;; esac - elif (( CURRENT > 4 )); then - case "${words[2]}::${words[3]}" in - plugin::(enable|disable|load)) - local -aU valid_plugins - - if [[ "${words[3]}" = disable ]]; then - # if command is "disable", only offer already enabled plugins - valid_plugins=($plugins) - else - valid_plugins=("$OSH"/plugins/*/{_*,*.plugin.zsh}(.N:h:t) "$OSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(.N:h:t)) - # if command is "enable", remove already enabled plugins - [[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins}) - fi - - # Remove plugins already passed as arguments - # NOTE: $(( CURRENT - 1 )) is the last plugin argument completely passed, i.e. that which - # has a space after them. This is to avoid removing plugins partially passed, which makes - # the completion not add a space after the completed plugin. - local -a args - args=(${words[4,$(( CURRENT - 1))]}) - valid_plugins=(${valid_plugins:|args}) - - _describe 'plugin' valid_plugins ;; + elif ((COMP_CWORD > 3)); then + case "${COMP_WORDS[1]}::${COMP_WORDS[2]}" in + plugin::@(enable|disable|load)) + local -a valid_plugins + _comp_cmd_omb__get_valid_plugins "${COMP_WORDS[2]}" + + # Remove plugins already passed as arguments + # NOTE: $((COMP_CWORD - 1)) is the last plugin argument completely passed, i.e. that which + # has a space after them. This is to avoid removing plugins partially passed, which makes + # the completion not add a space after the completed plugin. + _omb_util_array_remove valid_plugins "${COMP_WORDS[@]:3:COMP_CWORD-3}" + + _comp_cmd_omb__describe 'plugin' valid_plugins ;; esac fi + [[ :$shopt: == *:exptglob:* ]] || shopt -u extglob return 0 } -compdef _omb omb \ No newline at end of file +complete -F _comp_cmd_omb omb + +[[ :$_omb_lib_cli__init_shopt: == *:exptglob:* ]] || shopt -u extglob +unset -v _omb_lib_cli__init_shopt diff --git a/lib/utils.sh b/lib/utils.sh index e85961bf0..815038790 100644 --- a/lib/utils.sh +++ b/lib/utils.sh @@ -233,16 +233,30 @@ pushover () { ## @fn _omb_util_get_shopt optnames... if ((_omb_bash_version >= 40100)); then - _omb_util_get_shopt() { shopt=$BASHOPTS; } + _omb_util_get_shopt() { + if [[ $1 == -v ]]; then + [[ $2 == shopt ]] || local shopt + _omb_util_get_shopt "${@:3}" + [[ $2 == shopt ]] || printf -v "$2" '%s' "$shopt" + else + shopt=$BASHOPTS + fi + } else _omb_util_get_shopt() { - shopt= - local opt - for opt; do - if shopt -q "$opt" &>/dev/null; then - shopt=${shopt:+$shopt:}$opt - fi - done + if [[ $1 == -v ]]; then + [[ $2 == shopt ]] || local shopt + _omb_util_get_shopt "${@:3}" + [[ $2 == shopt ]] || printf -v "$2" '%s' "$shopt" + else + shopt= + local opt + for opt; do + if shopt -q "$opt" &>/dev/null; then + shopt=${shopt:+$shopt:}$opt + fi + done + fi } fi @@ -295,3 +309,41 @@ _omb_util_glob_expand() { [[ :$shopt: == *:failglob:* ]] && shopt -s failglob return 0 } + +## @fn _omb_util_split_lines array_name string +_omb_util_split_lines() { + local set=$- IFS=$'\n' + set -f + eval "$1=($2)" + [[ $set == *f* ]] || set +f + return 0 +} + +## @fn _omb_util_array_remove array_name value +_omb_util_array_contains() { + [[ $1 == ret ]] || + eval "local -a ret=(\"\${$2[@]}\")" + local value + for value in "${ret[@]}"; do + if [[ $value == "$2" ]]; then + return 0 + fi + done + return 1 +} + +## @fn _omb_util_array_remove array_name values... +function _omb_util_array_remove { + local __script=' + local iA yA + for iA in ${!A[@]}; do + for yA in "${@:2}"; do + if [[ ${A[iA]} == "$yA" ]]; then + unset -v '\''A[iA]'\'' + continue 2 + fi + done + done + A=("${A[@]}") # compaction + '; eval -- "${__script//A/$1}" +}