Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does zsh expand globs for me in a bash script using GNU parallel?

In a bash script, I have a command using rsync:

#!/usr/bin/bash -e
...
parallel rsync --exclude '*to?be?deleted*' ... 
    --files-from some_file /auto $instance_ip:/somewhere_else/

According to rsync's documentation, their --exclude field has a different style of pattern matching.

When I run this in a bash terminal, it works fine.

However, running this on zsh gives me an error because zsh tries to expand this literal string I'm trying to pass in:

zsh:1: no matches found: *to?be?deleted*

This should not happen. Why is zsh even expanding my globs in my bash script in the first place? Is there some settings on my zsh that I can set to make the two behave the same way? I don't want to develop in zsh and deploy to an environment with bash and have to behave in different ways.

I am using oh-my-zsh's plugins:

plugins=(
  git
  colored-man-pages
  zsh-autosuggestions
  zsh-syntax-highlighting
)

Specifically, with this set of commands it fails:

#!/usr/bin/bash -e
find . -name '*filelist' | parallel -j10 rsync --exclude "*to?be?deleted*" testing somewhere_else:/some/where/else

But with rsync's command by itself, it doesn't break.

like image 253
OneRaynyDay Avatar asked Apr 09 '26 02:04

OneRaynyDay


2 Answers

The problem is the GNU Parallel utility. Even though it looks like you're passing it a program to run with arguments, what it's actually doing is to concatenate the arguments and pass them to a shell.

Furthermore Parallel either runs the same shell that you ran parallel from, or chooses the shell based on the SHELL environment variable (which is questionable since this environment variable is also used by terminal emulators to decide which interactive shell to run). Either way, this is why it's picking zsh rather than sh. You'd have the same problem with a shell that's compatible with sh (bash, dash, ksh, …), but more rarely: sh leaves patterns alone if they don't match anything, so with sh your script would work as long as there was no file matching *to?be?deleted* in the current directory.

The solution is given in the manual, but it's a bit hard to find: pass the -q option. The manual has a very long chapter on quoting which you can ignore 99% of the time: just pass -q unless you meant to run a shell script and not a command. Furthermore, you should use the full path to the command, otherwise parallel might invoke a shell builtin or even a function (if your shell is bash). Also, set SHELL to /bin/sh, because even with -q, Parallel runs a shell, and assumes that it's compatible with sh (I think zsh is sufficiently compatible, but I'm not completely sure.). See also a similar question on Unix Stack Exchange

SHELL=/bin/sh parallel -q -j10 "$(command -v rsync)" --exclude "*to?be?deleted*" testing somewhere_else:/some/where/else

(Yes, the manual discourages you from using -q, but it's wrong. I've argued with the author about this before.)

like image 67
Gilles 'SO- stop being evil' Avatar answered Apr 10 '26 22:04

Gilles 'SO- stop being evil'


parallel is starting an instance of your login shell using a string consisting of the arguments it is passed. Your bash script strips the quotes before passing the argument, so parallel is executing the equivalent of

zsh -c "rsync --exclude *to?be?deleted* testing somewhere_else:/some/where/else"

in which the pattern is not quoted. To prevent this, pass a single string as an argument to parallel:

... | parallel -j10 'rsync --exclude "*to?be?deleted*" testing somewhere_else:/some/where/else'
like image 36
chepner Avatar answered Apr 10 '26 21:04

chepner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!