Is there a way to programatically access and save a list of completion candidates in Zsh?

5

1

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.

John Lunzer

Posted 2018-09-10T15:45:16.730

Reputation: 91

1

Interesting question. Have you looked at this Bash implementation of the same concept?

– JakeGould – 2018-09-10T16:52:24.580

Answers

4

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:

#!/bin/zsh

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
    done
    echo 'error initializing.' >&2
    exit 2
} =( <<< '
# no prompt!
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 $?
    fi
    # 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
        __tmp=${@[$[${@[(i)-d]}+1]]}
        # description can be given as an array parameter name, or inline () array
        if [[ $__tmp == \(* ]]; then
            eval "__dscr=$__tmp"
        else
            __dscr=( "${(@P)__tmp}" )
        fi
    fi
    # 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
        dirsuf=1
    fi
    # 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
    done
}
# 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
    fi
    # display between toggles
    (( tog )) && echo -E - $line
done

return 2

Here is an example of script usage:

══► % cd ~/.zsh_plugins
══► % zsh ./zsh-capture-completion/capture.zsh 'cd '
zaw/
zsh-capture-completion/
zsh-syntax-highlighting/
zsh-vimode-visual/

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.

John Lunzer

Posted 2018-09-10T15:45:16.730

Reputation: 91

1Great work! Added the source code for the script itself because—at the end of the day—of the code is short enough, it’s always better to post it in an answer since links (and they content) can often disappear. – JakeGould – 2018-09-10T17:31:06.903

Thanks, I was sort of on the fence about adding it. I think it's just on the precipice of being "short enough" or "too long". Given your experience I'll defer to your judgement. – John Lunzer – 2018-09-10T17:34:10.407

No problem. Note how source code is placed in a scrolling element. So length isn’t a complete concern unless it’s really long and out of control. – JakeGould – 2018-09-10T17:35:01.203

1Also, there is potentially offensive/insensitive language in the source code. – John Lunzer – 2018-09-10T17:38:02.593

Yikes! At least it was just a comment and I removed it. – JakeGould – 2018-09-10T17:40:08.137