How to take full advantage of Git Bash completion for a passthrough command that takes its own args?

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 COMPREPLYs 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 COMPREPLYs 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 COMPREPLYs for partial word matches?

I’d appreciate any insight in the right direction.

New contributor

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
}

New contributor

Marĉjo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật