I have created a custom Git command called git-profile
. It uses the git-<command>
naming convention to enable use as compword in Git Bash completion. implemented as a Bash script, that accepts a command line of the form:
git profile PROFILE_NAME [git-opts]
where git-opts
is any valid sequence of Git command line options with respective arguments. The use case of this command is simply to set GIT_CONFIG_GLOBAL
for the current invocation.
I’m trying to get Bash completion working with this command such that, starting with the third command line word, the chain of available COMPREPLY
s is sourced directly from Git’s default completion. (In my installation, that’s the completion script included in GitHub’s contrib
tree [cf. GitHub]).
I have passthrough completion working, with the caveat that subsequent COMPREPLY
s contain appropriate choice sets, but they are never autocompleted, even for partial matches. For any input after PROFILE_NAME
, I only get a subsequent option or arg’s set of compwords if I type the entire prior arg and press SPACE
.
Here’s the full completion script. (Note that the shebang is included as a temp workaround to enable LSP settings in my IDE; including here for the sake of high fidelity.)
#!/bin/bash
source /usr/share/bash-completion/completions/git
shopt -s nullglob
__git_profile_word_in() {
local _needle="$1"
local _haystack="${*:2}"
# Disable checking for regex matching syntax
# This is a deliberate literal match test
# shellcheck disable=SC2076
[[ " ${_haystack} " =~ " ${_needle} " ]]
}
__git_profile_get_profiles() {
local cur="$1"
local -r profiles_dir="$2"
local -a profiles=()
for profile_dir in "${profiles_dir%/}/$cur"*; do
profiles+=("${profile_dir##*/}")
done
echo "${profiles[@]}"
}
_git_profile() {
local -r _cur="${COMP_WORDS[COMP_CWORD]}"
local profiles_dir="${GIT_PROFILE_CONFIG_HOME:-${HOME}/.config/git/profiles}"
local profile
local -ar profiles=("$(__git_profile_get_profiles "$_cur" "$profiles_dir")")
local -ar word_opts=("${profiles[@]}" 'add')
case $COMP_CWORD in
0 | 1)
return
;;
2)
__gitcomp "${word_opts[*]}"
return
;;
*)
local -r profile_cmd_arg=${COMP_WORDS[2]}
if [[ "$profile_cmd_arg" == 'add' ]]; then
# git-profile subcommand - takes no args
return
fi
if ! __git_profile_word_in "$profile_cmd_arg" "${profiles[@]}"; then
# Unsupported scenario
return
fi
profile="${profiles_dir}/${profile_cmd_arg}/config"
;;
esac
local -r OLD_COMP_SUBSTR="${COMP_WORDS[*]:1:2}"
local -r COMP_WORDS_REBUILT=('git' "${COMP_WORDS[@]:3}")
# Forward a Git command line without the `profile` option and arg so that we
# can take advantage of completion for all available commands
# Note that we substract the trailing space from COMP_POINT
# to prevent displacing the completion cursor
COMP_WORDS=("${COMP_WORDS_REBUILT[@]}")
COMP_LINE="${COMP_WORDS_REBUILT[*]}"
COMP_CWORD=$((COMP_CWORD - 2))
COMP_POINT=$((COMP_POINT - ${#OLD_COMP_SUBSTR} - 1))
GIT_CONFIG_GLOBAL="$profile" __git_func_wrap __git_main
}
The last block before the call to __git_func_wrap
, strips the command line of words specific to git-profile
and resets the corresponding indexes as if those words were never part of the original command line. I then forward this newly stripped command line to the Git-provided completion script with the configured global config (this is probably not necessarily useful atm, but I’m also working on a separate extension to allow reading in custom [/non-“Git-native”] settings from the current config). I’m confident that this is a viable approach as I’ve had success wrapping completion routines for similar wrapper/extension scripts that I’ve written, such as for pass, and they notably don’t exhibit the unexpected behavior described below.
If I type git profile projs
and press the TAB
key twice, I get the following COMPREPLY
:
add describe mergetool revert
am diff mv rm
apply difftool notes scalar
archive fetch profile send-email
bisect flow prune shortlog
blame format-patch pull show
branch fsck push show-branch
bundle gc range-diff sparse-checkout
checkout gitk rebase stage
cherry grep reflog stash
cherry-pick gui remote status
citool help remote-bzr submodule
clang-format init repack switch
clean instaweb replace tag
clone log request-pull whatchanged
commit maintenance reset worktree
config merge restore
If I type git profile projs r
and press the TAB
key twice, I get an empty COMPREPLY
. This continues as I type additional letters until I have the full word remote
on the command line and before I press SPACE
.
If I type git profile projs remote
(note the final space) and press the TAB
key twice, I get the following COMPREPLY
:
add prune rename set-head show
get-url remove set-branches set-url update
I’m struggling to understand the Git-provided completion script, so my attempts around this have not been particularly systematic (my confusion starts roughly after chasing references down to _get_comp_words_by_ref
, but most of the functions resolved for refs seem to be utilized by command-specific completion routines). I’ve tried looking for an entry point that allows me to pass in the current compword directly – the fact that I’m getting the right compreplies leads me to think that the completion script somehow isn’t aware of the current compword, but AFAICT I’ve modulated the cursor/COMP_POINT correctly. What am I missing to enable TAB
-triggered COMPREPLY
s for partial word matches?
I’d appreciate any insight in the right direction.
Marĉjo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
2
Thanks to @KamilCuk pointing me to set -x
, I was able to finally observe the bug in my script in real time.
Note the following line near the top of _git_profile
:
local -ar profiles=("$(__git_profile_get_profiles "$_cur" "$profiles_dir")")
The fact that this check occurs prior to the case
block, means that we check for profiles against the current word, every time we enter the function.
But in the case of args to the right of $2
, we also run the following check:
if ! __git_profile_word_in "$profile_cmd_arg" "${profiles[@]}"; then
# Unsupported scenario
return
fi
We’re not checking against the current word in this case; we’re checking specifically against the value of $2
.
Hence for the case of args to the right of $2
, if $cur
is not empty but does not coincidentally match all or the first n
characters of a profile name, then profiles
will be empty, and this check will force en early exit from the completion routine.
If the character to the left of the cursor is a space, then cur
is empty, which means that, in the current, profiles
will be an empty array, and the is-profile check will pass coincidentally, allowing compwords to populate in this instance, but never against a partial word match.
A fix, then, is to move the profile-/arg-matching checks to occur only after matching on arg $2
or later, and to ensure that the correct arg is matched against in both cases.
#!/bin/bash
source /usr/share/bash-completion/completions/git
shopt -s nullglob
__git_profile_get_profiles() {
local -r cur="$1"
local -r profiles_dir="$2"
local -a profiles=()
for profile_dir in "${profiles_dir%/}/$cur"*; do
profiles+=("${profile_dir##*/}")
done
echo "${profiles[@]}"
}
_git_profile() {
local -r _cur="${COMP_WORDS[COMP_CWORD]}"
local -r profiles_dir="${GIT_PROFILE_CONFIG_HOME:-${HOME}/.config/git/profiles}"
local -a profiles
local -a profile_opt_arg_choices
local profile
case $COMP_CWORD in
0 | 1)
return
;;
2)
profiles=("$(__git_profile_get_profiles "$_cur" "$profiles_dir")")
profile_opt_arg_choices=("${profiles[@]}" 'add')
__gitcomp "${profile_opt_arg_choices[*]}"
return
;;
*)
local -r profile_cmd_arg=${COMP_WORDS[2]}
if [[ "$profile_cmd_arg" == 'add' ]]; then
# git-profile subcommand - takes no args
return
fi
profiles=("$(__git_profile_get_profiles "$profile_cmd_arg" "$profiles_dir")")
if [[ "${#profiles[@]}" == 0 ]]; then
# Unsupported scenario
return
fi
profile="${profiles_dir%/}/${profile_cmd_arg}/config"
;;
esac
local -r OLD_COMP_SUBSTR="${COMP_WORDS[*]:1:2}"
local -r COMP_WORDS_REBUILT=('git' "${COMP_WORDS[@]:3}")
# Forward a Git command line without the `profile` option and arg so that we
# can take advantage of completion for all available commands
# Note that we substract the trailing space from COMP_POINT
# to prevent displacing the completion cursor
COMP_WORDS=("${COMP_WORDS_REBUILT[@]}")
COMP_LINE="${COMP_WORDS_REBUILT[*]}"
COMP_CWORD=$((COMP_CWORD - 2))
COMP_POINT=$((COMP_POINT - ${#OLD_COMP_SUBSTR} - 1))
GIT_CONFIG_GLOBAL="$profile" __git_func_wrap __git_main
}
Marĉjo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.