Why doesn’t zsh like a file glob like *17*

scpwildcardszsh

I am a relatively new zsh user, and love it 99% of the time. When I try to copy a bunch of files, eg:

[I] ➜ ls *17*
core.txt.17   info.17       vmcore.17.zst

but when I scp *17* xxx:xxx/xxx, I get:

[I] ➜ scp *17* freefall.freebsd.org:
*17*: No such file or directory

ler in /var/crash  at borg
[I] ➜

Why?

If I run type scp, I get:

scp is an alias for noglob scp
scp is /usr/bin/scp

Best Answer

As you've discovered, zsh has a way to disable wildcard expansion on certain commands. If you put noglob before a command, zsh doesn't do wildcard expansion on the words of the command. Since typing noglob is often as much effort as quoting wildcard characters in arguments, the most common use is to automatically cause certain commands to be “noglobbed” by making the command name an alias to the same command name with the noglob prefix. For example, after

alias scp='noglob scp'

you won't get any wildcard expansion on the scp command line, but other than that the scp command will run normally.

You can bypass the alias with

\scp *17* freefall.freebsd.org: 

or

=scp *17* freefall.freebsd.org: 

\scp bypasses the alias and looks for a function, builtin or external command by that name. =scp only looks for an external command.

Preventing wildcard expansion for arguments of scp lets you write scp remote:*17* without quotes, but it comes with the major downside of not letting you write scp *17* remote:. It's possible to take this further and cause arguments of scp to have wildcards expanded only if they don't look like remote file names. Here's a function that analyzes the arguments of scp and only expands wildcards in arguments that don't look like options or remote file names.

function glob_non_remote {
  local a x
  a=($1); shift
  for x; do
    # Do wildcard expansion manually, but only for arguments that don't look
    # like options or remote file names.
    case $x in
      (*:*|-*|) a+=("$x");;
      *) a+=($~x);;
    esac
  done
  "${(@)a}"
}
alias scp='noglob glob_non_remote scp'
Related Question