TransWikia.com

zsh completion for ssh confuses hostnames with local files

Unix & Linux Asked on October 31, 2021

suppose I have following entry in my /etc/hosts

192.168.1.10      server1.mydomain.com

and I have a directory SERVER-FILES in current dir. I want to scp the directory SERVER-FILES somewhere. I type SE and use autocompletion to complete the directory name:

$ scp -rp SE<TAB>

This completion should be totally unambiguous. But zsh autocomplletion tries to be too smart, and treats hostnames case-insensitive, and thus attempts to match SE to hostnames:

$ scp -rp SE<TAB>
SERVER-FILES/
server1.mydomain.com

How can I disable this annoying feature, where zsh is trying to match hostnames case-insensitive, and therefore completes SE<TAB> to server1.mydomain.com` ?

UPDATE:

Based on suggestions from @zeppelin, I have changed the following line in the ssh completion file Unix/_ssh:

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

but that did not help. It has absolutely no effect.

And I don’t understand the answer from @Tomasz Pala. My zsh completion is not case-insensitive.

Please sopmebody just tell me what I need to change in /usr/share/zsh/functions/Completion/Unix/_foo to change this behaviour.

UPDATE 2

I have finally narrowed the problem down, and found out why the solution from @Tomasz Pala did work for him, but not for me:

When I change the Unix/_hosts file on a newly setup machine/user account, the solution works.

scp -r SE<TAB>

The above command ignores server1.mydomain.com in /etc/hosts, and only offers local directory SERVER-FILES for completion.

But this does not work for me on my existing user account, because I have
server.mydomain.com in my ~/.ssh/config. When I remove the entry, then everything works as desired.

But how can I make this hack work even with my current ~/.ssh/config ?

4 Answers

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

Answered by Tomasz Pala on October 31, 2021

You do not need to change any completion functions to get what you want. Just add this to your ~/.zshrc file:

# $PREFIX is the part of the current word that's to the left of the cursor.
# $SUFFIX is the part of the current word that's to the right of the cursor.
# Let's ignore all host completions that don't explicitly match what we've typed, 
# but allow for additional characters at the cursor position and at the end of what 
# we've typed. This makes the matching case sensitive.
# `(b)` escapes characters that are significant to globbing.
zstyle -e ':completion:*:hosts' ignored-patterns 'reply=(
  "^(${(b)PREFIX}*${(b)SUFFIX}*)"
)'

# But if that would lead to no results, then offer the ignored completions anyway.
zstyle ':completion:*' completer _expand _complete _ignored

I've tested this with the config and test cases you posted and it works.

Documentation:

Answered by Marlon Richert on October 31, 2021

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

Answered by Tomasz Pala on October 31, 2021

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) }

Answered by zeppelin on October 31, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP