9

I'm working on a script and I need to build the tar command dynamically.

Here are two examples to illustrate what I'm trying to do :

#!/bin/bash

TAR_ME="/tmp"

EXCLUDE=("/tmp/hello hello" "/tmp/systemd*" "/tmp/Temp*")
_tar="tar "`printf -- '--exclude="%s" ' "${EXCLUDE[@]}"`" -zcf tmp.tar.gz"
echo COMMAND: "${_tar}"
${_tar} "$TAR_ME"

echo -e "\n\nNEXT:\n\n"

EXCLUDE=("--exclude=/tmp/hello\ hello" "--exclude=/tmp/systemd*" "--exclude=/tmp/Temp*")
_tar="tar "`printf -- '%s ' "${EXCLUDE[@]}"`" -zcf test.tar.gz"
echo COMMAND: "${_tar}"
${_tar} "$TAR_ME"

I want to be able to use _tar as a command, I've been able to make it work with classic path, but I need it to work with spaces in folders' name. And every single time I got errors that look like :

COMMAND: tar --exclude="/tmp/hello hello" --exclude="/tmp/systemd*" --exclude="/tmp/Temp*"  -zcf tmp.tar.gz /tmp
tar: hello": Cannot stat: No such file or directory

COMMAND: tar --exclude=/tmp/hello\ hello --exclude=/tmp/systemd* --exclude=/tmp/Temp*  -zcf test.tar.gz 
tar: hello: Cannot stat: No such file or directory

Just one thing you need to know, I need my script to work on very old machines, meaning I can't use last bash features.

8
  • I believe the --exclude option can only accept a single string after it. You can have multiple --exclude statements though. Maybe try "--exclude=/tmp/hello --exclude=hello" Oops. Nevermind. I misunderstood.
    – Lewis M
    Oct 4 '18 at 13:43
  • @LewisM I think OP want to exclude directory "/tmp/hello hello" (yes, with a space.
    – Archemar
    Oct 4 '18 at 13:44
  • @ShellCode what about quoting all exclude, e.g. "--exclude=/tmp/hello hello"
    – Archemar
    Oct 4 '18 at 13:45
  • Yeah. That's why I put the Oops statement later. :)
    – Lewis M
    Oct 4 '18 at 13:46
  • How about putting eval in front of the execution?
    – jimmij
    Oct 4 '18 at 13:52
12

Don't try to make an executable string. Instead build the arguments in an array and use that when calling tar (you are already using an array properly for EXCLUDE):

#!/bin/bash

directory=/tmp

exclude=( "hello hello" "systemd*" "Temp*" )

# Now build the list of "--exclude" options from the "exclude" array:
for elem in "${exclude[@]}"; do
    exclude_opts+=( --exclude="$directory/$elem" )
done

# Run tar
tar -cz -f tmp.tar.gz "${exclude_opts[@]}" "$directory"

With /bin/sh:

#!/bin/sh

directory=/tmp

set -- "hello hello" "systemd*" "Temp*"

# Now build the list of "--exclude" options from the "$@" list
# (overwriting the values in $@ while doing so):
for elem do
    set -- "$@" --exclude="$directory/$elem"
    shift
done

# Run tar
tar -cz -f tmp.tar.gz "$@" "$directory"

Note the quoting of $@ in the sh code and of both ${exclude[@]} and ${exclude_opts[@]} in the bash code. This ensures that the lists are expanded to individually quoted elements.

Related:

0
2
mix(){
        p=$1; shift; q=$1; shift; c=
        i=1; for a; do c="$c $q \"\${$i}\""; i=$((i+1)); done
        eval "${p%\%*}$c${p#*\%}"
}
mix 'tar % -zcf tmp.tar.gz' --exclude "/tmp/hello hello" "/tmp/systemd*" "/tmp/Temp*"

EXCLUDE=("/tmp/hello hello" "/tmp/systemd*" "/tmp/Temp*")
mix 'tar % -zcf tmp.tar.gz' --exclude "${EXCLUDE[@]}"

Extending the answer here. This doesn't rely on any bashisms, it will also work fine with debian's /bin/sh, and with busybox.

5
  • Thank you very much for your help, but I don't really like the eval, it's quite dangerous... Moreover, this code is quite hard to understand, don't you have something easier ? :/ The script will be distributed so I have to keep it as simple as possible...
    – ShellCode
    Oct 4 '18 at 14:03
  • It's not dangerous. Run it with set -x. What exactly you don't understand?
    – mosvy
    Oct 4 '18 at 14:04
  • Also, read the original answer on stackoverflow. It includes a demo.
    – mosvy
    Oct 4 '18 at 14:10
  • It works quite well though... Waiting to see if anybody has a cleaner answer, otherwise I will accept yours. Maybe there is nothing wrong with that code, but every time I see an eval, I'm afraid the code could lead to command injection, that's why I try to avoid it
    – ShellCode
    Oct 4 '18 at 14:13
  • I've updated the answer with a fix for indexes > 9. You can replace the eval with an echo to see what's actually getting (the eval doesn't see the filenames)
    – mosvy
    Oct 4 '18 at 14:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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