I'm new to Bash scripting and I'm working on a personal backup script. It accepts up to four arguments, such as "essentials", "archive", and "paperwork". For each selected category, it creates a date-stamped tar.xz archive, shows compression progress with pv, and transfers the result to a mounted encrypted drive over the network using rsync.
At the moment, the script loops over the arguments and contains a separate if statement for every possible value. Each branch repeats the compression and transfer commands, even though the source directories and archive names differ. I'd like to know whether a function would make this cleaner, and how best to structure it when the source directories are in different locations.
I'm also unsure about error handling. Compression or transfer could fail, but I don't want to bury the useful output under layers of nested if statements. Is there a clean way to check the pipeline and rsync result, report failures, and keep the script readable?
4 Answers
A simple structure would look roughly like this:
backup() {
local source=$1 label=$2
local archive="${label}-${datestring}.tar.xz"
local temporary="/tmp/$archive"
if tar -cf - -- "$source" | pv -s "$(du -sb -- "$source" | awk '{print $1}')" | xz -T0 > "$temporary" && rsync -ah --progress "$temporary" "$destination/$archive"; then
printf '%sn' "Backup complete: $label"
else
printf '%sn' "Backup failed: $label" >&2
return 1
fi
}
Then dispatch arguments with case, for example essentials) backup "$HOME/.mozilla" firefox ;;. Quote variable expansions, use local variables inside functions, and consider removing temporary files afterward. Whether you print an additional error message is up to you; tar, xz, and rsync will already provide useful diagnostics.
You normally don't need to inspect $? explicitly. In Bash, if already tests the exit status of the command that follows it. For example, use if command; then ...; else ...; fi, or chain operations with && when the second operation should happen only after the first succeeds.
For a pipeline, remember that its status may otherwise come from only the last command. If you need the pipeline to fail when tar or xz fails, enable pipefail with set -o pipefail, or structure the command so the status is checked clearly. Avoid checking $? after running another command, because by then it refers to that newer command.
A function is a good fit here. Put the repeated archive-and-transfer sequence in one function that accepts the source directory and archive label as arguments. Then use a case statement to map each command-line argument to the appropriate function call. This removes the repeated code and makes adding another backup category straightforward.
Also run ShellCheck against the script. It will catch quoting problems, unsafe variable expansion, and several common Bash mistakes. A standard Bash shebang such as #!/usr/bin/env bash is helpful too. If the script depends on external tools like pv, rsync, tar, and xz, you can check that they are available before starting.
For a personal script, keep the error handling proportional to what you need. Checking the combined compression-and-transfer operation and returning a nonzero status is usually enough. You can later add a trap to clean up temporary archives or write failures to a log, but a large framework of nested conditionals would probably make this harder to maintain.
You might also compare compression tools. zstd or pigz can be substantially faster than xz, and options such as --rsyncable may improve incremental network transfers. Test the resulting archive sizes and runtimes with your own data before changing formats.

You can use command -v to check whether a command is available. Shell built-ins are also reported by command -v, usually without an executable path, so checking availability is generally more useful than trying to distinguish built-ins from external commands.