11

I have a rsync command with following parameters:

rsync -avz --{partial,stats,delete,exclude=".*"}

I want to put that parameters inside a variable to reuse it after in the script. Something like this:

#!/bin/bash
VAR=rsync -avz --{partial,stats,delete,exclude=".*"}
$VAR /dir1 /dir2

I've tried with quotes, single quotes, brackets, without any success.

Inian
  • 12,807
Kellyson
  • 135
  • Similar question on SO: https://stackoverflow.com/questions/13365553/setting-an-argument-with-bash – Barmar Feb 28 '18 at 21:08
  • Having been down this route before: Make sure your final resulting command string doesn't have any empty strings in it. If it does, rsync may use them as parameters and since they're invisible, it's pretty hard to debug. I had an empty first parameter and rsync interpreted it as including the current directory in the sources of things to copy. – Joe Mar 03 '18 at 07:41

3 Answers3

13

Putting a complex command in a variable is a never a recommended approach. See BashFAQ/050 - I'm trying to put a command in a variable, but the complex cases always fail!

Your requirement becomes really simple, if you just decide to use a function instead of a variable and pass arguments to it.

Something like

rsync_custom() {
    [ "$#" -eq 0 ] && { printf 'no arguments supplied' >&2; exit 1 ; }
    rsync -avz --{partial,stats,delete,exclude=".*"} "$@"
}

and now pass the required arguments to it as

rsync_custom /dir1 /dir2

The function definition is quite simple in a way, we first check the input argument count using the variable $# which shouldn't be zero. We throw a error message saying that no arguments are supplied. If there are valid arguments, then "$@" represents the actual arguments supplied to the function.

If this is a function you would be using pretty frequently i.e. in scripts/command-line also, add it to the shell startup-files, .bashrc, .bash_profile for instance.

Or as noted, it may be worthwhile to expand the brace expansion to separate args for a better readability as

rsync_custom() {
    [ "$#" -eq 0 ] && { printf 'no arguments supplied' >&2; exit 1 ; }
    rsync -avz --partial --stats --delete --exclude=".*" "$@"
}
Inian
  • 12,807
5
VAR=rsync -avz --{partial,stats,delete,exclude=".*"}

This tries to run the command -avz with arguments --partial, --stats etc.. and with VAR set to rsync in the environment.

VAR='rsync -avz --{partial,stats,delete,exclude=".*"}'

The quoted form doesn't work because braces aren't expanded in quotes, and not inside assigments, and neither are they expanded after a variable is expanded.

If you need to store command line arguments in a variable, use an array:

args=(rsync -avz --{partial,stats,delete,exclude=".*"})

Now "${args[@]}" will expand to rsync, -avz, --partial, etc. as distinct words.

Arrays also allow you to append options to the list, conditionally if need be, so you can e.g.:

args=(this that)
if something ; then
    args+=(another_arg)
fi
"$cmd" "${args[@]}"

See also:

ilkkachu
  • 138,973
1

You can at least save the options partially in a variable:

opts=$(echo --{ignore-case,word-regexp,count,exclude="sys*.*"})

Testing is important, because masking can be difficult:

echo $opts
--ignore-case --word-regexp --count --exclude="sys*.*"

grep $opts bytes *.log

Since there are multiple alternatives, like using the history, using an alias, using a function, there is no obvious use-case I can think of. There is seldom a complex option sharing between different programs, so for an ad-hoc solution for the interactive shell, aliasing seems a better way:

alias cgrep='grep --ignore-case --word-regexp --count --exclude="sys*"'
cgrep bytes *.log

Your sample

VAR=rsync -avz --{partial,stats,delete,exclude=".*"}

can't work, because the assignment is endet at the first blank. You have to mask the blanks:

VAR='rsync -avz --{partial,stats,delete,exclude=".*"}'

a pretty dangerous thing for testing, with that --delete option, isn't it? Since options can contain again "," and single quotes, masking can get difficult very soon. I would go for an alias or rely on the history.

An alias can be stored in the ~/.bashrc file for continuous use over multiple sessions. Functions can be stored in the bashrc too, but you only need them, if you want to handle parameters, passed into the function to be evaluated therein.

user unknown
  • 10,482
  • 1
    That first one would put literal quotes in opts, which won't work, the quotes don't work as quotes when they come out of the variable expansion. Other than that, putting the arguments in a string and using the unquoted $opts can work, provided none of the arguments themselves need to contain white space, and there are no globs which match existing files, or any globs at all if something like Bash's nullglob or failglob are in effect. – ilkkachu Aug 11 '21 at 09:34
  • Thanks, corrected the quoting. – user unknown Aug 13 '21 at 16:16