diff --git a/shell-completion/hledger-completion.bash b/shell-completion/hledger-completion.bash index 2d86c2d33..8a6db7e7c 100644 --- a/shell-completion/hledger-completion.bash +++ b/shell-completion/hledger-completion.bash @@ -1,4 +1,6 @@ -# shellcheck disable=2034 +# -*- mode: sh; sh-basic-offset: 4; indent-tabs-mode: nil -*- +# ex: ts=4 sw=4 et +# shellcheck disable=2034,2154 # Completion script for hledger. # Created using a Makefile and real hledger. @@ -415,6 +417,7 @@ read -r -d "" _hledger_complist_commands < register, register-match, - # $subcommand == bal --> balance, balancesheet, balancesheetequity, etc. - # Do not ignore them! - if ((i == cword)); then - _hledger_compreply "$( - _hledger_compgen "$_hledger_complist_commands" - )" - return 0 - fi - - # Replace dashes with underscores and use indirect expansion - subcommandOptions=_hledger_complist_options_${subcommand//-/_} - - if [[ $cur == -* ]]; then - _hledger_compreply "$(_hledger_compgen "${!subcommandOptions}")" - # Suspend space on completion of long options requiring an argument - [[ ${COMPREPLY[0]} == --*= ]] && compopt -o nospace - - return 0 - fi - break - done - - # Option argument completion - _hledger_compreply_optarg && return - - if [[ -z $subcommand ]]; then - if [[ $cur == -* ]]; then - _hledger_compreply "$( - _hledger_compgen "$_hledger_complist_generic_options" - )" - # Suspend space on completion of long options requiring an argument - [[ ${COMPREPLY[0]} == --*= ]] && compopt -o nospace - else - _hledger_compreply "$( - _hledger_compgen "$_hledger_complist_commands" - )" - fi - - return 0 - fi - - # Set this from here on because queries tend to have lots of special chars - # TODO: better handling of special characters - compopt -o filenames - - # Query completion - _hledger_compreply_query && return - - # Subcommand specific - case $subcommand in - help) - compopt -o nosort +o filenames - _hledger_compreply "$( - compgen -W "$(hledger help | tail -n 1)" -- "$cur" - )" - return 0 - ;; - # These do not expect or support any query arguments - commodities|check-dupes|files|import|print-unique|test) - return 0 - ;; - esac - - # Offer query filters and accounts for the rest - _hledger_compreply "$(_hledger_compgen "$_hledger_complist_query_filters")" - if [[ -z $cur ]]; then - _hledger_compreply_append "$( - _hledger_compgen "$(_hledger accounts --flat --depth 1)" - )" - else - _hledger_compreply_append "$( - _hledger_compgen "$(_hledger accounts --flat)" - )" - fi - - # Suspend space on completion of query prefix - # Do not sort, keep accounts and query filters grouped separately - [[ ${COMPREPLY[0]} == *: ]] && compopt -o nospace - compopt -o nosort - - return 0 -} - -_hledger_extension_completion() { - local cmd=${1##*/} - local ext=${cmd#hledger-} - # Pretend that hledger is called with the given extension - # as the first argument and call main completion function - COMP_WORDS=("hledger" "$ext" "${COMP_WORDS[@]:1}") - COMP_CWORD=$((COMP_CWORD + 1)) - _hledger_completion "hledger" "${@:1}" -} - -# Register completion function for hledger: -complete -F _hledger_completion hledger - -# Register completion functions for hledger extensions: -complete -F _hledger_extension_completion hledger-ui hledger-web - -# Helpers - -# Comment out when done -_hledger_debug() { - ((HLEDGER_DEBUG)) || return 0 - local var vars=(words) - (($#)) && vars=("$@") - for var in "${vars[@]}"; do - printf '\ndebug: %s\n' "$(declare -p "$var")" >&2 - done -} - -# Stolen from bash-completion -# This function quotes the argument in a way so that readline dequoting -# results in the original argument. This is necessary for at least -# `compgen' which requires its arguments quoted/escaped: -_hledger_quote_by_ref() -{ - printf -v "$2" %q "$1" - - # If result becomes quoted like this: $'string', re-evaluate in order to - # drop the additional quoting. See also: http://www.mail-archive.com/ - # bash-completion-devel@lists.alioth.debian.org/msg01942.html - [[ ${!2} == \$* ]] && eval "$2=${!2}" -} - -# Set the value of COMPREPLY from newline delimited completion candidates -_hledger_compreply() { - local IFS=$'\n' - # shellcheck disable=2206 - COMPREPLY=($1) -} - -# Append the value of COMPREPLY from newline delimited completion candidates -_hledger_compreply_append() { - local IFS=$'\n' - # shellcheck disable=2206 - COMPREPLY+=($1) -} - -# Generate input suitable for _hledger_compreply() from newline delimited -# completion candidates. It doesn't seem there is a way to feed a literal -# word list to compgen -- it will eat your quotes, drink your booze and... -# Completion candidates are quoted accordingly first and then we leave it to -# compgen to deal with readline. -# -# Arguments: -# $1: a newline separated list with completion cadidates -# $2: (optional) a prefix string to add to generated completions -# $3: (optional) a word to match instead of $cur, the default. -# If $match is null and $prefix is defined the match is done against $cur -# stripped of $prefix. If both $prefix and $match are null we match against -# $cur and no prefix is added to completions. -_hledger_compgen() { - local complist=$1 - local prefix=$2 - local match=$3 - local quoted=() - local word - local i=0 - - while IFS= read -r word; do - _hledger_quote_by_ref "$word" word - quoted[i++]=$word - done <<< "$complist" - - if (($# < 3)); then - match=${cur:${#prefix}} - fi - - local IFS=$'\n' - compgen -P "$prefix" -W "${quoted[*]}" -- "$match" -} - -# Try required option argument completion. Set COMPREPLY and return 0 on -# success, 1 if option doesn't require an argument or out of context -_hledger_compreply_optarg() { - local option=${words[cword - 1]} - local match=$cur - local wordlist - - # Match the empty string on --file=, not the equal sign itself - if [[ $cur == = ]]; then - match="" - # Once input is present, cword is incremented so we compensate - elif [[ $prev == = ]]; then - option=${words[cword - 2]} - fi - - [[ $option == -* ]] || return - - case $option in - --alias) - compopt -o nospace -o filenames - _hledger_compreply "$( - _hledger_compgen "$(_hledger accounts --flat)" "" "$match" - )" - ;; - -f|--file|--rules-file|-o|--output-file) - compopt -o filenames - _hledger_compreply "$(compgen -f -- "$match")" - ;; - --pivot) - compopt -o nosort - wordlist="code description note payee" - _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" - _hledger_compreply_append "$( - _hledger_compgen "$(_hledger tags)" "" "$match" - )" - ;; - --value) - wordlist="cost then end now" - _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" - ;; - -X|--exchange) - _hledger_compreply "$( - _hledger_compgen "$(_hledger commodities)" "" "$match" - )" - ;; - --color|--colour) - compopt -o nosort - wordlist="auto always yes never no" - _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" - ;; - -O|--output-format) - wordlist="txt csv json sql" - _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" - ;; - --close-acct|--open-acct) - compopt -o filenames - _hledger_compreply "$( - _hledger_compgen "$(_hledger accounts --flat)" "" "$match" - )" - ;; - --debug) - wordlist="{1..9}" - _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" - ;; - # Argument required, but no handler (yet) - -b|-e|-p) - _hledger_compreply "" - ;; - # Check if an unhandled long option requires an argument - *) - local optionList argRequired - - if [[ -n $subcommandOptions ]]; then - optionList=${!subcommandOptions} - else - optionList=$_hledger_complist_generic_options - fi - - while IFS= read -r argRequired; do - if [[ $argRequired == "$option=" ]]; then - _hledger_compreply "" - return 0 - fi - done <<< "$optionList" - - return 1 - ;; - esac - - return 0 -} - -# Query filter completion through introspection -_hledger_compreply_query() { - [[ $cur =~ .: ]] || return - local query=${cur%%:*}: - local match=${cur#*:} - grep -Fxqe "$query" <<< "$_hledger_complist_query_filters" || return - - local hledgerArgs=() - case $query in - acct:) - if (( ${#match} )); then - hledgerArgs=(accounts --flat) - else - hledgerArgs=(accounts --flat --depth 1) - fi - ;; - code:) hledgerArgs=(codes) ;; - cur:) hledgerArgs=(commodities) ;; - desc:) hledgerArgs=(descriptions) ;; - note:) hledgerArgs=(notes) ;; - payee:) hledgerArgs=(payees) ;; - tag:) hledgerArgs=(tags) ;; - *) - local wordlist - case $query in - amt:) wordlist="< <= > >=" ;; - real:) wordlist="\ 0" ;; - status:) wordlist="\ * !" ;; - *) return 1 ;; - esac - _hledger_compreply "$( - compgen -P "$query" -W "$wordlist" -- "$match" - )" - return 0 - ;; - esac - - _hledger_compreply "$( - _hledger_compgen "$(_hledger "${hledgerArgs[@]}")" "$query" - )" - - return 0 -} - -# Parse the command line so far and fill the array $optarg with the arguments to -# given options. $optarg should be declared by the caller -_hledger_optarg() { - local options=("$@") - local i j offset - optarg=() - - # hledger balance --file ~/ledger _ - # 0 1 2 3 4 - for ((i=1; i < ${#words[@]} - 2; i++)); do - offset=0 - for j in "${!options[@]}"; do - if [[ ${words[i]} == "${options[j]}" ]]; then - if [[ ${words[i+1]} == '=' ]]; then - offset=2 - else - offset=1 - fi - # Pass it through compgen to unescape it - optarg+=("$(compgen -W "${words[i + offset]}")") - fi - done - ((i += offset)) - done -} - -# Get ledger file from -f --file arguments from COMP_WORDS and pass it to the -# 'hledger' call. Note that --rules-file - if present - must also be passed! -# Multiple files are allowed so pass them all in the order of appearance. -_hledger() { - local hledgerArgs=("$@") - local file - local -a optarg - - _hledger_optarg -f --file - for file in "${optarg[@]}"; do - [[ -f $file ]] && hledgerArgs+=(--file "$file") - done - - _hledger_optarg --rules-file - for file in "${optarg[@]}"; do - [[ -f $file ]] && hledgerArgs+=(--rules-file "$file") - done - - # Discard errors. Is there a way to validate files before using them? - hledger "${hledgerArgs[@]}" 2>/dev/null -} +include(`hledger-completion.bash.stub')dnl # Include lists of commands and options generated by the Makefile using the # m4 macro processor. @@ -434,9 +27,3 @@ TEXT ')dnl return 0 - -# Local Variables: -# sh-basic-offset: 4 -# indent-tabs-mode: nil -# End: -# ex: ts=4 sw=4 et diff --git a/shell-completion/hledger-completion.bash.stub b/shell-completion/hledger-completion.bash.stub new file mode 100644 index 000000000..0853dbdd8 --- /dev/null +++ b/shell-completion/hledger-completion.bash.stub @@ -0,0 +1,410 @@ +# -*- mode: sh; sh-basic-offset: 4; indent-tabs-mode: nil -*- +# ex: ts=4 sw=4 et +# shellcheck disable=2034,2154 + +# Completion script for hledger. +# Created using a Makefile and real hledger. + +# This script is sourced by an interactive shell, so do NOT do things like +# 'set -o pipefail' or mangle the global environment in any other way! +# That said, we *do* remove colon (:) from COMP_WORDBREAKS which impacts +# the rest of the session and completion for other programs. + +# INSTALLATION: +# To install you can simply source this file from your shell's startup files. +# +# Alternatively, copy/symlink it into `${BASH_COMPLETION_USER_DIR}/completions` +# or `${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions`, rename +# it to either `hledger`, `_hledger` or `hledger.bash`, and it will be loaded +# dynamically the first time you use the `hledger` command. Optionally, create +# symlinks to this file for any extensions used e.g.: +# +# mkdir -p "${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions" && +# cd "${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions" && +# cp /path/to/hledger-completion.bash hledger && +# ln -s hledger hledger-ui && +# ln -s hledger hledger-web && +# : done. + + +_hledger_completion() { + local cur prev words cword + _init_completion -n : || return 0 + + # Current treatment for special characters: + # - exclude colon (:) from COMP_WORDBREAKS + # - option processing assumes that `=` is in COMP_WORDBREAKS + # - use compopt -o filenames selectively to escape the rest + COMP_WORDBREAKS=${COMP_WORDBREAKS//:} + case $COMP_WORDBREAKS in + *=*) : ;; + *) COMP_WORDBREAKS=$COMP_WORDBREAKS= ;; + esac + + local subcommand + local subcommandOptions + local i + for ((i=1; i<${#words[@]}; i++)); do + subcommand=${words[i]} + if ! grep -Fxqe "$subcommand" <<< "$_hledger_complist_commands"; then + subcommand= + continue + fi + # There could be other commands begining with $subcommand, e.g.: + # $subcommand == reg --> register, register-match, + # $subcommand == bal --> balance, balancesheet, balancesheetequity, etc. + # Do not ignore them! + if ((i == cword)); then + _hledger_compreply "$( + _hledger_compgen "$_hledger_complist_commands" + )" + return 0 + fi + + # Replace dashes with underscores and use indirect expansion + subcommandOptions=_hledger_complist_options_${subcommand//-/_} + + if [[ $cur == -* ]]; then + _hledger_compreply "$(_hledger_compgen "${!subcommandOptions}")" + # Suspend space on completion of long options requiring an argument + [[ ${COMPREPLY[0]} == --*= ]] && compopt -o nospace + + return 0 + fi + break + done + + # Option argument completion + _hledger_compreply_optarg && return + + if [[ -z $subcommand ]]; then + if [[ $cur == -* ]]; then + _hledger_compreply "$( + _hledger_compgen "$_hledger_complist_generic_options" + )" + # Suspend space on completion of long options requiring an argument + [[ ${COMPREPLY[0]} == --*= ]] && compopt -o nospace + else + _hledger_compreply "$( + _hledger_compgen "$_hledger_complist_commands" + )" + fi + + return 0 + fi + + # Set this from here on because queries tend to have lots of special chars + # TODO: better handling of special characters + compopt -o filenames + + # Query completion + _hledger_compreply_query && return + + # Subcommand specific + case $subcommand in + help) + compopt -o nosort +o filenames + _hledger_compreply "$( + compgen -W "$(hledger help | tail -n 1)" -- "$cur" + )" + return 0 + ;; + # These do not expect or support any query arguments + commodities|check-dupes|files|import|print-unique|test) + return 0 + ;; + esac + + # Offer query filters and accounts for the rest + _hledger_compreply "$(_hledger_compgen "$_hledger_complist_query_filters")" + if [[ -z $cur ]]; then + _hledger_compreply_append "$( + _hledger_compgen "$(_hledger accounts --flat --depth 1)" + )" + else + _hledger_compreply_append "$( + _hledger_compgen "$(_hledger accounts --flat)" + )" + fi + + # Suspend space on completion of query prefix + # Do not sort, keep accounts and query filters grouped separately + [[ ${COMPREPLY[0]} == *: ]] && compopt -o nospace + compopt -o nosort + + return 0 +} + +_hledger_extension_completion() { + local cmd=${1##*/} + local ext=${cmd#hledger-} + # Pretend that hledger is called with the given extension + # as the first argument and call main completion function + COMP_WORDS=("hledger" "$ext" "${COMP_WORDS[@]:1}") + COMP_CWORD=$((COMP_CWORD + 1)) + _hledger_completion "hledger" "${@:1}" +} + +# Register completion function for hledger: +complete -F _hledger_completion hledger + +# Register completion functions for hledger extensions: +complete -F _hledger_extension_completion hledger-ui hledger-web + +# Helpers + +# Comment out when done +_hledger_debug() { + ((HLEDGER_DEBUG)) || return 0 + local var vars=(words) + (($#)) && vars=("$@") + for var in "${vars[@]}"; do + printf '\ndebug: %s\n' "$(declare -p "$var")" >&2 + done +} + +# Stolen from bash-completion +# This function quotes the argument in a way so that readline dequoting +# results in the original argument. This is necessary for at least +# `compgen' which requires its arguments quoted/escaped: +_hledger_quote_by_ref() +{ + printf -v "$2" %q "$1" + + # If result becomes quoted like this: $'string', re-evaluate in order to + # drop the additional quoting. See also: http://www.mail-archive.com/ + # bash-completion-devel@lists.alioth.debian.org/msg01942.html + [[ ${!2} == \$* ]] && eval "$2=${!2}" +} + +# Set the value of COMPREPLY from newline delimited completion candidates +_hledger_compreply() { + local IFS=$'\n' + # shellcheck disable=2206 + COMPREPLY=($1) +} + +# Append the value of COMPREPLY from newline delimited completion candidates +_hledger_compreply_append() { + local IFS=$'\n' + # shellcheck disable=2206 + COMPREPLY+=($1) +} + +# Generate input suitable for _hledger_compreply() from newline delimited +# completion candidates. It doesn't seem there is a way to feed a literal +# word list to compgen -- it will eat your quotes, drink your booze and... +# Completion candidates are quoted accordingly first and then we leave it to +# compgen to deal with readline. +# +# Arguments: +# $1: a newline separated list with completion cadidates +# $2: (optional) a prefix string to add to generated completions +# $3: (optional) a word to match instead of $cur, the default. +# If $match is null and $prefix is defined the match is done against $cur +# stripped of $prefix. If both $prefix and $match are null we match against +# $cur and no prefix is added to completions. +_hledger_compgen() { + local complist=$1 + local prefix=$2 + local match=$3 + local quoted=() + local word + local i=0 + + while IFS= read -r word; do + _hledger_quote_by_ref "$word" word + quoted[i++]=$word + done <<< "$complist" + + if (($# < 3)); then + match=${cur:${#prefix}} + fi + + local IFS=$'\n' + compgen -P "$prefix" -W "${quoted[*]}" -- "$match" +} + +# Try required option argument completion. Set COMPREPLY and return 0 on +# success, 1 if option doesn't require an argument or out of context +_hledger_compreply_optarg() { + local option=${words[cword - 1]} + local match=$cur + local wordlist + + # Match the empty string on --file=, not the equal sign itself + if [[ $cur == = ]]; then + match="" + # Once input is present, cword is incremented so we compensate + elif [[ $prev == = ]]; then + option=${words[cword - 2]} + fi + + [[ $option == -* ]] || return + + case $option in + --alias) + compopt -o nospace -o filenames + _hledger_compreply "$( + _hledger_compgen "$(_hledger accounts --flat)" "" "$match" + )" + ;; + -f|--file|--rules-file|-o|--output-file) + compopt -o filenames + _hledger_compreply "$(compgen -f -- "$match")" + ;; + --pivot) + compopt -o nosort + wordlist="code description note payee" + _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" + _hledger_compreply_append "$( + _hledger_compgen "$(_hledger tags)" "" "$match" + )" + ;; + --value) + wordlist="cost then end now" + _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" + ;; + -X|--exchange) + _hledger_compreply "$( + _hledger_compgen "$(_hledger commodities)" "" "$match" + )" + ;; + --color|--colour) + compopt -o nosort + wordlist="auto always yes never no" + _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" + ;; + -O|--output-format) + wordlist="txt csv json sql" + _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" + ;; + --close-acct|--open-acct) + compopt -o filenames + _hledger_compreply "$( + _hledger_compgen "$(_hledger accounts --flat)" "" "$match" + )" + ;; + --debug) + wordlist="{1..9}" + _hledger_compreply "$(compgen -W "$wordlist" -- "$match")" + ;; + # Argument required, but no handler (yet) + -b|-e|-p) + _hledger_compreply "" + ;; + # Check if an unhandled long option requires an argument + *) + local optionList argRequired + + if [[ -n $subcommandOptions ]]; then + optionList=${!subcommandOptions} + else + optionList=$_hledger_complist_generic_options + fi + + while IFS= read -r argRequired; do + if [[ $argRequired == "$option=" ]]; then + _hledger_compreply "" + return 0 + fi + done <<< "$optionList" + + return 1 + ;; + esac + + return 0 +} + +# Query filter completion through introspection +_hledger_compreply_query() { + [[ $cur =~ .: ]] || return + local query=${cur%%:*}: + local match=${cur#*:} + grep -Fxqe "$query" <<< "$_hledger_complist_query_filters" || return + + local hledgerArgs=() + case $query in + acct:) + if (( ${#match} )); then + hledgerArgs=(accounts --flat) + else + hledgerArgs=(accounts --flat --depth 1) + fi + ;; + code:) hledgerArgs=(codes) ;; + cur:) hledgerArgs=(commodities) ;; + desc:) hledgerArgs=(descriptions) ;; + note:) hledgerArgs=(notes) ;; + payee:) hledgerArgs=(payees) ;; + tag:) hledgerArgs=(tags) ;; + *) + local wordlist + case $query in + amt:) wordlist="< <= > >=" ;; + real:) wordlist="\ 0" ;; + status:) wordlist="\ * !" ;; + *) return 1 ;; + esac + _hledger_compreply "$( + compgen -P "$query" -W "$wordlist" -- "$match" + )" + return 0 + ;; + esac + + _hledger_compreply "$( + _hledger_compgen "$(_hledger "${hledgerArgs[@]}")" "$query" + )" + + return 0 +} + +# Parse the command line so far and fill the array $optarg with the arguments to +# given options. $optarg should be declared by the caller +_hledger_optarg() { + local options=("$@") + local i j offset + optarg=() + + # hledger balance --file ~/ledger _ + # 0 1 2 3 4 + for ((i=1; i < ${#words[@]} - 2; i++)); do + offset=0 + for j in "${!options[@]}"; do + if [[ ${words[i]} == "${options[j]}" ]]; then + if [[ ${words[i+1]} == '=' ]]; then + offset=2 + else + offset=1 + fi + # Pass it through compgen to unescape it + optarg+=("$(compgen -W "${words[i + offset]}")") + fi + done + ((i += offset)) + done +} + +# Get ledger file from -f --file arguments from COMP_WORDS and pass it to the +# 'hledger' call. Note that --rules-file - if present - must also be passed! +# Multiple files are allowed so pass them all in the order of appearance. +_hledger() { + local hledgerArgs=("$@") + local file + local -a optarg + + _hledger_optarg -f --file + for file in "${optarg[@]}"; do + [[ -f $file ]] && hledgerArgs+=(--file "$file") + done + + _hledger_optarg --rules-file + for file in "${optarg[@]}"; do + [[ -f $file ]] && hledgerArgs+=(--rules-file "$file") + done + + # Discard errors. Is there a way to validate files before using them? + hledger "${hledgerArgs[@]}" 2>/dev/null +}