I was looking at how other programs that have an overwhelming number of sub-commands and options deal with completion, namely how git does it, and I liked the clean workflow not spitting every available option until asked for. Hledger's main workflow is: > hledger COMMAND QUERY so I have tried to reproduce this with this change. Options are of course still there, but not shown until you ask for them by entering a dash on the command line. Also, the `acct:` filter proposes only top level accounts until there is some input from the user because accounts tend to be numerous as well.
		
			
				
	
	
		
			387 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			387 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| # shellcheck disable=2034
 | |
| 
 | |
| # 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.
 | |
| 
 | |
| _hledger_completion_function() {
 | |
|     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 to escape the rest
 | |
|     COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
 | |
|     case "$COMP_WORDBREAKS" in
 | |
|         *=*) : ;;
 | |
|         *)   COMP_WORDBREAKS="$COMP_WORDBREAKS=" ;;
 | |
|     esac
 | |
|     compopt -o filenames
 | |
| 
 | |
|     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
 | |
|             local subcommandMatches
 | |
|             subcommandMatches=$(grep -c "^$subcommand" <<< "$_hledger_complist_commands")
 | |
|             if ((subcommandMatches > 1)); then
 | |
|                 subcommand=
 | |
|                 break
 | |
|             else
 | |
|                 _hledger_compreply "$subcommand"
 | |
|                 return 0
 | |
|             fi
 | |
|         fi
 | |
|         if [[ $cur == -* ]]; then
 | |
|             # Replace dashes with underscores and use indirect expansion
 | |
|             subcommandOptions=_hledger_complist_options_${subcommand//-/_}
 | |
|             _hledger_compreply "$(_hledger_compgen "${!subcommandOptions}")"
 | |
|         fi
 | |
|         break
 | |
|     done
 | |
| 
 | |
|     # Option argument completion
 | |
|     _hledger_compreply_optarg && return
 | |
| 
 | |
|     if [[ -z $subcommand ]]; then
 | |
|         compopt +o filenames
 | |
|         if [[ $cur == -* ]]; then
 | |
|             _hledger_compreply "$(_hledger_compgen "$_hledger_complist_generic_options")"
 | |
|         else
 | |
|             _hledger_compreply "$(_hledger_compgen "$_hledger_complist_commands")"
 | |
|         fi
 | |
| 
 | |
|         return 0
 | |
|     fi
 | |
| 
 | |
|     # Avoid setting compopt bellow if completing an option
 | |
|     [[ $cur == -* ]] && return
 | |
| 
 | |
|     # Query completion
 | |
|     _hledger_compreply_query && return
 | |
| 
 | |
|     # Subcommand specific
 | |
|     case $subcommand in
 | |
|         files|test) return 0 ;;
 | |
|         help)
 | |
|             compopt -o nosort +o filenames
 | |
|             _hledger_compreply_append "$(compgen -W "$(hledger help | tail -n 1)" -- "$cur")"
 | |
|             return 0
 | |
|             ;;
 | |
|     esac
 | |
| 
 | |
|     # Offer query filters and accounts for the rest
 | |
|     # Do not sort. Keep options, accounts and query filters grouped separately
 | |
|     compopt -o nosort -o nospace
 | |
|     _hledger_compreply_append "$(_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
 | |
| 
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| _hledger_extension_completion_function() {
 | |
|     # Pretend that hledger is called with given extension
 | |
|     # as first argument and call main completion function
 | |
|     COMP_WORDS=("hledger" "${1#*-}" "${COMP_WORDS[@]:1}")
 | |
|     COMP_CWORD=$((COMP_CWORD + 1))
 | |
|     _hledger_completion_function "hledger" "${@:1}"
 | |
| }
 | |
| 
 | |
| # Register completion function for hledger:
 | |
| complete -F _hledger_completion_function hledger
 | |
| 
 | |
| # Register completion functions for hledger extensions:
 | |
| complete -F _hledger_extension_completion_function hledger-ui
 | |
| complete -F _hledger_extension_completion_function 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}"
 | |
| }
 | |
| 
 | |
| _hledger_quote()
 | |
| {
 | |
|     local quoted
 | |
|     _hledger_quote_by_ref "$1" quoted
 | |
|     printf %s "$quoted"
 | |
| }
 | |
| 
 | |
| # 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 optionIndex=$((cword - 1))
 | |
|     local match=$cur
 | |
|     local wordlist
 | |
| 
 | |
|     # Match the empty string on --file=<TAB>, not the equal sign itself
 | |
|     if [[ $cur == = ]]; then
 | |
|         match=""
 | |
|     # Once input is present, cword is incremented so we compensate
 | |
|     elif [[ $prev == = ]]; then
 | |
|         optionIndex=$((cword - 2))
 | |
|     fi
 | |
| 
 | |
|     case ${words[optionIndex]} in
 | |
|         --alias)
 | |
|             compopt -o nospace
 | |
|             _hledger_compreply "$(_hledger_compgen "$(_hledger accounts --flat)" "" "$match")"
 | |
|             ;;
 | |
|         -f|--file|--rules-file|-o|--output-file)
 | |
|             _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 nospace
 | |
|             _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|--begin|-e|--end|-p|--period|--depth|--drop)
 | |
|             _hledger_compreply ""
 | |
|             ;;
 | |
|         *)
 | |
|             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
 | |
|             _get_comp_words_by_ref -n '<=>' -c cur
 | |
|             _hledger_compreply "$(compgen -P "$query" -W "$wordlist" -- "${cur#*:}")"
 | |
|             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 lists of commands and options generated by the Makefile using the
 | |
| # m4 macro processor.
 | |
| # Included files must have exactly one newline at EOF to prevent weired errors.
 | |
| 
 | |
| read -r -d "" _hledger_complist_commands <<TEXT
 | |
| include(`commands.txt')dnl
 | |
| TEXT
 | |
| 
 | |
| read -r -d "" _hledger_complist_query_filters <<TEXT
 | |
| include(`query-filters.txt')dnl
 | |
| TEXT
 | |
| 
 | |
| read -r -d "" _hledger_complist_generic_options <<TEXT
 | |
| include(`generic-options.txt')dnl
 | |
| TEXT
 | |
| 
 | |
| # Dashes are replaced by m4 with underscores to form valid identifiers
 | |
| # Referenced by indirect expansion of $subcommandOptions
 | |
| dnl
 | |
| include(`foreach2.m4')dnl
 | |
| foreach(`cmd', (include(`commands-list.txt')), `
 | |
| read -r -d "" _hledger_complist_options_`'translit(cmd, -, _) <<TEXT
 | |
| include(options-cmd.txt)dnl
 | |
| TEXT
 | |
| ')dnl
 | |
| 
 | |
| # Local Variables:
 | |
| # sh-basic-offset: 4
 | |
| # indent-tabs-mode: nil
 | |
| # End:
 | |
| # ex: ts=4 sw=4 et
 |