Take the 2-minute tour ×
Super User is a question and answer site for computer enthusiasts and power users. It's 100% free, no registration required.

I have been struggling for a while with the folllowing problems I have written scripts in which any command run is first put into a string, and then executed. That way, the command is written only once and it can be analysed, displayed, used several times. It works fine most of the time but not in the following detailed cases below. So if any of you can help, I would be more then happy ;) I must admit I have always struggle to understand the way script works when dealing with variables containing quotes and spaces, so if you know of a good tutorial ...

Case 1: mkdir command

dir="/tmp/Mytests/DirWith2 spaces 2"
cmdToRun="mkdir -p ${dir}"
echo "Running [${cmdToRun}]"
${cmdToRun} # <= this does not work, OK it seems normal !

cmdToRun="mkdir -p """${dir}""""
${cmdToRun} # <= this does not work either !

cmdToRun="mkdir -p \"${dir}\""
${cmdToRun} # <= this does not work either !

Case 2: rsync command

rsync_cmd="rsync -avz --stats " 
rsync_destinationBaseDir="/tmp/ToTestRsync"
ListOfDirToSynchronize=( \
"/opt/dir1/subdir1" "/opt/dir1space 1/subdir1" "/opt/dir1space 1/subdir1space 1" )
rsync_host_from="root@SRV1:" 
A loop on the directories in my ListOfDirToSynchronize
{
  SourceDir="${ListOfDirToSynchronize[$i]}"  
  ( # here we start a subshell to run // rsyncsto speed-up the whole process
    cmdToRun="${rsync_cmd} ${rsync_host_from}'${SourceDir}/' ${rsync_destinationBaseDir}${SourceDir}"
    funcLog " | INFO | Directories synchronisation | SubprocessID [${subProcessID}] | running command [${cmdToRun}]"
    ${cmdToRun} >> ${LOG_FILE} 2>&1 #<== this does not work when spaces in the directories
  ) # end of subshell
} # end of the loop
share|improve this question
add comment

2 Answers

Short answer: see BashFAQ #050.

Long answer: usually, the best way to do this is by putting commands in arrays rather than simple variables (eval is not recommended -- it tends to create new and dangerous bugs). Here's case 1 done array-style:

dir="/tmp/Mytests/DirWith2 spaces 2"
cmdToRun=(mkdir -p "${dir}")  # Note double-quotes to preserve spaces within $dir
"${cmdToRun[@]}"  # This is the standard idiom for expanding an array preserving both spaces and word breaks

Case 2:

rsync_cmd=(rsync -avz --stats)
rsync_destinationBaseDir="/tmp/ToTestRsync"
ListOfDirToSynchronize=( \
  "/opt/dir1/subdir1" "/opt/dir1space 1/subdir1" "/opt/dir1space 1/subdir1space 1" )
rsync_host_from="root@SRV1:" 
# A loop on the directories in my ListOfDirToSynchronize
for SourceDir in "${ListOfDirToSynchronize[@]}"; do
  ( # here we start a subshell to run // rsyncsto speed-up the whole process
    cmdToRun=("${rsync_cmd[@]}" "${rsync_host_from}${SourceDir}/" "${rsync_destinationBaseDir}${SourceDir}")
    funcLog " | INFO | Directories synchronisation | SubprocessID [${subProcessID}] | running command [${cmdToRun[*]}]"
    "${cmdToRun[@]}" >> ${LOG_FILE} 2>&1 #<== this does not work when spaces in the directories
  ) # end of subshell
done # end of the loop

Note that in the log entry I used ${cmdToRun[*]} instead of ${cmdToRun[@]}, so it'll separate the array entries with spaces rather than treating them as separate arguments to funcLog. This means the log is ambiguous (you can't tell spaces within filenames from spaces between filenames); if this is a problem, use $(printf " %q" "${cmdToRun[@]}") instead, and it'll add quotes/escapes/etc for spaces within filenames.

share|improve this answer
    
Thank you Gordon, I just read now your comment and I have alreday tested the eval function successfully (see my previous comment). I will test that tomorow but, for me, it will be a better solution as I have some difficulties understanding all about the eval function, whereas with arrays, there is no hiden magic. Yann –  user104688 Nov 9 '11 at 17:12
add comment

The eval command is your friend:

dir="/tmp/Mytests/DirWith2 spaces 2"

cmdToRun="mkdir -p \"${dir}\""
eval "${cmdToRun}"

And:

cmdToRun="${rsync_cmd} ${rsync_host_from}'${SourceDir}/' '${rsync_destinationBaseDir}${SourceDir}'"
funcLog ...
eval "${cmdToRun}" >> "${LOG_FILE}" 2>&1
share|improve this answer
    
Super ! it works fine. Thank you, really. –  user104688 Nov 9 '11 at 14:59
    
I hope that the other part of my problem will be solved as quickly and efficiently .... –  user104688 Nov 9 '11 at 15:01
    
I added a solution for the second part. The idea is the same, but I'm not going to set up a whole rsync setup to test it. ;-) –  Peter Eisentraut Nov 9 '11 at 15:33
    
Don't use eval if at all possible, it causes weird bugs. For example, try the above on a filename with a double-quote in it. Do NOT try it on a file named foo$(rm -R ~)bar or you will be very very unhappy. –  Gordon Davisson Nov 9 '11 at 17:02
    
Hi Peter, Thank you for all. After you told me the first time me about eval for mkdir, I then thought on trying the same solution on rsync. I have found a solution which I have tested which uses the "\ " option # escape all white spaces (if any): " " becomes "\ " but we don't deal here with filepath containing two or more consecutive spaces SourceDir=$(echo "${SourceDir}" | sed -e "s:[ \t][ \t]*:\\\ :g") cmdToRun="${rsync_cmd_base} ${rsync_cmd_options} ${rsync_host_from}'${SourceDir}/' ${rsync_destinationBaseDir}${SourceDir}" eval "${cmdToRun}" >> "${LOG_FILE}" 2>&1 –  user104688 Nov 9 '11 at 17:07
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.