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

terdon
  • 242,166
Jon
  • 155
  • 1
  • 8
  • 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 '22 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 – steeldriver Dec 19 '22 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 '22 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 '22 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 '22 at 21:41
  • General comment: I wouldn't create filenames that contain colons (:) as they may fail on certain filesystems. And even if they don't fail immediately they can be unreadable by certain OSes. – Chris Davies Dec 19 '22 at 21:52

1 Answers1

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.

terdon
  • 242,166