Search code examples
shellsshubuntu-14.04rsync

Shell: rsync parsing spaces incorrectly in file name/path


I'm trying to pull a list of files over ssh with rsync, but I can't get it to work with filenames that have spaces on it! One example file is this:

/home/pi/Transmission_Downloads/FUNDAMENTOS_JAVA_E_ORIENTAÇÃO_A_OBJETOS/2. Fundamentos da linguagem/estruturas-de-controle-if-else-if-e-else-v1.mp4

and I'm trying to transfer it using this shell code.

cat $file_name | while read LINE
do
    echo $LINE
    rsync -aP "$user@$server:$LINE" $local_folder
done

and the error I'm getting is this:

receiving incremental file list
rsync: link_stat "/home/pi/Transmission_Downloads/FUNDAMENTOS_JAVA_E_ORIENTAÇÃO_A_OBJETOS/2." failed: No such file or directory (2)
rsync: link_stat "/home/pi/Fundamentos" failed: No such file or directory (2)
rsync: link_stat "/home/pi/da" failed: No such file or directory (2)
rsync: change_dir "/home/pi//linguagem" failed: No such file or directory (2)
rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1655) [Receiver=3.1.0]

I don't get it why does it print OK on the screen, but parses the file name/path incorrectly! I know spaces are actually backslash with spaces, but don't know how to solve this. Sed (find/replace) didn't help either, and I also tried this code without success

while IFS='' read -r line || [[ -n "$line" ]]; do
    echo "Text read from file: $line"
    rsync -aP "$user@$server:$line" $local_folder
done < $file_name

What should I do to fix this, and why this is happening?

I read the list of files from a .txt file (each file and path on one line), and I'm using ubuntu 14.04. Thanks!


Solution

  • The shell is correctly passing the filename to rsync, but rsync interprets spaces as separating multiple paths on the same server. So in addition to double-quoting the variable expansion to make sure rsync sees the string as a single argument, you also need to quote the spaces within the filename.

    If your filenames don't have apostrophes in them, you can do that with single quotes inside the double quotes:

    rsync -aP "$user@$server:'$LINE'" "$local_folder"
    

    If your filenames might have apostrophes in them, then you need to quote those (whether or not the filenames also have spaces). You can use bash's built-in parameter substitution to do that (as long as you're on bash 4; older versions, such as the /bin/bash that ships on OS X, have issues with backslashes and apostrophes in such expressions). Here's what it looks like:

    rsync -aP "$user@$server:'${LINE//\'/\'\\\'\'}'" "$local_folder"
    

    Ugly, I know, but effective. Explanation follows after the other options.

    If you're using an older bash or a different shell, you can use sed instead:

    rsync -aP "$user@$server:'$(sed "s/'/'\\\\''/g" <<<"$LINE")'" "$local_folder"
    

    ... or if your shell also doesn't support <<< here-strings:

    rsync -aP "$user@$server:'$(echo "$LINE" | sed "s/'/'\\\\''/g")'" "$local_folder"
    

    Explanation: we want to replace all apostrophes with.. something that becomes a literal apostrophe in the middle of a single-quoted string. Since there's no way to escape anything inside single quotes, we have to first close the quotes, then add a literal apostrophe, and then re-open the quotes for the rest of the string. Effectively, that means we want to replace all occurrences of an apostrophe (') with the sequence (apostrophe, backslash, apostrophe, apostrophe): '\''. We can do that with either bash parameter expansion or sed.

    In bash, ${varname/old/new} expands to the value of the variable $varname with the first occurrence of the string old replaced by the string new. Doubling the first slash ( ${varname//old/new} ) replaces all occurrences instead of just the first one. That's what we want here. But since both apostrophe and backslash are special to the shell, we have to put a(nother) backslash in front of every one of those characters in both expressions. That turns our old value into \', and our new one into \'\\\'\'.

    The sed version is a little simpler, since apostrophes aren't special. Backslashes still are, so we have to put a \\ in the string to get a \ back. Since we want apostrophes in the string, it's easier to use a double-quoted string instead of a single-quoted one, but that means we need to double all the backslashes again to make sure the shell passes them on to sed unmolested. That's why the shell command has \\\\: that gets handed to sed as \\, which it outputs as \.