zsh completion for ssh confuses hostnames with local files

Second answer tries to explain that you need to do two things:

1_ make sure your general matching rules are not case-insensitive (matcher-list) - from the updated question it's not,

2_ change Unix/(Type/)_hosts (the actual location might vary, but not the Unix/_ssh - this one handles ~/.ssh/config hosts, see below) last 2 lines to:

_wanted hosts expl host \
   compadd -M 'm:{a-z}={A-Z} r:|.=* r:|=*' -a "$@" - _hosts

All of this was already summarized in my answer, so simply try doing this without reading all the rationale before. Also, since your global config is not case-insensitive, the @zeppelin's answer should also work, although it doesn't use $fpath and removes also small->CAPS matching of the hosts.

I did test this with your settings from the update and it works as expected.

Update: remember that zsh keeps it's functions loaded, so after modifying the _hosts you need to reload it either by logging in fresh, or:

unfunction _hosts
autoload -Uz _hosts

Also remember that zsh can have the scripts 'compiled' in zwc form (zcompile [file]) and if such file exists and is newer than the source it would be used instead.

Ad. update 2:

Handling the ~/.ssh/config defined hosts is actually pretty much the same as for _hosts - depending on your zsh version in either Unix/(Command/)_ssh or Unix/(Type/)_ssh_hosts change the

compadd -M 'm:{a-zA-Z}={A-Za-z} r:|.=* r:|=*' "$@" $config_hosts

line to

compadd -M 'm:{a-z}={A-Z} r:|.=* r:|=*' "$@" $config_hosts


I believe you are editing a wrong line.

AFAIK config_hosts in Unix/_ssh refers to the host entries in your ~./ssh/config, not /etc/hosts.

The completion rules for /etc/hosts are defined a bit earlier, in the following block:

# If users-hosts matches, we shouldn't complete anything else.
if [[ "$IPREFIX" == *@ ]]; then
  _combination -s '[:@]' my-accounts users-hosts "users=${IPREFIX/@}" hosts "$@" && return
else
  _combination -s '[:@]' my-accounts users-hosts \
    ${opt_args[-l]:+"users=${opt_args[-l]:q}"} hosts "$@" && return
fi

but this in turn just reuses the hosts style defined in Unix/_hosts

So if you edit the compadd definition at the end of the Unix/_hosts file like this:

#_wanted hosts expl host \
#    compadd -M 'm:{a-zA-Z}={A-Za-z} r:|.=* r:|=*' -a "$@" - _hosts
_wanted hosts expl host \
     compadd -a "$@" - _hosts

you should get the behavior you want.

P.S.

Please note that editing a system-wide completion files is not generally a very good practice, so you may want to just redefine hosts in your local ZSH config instead, e.g. by adding a function like that to your ~./zsh:

_hosts() { compadd $(getent hosts | tr -s ' ' '\t' | cut -f2) }

There are two aspects of this question.

  1. the global zsh completion with case-insensitiveness. It's very popular, but IMHO pointless, to do it two-way. Since I don't type in caps usually (or by mistake), let's treat all the CAPS as intentional:

zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

mkdir DOC drone
ls D[tab] => DOC
ls d[tab] => drone DOC
ls Dr[tab] => [empty]

The problem with matcher-list is that this is called very early and cannot be replaced on per-command/argument basis.

And since matcher-list repeated values are being concatenated (quote from the documentation: "This option may be given more than once. In this case all match-specs given are concatenated with spaces between them to form the specification string to use"), you cannot override this later, e.g.

compdef '_hosts -M m:' foo

won't revoke the previous definition from _hosts file. So if you have {a-zA-Z}={A-Za-z} in your global configuration, you won't be able to clear this, even with matcher, e.g.:

zstyle ':completion:*:scp:*:*' matcher "m:{A-Z}={A-Z}"

won't help. Sorry, you need to start resolving this issue at global level. The trade-off solution could be to have two-pass global:

zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' 'm:{A-Z}={a-z}'

ls d[tab] => DOC drone
ls D[tab] => DOC
ls Dr[tab] => drone
  1. Second part of the problem is function-defined matcher-list. As stated above, this is not clearable, so you need to cope with this like already covered in @zeppelin answer, by changing zsh-provided file:

_hosts:

[....]
_wanted hosts expl host \
    compadd -M 'm:{a-z}={A-Z} r:|.=* r:|=*' -a "$@" - _hosts

Now, and with system-wide matcher-list 'm:{a-z}={A-Z}':

scp d[tab] => /directory/ DOC drone /remote host name/ docker /user/ daemon
scp D[tab] => DOC

The proposed solution with altering original completion files is not the right way to do, just like @zeppelin noted ...but creating own host-fetching function is also flawned. Proper solution is unfortunately up to zsh developers, who might either add a matcher-clearing option, define some new style or simply fix the completion function to not ignore case on it's own, just follow the global settings.

Yet even with this issue there is nice and clean solution - instead of modifying (probably some distro package provided) /usr/share/zsh/... one can put the fixed copy in /etc/zsh/functions and simply tell zsh to use this location: fpath=(/etc/zsh/functions $fpath). This way you avoid overwriting the file after system update.

Summarizing

  1. change your global matcher-list:

zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' 'm:{A-Z}={a-z}'

  1. put modified _hosts file in some /etc/zsh or $HOME location:

fpath=(/etc/zsh/functions $fpath)

/etc/zsh/functions/_hosts:

[....]
_wanted hosts expl host \
    compadd -M 'm:{a-z}={A-Z} r:|.=* r:|=*' -a "$@" - _hosts