Rsync syntax error when run from bash script - bash

I have been working on a backup script that uses rsync to do an incremental backup.
I have tested the following rsync command manually, and it runs and completes a backup without error:
rsync -aAXv --delete --progress --link-dest=/backup/Uyuk/Uyuk-backup-part1/2014-02-24/ /mnt/backup/ /backup/Uyuk/Uyuk-backup-part1/2014-02-25/
however when I run that same command in my backup script it gives me the following error:
rsync: -aAXv --delete --progress --link-dest=/backup/Uyuk/Uyuk-backup-part1/2014-02-24/ /mnt/backup/ /backup/Uyuk/Uyuk-backup-part1/2014-02-25/: unknown option
rsync error: syntax or usage error (code 1) at main.c(1422) [client=3.0.6]
I ran bash -x on my script to figure out exactly what is sent to the console and here is what was printed:
+ rsync '-aAXv --delete --progress --link-dest=/backup/Uyuk/Uyuk-backup-part1/2014-02-24/ /mnt/backup/ /backup/Uyuk/Uyuk-backup-part1/2014-02-25/'
Does anyone see what is wrong? I cant find anything that would cause the syntax error.
EDIT:
Here is the actual code I have in the script, and this is a pretty large script so yes some variables are not defined here, but you get the idea.
mkdir -p "/backup/$HOST/$NAME/$TODAY"
#source directory
SRC="$MNT"
#link directory
LNK="/backup/$HOST/$NAME/$LAST/"
#target directory
TRG="/backup/$HOST/$NAME/$TODAY/"
#rsync options
OPT1="-aAXv --delete --progress --link-dest=$LNK"
#run the rsync command
echo "rsync $OPT1 $SRC $TRG"
rsync "$OPT1 $SRC $TRG" > /var/log/backup/backup.rsync.log 2>&1

You are passing your option list as a single argument, when it needs to be passed as a list of arguments. In general, you should use an array in bash to hold your arguments, in case any of them contain whitespace. Try the following:
mkdir -p "/backup/$HOST/$NAME/$TODAY"
#source directory
SRC="$MNT"
#link directory
LNK="/backup/$HOST/$NAME/$LAST/"
#target directory
TRG="/backup/$HOST/$NAME/$TODAY/"
#rsync options
OPTS=( "-aAXv" "--delete" "--progress" "--link-dest=$LNK" )
#run the rsync command
echo "rsync $OPT1 $SRC $TRG"
rsync "${OPTS[#]}" "$SRC" "$TRG" > /var/log/backup/backup.rsync.log 2>&1
An array expansion ${OPTS[#]}, when quoted, is treated specially as a sequence of arguments, each of which is quoted individually to preserve any whitespace or special characters in the individual elements. If arr=("a b" c d), then echo "${arr[#]}" is the same as
echo "a b" "c" "d"
rather than
echo "a b c d"
This will not work in a shell that doesn't support arrays, but then, arrays were invented because there wasn't a safe way (that is, without using eval) to handle this use case without them.

This:
rsync "$OPT1 $SRC $TRG"
passes all your intended arguments lumped together as one argument, which rsync doesn't know how to deal with.
Try this instead:
rsync ${OPT1} ${SRC} ${TRG}

The approach suggested by #chepner didn't work on my Mac OS X (10.9.4), but eval did.
eval rsync "$OPT1 $SRC $TRG"

Related

Adding arguments for batch script in command line

I'm trying to call arguments for running a script with the rsync command. I've tried various forms and this is the most "simple", however I continue to get an error.
(bash.sh)
#! /bin/bash
root_dir = $1
target_dir = $2
rsync -avh -P --stats $root_dir $target_dir
echo "Files Transferred!"
ErrorMessage
test.sh: line 2: root_dir: command not found
test.sh: line 3: target_dir: command not found
Command Line
sh bash.sh ./path_root_dir ./path_target_dir
Using a variable with a space, without using speech marks or a quote on each side can cause an error to occur, especially when the variable is used and there was a space in the value.
Another way you could try is to use an array, the options variable is where the array is, and then it is called in the rsync command below,
directory="/etc"
backupDirectory="/backup"
incrementalBackup="/incremental"
options=(-a -e 'ssh -p 10022' -b --backup-dir="$incrementalBackup" --delete)
# rsync
rsync "${options[#]}" user#server:"$directory" "$backupDirectory"

Rsync command variable, bash script

RSYNC="rsync -avzhe 'ssh -i /path/to/deploy_keys/id_rsa' --delete "
# Files
$RSYNC deploy#ip:/var/www/path1 /var/www/path1
$RSYNC deploy#ip:/var/www/path2 /var/www/path2
I'd like to introduce this RSYNC Variable to be more compact, but it throws an error:
Unexpected remote arg: deploy#ip:/var/www/path1
If i use only rsync inside the doublequotes, it works fine. For the sake of readability, i'd keep them separate command invocations.
I agree that eval is dangerous. In addition to the array approach #Eugeniu Rosca suggested, you could also use a shell function:
my_rsync() {
rsync -avzhe 'ssh -i /path/to/deploy_keys/id_rsa' --delete "$#"
}
my_rsync deploy#ip:/var/www/path1 /var/www/path1
my_rsync deploy#ip:/var/www/path2 /var/www/path2
BTW, you should read BashFAQ #50: I'm trying to put a command in a variable, but the complex cases always fail!.
If you want to store the command string into a variable and evaluate it later, you can use eval or an alternative safer technique:
#!/bin/bash
# Store the command into an array
RSYNC=(rsync -avzhe 'ssh -i /path/to/deploy_keys/id_rsa' --delete)
# Run the command
"${RSYNC[#]}" deploy#ip:/var/www/path1 /var/www/path1
Why eval should be avoided
After you assign a string to a variable and then submit it to shell again it is tokenized differently. Consider the following script
VAR="/bin/ls 'Test1 Test2'"
$VAR
It will throw two errors:
/bin/ls: cannot access 'Test1
/bin/ls: cannot access Test2'
You guessed it right, the apostrophes are no longer affecting tokenization. Instead they are treated as normal characters when the commandline arguments are fed to /bin/ls
While eval is rightly considered evil, it is by far the easiest way to get a script running. It stitches all the arguments into one line and applies the tokenizing procedure again.
RSYNC="rsync -avzhe 'ssh -i /path/to/deploy_keys/id_rsa' --delete "
# Files
eval $RSYNC deploy#ip:/var/www/path1 /var/www/path1
eval $RSYNC deploy#ip:/var/www/path2 /var/www/path2

Using wildcard in bash loop array

I've got a collection of hundreds of directories ordered in alphabetical order and with differently named files inside. These directories I want to copy over to another location using rsync.
I don't wanna go over all the directories manually, but instead I want to use the --include option of rsync or create a loop in bash to go over the directories.
For far I've tried using the bash script below, but had no success yet.
for dir in {A..Z}; do
echo "$dir";
rsync --progress --include $dir'*' --exclude '*' -rt -e ssh username#192.168.1.123:/source/directory/ ~/target/directory/
done;
Does anyone know what would be the correct way to go over the directories using rsync's --include option?
Update:
The bash script above was more to try out the loop to go over my directories and see what comes out. The command I actually wanted to use was this one:
for dir in /*; do
rsync --progress --include $dir'*' --exclude '*' --bwlimit=2000 -rt -e ssh username#192.168.1.123:/source/directory/ ~/target/directory/
done;
I know bash can do something like {A..Z}, but this doesn't seem to get me the result I want. I already copied half of the alphabet of directories so I was trying {F..Z} as an array.
Update
I've come up with the following script to run from my source directories location.
#!/bin/bash
time=$(date +"%Y-%m-%d %H:%M:%S") # For time indication
dir=/source/directory/[P-Z]* # Array of directories with name starting with "P" to "Z"
printf "[$time] Transferring: /source/directory/\"$dir\"\n"
rsync -trP -e 'ssh -p 123' --bwlimit=2000 $dir username#192.168.1.123:/target/directory
This will transfer all directories from the source directory with names starting with character "P" to "Z" over ssh using port 123.
This works for me in a shell script. I'm sure there are better ways to do this in a single line command, but this one I just came up with to help myself out.
Sounds like you want recursive rsync. I'd go with:
rsync -r / --restOfYourRsyncArgs
That walks over every file/folder/subfolder in / (could be A LOT, consider excludes and/or a different target path) and uploads/downloads. Set excludes for files and folders you don't want sent.
I've come up with the following script to run from my source directories location.
#!/bin/bash
time=$(date +"%Y-%m-%d %H:%M:%S") # For time indication
dir=/source/directory/[P-Z]* # Array of directories with name starting with "P" to "Z"
printf "[$time] Transferring: /source/directory/\"$dir\"\n"
rsync -trP -e 'ssh -p 123' --bwlimit=2000 $dir username#192.168.1.123:/target/directory
This will transfer all directories from the source directory with names starting with character "P" to "Z" over ssh using port 123. This works for me in a shell script. I'm sure there are better ways to do this in a single line command, but this one I just came up with to help myself out.

How to properly pass run-time determined command line switches that include *'s in bash?

I am writing a simple script that rsync's a remote site to my local computer, and dynamically generates --exclude=dir flags depending on what option is specified on the command line.
#!/bin/bash -x
source="someone#somewhere.org:~/public_html/live/"
destination="wordpress/"
exclude_flags='--exclude=cache/* '
if [ "$1" == "skeleton" ] ; then
exclude_flags+='--exclude=image-files/* '
fi
rsync --archive --compress --delete $exclude_flags -e ssh $source $destination
I'm running into trouble when I try to interpolate the $exclude_flags variable on the last line. Since the variable has spaces in it, bash is automatically inserting single quotes before and after the interpolation. Here is the command which bash tried to execute (the relevant output of /bin/bash +x):
+ /usr/bin/rsync --archive --compress --delete '--exclude=cache/*' '--exclude=image-files/*' -e /usr/bin/ssh someone#somewhere.org:~/public_html/live/ wordpress/
As you can see, bash has inserted a bunch of single quotes around the individual tokens of $exclude_flags, which is causing rsync to choke.
I have tried:
What I have listed above.
Putting it in double quotes ... "$exclude_flags" .... This almost fixes the problem, but not quite. The single quotes only appear around the full content of $exclude_flags, rather than around each token.
Making $exclude_flags an array, and then interpolating it using ${exclude_flags[#]}. This gives the same output as #2.
Wrapping the whole rsync line in back-tick quotes. This gives the same output as #1.
Any ideas? This seems like a really simple and common problem in bash, so I'm sure that I'm doing something wrong, but google didn't help at all.
Thank you.
The proper way to store multiple command-line options in a variable in bash is to use an array:
source="someone#somewhere.org:~/public_html/live/"
destination="wordpress/"
options=( '--exclude=cache/*' )
if [[ "$1" == "skeleton" ]] ; then
options+=( '--exclude=image-files/*' )
fi
rsync --archive --compress --delete "${exclude_flags[#]}" -e ssh "$source" "$destination"

bash: rsync with options as variable

I am writing bash script, which in some part will rsync files over ssh. Unfortunately I am facing problem with keeping rsync options as variable. Please take a look below:
# variables
directory="/etc"
backupDirectory="/backup"
incrementalBackup="/incremental"
options="-a -e 'ssh -p 10022' -b --backup-dir=$incrementalBackup --delete"
# rsync
rsync $options user#server:$directory $backupDirectory
Unfortunately above script fails with rsync error:
Unexpected remote arg: user#server:/etc
rsync error: syntax or usage error (code 1) at main.c(1201) [sender=3.0.6]
What I saw during script debugging is the fact, that ssh options ('ssh -p 10022') are treated as rsync options.
The question is how to pass correctly those additional ssh settings into rsync?
Thanks in advance for a tip.
Use an array; it's why they were added to bash:
# variables
directory="/etc"
backupDirectory="/backup"
incrementalBackup="/incremental"
options=(-a -e 'ssh -p 10022' -b --backup-dir="$incrementalBackup" --delete)
# rsync
rsync "${options[#]}" user#server:"$directory" "$backupDirectory"
eval is not a safe option to use; it isn't limited to just evaluating the quotations you intend it to, but will evaluate any code. It might work for your current situation, but changes to the value of options might bring unforeseen consequences, and it's generally a bad idea to get into the habit of using eval when it isn't necessary.
Use eval. Try:
eval rsync $options user#server:$directory $backupDirectory

Resources