Way to programatically access and save a list of completion candidates in Zsh


In Zsh by default the tab key is bound to expand-or-complete. I would like to programatically access the list of completion candidates that would have been produced by pressed tab, so that I can write my own function and filter the list on my own. I understand that there is a "completion framework" that comes with Zsh but I would like to do it myself.

There is also the list-choices function/widget which produces the same output as expand-or-complete but doesn't offer the tab cycling functionality.

I've done a reasonably extensive search on Google and also poked through the Zsh source but came up dry. Any help would be appreciated.

Best Answer

  • Indirectly thanks to JakeGould I stumbled upon one solution: zsh-capture-completion. Actually there are two other nearly identical questions on the Unix Stack Exchange sites, both with the answer I've given here.

    Script source code for zsh-capture-completion can be found here:

    zmodload zsh/zpty || { echo 'error: missing module zsh/zpty' >&2; exit 1 }
    # spawn shell
    zpty z zsh -f -i
    # line buffer for pty output
    local line
    setopt rcquotes
    () {
        zpty -w z source $1
        repeat 4; do
            zpty -r z line
            [[ $line == ok* ]] && return
        echo 'error initializing.' >&2
        exit 2
    } =( <<< '
    # no prompt!
    # load completion system
    autoload compinit
    compinit -d ~/.zcompdump_capture
    # never run a command
    bindkey ''^M'' undefined
    bindkey ''^J'' undefined
    bindkey ''^I'' complete-word
    # send a line with null-byte at the end before and after completions are output
    null-line () {
        echo -E - $''\0''
    compprefuncs=( null-line )
    comppostfuncs=( null-line exit )
    # never group stuff!
    zstyle '':completion:*'' list-grouped false
    # don''t insert tab when attempting completion on empty line
    zstyle '':completion:*'' insert-tab false
    # no list separator, this saves some stripping later on
    zstyle '':completion:*'' list-separator ''''
    # we use zparseopts
    zmodload zsh/zutil
    # override compadd (this our hook)
    compadd () {
        # check if any of -O, -A or -D are given
        if [[ ${@[1,(i)(-|--)]} == *-(O|A|D)\ * ]]; then
            # if that is the case, just delegate and leave
            builtin compadd "$@"
            return $?
        # ok, this concerns us!
        # echo -E - got this: "$@"
        # be careful with namespacing here, we don''t want to mess with stuff that
        # should be passed to compadd!
        typeset -a __hits __dscr __tmp
        # do we have a description parameter?
        # note we don''t use zparseopts here because of combined option parameters
        # with arguments like -default- confuse it.
        if (( $@[(I)-d] )); then # kind of a hack, $+@[(r)-d] doesn''t work because of line noise overload
            # next param after -d
            # description can be given as an array parameter name, or inline () array
            if [[ $__tmp == \(* ]]; then
                eval "__dscr=$__tmp"
                __dscr=( "${(@P)__tmp}" )
        # capture completions by injecting -A parameter into the compadd call.
        # this takes care of matching for us.
        builtin compadd -A __hits -D __dscr "$@"
        setopt localoptions norcexpandparam extendedglob
        # extract prefixes and suffixes from compadd call. we can''t do zsh''s cool
        # -r remove-func magic, but it''s better than nothing.
        typeset -A apre hpre hsuf asuf
        zparseopts -E P:=apre p:=hpre S:=asuf s:=hsuf
        # append / to directories? we are only emulating -f in a half-assed way
        # here, but it''s better than nothing.
        integer dirsuf=0
        # don''t be fooled by -default- >.>
        if [[ -z $hsuf && "${${@//-default-/}% -# *}" == *-[[:alnum:]]#f* ]]; then
        # just drop
        [[ -n $__hits ]] || return
        # this is the point where we have all matches in $__hits and all
        # descriptions in $__dscr!
        # display all matches
        local dsuf dscr
        for i in {1..$#__hits}; do
            # add a dir suffix?
            (( dirsuf )) && [[ -d $__hits[$i] ]] && dsuf=/ || dsuf=
            # description to be displayed afterwards
            (( $#__dscr >= $i )) && dscr=" -- ${${__dscr[$i]}##$__hits[$i] #}" || dscr=
            echo -E - $IPREFIX$apre$hpre$__hits[$i]$dsuf$hsuf$asuf$dscr
    # signal success!
    echo ok')
    zpty -w z "$*"$'\t'
    integer tog=0
    # read from the pty, and parse linewise
    while zpty -r z; do :; done | while IFS= read -r line; do
        if [[ $line == *$'\0\r' ]]; then
            (( tog++ )) && return 0 || continue
        # display between toggles
        (( tog )) && echo -E - $line
    return 2

    Here is an example of script usage:

    ══► % cd ~/.zsh_plugins
    ══► % zsh ./zsh-capture-completion/capture.zsh 'cd '

    Note the space character in the command above. With the space the script provides the list of folders which you can cd into from the current directory. Without it the script would provide all the completions for an commands beginning with cd.

    I should also note that even the author of the provided script/plugin considers his solution "hacky". If anyone knows of shorter or more straight forward solution I would be delighted to accept it as the answer.