我正在 Bash 中编写一个脚本,该脚本应该自动执行使用该命令备份硬盘驱动器所需的操作rsync
。在此实现中,我设置脚本来完成以下步骤
- 根据Linux发行版确定备份挂载的基目录
- 判断是否安装了rsync,如果没有则提示用户安装
- 提示用户在基本目录中选择正确安装的驱动器
- 在正确的目录中创建备份文件,其中备份目录的标题格式为
YYYY-MM-DD:H:M:S
- 使用进行备份
rsync
- 判断备份目录是否超过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:S
sudo
sudo
前向参考/usr/local/bin
位于 my 中,因此我可以通过代替或PATH
来调用脚本backup
./backup
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
答案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
方法。
最后,关于您的脚本的一些注释:
您有很多未加引号的变量,这通常是一个坏主意,如果您正在处理任意路径和文件名,则尤其糟糕。看为什么我的 shell 脚本会因为空格或其他特殊字符而卡住?和忘记在 bash/POSIX shell 中引用变量的安全隐患。
您没有进行任何错误检查,并假设所有命令都有效。在运行包含
sudo rm
命令的脚本时,这非常非常危险。如果变量未设置或cd
失败(例如由于$USERNAME
未设置或类似原因),导致您进入cd
不存在的目录,那么您很容易破坏整个系统。这确实有可能发生。这是几年前的一个著名案例:适用于 Linux 的可怕 Steam 错误会删除您 PC 上的所有个人文件。因此,请确保添加
|| exit
到各种重要命令,甚至添加到set -x
脚本的开头。您有几个正在解析的实例
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
则 和都会失败,并且由于您没有引用 ,因此甚至会因空格而失败。更重要的是,您实际上没有任何错误检查,因此如果前一个由于某种原因失败,您将从其他地方删除文件!rm
rm
"$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
。对于名称中包含换行符的情况,您计算目录的方法将失败:
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)
避免使用反引号 (
`command`
),它们已被弃用$(command)
。无论哪里有var=`command`
,都可以替换为var=$(command)
.不需要
cur_dir=`pwd`
,当前目录已经保存在特殊$PWD
变量中,因此您可以简单地使用$PWD
.在脚本的最后,你有这样的内容:
cd `pwd`
这基本上是一个空操作,因为您说的是“移动到命令返回的任何目录
pwd
”,所以您实际上永远不会移动到任何地方,因为`pwd`
只会打印当前目录。我想你的意思是cd "$cur_dir"
,但即使这样也是不必要的。正如您在上面第 3 点中看到的,cd
首先没有必要。