7

I've hijacked a pretty neat backup script from the internet but somewhere along the lines there is something like this going on

DIRS="/home/ /var/www/html /etc"

tar -czf /backup/file.tar.gz "${DIRS}"

Which happens to work fine on my machine but on my server it appears to think it's a single path and tells me it does not exist:

/bin/tar: /home/ /var/www/html /etc: Cannot stat: No such file or directory 
/bin/tar: Exiting with failure status due to previous errors

tar version locally is 1.29 while server is 1.28

What's the proper way to supply the directories to tar separately from that variable?

2 Answers2

11

As long as this is a bash script (or even most versions of sh) you should use an array to pass arguments rather than a variable:

DIRS=('/home/' '/var/www/html' '/etc')

tar -czf /backup/file.tar.gz "${DIRS[@]}"

This can be written as follows if you prefer (usually easier to read if the array gets large):

DIRS=(
    '/home/' 
    '/var/www/html' 
    '/etc'
)

In a shell that does not support arrays you will need to unquote your variable to allow word splitting (Not recommended if it can be avoided):

DIRS="/home/ /var/www/html /etc"

tar -czf /backup/file.tar.gz $DIRS


When you quote the variable to pass these arguments it's essentially the same as:

tar -czf /backup/file.tar.gz "/home/ /var/www/html /etc"

However when you pass them through a quoted array it will be more like:

tar -czf /backup/file.tar.gz "/home/" "/var/www/html" "/etc"

Passing them through an unquoted array or variable will perform somthing similar to:

tar -czf /backup/file.tar.gz /home/ /var/www/html /etc

Which in this example should not be an issue but leaves it open to additional word splitting and other types of expansion that may be undesirable or potentially harmful.

jesse_b
  • 37,005
0

I agree with jesse_b. Use an array if possible.

dirs="/home/ /var/www/html /etc"
dirs=($dirs)
tar -czf /backup/file.tar.gz "${dirs[@]}"

I just wanted to add that all caps variables in bash are best avoided. By convention, environment and internal shell variables are capitalized. It's best to keep all other variable names in lower case to avoid overwriting something important.

Jon Red
  • 176
  • 1
    No, that won't work and is a bit silly. readarray -t dirs expects one element per line and you're feeding the output of echo $dirs which will be on one line. You left $dirs unquoted which means split+glob will be applied on it. Here, you could use that split+glob, if you disabled the glob part and made sure the splitting was made on space: set -o noglob; IFS=' '; tar -czf file.tar.gz -- $dirs. If you wanted an intermediary array: set -o noglob; IFS=' '; dirs=($dirs) – Stéphane Chazelas Jan 31 '20 at 18:59
  • @StéphaneChazelas For me that's only true if reading from a file. For me when reading from a variable like this, the elements are delimited by whitespace unless I set IFS to '\n'. – Jon Red Jan 31 '20 at 19:42
  • Try s='a b'; readarray -t x < <(echo $s); typeset -p x. Compare with s='a b'; IFS=' '; set -o noglob; x=($s); typeset -p x – Stéphane Chazelas Jan 31 '20 at 19:46
  • I see the difference but it's irrelevant in this case: s="a b"; readarray -t x < <(echo $s); for i in ${s[@]};do echo "|$i|";done is the same as s='a b'; IFS=' '; set -o noglob; s=($s); for i in ${s[@]};do echo "|$i|";done The only difference relevant to the original question is I don't set IFS or noglob. – Jon Red Jan 31 '20 at 19:54
  • By leaving ${x[@]} unquoted (which I suppose you meant instead of ${s[@]} though in effect it's the same), you're applying split+glob on it. You have an array with one element, but splitting that one element on $IFS, and applying globbing on it which makes even less sense. ${arry[@]} should never be left unquoted. (note that noglob only applies for values that contain wildcard characters like *, ?, [...] and possibly more depending on the shell). – Stéphane Chazelas Jan 31 '20 at 20:10
  • No, it's irrelevant to the question and more than a bit silly. I meant ${s[@]}. I'm converting a variable containing string patterns referencing files/directories to a variable that is suitable to pass to a tar command. If you have directories with names that contain wildcard characters stored in a string variable then your situation is outside the scope of the original question and you'll also likely need more than quotes to make it work. – Jon Red Jan 31 '20 at 20:32
  • I give up. If you don't want to take what I say at face value, at least try your code and try to understand why it doesn't work. – Stéphane Chazelas Jan 31 '20 at 21:41
  • I tested this thing out on freshly created users on rhel6 and rhel7. I actually am going to edit the answer but not based on your comments (setting IFS and noglob is still way silly in this scenario). No, you can actually cut right to dirs=($dirs) to get there. – Jon Red Jan 31 '20 at 23:03
  • That's better. Now, if you don't set $IFS, then to answer the question "how to pass space separated arguments to commands" you need to add a comment: # assuming $IFS contains SPC (as it does by default) and none of the characters in the dir names. If you don't set -o noglob, then you need to add the comment # assuming none of the dir names contain wildcard characters (or {..,..} or backslash in some shells). With IFS=' '; set -o noglob, it answers the question without those limitations. At the moment it's just the same but longer as @jesse_b's not recommanded approach. – Stéphane Chazelas Feb 01 '20 at 08:18
  • Demonstrably false: unset IFS ; set +o noglob ; dirs="/example/[dir1]/ /example/dir*2/ /example/dir...3/ /example/dir?4/" ; tar -czf file.tar.gz "$dirs"&&tar -tvf file.tar.gz||echo $? ; tar -czf file.tar.gz "${dirs[@]}"&&tar -tvf file.tar.gz||echo $? ; dirs=($dirs) ; tar -czf file.tar.gz "${dirs[@]}"&&tar -tvf file.tar.gz||echo $? – Jon Red Feb 05 '20 at 20:52