diff options
| author | Polesznyák Márk <contact@pml68.dev> | 2025-11-29 01:45:07 +0100 |
|---|---|---|
| committer | Polesznyák Márk <contact@pml68.dev> | 2025-12-29 14:50:02 +0100 |
| commit | bf7347380207d80183ce80ae6547ef08fa579c6a (patch) | |
| tree | ec64896997e4bd76d89738f0b156dde7423ff107 /scripts/bash-completor | |
| parent | feat: initial commit (diff) | |
| download | dotfiles-bf7347380207d80183ce80ae6547ef08fa579c6a.tar.gz | |
feat: add scripts, TARGET variable for Makefile
Diffstat (limited to '')
| -rwxr-xr-x | scripts/bash-completor | 734 |
1 files changed, 734 insertions, 0 deletions
diff --git a/scripts/bash-completor b/scripts/bash-completor new file mode 100755 index 0000000..b013ec6 --- /dev/null +++ b/scripts/bash-completor @@ -0,0 +1,734 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail +set -o errtrace +(shopt -p inherit_errexit &>/dev/null) && shopt -s inherit_errexit + +readonly VERSION=v0.2.0 +readonly ARGS=$* +readonly SPACES_8=' ' +readonly SPACES_6=' ' +readonly SPACES_4=' ' +readonly SPACES_2=' ' + +declare -r RED="\\e[31m" +declare -r GREEN="\\e[32m" +declare -r YELLOW="\\e[33m" +declare -r CYAN="\\e[36m" +declare -r RESET_ALL="\\e[0m" + +debug() { + printf "%b[Debug] %s%b\n" "$CYAN" "$*" "$RESET_ALL" >/dev/tty +} + +warn() { + printf "%b[Warn] %s%b\n" "$YELLOW" "$*" "$RESET_ALL" >/dev/tty +} + +error() { + printf "%b[Error] %s%b\n" "$RED" "$*" "$RESET_ALL" >/dev/tty +} + +suggest() { + printf "%b[Suggest] %s%b\n" "$GREEN" "$*" "$RESET_ALL" >/dev/tty +} + +# Copy from https://github.com/adoyle-h/lobash/blob/develop/src/modules/is_array.bash +is_array() { + local attrs + # shellcheck disable=2207 + attrs=$(declare -p "$1" 2>/dev/null | sed -E "s/^declare -([-a-zA-Z]+) .+/\\1/" || true) + + # a: array + # A: associate array + if [[ ${attrs} =~ a|A ]]; then return 0; else return 1; fi +} + +is_func() { + declare -F "$1" &>/dev/null +} + +get_varname() { + local name=${1:-} + local encoded=${word_to_varname[$name]:-} + + if [[ -z ${encoded} ]]; then + encoded=${name//[^a-zA-Z_]/_} + fi + + echo "${encoded}" +} + +is_gnu_sed() { + local out + out=$(${1:-sed} --version 2>/dev/null) + [[ $out =~ 'GNU sed' ]] +} + +reply_words() { + local IFS=$'\n' + # shellcheck disable=2207 + COMPREPLY=( $(IFS=', ' compgen -W "$*" -- "${cur#=}") ) +} + +reply_list() { + local IFS=', ' + local array_list="" array_name + # shellcheck disable=2068 + for array_name in "$@"; do + array_list="$array_list \${${array_name}[*]}" + done + array_list="${array_list[*]:1}" + + IFS=$'\n'' ' + eval "COMPREPLY=( \$(compgen -W \"$array_list\" -- \"\$cur\") )" +} + +reply_files() { + local IFS=$'\n' + compopt -o nospace -o filenames + # shellcheck disable=2207 + COMPREPLY=( $(compgen -A file -- "$cur") ) +} + +reply_files_in_pattern() { + compopt -o nospace -o filenames + + local path + while read -r path; do + if [[ $path =~ $1 ]] || [[ -d $path ]]; then + COMPREPLY+=( "$path" ) + fi + done < <(compgen -A file -- "$cur") +} + +reply_dirs() { + local IFS=$'\n' + compopt -o nospace -o filenames + # shellcheck disable=2207 + COMPREPLY=( $(compgen -A directory -- "$cur") ) +} + + +make_get_varnames() { + echo "" + + declare -p word_to_varname | sed -e "s/word_to_varname/_${cmd}_comp_word_to_varname/" + + declare -f get_varname | sed -e "s/get_varname/_${cmd}_comp_util_get_varname/" -e 's/ *$//g' \ + -e "s/word_to_varname/_${cmd}_comp_word_to_varname/" +} + +make_dumped_variables() { + echo "" + local name + for name in $(compgen -A variable var_); do + declare -p "$name" | sed "s/^declare -.* var_/_${cmd}_comp_var_/" + done +} + +make_header() { + cat <<EOF +# This file is generated by [bash-completor](https://github.com/adoyle-h/bash-completor/tree/$VERSION). Do not modify it manually. +# +# [Usage] +# Put "source $output" in your bashrc. +# +# If you want to debug the completion. +# Search '# Uncomment this line for debug' line in this file. +# +# [Update Script] +# bash-completor $ARGS +EOF + + if [[ -n ${version:-} ]]; then + cat <<EOF +# +# [Version] $version +EOF + fi + + if [[ -n ${license:-} ]]; then + cat <<EOF +# +# [License] $license +EOF + fi + + if (( ${#authors[@]} > 0 )); then + cat <<EOF +# +# [Authors] +EOF + + { + local IFS=$'\n' + local author + for author in "${authors[@]}"; do + echo "# ${author}" + done + } + fi + + if (( ${#maintainers[@]} > 0 )); then + cat <<EOF +# +# [Maintainers] +EOF + + { + local IFS=$'\n' + local maintainer + for maintainer in "${maintainers[@]}"; do + echo "# ${maintainer}" + done + } + fi + + if [[ -n ${description:-} ]]; then + cat <<EOF +# +# [Description] +# ${description} +EOF + fi + + if [[ -n ${notice:-} ]]; then + cat <<EOF +# +# [Notice] +# ${notice} +EOF + fi + + cat <<EOF + +# shellcheck disable=2207 +# editorconfig-checker-disable +EOF +} + +make_opts_variable() { + local opts_varname=$1 + local -n opts=$opts_varname + + printf '\n_%s_comp_%s=( ' "$cmd" "$opts_varname" + for opt in "${opts[@]}"; do + printf '%s ' "${opt/:*/}" + done + printf ')\n' +} + +parse_action() { + local var=$1 + local position=$2 + + if [[ ${var:0:1} == '@' ]]; then + case $var in + @hold) + printf '' + ;; + + *) + local func_name=${var:1} + func_name=${func_name/:*/} + + if [[ ${map_reply_funcs["reply_${func_name}"]:-} == true ]]; then + local func_arg=${var/@${func_name}:/} + if ((${#func_arg} > 0)) && [[ ${func_arg[*]:0:1} != '@' ]]; then + printf -- "_%s_comp_reply_%s '%s'" "$cmd" "$func_name" "$func_arg" + else + printf -- '_%s_comp_reply_%s' "$cmd" "$func_name" + fi + else + error "Invalid '$position': The action '$var' is not defined." + + case $var in + @f*) suggest "Try '@files' instead of '$var'." ;; + @d*) suggest "Try '@dirs' instead of '$var'." ;; + @h*) suggest "Try '@hold' instead of '$var'." ;; + *) suggest "Try '@files', '@dirs', '@hold' or other reply functions. See https://github.com/adoyle-h/bash-completor/docs/syntax.md#reply-functions " ;; + esac + + exit 5 + fi + ;; + esac + else + if [[ -n "$var" ]]; then + printf -- "_%s_comp_reply_words '%s'" "$cmd" "$var" + else + printf ':' + fi + fi +} + +make_reply_action() { + local varname=$1 + local -n var=$varname + local reply + + if [[ -v "$varname" ]]; then + reply=$(parse_action "$var" "$varname=$var") + elif is_array "$varname"; then + reply="_${cmd}_comp_reply_list '${var}'" + else + reply="_${cmd}_comp_reply_files" + fi + + echo "$reply" +} + +make_reply_set() { + cat <<EOF + +_${cmd}_comp_reply_set() { + local IFS=', ' + local array_list="" array_name + # shellcheck disable=2068 + for array_name in "\$@"; do + array_list="\$array_list \\\${_${cmd}_comp_var_\${array_name}[*]}" + done + array_list="\${array_list[*]:1}" + + IFS=\$'\n'' ' + eval "COMPREPLY=( \\\$(compgen -W \"\$array_list\" -- \"\\\$cur\") )" +} +EOF + + map_reply_funcs[reply_set]=true +} + +if is_gnu_sed; then + # For GNU sed + sed_reply_utils() { + declare -f "$name" | sed -e "s/reply_/_${cmd}_comp_reply_/g" -e 's/ *$//g' |\ + sed -e ":a;N;\$!ba;s/IFS='\n'/IFS=\$'\\\\n'/g" + } +else + # For BSD sed + sed_reply_utils() { + declare -f "$name" | sed -e "s/reply_/_${cmd}_comp_reply_/g" -e 's/ *$//g' |\ + sed -e ':a' -e 'N' -e '$!ba' -e "s/IFS='\n'/IFS=\$'\\\\n'/g" + } +fi + +make_reply_utils() { + local name + + map_reply_funcs[reply_hold]=true + + # Make framework and developer defined reply functions + for name in $(compgen -A function reply_); do + echo "" + sed_reply_utils + + map_reply_funcs[$name]=true + done + + make_reply_set + + # Make developer custom reply functions + for name in $(compgen -A variable reply_); do + local -n list="$name" + local func_name=${list[0]} + + if is_func "$func_name"; then + local rest=() + local str + for str in "${list[@]:1}"; do + rest+=("'$str'") + done + + cat <<EOF + +_${cmd}_comp_${name}() { + _${cmd}_comp_${func_name} ${rest[@]} +} +EOF + else + error "Not found function '$func_name' for config '$name'" + exit 7 + fi + + map_reply_funcs[$name]=true + done +} + +_make_cmd_option() { + local opt=$1 + local indent=$2 + local default_action=$3 + + if [[ $opt =~ : ]]; then + local option=${opt/:*/} + local var=${opt/${option}:/} + else + local option=$opt + local var='' + fi + + if [[ ${option: -1} == '=' ]]; then + # skip --option= + return 0 + fi + + if [[ $option == "$opt" ]]; then + # skip --option without : + return 0 + fi + + local action + action=$(parse_action "$var" "$opt") + + # Skip to print case condition. Because this condition action is same to default_action. + # default_action means "*) $default_action ;;" + if [[ "$action" != "$default_action" ]]; then + printf -- '%s) %s ;;\n' "$indent$option" "$action" + fi +} + +_make_cmd_options() { + echo " # rely the value of command option" + local opt + for opt in "${opts[@]}"; do + _make_cmd_option "$opt" "$SPACES_6" "$reply_opts_fallback" + done +} + +_make_equal_sign_option() { + local opt=$1 + local indent="$SPACES_4" + + if [[ $opt =~ : ]]; then + local option=${opt/:*/} + local var=${opt/${option}:/} + else + local option=$opt + local var='' + fi + + if [[ $option =~ =$ ]]; then + local action + action=$(parse_action "$var" "$opt") + printf -- '%s) %s ;;\n' "$indent$option" "$action" + else + if [[ $option =~ =[@-_a-zA-Z] ]]; then + local recommend=${option/=/=:} + recommend=${recommend// /,} # developer may use space delimiter + warn "The option '$option' maybe missing the ':'. Do you need '${recommend}'?" + fi + fi +} + +_make_equal_sign_options() { + for opt in "${opts[@]}"; do + _make_equal_sign_option "$opt" + done +} + +make_equal_sign_opts_func() { + local opts_varname=$1 + local -n opts=$opts_varname + + local equal_sign_options + equal_sign_options=$(_make_equal_sign_options) + + if [[ -n $equal_sign_options ]]; then + cat <<EOF + +_${cmd}_comp_equal_sign_${opts_varname}() { + case "\${1}=" in +$equal_sign_options + esac +} +EOF + map_equal_signs[${opts_varname}]=true + fi +} + +make_cmd_core() { + local opts_varname=$1 + local reply_args=$2 + local reply_opts_fallback=$3 + local -n opts=$opts_varname + + if [[ " ${opts[*]} " == *' -- '* ]]; then + cat <<EOF + if [[ \$COMP_LINE == *' -- '* ]]; then + # When current command line contains the "--" option, other options are forbidden. + ${reply_args} + elif [[ \${cur:0:1} == [-+] ]]; then +EOF + else + cat <<EOF + if [[ \${cur:0:1} == [-+] ]]; then +EOF + fi + + cat <<EOF + # rely options of command + _${cmd}_comp_reply_list _${cmd}_comp_${opts_varname} +EOF + + if [[ "${opts[*]}" =~ = ]]; then + # The options contain equal_sign + cat <<EOF + if [[ \${COMPREPLY[*]} =~ =\$ ]]; then compopt -o nospace; fi +EOF + fi + + if [[ -n ${map_equal_signs[${opts_varname}]:-} ]]; then + cat <<EOF + elif [[ \${cur} == = ]]; then + _${cmd}_comp_equal_sign_${opts_varname} "\$prev" +EOF + fi + + cat <<EOF + elif [[ \${prev:0:1} == [-+] ]]; then + case "\${prev}" in +$(_make_cmd_options) + *) $reply_opts_fallback ;; + esac +EOF + + if [[ -n ${map_equal_signs[${opts_varname}]:-} ]]; then + cat <<EOF + elif [[ \${prev} == = ]]; then + _${cmd}_comp_equal_sign_${opts_varname} "\${COMP_WORDS[\$(( COMP_CWORD - 2 ))]}" +EOF + fi + + cat <<EOF + else + # rely the argument of command + $reply_args + fi +EOF +} + + +make_subcmd_opts() { + local subcmd_opts + for subcmd_opts in $(compgen -A variable subcmd_opts_); do + make_opts_variable "$subcmd_opts" + done +} + +make_subcmds() { + cat <<EOF + +_${cmd}_comp_subcmds=( ${subcmds[*]} ) +EOF +} + +make_subcmd_completion() { + local subcmd_varname=$1 + + local reply_args + if [[ -v "subcmd_args_${subcmd_varname}" ]]; then + reply_args=$(make_reply_action "subcmd_args_${subcmd_varname}") + else + reply_args=$(make_reply_action subcmd_args__fallback) + fi + + local reply_opts_fallback + if [[ -v "subcmd_opts_${subcmd_varname}_fallback" ]]; then + reply_opts_fallback=$(make_reply_action "subcmd_opts_${subcmd_varname}_fallback") + else + reply_opts_fallback=$reply_args + fi + + cat <<EOF + +_${cmd}_completions_$subcmd_varname() { +$(make_cmd_core "subcmd_opts_${subcmd_varname}" "$reply_args" "$reply_opts_fallback") +} +EOF +} + +make_subcmd_alias_completion() { + local src + for src in "${!subcmd_comp_alias[@]}"; do + printf '_%s_completions_%s() { _%s_completions_%s; }\n' \ + "$cmd" "$(get_varname "$src")" "$cmd" "$(get_varname "${subcmd_comp_alias[$src]}")" + done +} + +make_subcmd_completions() { + local subcmd subcmd_varname + for subcmd in "${subcmds[@]}"; do + subcmd_varname=$(get_varname "$subcmd") + if is_array "subcmd_opts_${subcmd_varname}"; then + make_equal_sign_opts_func "subcmd_opts_${subcmd_varname}" + make_subcmd_completion "$subcmd_varname" + fi + done + + make_subcmd_alias_completion + make_subcmd_completion _fallback +} + + +make_main_completion() { + make_equal_sign_opts_func "cmd_opts" + + cat <<EOF + +_${cmd}_completions() { + COMPREPLY=() + local cur=\${COMP_WORDS[COMP_CWORD]} + local prev=\${COMP_WORDS[COMP_CWORD-1]} + + # Uncomment this line for debug + # echo "[COMP_CWORD:\$COMP_CWORD][cur:\$cur][prev:\$prev][WORD_COUNT:\${#COMP_WORDS[*]}][COMP_WORDS:\${COMP_WORDS[*]}]" >> bash-debug.log +EOF + + local reply_args + + if $has_subcmds; then + cat <<EOF + + if (( COMP_CWORD > 1 )); then + # Enter the subcmd completion + local subcmd_varname + subcmd_varname="\$(_${cmd}_comp_util_get_varname "\${COMP_WORDS[1]}")" + if type "_${cmd}_completions_\$subcmd_varname" &>/dev/null; then + "_${cmd}_completions_\$subcmd_varname" + else + # If subcmd completion function not defined, use the fallback + "_${cmd}_completions__fallback" + fi + return 0 + fi +EOF + + reply_args="_${cmd}_comp_reply_list _${cmd}_comp_subcmds" + else + reply_args=$(make_reply_action cmd_args) + fi + + local reply_opts_fallback + if [[ -v cmd_opts_fallback ]]; then + reply_opts_fallback=$(make_reply_action cmd_opts_fallback) + else + reply_opts_fallback=$(make_reply_action cmd_args) + fi + + cat <<EOF + + # Enter the cmd completion +$(make_cmd_core "cmd_opts" "$reply_args" "$reply_opts_fallback") +} + +complete -F _${cmd}_completions -o bashdefault ${cmd_name} +# vi: sw=2 ts=2 +EOF +} + + +make() { + make_header + make_opts_variable cmd_opts + make_dumped_variables + + if $has_subcmds; then + make_subcmd_opts + make_get_varnames + fi + + make_reply_utils + + if $has_subcmds; then + make_subcmds + make_subcmd_completions + fi + make_main_completion +} + +do_make() { + # NOTE: Naming variable should avoid some prefixes like "reply_" and "subcmd_opts_". Search "compgen -A". + local conf_path=$1 + local has_subcmds=false + local equal_sign_idx=0 + local -A map_reply_funcs=() map_equal_signs=() + local output cmd cmd_name cmd_args notice + local -a authors=() maintainers=() subcmds=() cmd_opts=() + local -A subcmd_comp_alias=() word_to_varname=() + + check_conf "$conf_path" + + local output_path + output_path="$(dirname "$conf_path")/$output" + make > "$output_path" + printf '%bGenerated file: %s%b\n' "${GREEN}" "$output_path" "$RESET_ALL" +} + +usage() { + cat <<EOF +Usage: bash-completor [options] + +Options: + -c <config_path> To generate Bash completion script based on configuration + -h|--help Print the usage + --version Print the version of bash-completor + +Description: Quickly generate Bash completion script based on configuration. + +Config Syntax: https://github.com/adoyle-h/bash-completor/docs/syntax.md + +Project: https://github.com/adoyle-h/bash-completor + +Version: $VERSION +EOF +} + +check_conf() { + local conf_path=$1 + + if [[ ! -f $conf_path ]]; then + echo "Not found config file at $conf_path" >&2 + exit 3 + fi + + # shellcheck disable=1090 + . "$conf_path" + + # Set default values of config options + cmd_name=$cmd + cmd=$(get_varname "$cmd_name") + cmd_args=${cmd_args:-@files} + subcmd_args__fallback=${subcmd_args__fallback:-@files} + + if (( ${#subcmds[@]} > 0 )); then + has_subcmds=true + fi +} + +main() { + if (( $# == 0 )); then usage; exit 0; fi + + case "$1" in + -c) + do_make "$2" + ;; + + -h|--help) + usage + ;; + + --version) + echo "$VERSION" + ;; + + *) + echo "Invalid option '$1'." >&2 + exit 2 + ;; + esac +} + +main "$@" |
