diff --git a/shell-completion/Makefile b/shell-completion/Makefile new file mode 100644 index 000000000..92c4e53a6 --- /dev/null +++ b/shell-completion/Makefile @@ -0,0 +1,28 @@ + +.PHONY: commands clean + +all: generic-options.txt commands hledger-completion.bash + +generic-options.txt: + hledger -h | ./output-options.sh > $@ + +commands.txt: + hledger | ./output-commands.sh > $@ + +commands-list.txt: commands.txt + paste -sd, $^ | tr -d '\n' > $@ + +commands: commands.txt + #parallel 'touch {}.command' < commands.txt + parallel 'hledger {} -h | ./output-options.sh > options-{}.txt' < commands.txt + +# It's possible to call this rule explicitly but it's not invoked automatically. +# Better generate *-options.txt with the 'commands' phony rule. +%-options.txt: %.command + hledger $* -h | ./output-options.sh > $@ + +hledger-completion.bash: hledger-completion.bash.m4 commands-list.txt + m4 hledger-completion.bash.m4 > $@ + +clean: + rm -f *.commands *.txt hledger-completion.bash diff --git a/shell-completion/foreach2.m4 b/shell-completion/foreach2.m4 new file mode 100644 index 000000000..74d00fb6d --- /dev/null +++ b/shell-completion/foreach2.m4 @@ -0,0 +1,10 @@ +include(`quote.m4')dnl +divert(`-1') +# foreach(x, (item_1, item_2, ..., item_n), stmt) +# parenthesized list, improved version +define(`foreach', `pushdef(`$1')_$0(`$1', + (dquote(dquote_elt$2)), `$3')popdef(`$1')') +define(`_arg1', `$1') +define(`_foreach', `ifelse(`$2', `(`')', `', + `define(`$1', _arg1$2)$3`'$0(`$1', (dquote(shift$2)), `$3')')') +divert`'dnl diff --git a/shell-completion/hledger-completion.bash.m4 b/shell-completion/hledger-completion.bash.m4 new file mode 100644 index 000000000..6f343f16d --- /dev/null +++ b/shell-completion/hledger-completion.bash.m4 @@ -0,0 +1,94 @@ +#!/bin/bash +# Completion script for hledger. +# Created using a Makefile and real hledger. + +# No set -e because this file is sourced and is not supposed to quit the current shell. +set -o pipefail + +# TODO grep "^$wordToComplete" is not safe to use if the word contains regex +# special chars. But it might be no problem because of COMP_WORDBREAKS. + +# TODO Try to get file from -f --file arguments from COMP_WORDS and pass it to +# the 'hledger accounts' call. + +# Working with bash arrays is nasty compared to editing a text file. Consider +# for example grepping an array or map a substitution on it. +# Therefore, we create temp files in RAM for completion suggestions (see below). + +declare -g HLEDGER_COMPLETION_TEMPDIR +HLEDGER_COMPLETION_TEMPDIR=$(mktemp -d) + +hledgerCompletionFunction() { + declare cmd=$1 + declare wordToComplete=$2 + declare precedingWord=$3 + + declare subcommand + for subcommand in "${COMP_WORDS[@]}"; do + if grep -Fxqe "$subcommand" "$HLEDGER_COMPLETION_TEMPDIR/commands.txt"; then + #declare -a options + #readarray -t options <(grep "^$wordToComplete" "$HLEDGER_COMPLETION_TEMPDIR/options-$subcommand.txt") + #COMPREPLY+=( "${options[@]}" ) + COMPREPLY+=( $(grep -h "^$wordToComplete" -- "$HLEDGER_COMPLETION_TEMPDIR/options-$subcommand.txt") ) + break + fi + subcommand= + done + + if [[ -z $subcommand ]]; then + + declare completeFiles filenameSoFar + case $wordToComplete in + --file=*|--rules-file=*) + completeFiles=1 + filenameSoFar=${wordToComplete#*=} + ;; + esac + case $precedingWord in + -f|--file|--rules-file) + completeFiles=1 + filenameSoFar=$wordToComplete + ;; + esac + + if [[ -n $completeFiles ]]; then + : + #COMP_WORDBREAKS='= ' + COMPREPLY+=( $(compgen -df | grep "^$filenameSoFar") ) + + else + COMPREPLY+=( $(grep -h "^$wordToComplete" -- "$HLEDGER_COMPLETION_TEMPDIR/commands.txt" "$HLEDGER_COMPLETION_TEMPDIR/generic-options.txt") ) + fi + + else + + # Almost all subcommands accpt [QUERY] -> always add accounts to completion list + + COMP_WORDBREAKS=' ' + COMPREPLY+=( $(hledger accounts --flat | grep "^$wordToComplete") ) + + fi + +} + +complete -F hledgerCompletionFunction hledger + +# Include lists of commands and options generated by the Makefile using m4 +# macro processor. +# Included files must have exactly one newline at EOF to prevent weired errors. + +cat < "$HLEDGER_COMPLETION_TEMPDIR/commands.txt" +include(`commands.txt')dnl +TEXT + +cat < "$HLEDGER_COMPLETION_TEMPDIR/generic-options.txt" +include(`generic-options.txt')dnl +TEXT + +include(`foreach2.m4') + +foreach(`cmd', (include(`commands-list.txt')), ` +cat < "$HLEDGER_COMPLETION_TEMPDIR/options-cmd.txt" +include(options-cmd.txt)dnl +TEXT +') diff --git a/shell-completion/hledger-completion.sh b/shell-completion/hledger-completion.sh new file mode 100644 index 000000000..67a3f6496 --- /dev/null +++ b/shell-completion/hledger-completion.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Completion script for hledger. +# Created using a Makefile and real hledger. + +#set -eo pipefail + +completeFunction() { + declare cmd=$1 + declare wordToComplete=$2 + declare precedingWord=$3 + + declare subcommand + for subcommand in "${COMP_WORDS[@]}"; do + if grep -Fxq "$subcommand" commands.txt; then + #declare -a options + #readarray -t options <(grep "^$wordToComplete" "$subcommand-options.txt") + #COMPREPLY+=( "${options[@]}" ) + COMPREPLY+=( $(cat "$subcommand-options.txt" | grep "^$wordToComplete") ) + break + fi + subcommand= + done + + if [[ -z $subcommand ]]; then + + # echo;echo no subcommand + + case $precedingWord in + -f|--file|--rules-file) + # COMPREPLY+=( $(compgen -df | grep "^$wordToComplete") ) + : + ;; + *) + # echo "completing sub commands and general options" + COMPREPLY+=( $(cat commands.txt generic-options.txt | grep "^$wordToComplete") ) + ;; + esac + + else + : + # echo;echo subcommand is $subcommand + + # if grep -Eqv '\b(register|reg|r)\b' <<< "$COMP_LINE"; then + # return + # fi + # case $precedingWord in + # register|reg|r) : ;; + # *) return 1 ;; + # esac + + declare journalFile + # TODO try to get file from -f --file first + if [[ -n $HLEDGER_FILE ]]; then + journalFile=$HLEDGER_FILE + else + journalFile=~/.hledger.journal + fi + COMP_WORDBREAKS=' ' + COMPREPLY+=( $(sed -rn 's/^ +([-_:a-zA-Z0-9]+).*/\1/p' "$journalFile" | grep "^$wordToComplete") ) + + fi + +} + +complete -F completeFunction hledger diff --git a/shell-completion/output-commands.sh b/shell-completion/output-commands.sh new file mode 100755 index 000000000..2bbc37996 --- /dev/null +++ b/shell-completion/output-commands.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Output subcommands from man/usage text + +set -o errexit -o pipefail -o nounset + +printCommands() { + declare tmp=$1 + sed -rn 's/^ ([-a-z]+).*/\1/gp' "$tmp" + sed -rn 's/^ .*\(([a-z]+)\).*/\1/gp' "$tmp" + # TODO missing: (reg, r) (multiple aliases) +} + +main() { + declare tmp + tmp=$(mktemp) + cat > "$tmp" + + printCommands "$tmp" | grep -v ^hledger +} + +main "$@" diff --git a/shell-completion/output-options.sh b/shell-completion/output-options.sh new file mode 100755 index 000000000..fef6ec12d --- /dev/null +++ b/shell-completion/output-options.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Output short and long options from man/usage text + +set -o errexit -o pipefail -o nounset + +main() { + declare tmp + tmp=$(mktemp) + cat > "$tmp" + sed -rn 's/.* (-[a-zA-Z0-9]).*/\1/gp' < "$tmp" + + # Do not print '=' after long options with arg because it makes completion + # for option arguments harder. + sed -rn 's/.* (--[-a-zA-Z0-9]+)=?.*/\1/gp' < "$tmp" +} + +main "$@" diff --git a/shell-completion/quote.m4 b/shell-completion/quote.m4 new file mode 100644 index 000000000..fae52c3ee --- /dev/null +++ b/shell-completion/quote.m4 @@ -0,0 +1,9 @@ +divert(`-1') +# quote(args) - convert args to single-quoted string +define(`quote', `ifelse(`$#', `0', `', ``$*'')') +# dquote(args) - convert args to quoted list of quoted strings +define(`dquote', ``$@'') +# dquote_elt(args) - convert args to list of double-quoted strings +define(`dquote_elt', `ifelse(`$#', `0', `', `$#', `1', ```$1''', + ```$1'',$0(shift($@))')') +divert`'dnl