How does `scp` completion work in zsh?

Date: 2024-03-02 | Author: Jason Eveleth

Table of Contents

Have you ever tried to complete a file on a remote server when you were scp-ing?

$ scp user@server.com:/home/user/do|
downloads/    documents/

I have known for awhile that if you've set up ssh-keys, it would work. It'd been in the back of my mind to research, but I've never gotten around to it. That changed today.

My initial findings led me to stack exchange. I'm 9 years late, but I figured I'd share some research I've done into this topic. You can ask zsh to give you the completion function for any command. So, to see for scp, you can use:

$ print $_comps[scp]
_ssh

From this, we learn that we're looking for _ssh. I did some looking into zsh source and I found this file: Completion/Unix/Command/_ssh. After poking around in here, it seems like the relevant parts are here (when it's completing the file part of the command):

file)
      if compset -P 1 '[^./][^/]#:'; then
        _remote_files -- ssh ${(kv)~opt_args[(I)-[FP1246]]/-P/-p} && ret=0
      elif compset -P 1 '*@'; then
        suf=( -S '' )
        compset -S ':*' || suf=( -r: -S: )
        _wanted hosts expl 'remote host name' _ssh_hosts $suf && ret=0
      else
        _alternative \
            'files:: _files' \
            'hosts:remote host name:_ssh_hosts -r: -S:' \
            'users:user:_ssh_users -qS@' && ret=0
      fi
      ;;

Looking at the first if condition, we see that if the argument doesn't start with ./ or /, then we use _remote_files command. Doing some more digging, we can find that there is a corresponding file (Completion/Unix/Type/_remote_files). You can find it on your computer with:

$ echo $functions_source[_remote_files]
/usr/share/zsh/5.9/functions/_remote_files

Here's an excerpt of header comment:

Needs key-based authentication with no passwords or a running ssh-agent to work.

So, we do need to have an ssh keys or some other passwordless way to ssh.

The file is just 104 lines long and pretty readable (if you're looked at zsh completion code before). The relevant line I didn't quite understand:

remfiles=(${(M)${(f)"$(
  _call_program files $cmd $cmd_args $host \
    command ls -d1FL -- "$rempat" 2>/dev/null
)"}%%[^/]#(|/)})

But after asking ChatGPT, the meat of the command is here: $cmd $cmd_args $host command ls -d1FL -- "$rempat" 2>/dev/null Here, $cmd is ssh, so it's running ls -d1FL -- $rempat on the remote machine.

I patched the file to print out the actual command that it runs (unfortunately Apple doesn't like this, so I had to get around SIP, see the last section). Here is the command in all it's glory:

ssh -o BatchMode=yes -a -x jason@machine.example.com command ls -d1FL -- /home/jason/\*

This is the command that zsh is using to find files on remote machines.

Breaking down the command

There's two parts to the command in the previous section. The part that gets executed on the remote machine: command ls -d1FL -- /home/jason/\* and the part that gives options to ssh: ssh -o BatchMode=yes -a -x jason@machine.example.com

Breaking down the remote command, we use ls with

Breaking down the ssh options

Getting around SIP

I first tried to edit /usr/share/zsh/5.9/functions/_remote_files directly. This didn't work obviously because my user isn't root. I tried sudo vim, but that didn't work either. So (and I know this is bad) I tried sudo sh and then vim. This also didn't work. I realized that SIP was what was preventing me.

So, I unloaded the function and loaded my patch:

unfunction _remote_files
autoload -U /tmp/_remote_files
© Jason Eveleth 2023 · Powered by Franklin.jl · Last modified: December 31, 2024 Page Source