如何自定义 Bash 命令完成?

der*_*ert 24 bash autocomplete

在 中bash,使用complete内置命令设置自定义完成命令参数非常容易。例如,对于一个具有以下概要的假设命令

foo --a | --b | --c
Run Code Online (Sandbox Code Playgroud)

你可以

complete -W '--a --b --c' foo
Run Code Online (Sandbox Code Playgroud)

当你按你还可以自定义你完成Tab提示符下使用complete -E,例如complete -E -W 'foo bar'。然后,在空提示下按 Tab 只会建议foobar

如何在空提示下自定义命令完成?例如,如果我写f,我如何自定义完成以使其完成foo

(我想,实际情况是locTABlocalc。而我的兄弟,谁促使我提出这一点,与希望它mplayer。)

mr.*_*tic 10

命令的完成(以及其他事情)是通过 bash readline completion处理的。这比通常的“可编程完成”(仅在识别命令时调用,以及您在上面识别的两种特殊情况下调用)的级别略低。

更新: bash-5.0 的新版本(2019 年 1 月)complete -I正好解决了这个问题。

相关的 readline 命令是:

complete (TAB)
       Attempt to perform completion on the text  before  point.   Bash
       attempts completion treating the text as a variable (if the text
       begins with $), username (if the text begins with  ~),  hostname
       (if  the  text begins with @), or command (including aliases and
       functions) in turn.  If none of these produces a match, filename
       completion is attempted.

complete-command (M-!)
       Attempt  completion  on  the text before point, treating it as a
       command name.  Command completion attempts  to  match  the  text
       against   aliases,   reserved   words,  shell  functions,  shell
       builtins, and finally executable filenames, in that order.
Run Code Online (Sandbox Code Playgroud)

与更常见的方式类似complete -F,其中一些可以通过使用bind -x.

function _complete0 () {
    local -a _cmds
    local -A _seen
    local _path=$PATH _ii _xx _cc _cmd _short
    local _aa=( ${READLINE_LINE} )

    if [[ -f ~/.complete.d/"${_aa[0]}" && -x  ~/.complete.d/"${_aa[0]}" ]]; then
        ## user-provided hook
        _cmds=( $( ~/.complete.d/"${_aa[0]}" ) )
    elif [[ -x  ~/.complete.d/DEFAULT ]]; then
        _cmds=( $( ~/.complete.d/DEFAULT ) )
    else 
        ## compgen -c for default "command" complete 
        _cmds=( $(PATH=$_path compgen -o bashdefault -o default -c ${_aa[0]}) )  
    fi

    ## remove duplicates, cache shortest name
    _short="${_cmds[0]}"
    _cc=${#_cmds[*]} # NB removing indexes inside loop
    for (( _ii=0 ; _ii<$_cc ; _ii++ )); do
        _cmd=${_cmds[$_ii]}
        [[ -n "${_seen[$_cmd]}" ]] && unset _cmds[$_ii]
        _seen[$_cmd]+=1
        (( ${#_short} > ${#_cmd} )) && _short="$_cmd"
    done
    _cmds=( "${_cmds[@]}" )  ## recompute contiguous index

    ## find common prefix
    declare -a _prefix=()
    for (( _xx=0; _xx<${#_short}; _xx++ )); do
        _prev=${_cmds[0]}
        for (( _ii=0 ; _ii<${#_cmds[*]} ; _ii++ )); do
            _cmd=${_cmds[$_ii]}
             [[ "${_cmd:$_xx:1}" != "${_prev:$_xx:1}" ]] && break
            _prev=$_cmd
        done
        [[ $_ii -eq ${#_cmds[*]} ]] && _prefix[$_xx]="${_cmd:$_xx:1}"
    done
    printf -v _short "%s" "${_prefix[@]}"  # flatten 

    ## emulate completion list of matches
    if [[ ${#_cmds[*]} -gt 1 ]]; then
        for (( _ii=0 ; _ii<${#_cmds[*]} ; _ii++ )); do
            _cmd=${_cmds[$_ii]}
            [[ -n "${_seen[$_cmds]}" ]] && printf "%-12s " "$_cmd" 
        done | sort | fmt -w $((COLUMNS-8)) | column -tx
        # fill in shortest match (prefix)
        printf -v READLINE_LINE "%s" "$_short"
        READLINE_POINT=${#READLINE_LINE}  
    fi
    ## exactly one match
    if [[ ${#_cmds[*]} -eq 1 ]]; then
        _aa[0]="${_cmds[0]}"
        printf -v READLINE_LINE "%s " "${_aa[@]}"
        READLINE_POINT=${#READLINE_LINE}  
    else
        : # nop
    fi
}

bind -x '"\C-i":_complete0'
Run Code Online (Sandbox Code Playgroud)

这将启用您自己的每个命令或前缀字符串挂钩~/.complete.d/。例如,如果您使用以下命令创建可执行文件~/.complete.d/loc

#!/bin/bash
echo localc
Run Code Online (Sandbox Code Playgroud)

这将(大致)满足您的期望。

上面的函数在一定程度上模拟了正常的 bash 命令完成行为,尽管它是不完美的(尤其是sort | fmt | column显示匹配列表的可疑随身携带)。

然而,一个重要的问题是它只能使用一个函数来替换与主complete函数的绑定(默认情况下使用 TAB 调用)。

这种方法适用于仅用于自定义命令完成的不同键绑定,但在此之后它根本不会实现完整的完成逻辑(例如,命令行中的后面的单词)。这样做需要解析命令行、处理光标位置以及其他在 shell 脚本中可能不应该考虑的棘手事情......