更多关于使用 Bash Complete 命令

作者:Mitch Frazier

在上周的视频中,我展示了如何使用 bashcomplete命令进行简单的用例。今天我将向您展示一些使用该命令处理更复杂场景的其他方法。

回顾一下我上周演示的简单用例,以下是其要点,或者观看视频

$ # Create a dummy command:
$ touch ~/bin/myfoo
$ chmod +x ~/bin/myfoo

$ # Create some files:
$ touch a.bar a.foo b.bar b.foo

$ # Use the command and try auto-completion.
$ # Note that all files are displayed:
$ myfoo <TAB><TAB>
a.bar a.foo b.bar b.foo

$ # Now tell bash that we only want foo files.
$ # This command tells bash args to myfoo are completed
$ # by generating a list of files and then excluding
$ # everything # that doesn't match *.foo:
$ complete -f -X '!*.foo' myfoo

$ # Tray again:
$ myfoo <TAB><TAB>
a.foo b.foo

对于需要更多控制完成方式的更复杂情况,您可以告诉 bash 调用一个函数来完成完成工作。这是一个您提供的函数,您可能会从您的.profile文件中获取。然后,函数名称作为-F选项的参数提供complete:

$ complete -F _mycomplete_ myfoo

的基本版本_mycomplete,此时它所做的并不比我们简单的命令行用法多complete以上,将如下所示

function _mycomplete_()
{
    local cmd="${1##*/}"
    local word=${COMP_WORDS[COMP_CWORD]}
    local line=${COMP_LINE}
    local xpat='!*.foo'

    COMPREPLY=($(compgen -f -X "$xpat" -- "${word}"))
}

complete -F _mycomplete_ myfoo

在函数体的前三行中,我们创建了一些有用的局部变量,这里主要是为了展示可用的变量,因为其中大多数在函数中没有使用。第一个,cmd,获取正在执行的命令,如果您的完成函数可以处理多个命令,则可以使用它。第二个,word,获取正在完成的单词,如果您的完成策略根据正在展开的单词而变化,则可以使用它,这也需要它,以便仅返回匹配的值。第三个,line,获取正在完成的整个命令行。第四个变量,xpat,是我们的排除模式,与上面简单示例中使用的模式相同。查看 bash 手册页以获取其他有用的COMP_*变量。

函数中唯一真正的代码是最后一行,它设置了变量COMPREPLY,这是我们对 bash 扩展某些内容的请求的回复。此行使用compgen来生成扩展。compgen命令接受与complete相同的大多数选项,但它生成结果,而不是仅仅存储规则以供将来使用。在这里,我们告诉compgen创建一个文件列表,使用-f。然后我们告诉它排除所有与我们的排除模式匹配的文件,使用-X "$xpat"。最后,我们传入正在完成的单词,以便仅返回与它匹配的项目。

现在,让我们考虑一个稍微复杂的例子。让我们使用我们的 complete 函数来完成多个命令,并且让我们也更改它,以便它根据给定的命令行参数,为myfoo命令以不同的方式展开内容myfoo.

具体来说,让我们假设myfoo可以 foo 文件和 unfoo 文件,所以myfoo -f myfile创建myfile.foomyfoo -u myfile.foo创建myfile。想想 -d 选项对于gzipbunzip2。因此,在这种情况下,如果指定了-f,我们只想显示非 foo 文件,如果指定了-u,则只想显示 foo 文件。

此外,让我们添加一个更进一步的增强功能,让我们的完成函数包含目录名,以便目录名可以用于完成参数,从而允许我们导航到子目录以 foo 和 unfoo 子目录中的文件。我们的新函数将是

function _mycomplete_()
{
    local cmd="${1##*/}"
    local word=${COMP_WORDS[COMP_CWORD]}
    local line=${COMP_LINE}
    local xpat

    # Check to see what command is being executed.
    case "$cmd" in
    myfoo)
        # See if we are fooing or unfooing.
        case "$line" in
        *-f*)
            xpat='*.foo'
            ;;
        *-u*)
            xpat='!*.foo'
            ;;
        *)
            xpat='*.foo'
            ;;
        esac
        ;;
    mybar)
        xpat='!*.bar'
        ;;
    *)
        xpat='!*'
        ;;
    esac

    COMPREPLY=($(compgen -f -X "$xpat" -- "${word}"))
}

complete -d -X '.[^./]*' -F _mycomplete_ myfoo mybar

在这里,我们使用cmd变量来查看我们正在完成哪个命令,并根据它更改我们的模式。当处理myfoo命令时,我们使用line变量来查看命令行是否包含-f-u选项,以确定我们应该排除还是包含 foo 文件。

为了在我们的输出中包含目录,我们只需修改complete命令,通过包含参数-d '-X' '.[^./]*'来安装我们的函数,这会生成目录列表,然后排除./../(当前目录和父目录)。然后,目录列表被添加到调用我们的完成函数返回的结果中。我们还将第二个命令mybar添加到我们的函数处理的命令中。

现在当我们运行它时

$ # Source our completion function:
$ . bcomp2.sh

$ # Make some files:
$ touch a b a.bar a.foo b.bar b.foo

$ # Make a directory for checking directory inclusion:
$ mkdir astuff

$ # See what we get when we want to foo something
$ myfoo -f <TAB><TAB>
a  a.bar  astuff/  b  b.bar  bcomp2.sh  bcomp.sh

$ # See what we get when we want to unfoo something
$ myfoo -u <TAB><TAB>
a.foo  astuff/  b.foo

一旦您理解了所有这些,请查看文件/etc/profile.d/complete.bash以查看 bash 自带的默认完成项。您会在该文件中注意到,我们在这里忽略了许多复杂情况,例如当有人尝试完成诸如${ABC$(ca之类的单词时会发生什么。在这些情况下,完成需要分别返回变量名和命令名,而不是数据文件名。

Mitch Frazier 是 Emerson Electric Co. 的嵌入式系统程序员。自 2000 年代初以来,Mitch 一直是Linux Journal 的贡献者和朋友。

加载 Disqus 评论