四部
第 24 章 编写简单的脚本实用工具
本章内容
- 自动备份
- 管理用户账户
- 监测磁盘空间
对Linux系统管理员而言,没什么比编写脚本实用工具更有意义。Linux系统管理员每天都会有各种各样的任务,从监测磁盘空间到备份重要文件再到管理用户账户。shell脚本实用工具可以让这些工作轻松许多!本章将演示一些可以通过在bash shell中编写脚本工具来实现的功能。
24.1 归档
不管你负责的是商业环境的Linux系统还是家用环境的,丢失数据都是一场灾难。为了防止这种倒霉事,最好是定时进行备份(或者是归档)。
但是好想法和实用性经常是两回事。制定一个存储重要文件的备份计划绝非易事。这时候shell脚本通常能够助你一臂之力。
本节将会演示两种使用shell脚本备份Linux系统数据的方法。
归档数据文件
如果你正在用Linux系统作为一个重要项目的平台,可以创建一个shell脚本来自动获取特定目录的快照。在配置文件中指定所涉及的目录,这样一来,在项目发生变化时,你就可以做出对应的修改。这有助于避免把时间耗在恢复主归档文件上。
本节将会介绍如何创建自动化shell脚本来获取指定目录的快照并保留旧数据的归档。
- 需要的功能
Linux中归档数据的主要工具是tar命令(参见第4章)。tar命令可以将整个目录归档到单个 文件中。下面的例子是用tar命令来创建工作目录归档文件。
$ tar -cf archive.tar /home/Christine/Project/*.*tar: Removing leading '/' from member names$$ ls -l archive.tar-rw-rw-r--. 1 Christine Christine 51200 Aug 27 10:51 archive.tar$
tar命令会显示一条警告消息,表明它删除了路径名开头的斜线,将路径从绝对路径名变成相对路径名(参见第3章)。这样就可以将tar归档文件解压到文件系统中的任何地方了。你很可能不想在脚本中出现这条消息。这种情况可以通过将STDERR重定向到/dev/null文件(参见第15章)实现。
$ tar -cf archive.tar /home/Christine/Project/*.* 2>/dev/null
$
$ ls -l archive.tar
-rw-rw-r--. 1 Christine Christine 51200 Aug 27 10:53 archive.tar
$
由于tar归档文件会消耗大量的磁盘空间,最好能够压缩一下该文件。这只需要加一个-z选项就行了。它会将tar归档文件压缩成gzip格式的tar文件,这种文件也叫作tarball。别忘了使用恰当的文件扩展名来表示这是个tarball,用.tar.gz或.tgz都行。下面的例子创建了项目目录的tarball。
$ tar -zcf archive.tar.gz /home/Christine/Project/*.* 2>/dev/null
$
$ ls -l archive.tar.gz
-rw-rw-r--. 1 Christine Christine 3331 Aug 27 10:53 archive.tar.gz
$
现在你已经完成了归档脚本的主要部分。
你不需要为待备份的新目录或文件修改或编写新的归档脚本,而是可以借助于配置文件。配置文件应该包含你希望进行归档的每个目录或文件。
$ cat Files_To_Backup
/home/Christine/Project
/home/Christine/Downloads
/home/Does_not_exist
/home/Christine/Documents
$
说明 如果你使用的是带图形化桌面的Linux发行版,那么归档整个$HOME目录时要注意。尽管这个想法很有吸引力,但$HOME目录含有很多跟图形化桌面有关的配置文件和临时文 件。它会生成一个比你想象中大很多的归档文件。选择一个用来存储工作文件的子目录, 然后在归档配置文件中加入那个子目录。
可以让脚本读取配置文件,然后将每个目录名加到归档列表中。要实现这一点,只需要使用read命令(参见第14章)来读取该文件中的每一条记录就行了。不过不用像之前那样(参见第13章)通过管道将cat命令的输出传给while循环,在这个脚本中我们使用exec命令(参见第14 章)来重定向标准输入(STDIN),用法如下。
exec < $CONFIG_FILE
read FILE_NAME
注意,我们为归档配置文件使用了一个变量,CONFIG_FILE。配置文件中每一条记录都会被读入。只要read命令在配置文件中发现还有记录可读,它就会在?变量中(参见第11章)返回一个表示成功的退出状态码0。可以将它作为while循环的测试条件来读取配置文件中的所有记录。
while [ $? -eq 0 ]
do
[...]
read FILE_NAME
done
一旦read命令到了配置文件的末尾,它就会返回一个非零状态码。这时脚本会退出while 循环。
在while循环中,我们需要做两件事。首先,必须将目录名加到归档列表中。更重要的是要检查那个目录是否存在!很可能你从文件系统中删除了一个目录却忘了更新归档配置文件。可以用一个简单的if语句来检查目录存在与否(参见第12章)。如果目录存在,它会被加入要归档目录列表FILE_LIST中,否则就显示一条警告消息。if语句如下。
if [ -f $FILE_NAME -o -d $FILE_NAME ]
then
# If file exists, add its name to the list.
FILE_LIST="$FILE_LIST $FILE_NAME"
else
# If file doesn't exist, issue warning
echo
echo "$FILE_NAME, does not exist."
echo "Obviously, I will not include it in this archive."
echo "It is listed on line $FILE_NO of the config file."
echo "Continuing to build archive list..."
echo
fi
#
FILE_NO=$[$FILE_NO + 1] # Increase Line/File number by one.
由于归档配置文件中的记录可以是文件名,也可以是目录名,所以if语句会用-f选项和-d 选项测试两者是否存在。or选项-o考虑到了,在测试文件或目录的存在性时,只要其中一个测 试为真,那么整个if语句就成立。
为了在跟踪不存在的目录和文件上提供一点额外帮助,我们添加了变量FILE_NO。这样,这个脚本就可以告诉你在归档配置文件中哪行中含有不正确或缺失的文件或目录。
- 创建逐日归档文件的存放位置
如果你只是备份少量文件,那么将这些归档文件放在你的个人目录中就行了。但如果要对多个目录进行备份,最好还是创建一个集中归档仓库目录。
$ sudo mkdir /archive
[sudo] password for Christine:
$
$ ls -ld /archive
drwxr-xr-x. 2 root root 4096 Aug 27 14:10 /archive
$
创建好集中归档目录后,你需要授予某些用户访问权限。如果忘记了这一点,在该目录下创建文件时就会出错。
$ mv Files_To_Backup /archive/
mv: cannot move 'Files_To_Backup' to
'/archive/Files_To_Backup': Permission denied
$
可以通过sudo命令或者创建一个用户组的方式,为需要在集中归档目录中创建文件的用户授权。可以创建一个特殊的用户组Archivers。
$ sudo groupadd Archivers
$
$ sudo chgrp Archivers /archive
$
$ ls -ld /archive
drwxr-xr-x. 2 root Archivers 4096 Aug 27 14:10 /archive
$
$ sudo usermod -aG Archivers Christine
[sudo] password for Christine:
$
$ sudo chmod 775 /archive
$
$ ls -ld /archive
drwxrwxr-x. 2 root Archivers 4096 Aug 27 14:10 /archive
$
将用户添加到Archivers组后,用户必须先登出然后再登入,才能使组成员关系生效。现在只要是该组的成员,无需超级用户权限就可以在目录中创建文件了。
$ mv Files_To_Backup /archive/
$
$ ls /archive
Files_To_Backup
$
记住,Archivers组的所有用户都可以在归档目录中添加和删除文件。为了避免组用户删除他人的归档文件,最好还是把目录的粘滞位加上。
现在你已经有足够的信息来编写脚本了。下一节将讲解如何创建按日归档的脚本。
- 创建按日归档的脚本
Daily_Archive脚本会自动在指定位置创建一个归档,使用当前日期来唯一标识该文件。下面是脚本中的对应部分的代码。
DATE=$(date +%y%m%d)
#
# Set Archive File Name
#
FILE=archive$DATE.tar.gz
#
# Set Configuration and Destination File
#
CONFIG_FILE=/archive/Files_To_Backup
DESTINATION=/archive/$FILE
#
DESTINATION变量会将归档文件的全路径名加上去。CONFIG_FILE变量指向含有待归档目录信息的归档配置文件。如果需要,二者都可以很方便地改成备用目录和文件。
说明 如果你刚开始编写脚本,那么在面对一个完整的脚本代码时(你马上就会看到),要养成通读整个脚本的习惯。试着理解内在的逻辑和脚本的控制流程。对于不理解的脚本语法或某些片段,就重新去阅读书中相关的章节。这种习惯能够帮助你非常快速地习得脚本编写技巧。
将所有的内容结合在一起,Daily_Archive脚本内容如下。
#!/bin/bash
#
# Daily_Archive - Archive designated files & directories
########################################################
#
# Gather Current Date
#
DATE=$(date +%y%m%d)
#
# Set Archive File Name
#
FILE=archive$DATE.tar.gz
#
# Set Configuration and Destination File
#
CONFIG_FILE=/archive/Files_To_Backup
DESTINATION=/archive/$FILE
#
######### Main Script #########################
#
# Check Backup Config file exists
#
if [ -f $CONFIG_FILE ]; then # Make sure the config file still exists.
# If it exists, do nothing but continue on.
echo
else # If it doesn't exist, issue error & exit script.
echo
echo "$CONFIG_FILE does not exist."
echo "Backup not completed due to missing Configuration File"
echo
exit
fi
#
# Build the names of all the files to backup
#
FILE_NO=1 # Start on Line 1 of Config File.
exec <$CONFIG_FILE # Redirect Std Input to name of Config File
#
read FILE_NAME # Read 1st record
#
while [ $? -eq 0 ]; do # Create list of files to backup.
# Make sure the file or directory exists.
if [ -f $FILE_NAME -o -d $FILE_NAME ]; then
# If file exists, add its name to the list.
FILE_LIST="$FILE_LIST $FILE_NAME"
else
# If file doesn't exist, issue warning
echo
echo "$FILE_NAME, does not exist."
echo "Obviously, I will not include it in this archive."
echo "It is listed on line $FILE_NO of the config file."
echo "Continuing to build archive list..."
echo
fi
#
FILE_NO=$(($FILE_NO + 1)) # Increase Line/File number by one.
read FILE_NAME # Read next record.
done
#
#######################################
#
# Backup the files and Compress Archive
#
echo "Starting archive..."
echo
#
tar -czf $DESTINATION $FILE_LIST 2>/dev/null
#
echo "Archive completed"
echo "Resulting archive file is: $DESTINATION"
echo
#
exit
- 运行按日归档的脚本
在测试脚本之前,别忘了修改脚本文件的权限(参见第11章)。必须赋予文件属主可执行权限(x)才能够运行脚本。
$ ls -l Daily_Archive.sh
-rw-rw-r--. 1 Christine Christine 1994 Aug 28 15:58 Daily_Archive.sh
$
$ chmod u+x Daily_Archive.sh
$
$ ls -l Daily_Archive.sh
-rwxrw-r--. 1 Christine Christine 1994 Aug 28 15:58 Daily_Archive.sh
$
测试Daily_Archive脚本非常简单。
$ ./Daily_Archive.sh
/home/Does_not_exist, does not exist.
Obviously, I will not include it in this archive.
It is listed on line 3 of the config file.
Continuing to build archive list...
Starting archive...
Archive completed
Resulting archive file is: /archive/archive140828.tar.gz
$ ls /archive
archive140828.tar.gz Files_To_Backup
$
你会看到这个脚本发现了一个不存在的目录:/home/Does_not_exist。脚本能够告诉你这个错误的行在配置文件中的行号,然后继续创建列表和归档数据。现在数据已经稳妥地归档到了tarball 文件中。
- 创建按小时归档的脚本
如果你是在文件更改很频繁的高容量生产环境中,那么按日归档可能不够用。如果要将归档频率提高到每小时一次,你还要考虑另一个因素。
在按小时备份文件时,如果依然使用date命令为每个tarball文件加入时间戳,事情很快就会变得丑陋不堪。筛选一个含有如下文件名的目录会很乏味:
archive010211110233.tar.gz
不必将所有的归档文件都放到同一目录中,你可以为归档文件创建一个目录层级。图24-1演示了这个原则。
这个归档目录包含了与一年中的各个月份对应的目录,将月的序号作为目录名。而每月的目录中又包含与当月各天对应的目录(用天的序号作为目录名)。这样你只用给每个归档文件加上时间戳,然后将它们放到与月日对应的目录中就行了。
首先,必须创建新目录/archive/hourly,并设置适当的权限。之前我们说过,Archivers组有权在目录中创建归档文件。因此,这个新创建的目录也得修改它的属组以及组权限。
$ sudo mkdir /archive/hourly
[sudo] password for Christine:
$
$ sudo chgrp Archivers /archive/hourly
$
$ ls -ld /archive/hourly/
drwxr-xr-x. 2 root Archivers 4096 Sep 2 09:24 /archive/hourly/
$
$ sudo chmod 775 /archive/hourly
$
$ ls -ld /archive/hourly
drwxrwxr-x. 2 root Archivers 4096 Sep 2 09:24 /archive/hourly
$

图24-1 创建归档目录层级结构
新目录设置好之后,将按小时归档的配置文件File_To_Backup移动到该目录中。
$ cat Files_To_Backup
/usr/local/Production/Machine_Errors
/home/Development/Simulation_Logs
$
$ mv Files_To_Backup /archive/hourly/
$
现在,还有个新问题要解决。这个脚本必须自动创建对应每月和每天的目录,如果这些目录已经存在的话,脚本就会报错。这可不是我们想要的结果!
如果仔细查看mkdir命令的命令行选项的话(参见第3章),会发现有一个-p命令行选项。这个选项允许在单个命令中创建目录和子目录。另外,额外的福利是:就算目录已经存在,它也不会产生错误消息。这正是我们的脚本中所需要的!
现在可以创建Hourly_Archive.sh脚本了。以下是前脚本的前半部分。
#!/bin/bash
#
# Hourly_Archive - Every hour create an archive
#########################################################
#
# Set Configuration File
#
CONFIG_FILE=/archive/hourly/Files_To_Backup
#
# Set Base Archive Destination Location
#
BASEDEST=/archive/hourly
#
# Gather Current Day, Month & Time
#
DAY=$(date +%d)
MONTH=$(date +%m)
TIME=$(date +%k%M)
#
# Create Archive Destination Directory
#
mkdir -p $BASEDEST/$MONTH/$DAY
#
# Build Archive Destination File Name
#
DESTINATION=$BASEDEST/$MONTH/$DAY/archive$TIME.tar.gz
#
########## Main Script ####################
[...]
一旦脚本Hourly_Archive.sh到了Main Script部分,就和Daily_Archive.sh脚本完全一样了。大部分工作都已经完成。
Hourly_Archive.sh会从date命令提取天和月,以及用来唯一标识归档文件的时间戳。然后它用这个信息创建与当天对应的目录(如果已经存在的话,就安静地退出)。最后,这个脚本用tar 命令创建归档文件并将它压缩成一个tarball。
- 运行按小时归档的脚本
跟Daily_Archive.sh脚本一样,在将Hourly_Archive.sh脚本放到cron表中之前最好先测试一下。脚本运行之前必须修改好权限。另外,通过date命令检查小时和分钟。知道了当前的时和分才 能够验证最终归档文件名的正确性。
$ chmod u+x Hourly_Archive.sh
$
$ date +%k%M
1011
$
$ ./Hourly_Archive.sh
Starting archive...
Archive completed
Resulting archive file is: /archive/hourly/09/02/archive1011.tar.gz
$
$ ls /archive/hourly/09/02/
archive1011.tar.gz
$
这个脚本第一次运行很正常,创建了相应的月和天的目录,随后生成的归档文件名也没问题。注意,归档文件名archive1011.tar.gz中包含了对应的小时(10)和分钟(11)。
说明 如果你当天运行Hourly_Archive.sh脚本,那么当小时数是单个数字时,归档文件名中只会出现3个数字。例如运行脚本的时间是1:15am,那么归档文件名就是archive115.tar.gz。如果你希望文件名中总是保留4 位数字,可以将脚本行TIME=$(date +%k%M) 修改成TIME=$(date +%k0%M)。在%k后加入数字0后,所有的单数字小时数都会被加入一个前导数字0,填充成两位数字。因此,archive115.tar.gz就变成了archive0115.tar.gz。
为了进行充分的测试,我们再次运行脚本,看看当目录/archive/hourly/09/02/已存在的时候会不会出现问题。
$ date +%k%M
1017
$
$ ./Hourly_Archive.sh
Starting archive...
Archive completed
Resulting archive file is: /archive/hourly/09/02/archive1017.tar.gz
$ ls /archive/hourly/09/02/
archive1011.tar.gz archive1017.tar.gz
$
没有问题!这个脚本仍正常运行,并创建了第二个归档文件。现在可以把它放到cron表中了。
24
24.2 管理用户账户
管理用户账户绝不仅仅是添加、修改和删除账户,你还得考虑安全问题、保留工作的需求以及对账户的精确管理。这可能是一份耗时的工作。在此将介绍另一个可以证明脚本工具能够促进效率的实例。
24.2.1 需要的功能
删除账户在管理账户工作中比较复杂。在删除账户时,至少需要4个步骤:
- 获得正确的待删除用户账户名;
- 杀死正在系统上运行的属于该账户的进程;
- 确认系统中属于该账户的所有文件;
- 删除该用户账户。
一不小心就会遗漏某个步骤。本节的shell脚本工具会帮你避免类似的错误。
- 获取正确的账户名
账户删除过程中的第一步最重要:获取待删除的用户账户的正确名称。由于这是个交互式脚本,所以你可以用read命令(参见第14章)获取账户名称。如果脚本用户一直没有给出答复, 你可以在read命令中用-t选项,在超时退出之前给用户60秒的时间回答问题。
echo "Please enter the username of the user "
echo -e "account you wish to delete from system: \c"
read -t 60 ANSWER
人毕竟难免因为其他事情而耽搁时间,所以最好给用户三次机会来回答问题。要实现这点, 可以用一个while循环(参见第13章)加-z选项来测试ANSWER变量是否为空。在脚本第一次进入while循环时,ANSWER变量的内容为空,用来给该变量赋值的提问位于循环的底部。
while [ -z "$ANSWER" ]
do
[...]
echo "Please enter the username of the user "
echo -e "account you wish to delete from system: \c"
read -t 60 ANSWER
done
当第一次提问出现超时,当只剩下一次回答问题的机会时,或当出现其他情况时,你需要跟脚本用户进行沟通。case语句(参见第12章)是最适合这里的结构化命令。通过给ASK_COUNT 变量增值,可以设定不同的消息来回应脚本用户。这部分的代码如下。
case $ASK_COUNT in
2)
echo
echo "Please answer the question."
echo
;;
3)
echo
echo "One last try...please answer the question."
echo
;;
4)
echo
echo "Since you refuse to answer the question..."
echo "exiting program."
echo
#
exit
;;
esac
#
现在,这个脚本已经拥有了它所需要的全部结构,可以问用户要删除哪个账户了。在这个脚本中,你还需要问用户另外一些问题,可之前只提那么一个问题就已经是一大堆代码了!因此, 让我们将这段代码放到一个函数中(参见第17章),以便在Delete_User.sh脚本中重复使用。
- 创建函数获取正确的账户名
你要做的第一件事是声明函数名get_answer。下一步,用unset命令(参见第6章)清除脚本用户之前给出的答案。完成这两件事的代码如下。
function get_answer {
#
unset ANSWER
在原来代码中你要修改的另一处地方是对用户脚本的提问。这个脚本不会每次都问同一个问题,所以让我们创建两个新的变量LINE1和LINE2来处理问题。
echo $LINE1
echo -e $LINE2" \c"
然而,并不是每个问题都有两行要显示,有的只要一行。你可以用if结构(参见第11章)解决这个问题。这个函数会测试LINE2是否为空,如果为空,则只用LINE1。
if [ -n "$LINE2" ]
then
echo $LINE1
echo -e $LINE2" \c"
else
echo -e $LINE1" \c"
fi
最终,我们的函数需要通过清空LINE1和LINE2变量来清除一下自己。因此,现在这个函数看起来如下。
function get_answer {
#
unset ANSWER
ASK_COUNT=0
#
while [ -z "$ANSWER" ]; do
ASK_COUNT=$(($ASK_COUNT + 1))
#
case $ASK_COUNT in
2)
echo
[...]
;;
esac
#
echo
if [ -n "$LINE2" ]; then #Print 2 lines
echo $LINE1
echo -e $LINE2" \c"
else #Print 1 line
echo -e $LINE1" \c"
fi
#
read -t 60 ANSWER
done
#
unset LINE1
unset LINE2
#
} #End of get_answer function
要问脚本用户删除哪个账户,你需要设置一些变量,然后调用get_answer函数。使用新函数让脚本代码清爽了许多。
LINE1="Please enter the username of the user "
LINE2="account you wish to delete from system:"
get_answer
USER_ACCOUNT=$ANSWER
- 验证输入的用户名
鉴于可能存在输入错误,应该验证一下输入的用户账户。这很容易,因为我们已经有了提问的代码。
LINE1="Is $USER_ACCOUNT the user account "
LINE2="you wish to delete from the system? [y/n]"
get_answer
在提出问题之后,脚本必须处理答案。变量ANSWER再次将脚本用户的回答带回问题中。 如果用户回答了yes,就得到了要删除的正确用户账户,脚本也可以继续执行。你可以用case 语句(参见第12章)来处理答案。case语句部分必须精心编码,这样它才会检查yes的多种输入方式。
case $ANSWER in
y|Y|YES|yes|Yes|yEs|yeS|YEs|yES )
#
;;
*)
echo
echo "Because the account, $USER_ACCOUNT, is not "
echo "the one you wish to delete, we are leaving the script..."
echo
exit
;;
esac
这个脚本有时需要处理很多次用户的yes/no回答。因此,创建一个函数来处理这个任务是有意义的。只要对前面的代码作很少的改动就可以了。必须声明函数名,还要给case语句中加两 个变量, EXIT_LINE1 和 EXIT_LINE2 。这些修改以及最后的一些变量清理工作就是process_answer函数的全部。
function process_answer {
#
case $ANSWER in
y|Y|YES|yes|Yes|yEs|yeS|YEs|yES )
;;
*)
echo
echo $EXIT_LINE1
echo $EXIT_LINE2
echo
exit
;;
esac
#
unset EXIT_LINE1
unset EXIT_LINE2
#
} #End of process_answer function
现在只用调用函数就可以处理答案了。
EXIT_LINE1="Because the account, $USER_ACCOUNT, is not "
EXIT_LINE2="the one you wish to delete, we are leaving the script..."
process_answer
- 确定账户是否存在
用户已经给了我们要删除的账户名并且验证过了。现在最好核对一下这个用户账户在系统上是否真实存在。还有,最好将完整的账户记录显示给脚本用户,核对这是不是真的要删除的那个账户。要完成这些工作,需使用变量USER_ACCOUNT_RECORD,将它设成grep(参见第4章)在/etc/passwd文件中查找该用户账户的输出。-w选项允许你对这个特定用户账户进行精确匹配。
USER_ACCOUNT_RECORD=$(cat /etc/passwd | grep -w $USER_ACCOUNT)
如果在/etc/passwd中没找到用户账户记录,那意味着这个账户已被删除或者从未存在过。不管是哪种情况,都必须通知脚本用户,然后退出脚本。grep命令的退出状态码可以在这里帮到我们。如果没找到这条账户记录,?变量会被设成1。
if [ $? -eq 1 ]
then
echo
echo "Account, $USER_ACCOUNT, not found. "
echo "Leaving the script..."
echo
exit
fi
如果找到了这条记录,你仍然需要验证这个脚本用户是不是正确的账户。我们先前建立的函数在这里就能发挥作用了!你要做的只是设置正确的变量并调用函数。
echo "I found this record:"
echo $USER_ACCOUNT_RECORD
echo
#
LINE1="Is this the correct User Account? [y/n]"
get_answer
#
EXIT_LINE1="Because the account, $USER_ACCOUNT, is not"
EXIT_LINE2="the one you wish to delete, we are leaving the script..."
process_answer
- 删除属于账户的进程
到目前为止,你已经得到并验证了要删除的用户账户的正确名称。为了从系统上删除该用户账户,这个账户不能拥有任何当前处于运行中的进程。因此,下一步就是查找并终止这些进程。这会稍微麻烦一些。
查找用户进程较为简单。这里脚本可以用ps命令(参见第4章)和-u选项来定位属于该账户的所有处于运行中的进程。可以将输出重定向到/dev/null,这样用户就看不到任何输出信息了。这样做很方便,因为如果没有找到相关进程,ps命令只会显示出一个标题,就会把脚本用户搞糊涂的。
ps -u $USER_ACCOUNT >/dev/null #Are user processes running?
可以用ps命令的退出状态码和case结构来决定下一步做什么。
case $? in
1) # No processes running for this User Account
#
echo "There are no processes for this account currently running."
echo
;;
0) # Processes running for this User Account.
# Ask Script User if wants us to kill the processes.
#
echo "$USER_ACCOUNT has the following processes running: "
echo
ps -u $USER_ACCOUNT
#
LINE1="Would you like me to kill the process(es)? [y/n]"
get_answer
#
[...]
;;
esac
如果ps命令的退出状态码返回了1,那么表明系统上没有属于该用户账户的进程在运行。但如果退出状态码返回了0,那么系统上有属于该账户的进程在运行。在这种情况下,脚本需要询问脚本用户是否要杀死这些进程。可以用get_answer函数来完成这个任务。
你可能会认为脚本下一步就是调用process_answer函数。很遗憾, 接下来的任务对process_answer来说太复杂了。你需要嵌入另一个case语句来处理脚本用户的答案。case语 句的第一部分看起来和process_answer函数很像。
case $ANSWER in
y | Y | YES | yes | Yes | yEs | yeS | YEs | yES) # If user answers "yes",
#kill User Account processes.
[...]
;;
*) # If user answers anything but "yes", do not kill.
echo
echo "Will not kill the process(es)"
echo
;;
esac
可以看出,case语句本身并没什么特别的。值得留意的是case语句的yes部分。在这里需要杀死该用户账户的进程。要实现这个目标,得使用三条命令。首先需要再用一次ps命令,收集当前处于运行状态、属于该用户账户的进程ID(PID)。命令的输出被保存在变量COMMAND_1中。
COMMAND_1="ps -u $USER_ACCOUNT --no-heading"
第二条命令用来提取PID。下面这条简单的gawk命令(参见第19章)可以从ps命令输出中提取第一个字段,而这个字段恰好就是PID。
gawk '{print $1}'
第三条命令是xargs,这个命令还没讲过。该命令可以构建并执行来自标准输入STDIN(参见第15章)的命令。它非常适合用在管道的末尾处。xargs命令负责杀死PID所对应的进程。
COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"
xargs命令被保存在变量COMMAND_3中。选项-d指明使用什么样的分隔符。换句话说,既然xargs命令接收多个项作为输入,那么各个项之间要怎么区分呢?在这里,\n(换行符)被作为各项的分隔符。当每个PID发送给xargs时,它将PID作为单个项来处理。又因为xargs命令被赋给了一个变量,所以\n中的反斜杠(\)必须再加上另一个反斜杠(\)进行转义。
注意,在处理PID时,xargs命令需要使用命令的完整路径名。sudo命令和kill命令(参见第4章)用于杀死用户账户的运行进程。另外还注意到kill命令使用了信号-9。
这三条命令通过管道串联在了一起。ps命令生成了处于运行状态的用户进程列表,其中包括每个进程的PID。gawk命令将ps命令的标准输出(STDOUT)作为自己的STDIN,然后从中只提取出PID(参见第15章)。xargs命令将gawk命令生成的每个PID作为STDIN,创建并执行kill 命令,杀死用户所有的运行进程。这个命令管道如下。
$COMMAND_1 | gawk '{print $1}' | $COMMAND_3
因此,用于杀死用户账户所有的运行进程的完整的case语句如下所示。
case $ANSWER in
y|Y|YES|yes|Yes|yEs|yeS|YEs|yES ) # If user answers "yes",
#kill User Account processes.
echo
echo "Killing off process(es)..."
#
# List user processes running code in variable, COMMAND_1
COMMAND_1="ps -u $USER_ACCOUNT --no-heading"
#
# Create command to kill proccess in variable, COMMAND_3
COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"
#
# Kill processes via piping commands together
$COMMAND_1 | gawk '{print $1}' | $COMMAND_3
#
echo
echo "Process(es) killed."
;;
这是目前为止脚本中最复杂的部分!现在用户账户所拥有的进程都已经被杀死了,脚本可以进行下一步:找出属于用户账户的所有文件。
- 查找属于账户的文件
在从系统上删除用户账户时,最好将属于该用户的所有文件归档。另外,还有一点比较重要的是,得删除这些文件或将文件的所属关系分配给其他账户。如果你要删除的账户的UID是1003, 而你没有删除或修改它们的所属关系,那么下一个创建的UID为1003的账户会拥有这些文件!在这种情况下显然会出现安全隐患。
脚本Delete_User.sh不会替你大包大揽,但它会创建一个在Daily_Archive.sh脚本中作为备份配置文件的报告。可以用这个报告帮助你删除文件或重新分配文件的所属关系。
要找到用户文件,你可以用find命令。find命令用-u选项查找整个文件系统,它能够准确查找到属于该用户的所有文件。该命令如下:
find / -user $USER_ACCOUNT > $REPORT_FILE
相比处理用户账户的进程,这非常简单。Delete_User.sh脚本接下来的工作就是删除用户账户。
- 删除账户
对删除系统中的用户账户慎之又慎总是好事。因此,你应该再问一次脚本用户是否真的想删除该账户:
LINE1="Remove $User_Account's account from system? [y/n]"
get_answer
#
EXIT_LINE1="Since you do not wish to remove the user account,"
EXIT_LINE2="$USER_ACCOUNT at this time, exiting the script..."
process_answer
最后就是脚本的主要目的了:从系统中真正地删除该用户账户。这里用到了userdel命令(参见第7章)。
userdel $USER_ACCOUNT
现在万事皆备,可以将它们一起拼成一个完整的实用脚本工具了。
24.2.2 创建脚本
记住,Delete_User.sh脚本跟用户的互动很多。因此,有大量的提示能在脚本执行时告诉用户正在做什么是很重要的。
在脚本的顶部声明了两个函数,get_answer和process_answer。脚本通过四个步骤删除 用户:获得并确认用户账户名,查找和终止用户的进程,创建一份属于该用户账户的所有文件的报告,删除用户账户。
窍门 如果你刚开始编写脚本,在面对一个完整的脚本代码时(你马上就会看到),要养成通读整个脚本的习惯。这种习惯能够增进你的脚本编写技巧。
下面是完整的Delete_User.sh脚本:
#!/bin/bash
#
#Delete_User - Automates the 4 steps to remove an account
#
###############################################################
# Define Functions
#
#####################################################
function get_answer {
#
unset ANSWER
ASK_COUNT=0
#
while [ -z "$ANSWER" ]; do #While no answer is given, keep asking.
ASK_COUNT=$(($ASK_COUNT + 1))
#
case $ASK_COUNT in #If user gives no answer in time allotted
2)
echo
echo "Please answer the question."
echo
;;
3)
echo
echo "One last try...please answer the question."
echo
;;
4)
echo
echo "Since you refuse to answer the question..."
echo "exiting program."
echo
#
exit
;;
esac
#
echo
#
if [ -n "$LINE2" ]; then #Print 2 lines
echo $LINE1
echo -e $LINE2" \c"
else #Print 1 line
echo -e $LINE1" \c"
fi
#
# Allow 60 seconds to answer before time-out
read -t 60 ANSWER
done
# Do a little variable clean-up
unset LINE1
unset LINE2
#
} #End of get_answer function
#
#####################################################
function process_answer {
#
case $ANSWER in
y | Y | YES | yes | Yes | yEs | yeS | YEs | yES)
# If user answers "yes", do nothing.
;;
*)
# If user answers anything but "yes", exit script
echo
echo $EXIT_LINE1
echo $EXIT_LINE2
echo
exit
;;
esac
#
# Do a little variable clean-up
#
unset EXIT_LINE1
unset EXIT_LINE2
#
} #End of process_answer function
#
##############################################
# End of Function Definitions
#
############# Main Script ####################
# Get name of User Account to check
#
echo "Step #1 - Determine User Account name to Delete "
echo
LINE1="Please enter the username of the user "
LINE2="account you wish to delete from system:"
get_answer
USER_ACCOUNT=$ANSWER
#
# Double check with script user that this is the correct User Account
#
LINE1="Is $USER_ACCOUNT the user account "
LINE2="you wish to delete from the system? [y/n]"
get_answer
#
# Call process_answer funtion:
# if user answers anything but "yes", exit script
#
EXIT_LINE1="Because the account, $USER_ACCOUNT, is not "
EXIT_LINE2="the one you wish to delete, we are leaving the script..."
process_answer
#
################################################################
# Check that USER_ACCOUNT is really an account on the system
#
USER_ACCOUNT_RECORD=$(cat /etc/passwd | grep -w $USER_ACCOUNT)
#
if [ $? -eq 1 ]; then # If the account is not found, exit script
echo
echo "Account, $USER_ACCOUNT, not found. "
echo "Leaving the script..."
echo
exit
fi
#
echo
echo "I found this record:"
echo $USER_ACCOUNT_RECORD
#
LINE1="Is this the correct User Account? [y/n]"
get_answer
#
#
# Call process_answer function:
# if user answers anything but "yes", exit script
#
EXIT_LINE1="Because the account, $USER_ACCOUNT, is not "
EXIT_LINE2="the one you wish to delete, we are leaving the script..."
process_answer
#
##################################################################
# Search for any running processes that belong to the User Account
#
echo
echo "Step #2 - Find process on system belonging to user account"
echo
#
ps -u $USER_ACCOUNT >/dev/null #Are user processes running?
#
case $? in
1) # No processes running for this User Account
#
echo "There are no processes for this account currently running."
echo
;;
0) # Processes running for this User Account.
# Ask Script User if wants us to kill the processes.
#
echo "$USER_ACCOUNT has the following processes running: "
echo
ps -u $USER_ACCOUNT
#
LINE1="Would you like me to kill the process(es)? [y/n]"
get_answer
#
case $ANSWER in
y | Y | YES | yes | Yes | yEs | yeS | YEs | yES) # If user answers "yes",
# kill User Account processes.
#
echo
echo "Killing off process(es)..."
#
# List user processes running code in variable, COMMAND_1
COMMAND_1="ps -u $USER_ACCOUNT --no-heading"
#
# Create command to kill proccess in variable, COMMAND_3
COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"
#
# Kill processes via piping commands together
$COMMAND_1 | gawk '{print $1}' | $COMMAND_3
#
echo
echo "Process(es) killed."
;;
*) # If user answers anything but "yes", do not kill.
echo
echo "Will not kill the process(es)"
echo
;;
esac
;;
esac
#################################################################
# Create a report of all files owned by User Account
#
echo
echo "Step #3 - Find files on system belonging to user account"
echo
echo "Creating a report of all files owned by $USER_ACCOUNT."
echo
echo "It is recommended that you backup/archive these files,"
echo "and then do one of two things:"
echo " 1) Delete the files"
echo " 2) Change the files' ownership to a current user account."
echo
echo "Please wait. This may take a while..."
#
REPORT_DATE=$(date +%y%m%d)
REPORT_FILE=$USER_ACCOUNT"_Files_"$REPORT_DATE".rpt"
#
find / -user $USER_ACCOUNT >$REPORT_FILE 2>/dev/null
#
echo
echo "Report is complete."
echo "Name of report: $REPORT_FILE"
echo "Location of report: $(pwd)"
echo
####################################
# Remove User Account
echo
echo "Step #4 - Remove user account"
echo
#
LINE1="Remove $USER_ACCOUNT's account from system? [y/n]"
get_answer
#
# Call process_answer function:
# if user answers anything but "yes", exit script
#
EXIT_LINE1="Since you do not wish to remove the user account,"
EXIT_LINE2="$USER_ACCOUNT at this time, exiting the script..."
process_answer
#
userdel $USER_ACCOUNT #delete user account
echo
echo "User account, $USER_ACCOUNT, has been removed"
echo
#
exit
工作量颇大!但Delete_User.sh脚本是非常棒的省时工具,会帮你避免很多删除用户账户时出现的琐碎问题。
24.2.3 运行脚本
由于被设计成了一个交互式脚本,Delete_User.sh脚本不应放入cron表中。但是,保证它能按期望工作仍然很重要。
24
说明 要运行这种脚本,你必须以root用户账户的身份登录,或者使用sudo命令以root用户账户身份运行脚本。
在测试脚本前,需要为脚本文件设置适合的权限。
$ chmod u+x Delete_User.sh
$
$ ls -l Delete_User.sh
-rwxr--r--. 1 Christine Christine 6413 Sep 2 14:20 Delete_User.sh
$
我们会通过删除一个系统上临时设置的consultant账户来测试这个脚本。
$ sudo ./Delete_User.sh
[sudo] password for Christine:
Step #1 - Determine User Account name to Delete
Please enter the username of the user
account you wish to delete from system: Consultant
Is Consultant the user account
you wish to delete from the system? [y/n]
Please answer the question.
Is Consultant the user account
you wish to delete from the system? [y/n] y
I found this record:
Consultant:x:504:506::/home/Consultant:/bin/bash
Is this the correct User Account? [y/n] yes
Step #2 - Find process on system belonging to user account
Consultant has the following processes running:
PID TTY TIME CMD
5443 pts/0 00:00:00 bash
5444 pts/0 00:00:00 sleep
Would you like me to kill the process(es)? [y/n] Yes
Killing off process(es)...
Process(es) killed.
Step #3 - Find files on system belonging to user account
Creating a report of all files owned by Consultant.
It is recommended that you backup/archive these files,
and then do one of two things:
1) Delete the files
2) Change the files' ownership to a current user account.
Please wait. This may take a while...
Report is complete.
Name of report: Consultant_Files_140902.rpt
Location of report: /home/Christine
Step #4 - Remove user account
Remove Consultant's account from system? [y/n] y
User account, Consultant, has been removed
$
$ ls Consultant*.rpt
Consultant_Files_140902.rpt
$
$ cat Consultant_Files_140902.rpt
/home/Consultant
/home/Consultant/Project_393
/home/Consultant/Project_393/393_revisionQ.py
/home/Consultant/Project_393/393_Final.py
[...]
/home/Consultant/.bashrc
/var/spool/mail/Consultant
$
$ grep Consultant /etc/passwd
$
脚本运行良好!注意,我们是使用sudo来运行脚本的,因为删除账户需要超级用户权限。另外还通过延迟回答下列问题测试了read的超时功能。
Is Consultant the user account
you wish to delete from the system? [y/n]
Please answer the question.
我们在不同的问题中使用了不同形式的yes进行回答,以确保case语句的测试功正常。最后, 脚本找出了用户Consultant所有的文件,并将其写入报告文件中,然后删除了该用户。
现在你已经拥有了一个在删除用户账户时能够辅助你的脚本实用工具。更妙的是你还可以修改它来满足组织的需要!
24.3 监测磁盘空间
对多用户Linux系统来说,最大的一个问题就是可用磁盘空间的总量。在有些情况下,比如在文件共享服务器上,磁盘空间很可能会因为一个粗心的用户而被立刻用完。
24
窍门 如果你的Linux系统应用于生产环境,那么就不能依赖磁盘空间报告来避免服务器的磁盘空间被填满。应该考虑使用磁盘配额。如果已经安装了quota软件包,可以在shell提示符下输入man –k quota获得有关磁盘限额管理的更多信息。如果没有安装这个软件包,可以使用任何你喜欢的搜索引擎获取进一步的信息。
这个shell脚本工具会帮你找出指定目录中磁盘空间使用量位居前十名的用户。它会生成一个以日期命名的报告,使得磁盘空间使用量可以监测。
24.3.1 需要的功能
你要用到的第一个工具是du命令(参见第4章)。该命令能够显示出单个文件和目录的磁盘使用情况。-s选项用来总结目录一级的整体使用状况。这在计算单个用户使用的总体磁盘空间时很方便。下面的例子是使用du命令总结/home目录下每个用户的$HOME目录的磁盘占用情况。
$ sudo du -s /home/*
[sudo] password for Christine:
4204 /home/Christine
56 /home/Consultant
52 /home/Development
4 /home/NoSuchUser
96 /home/Samantha
36 /home/Timothy
1024 /home/user1
$
-s选项能够很好地处理用户的$HOME目录,但如果我们要查看系统目录(比如/var/log)的磁盘使用情况呢?
$ sudo du -s /var/log/*
4 /var/log/anaconda.ifcfg.log
20 /var/log/anaconda.log
32 /var/log/anaconda.program.log
108 /var/log/anaconda.storage.log
40 /var/log/anaconda.syslog
56 /var/log/anaconda.xlog
116 /var/log/anaconda.yum.log
4392 /var/log/audit
4 /var/log/boot.log
[...]
$
这个列表很快就变得过于琐碎。这里,-S(大写的S)选项能更适合我们的目的,它为每个目录和子目录分别提供了总计信息。这样你就能快速地定位问题的根源。
$ sudo du -S /var/log/
4 /var/log/ppp
4 /var/log/sssd
3020 /var/log/sa
80 /var/log/prelink
4 /var/log/samba/old
4 /var/log/samba
4 /var/log/ntpstats
4 /var/log/cups
4392 /var/log/audit
420 /var/log/gdm
4 /var/log/httpd
152 /var/log/ConsoleKit
2976 /var/log/
$
由于我们感兴趣的是占用磁盘空间最多的目录,所以需要使用sort命令对du产生的输出进行排序(参见第4章)。
$ sudo du -S /var/log/ | sort -rn
4392 /var/log/audit
3020 /var/log/sa
2976 /var/log/
420 /var/log/gdm
152 /var/log/ConsoleKit
80 /var/log/prelink
4 /var/log/sssd
4 /var/log/samba/old
4 /var/log/samba
4 /var/log/ppp
4 /var/log/ntpstats
4 /var/log/httpd
4 /var/log/cups
$
-n选项允许按数字排序。-r选项会先列出最大数字(逆序)。这对于找出占用磁盘空间最多的用户很有用。
sed编辑器可以让这个列表更容易读懂。我们要关注的是磁盘用量的前10名用户,所以当到了第11行时,sed会删除列表的剩余部分。下一步是给列表中的每行加一个行号。第19章演示过如何使用sed的等号命令(=)来加入行号。要让行号和磁盘空间文本位于同一行,可以用N命令将文本行合并在一起,跟我们在第21章中的处理方法一样。所需的sed命令如下。
sed '{11,$D; =}' |
sed 'N; s/\n/ /' |
现在可以用gawk命令清理输出了(参见第22章)。sed编辑器的输出会通过管道输出到gawk 命令,然后用printf函数打印出来。
gawk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}'
在行号后面,我们加了一个冒号(:),还给输出的每行文本的字段间放了一个制表符。这样就能得到一个格式精致的磁盘空间用量前10名的用户列表。
$ sudo du -S /var/log/ |
> sort -rn |
> sed '{11,$D; =}' |
> sed 'N; s/\n/ /' |
> gawk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}'
[sudo] password for Christine:
1: 4396 /var/log/audit
2: 3024 /var/log/sa
3: 2976 /var/log/
4: 420 /var/log/gdm
5: 152 /var/log/ConsoleKit
6: 80 /var/log/prelink
7: 4 /var/log/sssd
8: 4 /var/log/samba/old
9: 4 /var/log/samba
10: 4 /var/log/ppp
$
现在你已经上手啦!下一步就是用这些信息创建脚本。
24.3.2 创建脚本
为了节省时间和精力,这个脚本会为多个指定目录创建报告。我们用一个叫作CHECK_DIRECTORIES的变量来完成这一任务。出于演示的目的,该变量只设置为包含两个目录。
CHECK_DIRECTORIES=" /var/log /home"
脚本使用for循环来对变量中列出的每个目录执行du命令。这个方法用来读取和处理列表中的值(参见第13章)。每次for循环都会遍历变量CHECK_DIRECTORIES中的值列表,它会将列表中的下一个值赋给DIR_CHECK变量。
for DIR_CHECK in $CHECK_DIRECTORIES
do
[...]
du -S $DIR_CHECK
[...]
done
为了方便识别,我们用date命令给报告的文件名加个日期戳。脚本用exec命令(参见第15 章)将它的输出重定向到加带日期戳的报告文件中。
DATE=$(date '+%m%d%y')
exec > disk_space_$DATE.rpt
为了生成格式精致的报告,这个脚本会用echo命令来输出一些报告标题。
echo "Top Ten Disk Space Usage"
echo "for $CHECK_DIRECTORIES Directories"
现在让我们看一下将这个脚本的各部分组合在一起会是什么样子。
#!/bin/bash
#
# Big_Users - Find big disk space users in various directories
###############################################################
# Parameters for Script
#
CHECK_DIRECTORIES=" /var/log /home" #Directories to check
#
############## Main Script #################################
#
DATE=$(date '+%m%d%y') #Date for report file
#
exec >disk_space_$DATE.rpt #Make report file STDOUT
#
echo "Top Ten Disk Space Usage" #Report header
echo "for $CHECK_DIRECTORIES Directories"
#
for DIR_CHECK in $CHECK_DIRECTORIES; do #Loop to du directories
echo ""
echo "The $DIR_CHECK Directory:" #Directory header
#
# Create a listing of top ten disk space users in this dir
du -S $DIR_CHECK 2>/dev/null |
sort -rn |
sed '{11,$D; =}' |
sed 'N; s/\n/ /' |
gawk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}'
#
done #End of loop
#
exit
现在你已经得到完整的脚本了。这个简单的shell脚本会为你选择的每个目录创建一个包含日期戳的磁盘空间用量前10名的用户报告。
24.3.3 运行脚本
在让Big_Users脚本自动运行之前,你会想手动测试几次,以保证它如你期望的那样运行。如你所知,在测试前必须为脚本文件设置适合的权限。不过在这里我们使用了bash命令,chmod u+x就不需要了。
$ ls -l Big_Users.sh
-rw-r--r--. 1 Christine Christine 910 Sep 3 08:43 Big_Users.sh
$
$ sudo bash Big_Users.sh
[sudo] password for Christine:
$
$ ls disk_space*.rpt
disk_space_090314.rpt
$
$ cat disk_space_090314.rpt
Top Ten Disk Space Usage
for /var/log /home Directories
The /var/log Directory:
1: 4496 /var/log/audit
2: 3056 /var/log
3: 3032 /var/log/sa
4: 480 /var/log/gdm
5: 152 /var/log/ConsoleKit
6: 80 /var/log/prelink
7: 4 /var/log/sssd
8: 4 /var/log/samba/old
9: 4 /var/log/samba
10: 4 /var/log/ppp
The /home Directory:
1: 34084 /home/Christine/Documents/temp/reports/archive
2: 14372 /home/Christine/Documents/temp/reports
3: 4440 /home/Timothy/Project__42/log/universe
4: 4440 /home/Timothy/Project_254/Old_Data/revision.56
5: 4440 /home/Christine/Documents/temp/reports/report.txt
6: 3012 /home/Timothy/Project__42/log
7: 3012 /home/Timothy/Project_254/Old_Data/data2039432
8: 2968 /home/Timothy/Project__42/log/answer
9: 2968 /home/Timothy/Project_254/Old_Data/data2039432/answer
10: 2968 /home/Christine/Documents/temp/reports/answer
$
完全没有问题!现在你可以让这个脚本在需要时自动运行了,可以用cron表来实现(参见第16章)。在周一一大早运行这个脚本是个不错的主意。这样你就可以在周一早上一边喝咖啡一边浏览磁盘使用情况周报了。
24.4 小结
本章充分利用了本书介绍的一些shell脚本编程知识来创建Linux实用工具。在负责Linux系统时,不管它是大型多用户系统,还是你自己的系统,都有很多的事情要考虑。与其手动运行命令, 不如创建shell脚本工具来替你完成工作。
本章首先带你逐步了解使用shell脚本归档和备份Linux系统上的数据文件。tar命令是归档数据的常用命令。这部分演示了如何在shell脚本中用它来创建归档文件,以及如何在归档目录中管理归档文件。
接下来介绍了使用shell脚本删除用户账户的四个步骤。为脚本中重复的shell代码创建函数会让代码更易于阅读和修改。这个脚本由多个不同的结构化命令组成,例如case和while命令。这部分还介绍了用于cron表脚本和交互式脚本在结构上的差异。
本章最后演示了如何用du命令来确定磁盘空间使用情况。sed和gawk命令用于提取数据中的特定信息。将命令的输出传给sed和gawk来分析数据是shell脚本中的一个常见功能,所以最好知道该怎么做。
接下来还会讲到更多的高级shell脚本,涉及数据库、Web和电子邮件等。
