为什么 sudo 会更改创建新目录的目录?

为什么 sudo 会更改创建新目录的目录?

我正在 Bash 中编写一个脚本,该脚本应该自动执行使用该命令备份硬盘驱动器所需的操作rsync。在此实现中,我设置脚本来完成以下步骤

  1. 根据Linux发行版确定备份挂载的基目录
  2. 判断是否安装了rsync,如果没有则提示用户安装
  3. 提示用户在基本目录中选择正确安装的驱动器
  4. 在正确的目录中创建备份文件,其中备份目录的标题格式为YYYY-MM-DD:H:M:S
  5. 使用进行备份rsync
  6. 判断备份目录是否超过4个,如果是,则删除最旧的

为了完整性,我发布了 while 脚本;但是,我遇到的问题发生在第 165 行。该脚本存储在我的usr/local/bin目录中,并且可以使用或不使用sudo命令运行。如果它在没有命令的情况下运行sudo(即backup),每当它到达脚本中sudo被调用的位置时,它都会提示用户输入他或她的密码并且它会正确运行。但是,我想sudo预先运行该文件(即sudo backup),以便用户不需要坐在他或她的计算机旁等待脚本提示他们输入密码。我宁愿他们输入密码一次,整个脚本就可以运行,而不需要更多的用户输入。不幸的是,当我这样做时,它不是在备份驱动器(即)中创建备份目录,而是在目录(即)/run/media/username/YYYY-MM-DD:H:M:S中创建目录。谁能告诉我为什么当我不调用脚本时这会起作用,但当我使用时却不起作用?media/run/media/YYYY-MM-DD:H:M:Ssudosudo

前向参考/usr/local/bin位于 my 中,因此我可以通过代替或PATH来调用脚本backup./backupbash 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

答案1

这是因为你正在使用$USERNAME.您可以轻松地看到它的实际效果:

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

正如您在上面看到的,$USERNAME使用时未定义该变量sudo,这意味着这些行导致了问题:

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

由于$USERNAME尚未设置,您只需获取/run/media/Arch 和/media/其他两个。一个简单的解决方案是使用$SUDO_USER

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

正如您在上面看到的,$SUDO_USER保存调用命令的用户的名称sudo。因此,如果您希望脚本仅以这种方式运行,则可以使用它。如果您仍然想允许不以 root 身份运行的选项,您可以添加一个测试:

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

然后,在脚本的其余部分中,$username无论您使用过什么地方,都使用$USERNAME.

或者,您也可以使用$USER即使在使用时设置的标准变量sudo

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

root但是当运行为时,这会返回sudo,所以无论如何你最好使用该SUDO_USER方法。


最后,关于您的脚本的一些注释:

  1. 您有很多未加引号的变量,这通常是一个坏主意,如果您正在处理任意路径和文件名,则尤其糟糕。看为什么我的 shell 脚本会因为空格或其他特殊字符而卡住?忘记在 bash/POSIX shell 中引用变量的安全隐患

  2. 您没有进行任何错误检查,并假设所有命令都有效。在运行包含sudo rm命令的脚本时,这非常非常危险。如果变量未设置或cd失败(例如由于$USERNAME未设置或类似原因),导致您进入cd不存在的目录,那么您很容易破坏整个系统。

    这确实有可能发生。这是几年前的一个著名案例:适用于 Linux 的可怕 Steam 错误会删除您 PC 上的所有个人文件。因此,请确保添加|| exit到各种重要命令,甚至添加到set -x脚本的开头。

  3. 您有几个正在解析的实例ls。这是也是一个非常糟糕的主意并且很容易破裂。所以,而不是这个:

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

    用这个:

    potential_dirs=( "$media_dir"/*/ )
    

    您使用的下一部分ls存在各种问题:

    # 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
    

    首先,这不是删除最旧的目录,而是按字母顺序删除第一个目录。接下来,如果任何目录名称包含换行符,ls则 和都会失败,并且由于您没有引用 ,因此甚至会因空格而失败。更重要的是,您实际上没有任何错误检查,因此如果前一个由于某种原因失败,您将从其他地方删除文件!rmrm"$variable"cd $base_dir

    因此,如果您想删除,请将上面的内容更改为类似的内容$num_delete 最老的目录:

    
    # 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
    

    注意这里没有必要cd

  4. 对于名称中包含换行符的情况,您计算目录的方法将失败:

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

    使用这个代替它可以工作任何姓名:

    dir_num=$($nfind $base_dir -mindepth 1 -maxdepth 1 -type d -printf '.\n' | wc -l)
    
  5. 避免使用反引号 ( `command`),它们已被弃用$(command)。无论哪里有var=`command`,都可以替换为var=$(command).

  6. 不需要cur_dir=`pwd`,当前目录已经保存在特殊$PWD变量中,因此您可以简单地使用$PWD.

  7. 在脚本的最后,你有这样的内容:

    cd `pwd`
    

    这基本上是一个空操作,因为您说的是“移动到命令返回的任何目录pwd”,所以您实际上永远不会移动到任何地方,因为`pwd`只会打印当前目录。我想你的意思是cd "$cur_dir",但即使这样也是不必要的。正如您在上面第 3 点中看到的,cd首先没有必要。

相关内容