1

I am writing a script in Bash that should automate the actions necessary to backup my hard drive using the rsync command. In this implementation, I am setting the script up to complete the following steps

  1. Determine the base directory where the backup will be mounted based on the Linux distobution
  2. Determine if rsync is installed, and prompt the user to install it if it is not
  3. Prompt the user to select the correct mounted drive at the base directory
  4. Create a backup file in the correct directory where the backup directory is given a title in the format YYYY-MM-DD:H:M:S
  5. Conduct the backup using rsync
  6. Determine if there are more than 4 backup directories, and if so, delete the oldest

I am posting the while script for completeness; however, the problem I am having is occurring on line 165. The script is stored in my usr/local/bin directory and run either with or without the sudo command. If it is run without the sudo command (i.e. backup), whenever it comes to a location in the script where sudo is invoked, it will prompt the user for his or her password and it runs correctly. However, I would like to run the file with sudo upfront (i.e. sudo backup) so the user does not need to sit by his or her computer waiting for the script to prompt them to enter their password. I would rather they entered their password once, and the whole script ran without the need for more user inputs. Unfortunately when I do this, instead of creating the backup directory in the backup drive (i.e. /run/media/username/YYYY-MM-DD:H:M:S) it creates the directory in the media directory (i.e. /run/media/YYYY-MM-DD:H:M:S). Can anyone tell me why this works when I do not invoke the script with sudo but does not work when I do use sudo?

Fore reference, /usr/local/bin is in my PATH, so I can call the script by backup instead of ./backup or bash backup

#!/usr/bin/bash
# backup file
# ================================================================================
# ================================================================================
# - Purpose: This file contains scripts that will create a user defined number
#            of backup snapshots, where each snapshot is a full backup
#            of the hard drive
#
# Source Metadata
# - Author:    First Name, Last Name
# - Date:      December 15, 2022
# - Version:   2.0
# - Copyright: Copyright 2022, XXXX Inc.
# ================================================================================
# ================================================================================
# Set command path lengths

NUM_DIRS=4 # Number of backups allowed on the backup drive
make_dir=/usr/bin/mkdir
remove_dir=/usr/bin/rm
nfind=/usr/bin/find
cdat=/usr/bin/rsync
log_path=~/backup.log
# --------------------------------------------------------------------------------
# Define base directory for backup drive based on Linux distrobution

cur_dir=`pwd`
linux_file=/etc/os-release

# - This if statement will determine the correct media directory where
#   the backup drive will be mounted
if grep -q "Arch" $linux_file
then
    # The host is an Arch based distribution
    media_dir=/run/media/$USERNAME/

elif grep -q "Ubuntu" $linux_file || grep -q "Pop" $linux_file
then
    # The host is an Ubuntu or Pop OS based distribution
    media_dir=/media/$USERNAME/

else
    # The host is not a compatible distribution
    echo "Linux distribution not supported, exiting"
    exit 1
fi
# --------------------------------------------------------------------------------
# Ensure that rsync is appropriately installed

if ! command -v rsync > /dev/null 2>&1 && grep -q "Arch" $linux_file
then
    echo "rsync is not installed"
    echo "Install rsync with the command 'sudo pacman -S rsync'"
    exit 3
elif ! command -v rsync > /dev/null 2>&1
then
    echo "rsync is not installed"
    echo "Install rsync with the command 'sudo apt install rsync'"
    exit 3
fi
# --------------------------------------------------------------------------------
# Determine which drive to pair with the base directory

# This command will determine the mounted directories in the media directory 
potential_dirs=($(ls -l $media_dir | awk '/^d/ {print $9}'))

# Let the user select the correct drive to contain the backup
count=0
echo "Select the number of the appropriate backup directory"
for dir in ${potential_dirs[@]};
do
    echo $count")" $dir
    let count++
done
echo $count") None" 
read option;

# Verify the user entered the correct value
if [ $option -eq $count ];
then
    echo "User required option not available, exiting!"
    exit 0
fi

if [ $option -gt $count ] || [ $option -lt 0 ];
then
    echo "User entered and invalid number, exiting!"
    exit 2
fi

# Verify the correct drive was selected
echo "Are you sure ${potential_dirs[option]} is the correct option (Y/N)"
read assertion
if [ "$answer" != "${answer#[Yy]}" ] ;
then
    echo "Exiting!"
    return 0
fi

DATE=`date +%F:%H:%M:%S`
base_dir=$media_dir${potential_dirs[option]}'/' 
backup_drive=$media_dir${potential_dirs[option]}'/'$DATE

# Create directory
sudo $make_dir $backup_drive
# --------------------------------------------------------------------------------
# Backup data
sudo $cdat -aAXHv --delete --exclude={"/dev/*","/proc/*","/sys/*","/run/*","/mnt/*","/media/*","lost+found","swapfile"} / $backup_drive
# --------------------------------------------------------------------------------
# Determine the number of directories in the backup dir and number to be deleted

# Count the number of directories in the backup directory
dir_num=`$nfind $base_dir -mindepth 1 -maxdepth 1 -type d | wc -l`

# Determine the number of directories to be deleted
num_delete="$(($dir_num-$NUM_DIRS))"

# Change to backup directory
cd $base_dir

# Delete the oldest directories if necessary
if [ $num_delete -gt 0 ] ; then
    dirs=`ls -d */ | cut -f1 -d'/' | head -n $num_delete`
    for variable in $dirs
    do
        echo "Removing $variable directory"
        sudo rm -r $variable
    done
fi

# Return to the initial directory
cd `pwd`

# Write succesful results to log file
good_msg=$USERNAME' hard drive succesfully backed up on '$DATE#
/usr/bin/echo $good_msg >> $log_path 
# ================================================================================
# ================================================================================
exit 0
6
  • 2
    Your question seems to center around the name of the subdirectory under /media. The script determines this by invoking either media_dir=/media/$USERNAME/ or media_dir=/run/media/$USERNAME/. What is the $USERNAME you're expecting, and what is the $USERNAME you're getting instead? Have you created a simple test script to test the value of $USERNAME when you invoke the script via sudo script-name vs. something like sudo -i script-name? Have you looked in the sudo man page or other docs to see what makes sudo change that variable?
    – Sotto Voce
    Dec 19, 2022 at 19:25
  • 1
    +1 afaik USERNAME is not a "standard" environment variable and is unlikely to be preserved under the default Ubuntu sudoers env_reset policy Dec 19, 2022 at 19:46
  • @SottoVoce why sudo script vs sudo -i script instead of sudo script vs script? In what case would the sudo -i be different? If there were something in root's bashrc or profile that set the variable? Or is there something else?
    – terdon
    Dec 19, 2022 at 21:10
  • @steeldriver Wow, this is a palm to the face moment. You are absolutely correct. $USERNAME will not be preserved in either Arch or Ubuntu environments when running in pseudo. I need to re-architect the script to account for that.
    – Jon
    Dec 19, 2022 at 21:24
  • @terdon yes, in case something in this machine's .profile, .bash_profile, or .login scripts sets $USERNAME (though it's more often $HOME that catches people invoking scripts via sudo). My emphasis was more on the something like than on sudo -i.
    – Sotto Voce
    Dec 19, 2022 at 21:41

1 Answer 1

2

It is because you are using $USERNAME. You can easily see this in action:

$ bash -c 'echo "USERNAME is $USERNAME"'
USERNAME is terdon
$ sudo bash -c 'echo "USERNAME is $USERNAME"'
[sudo] password for terdon: 
USERNAME is 

As you can see above, the $USERNAME variable isn't defined when you use sudo which means that these lines are causing the problem:

if grep -q "Arch" $linux_file
then
    # The host is an Arch based distribution
    media_dir=/run/media/$USERNAME/

elif grep -q "Ubuntu" $linux_file || grep -q "Pop" $linux_file
then
    # The host is an Ubuntu or Pop OS based distribution
    media_dir=/media/$USERNAME/

else
    # The host is not a compatible distribution
    echo "Linux distribution not supported, exiting"
    exit 1
fi

Since $USERNAME isn't set, you just get /run/media/ for Arch and /media/ for the other two. A simple solution is to use $SUDO_USER instead:

$ bash -c 'echo "USERNAME is $SUDO_USER"'
USERNAME is 
$ sudo bash -c 'echo "USERNAME is $SUDO_USER"'
USERNAME is terdon

As you can see above, $SUDO_USER holds the name of the user invoking the sudo command. So if you want your script to only be run this way, you can just use that. If you still want to allow the option of not running as root, you can add a test:

if [ -n "$SUDO_USER" ]; then
  username="$SUDO_USER"
else
  username="$USERNAME"
fi

And then, in the rest of your script use $username wherever you used $USERNAME.

Alternatively, you can also use the standard $USER variable which is set even when using sudo:

$ bash -c 'echo "USERNAME is $USER"'
USERNAME is terdon
$ sudo bash -c 'echo "USERNAME is $USER"'
USERNAME is root

But that will return root when running as sudo, so you're probably better off using the SUDO_USER approach anyway.


Finally, a few notes on your script:

  1. You have a lot of unquoted variables which is a bad idea in general and a particularly bad idea if you are dealing with arbitrary paths and file names. See Why does my shell script choke on whitespace or other special characters? and Security implications of forgetting to quote a variable in bash/POSIX shells.

  2. You don't have any error checking and are assuming all of your commands work. This is very, very dangerous when running a script that contains sudo rm commands. If a variable isn't set or if a cd failed, for example because $USERNAME is unset or similar, causing you to cd to a nonexistent directory, then you could easily destroy your entire system.

    And this really can happen. Here's a famous case from a few years ago: Scary Steam for Linux bug erases all the personal files on your PC. So, make sure to add || exit to various important commands or even a set -x at the start of your script.

  3. You have a couple of instances where you are parsing ls. Which is also a very bad idea and will break very easily. So, instead of this:

    potential_dirs=($(ls -l $media_dir | awk '/^d/ {print $9}'))
    

    Use this:

    potential_dirs=( "$media_dir"/*/ )
    

    The next part where you are using ls has various issues:

    # Change to backup directory
    cd $base_dir
    
    # Delete the oldest directories if necessary
    if [ $num_delete -gt 0 ] ; then
        dirs=`ls -d */ | cut -f1 -d'/' | head -n $num_delete`
        for variable in $dirs
        do
            echo "Removing $variable directory"
            sudo rm -r $variable
        done
    fi
    

    First, that isn't deleting the oldest directories, it is deleting the first ones in alphabetical order. Next both the ls and the rm will fail if any of the dir names contain newlines, and the rm will even fail with whitespace since you haven't quoted "$variable". More importantly, you don't actually have any error checking so if the previous cd $base_dir failed for whatever reason you are deleting files from somewhere else!

    So, change the above to something like this if you want to delete the $num_delete oldest directories:

    
    # Delete the oldest directories if necessary
    if [ $num_delete -gt 0 ] ; then
        readarray -d '' dirs < <(stat --printf='%Y %n\0' "$base_dir"/*/ |
                                  sort -znk1,1 | head -zn "$num_delete" | 
                                  cut -z -d ' ' -f2-)
        for variable in "${dirs[@]}"
        do
            echo "Removing $variable directory"
            sudo rm -r -- "$variable"
        done
    fi
    

    Note how there is no need to cd here.

  4. Your way of counting directories will fail for cases with newlines in their names:

    dir_num=`$nfind $base_dir -mindepth 1 -maxdepth 1 -type d | wc -l`
    

    Use this instead which will work for any name:

    dir_num=$($nfind $base_dir -mindepth 1 -maxdepth 1 -type d -printf '.\n' | wc -l)
    
  5. Avoid using backticks (`command`) they are deprecated in favor of $(command). Wherever you have var=`command`, you can replace it with var=$(command).

  6. There's no need for cur_dir=`pwd` , the current directory is already saved in the special $PWD variable so you can simply use $PWD.

  7. At the very end of your script, you have this:

    cd `pwd`
    

    That is basically a null operation since you are saying "move to whatever directory is returned by the command pwd", so you are never actually moving anywhere since `pwd` will just print the current directory. I think you meant cd "$cur_dir", but even that is unnecessary. As you can see in point 3 above, there is no need to cd in the first place.

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.