Variables as commands in Bash scripts

I am writing a very simple Bash script that tars a given directory, encrypts the output of that, and then splits the resultant file into multiple smaller files since the backup media doesn’t support huge files.

I don't have a lot of experience with Bash scripting. I believe I’m having issues with quoting my variables properly to allow spaces in the parameters. The script follows:

#! /bin/bash

# This script tars the given directory, encrypts it, and transfers
# it to the given directory (likely a USB key).

if [ $# -ne 2 ]
then
    echo "Usage: `basename $0` DIRECTORY BACKUP_DIRECTORY"
    exit 1
fi

DIRECTORY=$1
BACKUP_DIRECTORY=$2
BACKUP_FILE="$BACKUP_DIRECTORY/`date +%Y-%m-%dT%H-%M-%S.backup`"

TAR_CMD="tar cv $DIRECTORY"
SPLIT_CMD="split -b 1024m - \"$BACKUP_FILE\""

ENCRYPT_CMD='openssl des3 -salt'

echo "$TAR_CMD | $ENCRYPT_CMD | $SPLIT_CMD"

$TAR_CMD | $ENCRYPT_CMD | $SPLIT_CMD

say "Done backing up"

Running this command fails with:

split: "foo/2009-04-27T14-32-04.backup"aa: No such file or directory

I can fix it by removing the quotes around $BACKUP_FILE where I set $SPLIT_CMD. But, if I have a space in the name of my backup directory, it doesn't work. Also, if I copy and paste the output from the "echo" command directly into the terminal, it works fine. Clearly there's something I don't understand about how Bash is escaping things.


Solution 1:

Simply don't put whole commands in variables. You'll get into a lot of trouble trying to recover quoted arguments.

Also:

  1. Avoid using all-capitals variable names in scripts. It is an easy way to shoot yourself in the foot.
  2. Don't use backquotes. Use $(...) instead; it nests better.

#! /bin/bash

if [ $# -ne 2 ]
then
    echo "Usage: $(basename $0) DIRECTORY BACKUP_DIRECTORY"
    exit 1
fi

directory=$1
backup_directory=$2
current_date=$(date +%Y-%m-%dT%H-%M-%S)
backup_file="${backup_directory}/${current_date}.backup"

tar cv "$directory" | openssl des3 -salt | split -b 1024m - "$backup_file"

Solution 2:

eval is not an acceptable practice if your directory names can be generated by untrusted sources. See BashFAQ #48 for more on why eval should not be used, and BashFAQ #50 for more on the root cause of this problem and its proper solutions, some of which are touched on below:

If you need to build up your commands over time, use arrays:

tar_cmd=( tar cv "$directory" )
split_cmd=( split -b 1024m - "$backup_file" )
encrypt_cmd=( openssl des3 -salt )
"${tar_cmd[@]}" | "${encrypt_cmd[@]}" | "${split_cmd[@]}"

Alternately, if this is just about defining your commands in one central place, use functions:

tar_cmd() { tar cv "$directory"; }
split_cmd() { split -b 1024m - "$backup_file"; }
encrypt_cmd() { openssl des3 -salt; }
tar_cmd | split_cmd | encrypt_cmd

Solution 3:

I am not sure, but it might be worth running an eval (near "The args are read and concatenated together") on the commands first.

This will let Bash expand the variables $TAR_CMD and such to their full breadth (just as the echo command does to the console, which you say works).

Bash will then read the line a second time with the variables expanded.

eval $TAR_CMD | $ENCRYPT_CMD | $SPLIT_CMD

Page Bash: Why use eval with variable expansion? looks like it might do a decent job at explaining why that is needed.