I'd really appreciate help getting zmv to work as expected via ssh in a script.

I'm trying to change filenames on a remote server over which I have full control. Both local and remote machines are running macOS 10.14.6, with zsh as the default shell.

If I open a zsh terminal window, connect via ssh and then type zmv '(xxx_)(*)' 'yyy_$2', it works as expected. But if I put the zmv inside the ssh, as I assume I'll have to do to package it into a script:

ssh me@myserver "cd /Users/me/mypath; autoload zmv; zmv '(xxx_)(*)' 'yyy_$2'"

it fails because zmv says of each file pair: "both map to yyy_". In other words, either it doesn't identify the match groups or the backreferences don't work.

I just about understand that zsh and ssh are two different kinds of shell and have different environments, so for instance I've found out that I need autoload zmv even though it's in .zshrc on the remote machine. I've tried adding noglob zmv (again set in .zshrc), and I've tried escaping any number of things.

I've also tried the -w and -W options for zmv. The -W option is the only one that doesn't error, so for instance I can do:

ssh me@myserver "cd /Users/me/mypath; autoload zmv; zmv -n -W 'xxx_*_*_*.*' '*-*-*.*'"

and it works. But if I omit one of the * in the replace group, zmv says: "error: number of wildcards in each pattern must match", which isn't much help when I'm trying to leave out parts of the original filename.

How should I be doing this?

  • 1
    I think it's just because the outer double quotes don't prevent $2 from being expanded by the local shell (the inner single quotes are not special in this context). So you're passing zmv '(xxx_)(*)' 'yyy_' to the remote shell. Try yyy_\$2. Commented Jun 28, 2020 at 23:47
  • Thank you very much @steeldriver. That works. Sorry for writing such a long question to reveal such a basic mistake. I'll wait a while to see if anyone else has comments (I'd love to get my head round the -w/-W syntax), but I'll be pleased to accept your answer. Commented Jun 29, 2020 at 9:57

1 Answer 1


The issue here is with quoting - and is not specific to either zsh or zmv

In your command

ssh me@myserver "cd /Users/me/mypath; autoload zmv; zmv '(xxx_)(*)' 'yyy_$2'"

the "weak" outer double quotes do not prevent $2 from being expanded as (presumably empty) positional parameter $2 by the local shell - so what the remote shell sees is

cd /Users/me/mypath; autoload zmv; zmv '(xxx_)(*)' 'yyy_'

If (xxx_)(*) matches two files, then zmv tries to rename them both to yyy_ with the resulting error both map to yyy_

The version with wildcard patterns in the replacement (enabled by the -W switch) doesn't suffer the same problem, because wildcards (shell globs) are not expanded even in double quotes.

The simplest way to resolve the issue is to add another layer of quoting so that $2 is passed unmolested to the remote shell:

ssh me@myserver "cd /Users/me/mypath && autoload zmv && zmv '(xxx_)(*)' 'yyy_\$2'"

Here also using && instead of ; as you don't want zmv to be run if cd failed as otherwise you would end up renaming files in me's home directory on myserver instead.

For a more general discussion of the issues around quoting and SSH, including an alternative workaround using a here-document, see for example the related question

  • Thank you very much for that lucid and patient explanation. As someone says in the discussion you refer to: "one level of quoting is stripped by the local shell, then SSH invokes a remote shell which strips a second level of quoting." I'll remember that. I said I'd tried escaping things, but evidently not the right things. I've also started to look at the basics of error trapping… Commented Jun 29, 2020 at 19:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .