Bash – Using Shell Variables for Command Options

bashvariable

In a Bash script, I'm trying to store the options I'm using for rsync in a separate variable. This works fine for simple options (like --recursive), but I'm running into problems with --exclude='.*':

$ find source
source
source/.bar
source/foo

$ rsync -rnv --exclude='.*' source/ dest
sending incremental file list
foo

sent 57 bytes  received 19 bytes  152.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

$ RSYNC_OPTIONS="-rnv --exclude='.*'"

$ rsync $RSYNC_OPTIONS source/ dest
sending incremental file list
.bar
foo

sent 78 bytes  received 22 bytes  200.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

As you can see, passing --exclude='.*' to rsync "manually" works fine (.bar isn't copied), it doesn't work when the options are stored in a variable first.

I'm guessing that this is either related to the quotes or the wildcard (or both), but I haven't been able to figure out what exactly is wrong.

Best Answer

In general, it's a bad idea to demote a list of separate items into a single string, no matter whether it's a list of command line options or a list of pathnames.

Using an array instead:

rsync_options=( -rnv --exclude='.*' )

or

rsync_options=( -r -n -v --exclude='.*' )

and later...

rsync "${rsync_options[@]}" source/ target

This way, the quoting of the individual options is maintained (as long as you double quote the expansion of ${rsync_options[@]}). It also allows you to easily manipulate the individual entries of the array, would you need to do so, before calling rsync.

In any POSIX shell, one may use the list of positional parameters for this:

set -- -rnv --exclude='.*'

rsync "$@" source/ target

Again, double quoting the expansion of $@ is critical here.

Tangentially related:


The issue is that when you put the two sets of option into a string, the single quotes of the --exclude option's value becomes part of that value. Hence,

RSYNC_OPTIONS='-rnv --exclude=.*'

would have worked¹... but it's better (as in safer) to use an array or the positional parameters with individually quoted entries. Doing so would also allow you to use things with spaces in them, if you would need to, and avoids having the shell perform filename generation (globbing) on the options.


¹ provided that $IFS is not modified and that there's no file whose name starts with --exclude=. in the current directory, and that the nullglob or failglob shell options are not set.

Related Question