From b16d2d069511ebf58de303861d5ae783f0b7b8d8 Mon Sep 17 00:00:00 2001 From: DaKnig <37626476+DaKnig@users.noreply.github.com> Date: Mon, 1 Nov 2021 00:44:01 +0200 Subject: [PATCH] Add Bash completion script for rgbasm (#895) * Add Bash completion script for RGBASM Should have large feature parity with the Zsh completion Co-authored-by: DaKnig <37626476+DaKnig@users.noreply.github.com> * Register RGBASM Bash completion in `checkdiff` Co-authored-by: ISSOtm --- README.rst | 2 + contrib/bash_compl/_rgbasm.bash | 214 ++++++++++++++++++++++++++++++++ contrib/checkdiff.bash | 2 + 3 files changed, 218 insertions(+) create mode 100755 contrib/bash_compl/_rgbasm.bash diff --git a/README.rst b/README.rst index 39a24b63..b645a0f9 100644 --- a/README.rst +++ b/README.rst @@ -88,6 +88,8 @@ The RGBDS source code file structure somewhat resembles the following: * ``zsh_compl`` contains tab completion scripts for use with zsh. Put them somewhere in your ``fpath``, and they should auto-load. + * ``bash_compl`` contains tab completion scripts for use with bash. Run them with ``source`` somewhere in your ``.bashrc``, and they should load every time you open a shell. + - ``include/`` - header files for each respective C files in `src`. - ``src/`` - source code and manual pages for RGBDS. diff --git a/contrib/bash_compl/_rgbasm.bash b/contrib/bash_compl/_rgbasm.bash new file mode 100755 index 00000000..38d50bd2 --- /dev/null +++ b/contrib/bash_compl/_rgbasm.bash @@ -0,0 +1,214 @@ +#/usr/bin/env bash + +# Known bugs: +# - Newlines in file/directory names break this script +# This is because we rely on `compgen -A`, which is broken like this. +# A fix would require implementing it ourselves, and no thanks! +# - `rgbasm --binary-digits=a` is treated the same as `rgbasm --binary-digits=` (for example) +# This is not our fault, Bash passes both of these identically. +# Maybe it could be worked around, but such a fix would likely be involved. +# The user can work around it by typing `--binary-digits ''` instead, for example. +# - Directories are not completed as such in "coalesced" short-opt arguments. For example, +# `rgbasm -M d` can autocomplete to `rgbasm -M dir/` (no space), but +# `rgbasm -Md` would autocomplete to `rgbasm -Mdir ` (trailing space) instead. +# This is because dircetory handling is performed by Readline, whom we can't tell about the short +# opt kerfuffle. The user can work around by separating the argument, as shown above. +# (Also, there might be more possible bugs if `-Mdir` is actually a directory. Ugh.) + +# Something to note: +# `rgbasm --binary-digits=a` gets passed to us as ('rgbasm' '--binary-digits' '=' 'a') +# Thus, we don't need to do much to handle that form of argument passing: skip '=' after long opts. + +_rgbasm_completions() { + COMPREPLY=() + + # Format: "long_opt:state_after" + # Empty long opt = it doesn't exit + # See the `state` variable below for info about `state_after` + declare -A opts=( + [V]="version:normal" + [E]="export-all:normal" + [h]="halt-without-nop:normal" + [L]="preserve-ld:normal" + [v]="verbose:normal" + [w]=":normal" + [b]="binary-digits:unk" + [D]="define:unk" + [g]="gfx-chars:unk" + [i]="include:dir" + [M]="dependfile:glob-*.mk *.d" + [o]="output:glob-*.o" + [p]="pad-value:unk" + [r]="recursion-depth:unk" + [W]="warning:warning" + ) + # Parse command-line up to current word + local opt_ena=true + # Possible states: + # - normal = Well, normal. Options are parsed normally. + # - unk = An argument that can't be completed, and should just be skipped. + # - warning = A warning flag. + # - dir = A directory path + # - glob-* = A glob, after the dash is a whitespace-separated list of file globs to use + local state=normal + # The length of the option, used as a return value by the function below + local optlen=0 + # $1: a short option word + # `state` will be set to the parsing state after the last option character in the word. If + # "normal" is not returned, `optlen` will be set to the length (dash included) of the "option" + # part of the argument. + parse_short_opt() { + for (( i = 1; i < "${#1}"; i++ )); do + # If the option is not known, assume it doesn't take an argument + local opt="${opts["${1:$i:1}"]:-":normal"}" + state="${opt#*:}" + # If the option takes an argument, record the length and exit + if [[ "$state" != 'normal' ]]; then + let optlen="$i + 1" + return + fi + done + optlen=0 + } + + for (( i = 1; i < $COMP_CWORD; i++ )); do + local word="${COMP_WORDS[$i]}" + + # If currently processing an argument, skip this word + if [[ "$state" != 'normal' ]]; then + state=normal + continue + fi + + if [[ "$word" = '--' ]]; then + # Options stop being parsed after this + opt_ena=false + break + fi + + # Check if it's a long option + if [[ "${word:0:2}" = '--' ]]; then + # If the option is unknown, assume it takes no arguments: keep the state at "normal" + for long_opt in "${opts[@]}"; do + if [[ "$word" = "--${long_opt%%:*}" ]]; then + state="${long_opt#*:}" + # Check if the next word is just '='; if so, skip it, the argument must follow + # (See "known bugs" at the top of this script) + let i++ + if [[ "${COMP_WORDS[$i]}" != '=' ]]; then + let i-- + fi + optlen=0 + break + fi + done + # Check if it's a short option + elif [[ "${word:0:1}" = '-' ]]; then + # The `-M?` ones are a mix of short and long, augh + # They must match the *full* word, but only take a single dash + # So, handle them here + if [[ "$1" = "-M"[GP] ]]; then + state=normal + elif [[ "$1" = "-M"[TQ] ]]; then + state='glob-*.d *.mk *.o' + else + parse_short_opt "$word" + # The last option takes an argument... + if [[ "$state" != 'normal' ]]; then + if [[ "$optlen" -ne "${#word}" ]]; then + # If it's contained within the word, we won't complete it, revert to "normal" + state=normal + else + # Otherwise, complete it, but start at the beginning of *that* word + optlen=0 + fi + fi + fi + fi + done + + # Parse current word + # Careful that it might look like an option, so use `--` aggressively! + local cur_word="${COMP_WORDS[$COMP_CWORD]}" + + # Process options, as short ones may change the state + if $opt_ena && [[ "$state" = 'normal' && "${cur_word:0:1}" = '-' ]]; then + # We might want to complete to an option or an arg to that option + # Parse the option word to check + # There's no whitespace in the option names, so we can ride a little dirty... + + # Is this a long option? + if [[ "${cur_word:1:1}" = '-' ]]; then + # It is, try to complete one + COMPREPLY+=( $(compgen -W "${opts[*]%%:*}" -P '--' -- "${cur_word#--}") ) + return 0 + else + # Short options may be grouped, parse them to determine what to complete + # The `-M?` ones may not be followed by anything + if [[ "$1" != "-M"[GPTQ] ]]; then + parse_short_opt "$cur_word" + # We got some short options that behave like long ones + COMPREPLY+=( $(compgen -W '-MG -MP -MT -MQ' -- "$cur_word") ) + + if [[ "$state" = 'normal' ]]; then + COMPREPLY+=( $(compgen -W "${!opts[*]}" -P "$cur_word" '') ) + return 0 + elif [[ "$optlen" = "${#cur_word}" && "$state" != "warning" ]]; then + # This short option group only awaits its argument! + # Post the option group as-is as a reply so that Readline inserts a space, + # so that the next completion request switches to the argument + # An exception is made for warnings, since it's idiomatic to stick them to the + # `-W`, and it doesn't break anything. + COMPREPLY+=( "$cur_word" ) + return 0 + fi + fi + fi + fi + + case "$state" in + unk) # Return with no replies: no idea what to complete! + ;; + warning) + COMPREPLY+=( $(compgen -W " + assert + backwards-for + builtin-args + charmap-redef + div + empty-data-directive + empty-macro-arg + empty-strrpl + large-constant + long-string + macro-shift + nested-comment + obsolete + shift + shift-amount + truncation + user + all + extra + everything + error" -P "${cur_word:0:$optlen}" -- "${cur_word:$optlen}") ) + ;; + normal) # Acts like a glob... + state="glob-*.asm *.inc *.sm83" + ;& + glob-*) + while read -r word; do + COMPREPLY+=("${cur_word:0:$optlen}$word") + done < <(for glob in ${state#glob-}; do compgen -A file -X \!"$glob" -- "${cur_word:$optlen}"; done) + # Also complete directories + ;& + dir) + while read -r word; do + COMPREPLY+=("${cur_word:0:$optlen}$word") + done < <(compgen -A directory -- "${cur_word:$optlen}") + compopt -o filenames + ;; + esac +} + +complete -F _rgbasm_completions rgbasm diff --git a/contrib/checkdiff.bash b/contrib/checkdiff.bash index 7ab26d35..e1889a85 100755 --- a/contrib/checkdiff.bash +++ b/contrib/checkdiff.bash @@ -63,6 +63,8 @@ dependency src/asm/main.c src/asm/rgbasm.1 \ "Did the rgbasm CLI change?" dependency src/asm/main.c contrib/zsh_compl/_rgbasm \ "Did the rgbasm CLI change?" +dependency src/asm/main.c contrib/bash_compl/_rgbasm.bash \ + "Did the rgbasm CLI change?" dependency src/link/main.c src/link/rgblink.1 \ "Did the rgblink CLI change?" dependency src/link/main.c contrib/zsh_compl/_rgblink \