1. 编写简单的脚本实用工具
对 Linux 系统管理员而言,没什么比编写脚本实用工具更有意义。Linux 系统管理员每天都会有各种各样的任务,从监测磁盘空间到备份重要文件再到管理用户账户。shell脚本实用工具可以让这些工作轻松许多。
1.1 归档
不管负责的是商业环境还是家用环境的 Linux 系统,丢失数据都是一场灾难。为了防止这种倒霉事,最好是定时进行备份(或者是归档)。
如果正在用 Linux 系统作为一个重要项目的平台,可以创建一个 shell 脚本来自动获取特定目录的快照。在配置文件中指定所涉及的目录,这样一来,在
项目发生变化时,就可以做出对应的修改。这有助于避免把时间耗在恢复主归档文件上。
本节介绍如何创建自动化 shell 脚本来获取指定目录的快照并保留旧数据的归档。
1.1.1 需要的功能
-----------------------------------------------------------------------------------------------------------------------------------------
Linux 中归档数据的主要工具是 tar 命令。tar 命令可以将整个目录归档到单个文件中。
示例:
[devalone@devalone ~]$ ll project/
总用量 12
-rwxr-xr-x. 1 devalone devalone 707 7月 14 11:58 JavadocTagTest.java
-rwxr-xr-x. 1 devalone devalone 659 7月 14 11:58 JavadocTest.java
-rwxr-xr-x. 1 devalone devalone 433 7月 14 11:58 Test.java
[devalone@devalone ~]$ tar -cf archive.tar /home/devalone/project/*.*
tar: 从成员名中删除开头的“/”
[devalone@devalone ~]$ ll -l archive.tar
-rw-rw-r--. 1 devalone devalone 10240 7月 14 11:59 archive.tar
tar 命令会显示一条警告消息,表明它删除了路径名开头的斜线,将路径从绝对路径名变成相对路径名。这样就可以将 tar 归档文件解压到文件系统中的
任何地方了。如果不想在脚本中出现这条消息。这种情况可以通过将 STDERR 重定向到 /dev/null文件来实现:
[devalone@devalone ~]$ tar -cf archive.tar /home/devalone/project/*.* 2>/dev/null
[devalone@devalone ~]$
由于 tar归档文件会消耗大量的磁盘空间,最好能够压缩一下该文件。这只需要加一个 -z 选项就行了。它会将 tar归档文件压缩成 gzip 格式的tar文件,
这种文件也叫作 tarball。别忘了使用恰当的文件扩展名来表示这是个 tarball,用 .tar.gz或 .tgz都行。
示例:
[devalone@devalone ~]$ tar -zcf archive.tar.gz /home/devalone/project/*.* 2>/dev/null
[devalone@devalone ~]$ ll -l archive.tar*
-rw-rw-r--. 1 devalone devalone 10240 7月 14 12:04 archive.tar
-rw-rw-r--. 1 devalone devalone 880 7月 14 12:07 archive.tar.gz
现在已经完成了归档脚本的主要部分。
不需要为待备份的新目录或文件修改或编写新的归档脚本,而是可以借助于配置文件。配置文件应该包含希望进行归档的每个目录或文件。
示例:
[devalone@devalone 24]$ cat files_to_backup
/home/devalone/project
/home/devalone/test
/home/devalone/devel
/home/devalone/does_not_exist #用于测试不存在的文件
可以让脚本读取配置文件,然后将每个目录名加到归档列表中。要实现这一点,只需要使用 read 命令来读取该文件中的每一条记录就行了。不过不用像之前
那样通过管道将 cat 命令的输出传给 while 循环,在这个脚本中我们使用 exec 命令来重定向标准输入(STDIN),用法如下:
exec < $CONFIG_FILE
read FILE_NAME
注意,我们为归档配置文件使用了一个变量,CONFIG_FILE 。配置文件中每一条记录都会被读入。只要 read 命令在配置文件中发现还有记录可读,它就会
在 ? 变量中返回一个表示成功的退出状态码 0。可以将它作为 while 循环的测试条件来读取配置文件中的所有记录:
while [ $? -eq 0 ]
do
[...]
read FILE_NAME
done
一旦 read 命令到了配置文件的末尾,它就会返回一个非零状态码。这时脚本会退出 while 循环。
在 while 循环中,需要做两件事。首先,必须将目录名加到归档列表中。更重要的是要检查那个目录是否存在。很可能从文件系统中删除了一个目录却忘了
更新归档配置文件。可以用一个简单的 if 语句来检查目录存在与否。如果目录存在,它会被加入要归档目录列表 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。这样,这个脚本就可以告诉在归档配置文件中哪行中含有不正确或缺失的
文件或目录。
1.1.2 创建逐日归档文件的存放位置
-----------------------------------------------------------------------------------------------------------------------------------------
如果只是备份少量文件,那么将这些归档文件放在用户的个人目录中就行了。但如果要对多个目录进行备份,最好还是创建一个集中归档仓库目录。
示例:
[devalone@devalone 24]$ sudo mkdir /archive
[devalone@devalone 24]$ ls -ld /archive
drwxr-xr-x. 2 root root 4096 7月 14 12:55 /archive
创建好集中归档目录后,需要授予某些用户访问权限。如果忘记了这一点,在该目录下创建文件时就会出错。
示例:
[devalone@devalone 24]$ mv files_to_backup /archive/
mv: 无法创建普通文件'/archive/files_to_backup': Permission denied
可以通过 sudo 命令或者创建一个用户组的方式,为需要在集中归档目录中创建文件的用户授权。可以创建一个特殊的用户组 Archivers:
[devalone@devalone 24]$ sudo groupadd Archivers
[devalone@devalone 24]$ sudo chgrp Archivers /archive
[
devalone@devalone 24]$ ls -ld /archive/
drwxr-xr-x. 2 root Archivers 4096 7月 14 12:55 /archive/
[devalone@devalone 24]$ sudo usermod -aG Archivers devalone
[devalone@devalone 24]$ sudo chmod 775 /archive
[devalone@devalone 24]$ ls -ld /archive
drwxrwxr-x. 2 root Archivers 4096 7月 14 12:55 /archive
将用户添加到 Archivers组后,用户必须先退出然后再登录,才能使组成员关系生效。现在只要是该组的成员,无需超级用户权限就可以在目录中创建文件:
[devalone@devalone 24]$ mv files_to_backup /archive/
[devalone@devalone 24]$ ls -l /archive/
总用量 4
-rw-rw-r--. 1 devalone devalone 66 7月 14 12:13 files_to_backup
记住,Archivers 组的所有用户都可以在归档目录中添加和删除文件。为了避免组用户删除他人的归档文件,最好还是把目录的粘滞位加上。
1.1.3 创建按日归档的脚本
-----------------------------------------------------------------------------------------------------------------------------------------
daily_archive.sh 脚本会自动在指定位置创建一个归档,使用当前日期来唯一标识该文件。下面是脚本中的对应部分的代码:
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.sh 脚本内容如下:
[devalone@devalone 24]$ cat daily_archive.sh
#!/bin/bash
#
# dialy_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 ] # make sure the config file still exists
then # if it exist, 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 stdin to name of config file
#
read FILE_NAME # read 1st record
while [ $? -eq 0 ] # create list of files to backup
do
# 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 does'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 archivelist..."
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 -zcf $DESTINATION $FILE_LIST 2> /dev/null
#
echo "archive completed"
echo "resulting archive file is: $DESTINATION"
echo
#
exit
1.1.4 运行按日归档的脚本
-----------------------------------------------------------------------------------------------------------------------------------------
在测试脚本之前,别忘了修改脚本文件的权限。必须赋予文件属主可执行权限(x)才能够运行脚本。
示例:
[devalone@devalone 24]$ ll daily_archive.sh
-rwxrwxr--. 1 devalone devalone 1842 7月 14 14:36 daily_archive.sh
测试运行:
[devalone@devalone 24]$ ./daily_archive.sh
/home/devalone/does_not_exist, does not exist.
obviously, I will not include it in this archive.
it is listed on line 4 of the config file.
continuing to build archivelist...
starting archive...
archive completed
resulting archive file is: /archive/archive180714.tar.gz
[devalone@devalone 24]$ ll /archive/
总用量 487644
-rw-rw-r--. 1 devalone devalone 499336851 7月 14 14:44 archive180714.tar.gz
-rw-rw-r--. 1 devalone devalone 98 7月 14 14:43 files_to_backup
看到这个脚本发现了一个不存在的目录:/home/devalone/does_not_exist。脚本输出说明了这个错误的行在配置文件中的行号 4,然后继续创建列表和归档
数据。现在数据已经稳妥地归档到了 tarball 文件中。
1.1.5 创建按小时归档的脚本
-----------------------------------------------------------------------------------------------------------------------------------------
如果是在文件更改很频繁的高容量生产环境中,那么按日归档可能不够用。如果要将归档频率提高到每小时一次,还要考虑另一个因素。
在按小时备份文件时,如果依然使用 date 命令为每个 tarball 文件加入时间戳,文件名很快就会变得不还看。筛选一个含有如下文件名的目录会很乏味:
archive180714110233.tar.gz
不必将所有的归档文件都放到同一目录中,可以为归档文件创建一个目录层级。归档目录包含了与一年中的各个月份对应的目录,将月的序号作为目录名。
而每月的目录中又包含与当月各天对应的目录(用天的序号作为目录名)。这样只用给每个归档文件加上时间戳,然后将它们放到与月日对应的目录中。
首先,创建新目录/archive/hourly,并设置适当的权限。Archivers组有权在目录中创建归档文件。因此,这个新创建的目录也得修改它的属组以及组权限:
示例:
[devalone@devalone 24]$ sudo mkdir /archive/hourly
[devalone@devalone 24]$ sudo chgrp Archivers /archive/hourly/
[devalone@devalone 24]$ ll -d /archive/hourly/
drwxr-xr-x. 2 root Archivers 4096 7月 14 14:57 /archive/hourly/
[devalone@devalone 24]$ sudo chmod 775 /archive/hourly
[devalone@devalone 24]$ ls -ld /archive/hourly
drwxrwxr-x. 2 root Archivers 4096 7月 14 14:57 /archive/hourly
新目录设置好之后,将按小时归档的配置文件 files_to_backup 移动到该目录中:
[devalone@devalone 24]$ cat /archive/hourly/files_to_backup
/home/devalone/shell_test
/home/devalone/repo
/home/file_does_not_exist #测试不存在的文件
还有个新问题要解决。这个脚本必须自动创建对应每月和每天的目录,如果这些目录已经存在的话,脚本就会报错,这不是我们想要的结果。
mkdir 命令的命令行选项有一个 -p 命令行选项。这个选项允许在单个命令中创建目录和子目录,就算目录已经存在,也不会产生错误消息。
这正是我们的脚本中所需要的。
创建 hourly_archive.sh 脚本如下:
[devalone@devalone 24]$ cat hourly_archive.sh
#!/bin/bash
#
# hourly_archive - Every hour create an archive
#
#################################################################################
#
# set configuration and destination file
#
CONFIG_FILE=/archive/hourly/files_to_backup
#
# set base archive destination location
#
BASEDEST=/archive/hourly
#
# gather current year, day, month & time
YEAR=$(date +%Y)
DAY=$(date +%d)
MONTH=$(date +%m)
TIME=$(date +%H%M)
#
# create archive destination directory
#
mkdir -p $BASEDEST/$YEAR/$MONTH/$DAY
#
# build archive destination file name
#
DESTINATION=$BASEDEST/$YEAR/$MONTH/$DAY/archive$TIME.tar.gz
#
################### main script ######################
#
# check backup config file exists
#
if [ -f $CONFIG_FILE ] # make sure the config file still exists
then # if it exist, 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 stdin to name of config file
#
read FILE_NAME # read 1st record
while [ $? -eq 0 ] # create list of files to backup
do
# 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 does'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 archivelist..."
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 -zcf $DESTINATION $FILE_LIST 2> /dev/null
#
echo "archive completed"
echo "resulting archive file is: $DESTINATION"
echo
#
exit
1.1.6 运行按小时归档的脚本
-----------------------------------------------------------------------------------------------------------------------------------------
运行前修改脚本可执行权限:
[devalone@devalone 24]$ chmod a+x hourly_archive.sh
看下当前时间:
[devalone@devalone 24]$ date +%k%M
1520
运行脚本
[devalone@devalone 24]$ ./hourly_archive.sh
/home/file_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 archivelist...
starting archive...
archive completed
resulting archive file is: /archive/hourly/2018/07/14/archive1520.tar.gz
看到这个脚本发现了一个不存在的目录:/home/file_does_not_exist。脚本输出说明了这个错误的行在配置文件中的行号3,然后继续创建列表和归档数据。
现在数据已经稳妥地归档到了 tarball 文件中。
验证:
[devalone@devalone 24]$ ll /archive/hourly/2018/07/14/
总用量 2868036
-rw-rw-r--. 1 devalone devalone 2936863020 7月 14 15:24 archive1520.tar.gz
删除配置文件中不存在的目录配置。
1.2 管理用户账户
-----------------------------------------------------------------------------------------------------------------------------------------
管理用户账户绝不仅仅是添加、修改和删除账户,还需要考虑安全问题、保留工作的需求以及对账户的精确管理。这可能是一份耗时的工作。在此将介绍另
一个可以证明脚本工具能够促进效率的实例。
1.2.1 需要的功能
-----------------------------------------------------------------------------------------------------------------------------------------
删除账户在管理账户工作中比较复杂。在删除账户时,至少需要4个步骤:
(1) 获得正确的待删除用户账户名;
(2) 杀死正在系统上运行的属于该账户的进程;
(3) 确认系统中属于该账户的所有文件;
(4) 删除该用户账户。
1.2.1.1 获取正确的账户名
-----------------------------------------------------------------------------------------------------------------------------------------
账户删除过程中的第一步最重要:获取待删除的用户账户的正确名称。
由于这是个交互式脚本,所以可以用 read 命令获取账户名称。如果脚本用户一直没有给出答复,可以在 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 循环加 -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 语句是最适合这里的结构化命令。通过给
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
#
现在,这个脚本已经拥有了它所需要的全部结构,可以问用户要删除哪个账户了。在这个脚本中,还需要问用户另外一些问题,可之前只提那么一个问题就
已经是一大堆代码了,因此,将这段代码放到一个函数中,以便在delete_user.sh脚本中重复使用。
1.2.1.2 创建函数获取正确的账户名
-----------------------------------------------------------------------------------------------------------------------------------------
要做的第一件事是声明函数名 get_answer。下一步,用 unset 命令清除脚本用户之前给出的答案。完成这两件事的代码如下:
function get_answer {
#
unset ANSWER
在原来代码中要修改的另一处地方是对用户脚本的提问。这个脚本不会每次都问同一个问题,所以让创建两个新的变量 LINE1 和 LINE2来处理问题:
echo $LINE1
echo -e $LINE2" \c"
然而,并不是每个问题都有两行要显示,有的只要一行。可以用 if 结构解决这个问题。这个函数会测试 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
1.2.1.3 验证输入的用户名
-----------------------------------------------------------------------------------------------------------------------------------------
鉴于可能存在输入错误,应该验证一下输入的用户账户。这很容易,因为我们已经有了提问的代码。
LINE1="Is $USER_ACCOUNT the user account "
LINE2="you wish to delete from the system? [y/n]"
get_answer
在提出问题之后,脚本必须处理答案。变量 ANSWER 再次将脚本用户的回答带回问题中。如果用户回答了 yes,就得到了要删除的正确用户账户,脚本也可以
继续执行。可以用 case 语句来处理答案。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
1.2.1.4 确定账户是否存在
-----------------------------------------------------------------------------------------------------------------------------------------
用户已经给了我们要删除的账户名并且验证过了。现在最好核对一下这个用户账户在系统上是否真实存在。还有,最好将完整的账户记录显示给脚本用户,
核对这是不是真的要删除的那个账户。要完成这些工作,需使用变量 USER_ACCOUNT_RECORD,将它设成 grep 在 /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
1.2.1.5 删除属于账户的进程
-----------------------------------------------------------------------------------------------------------------------------------------
到目前为止,你已经得到并验证了要删除的用户账户的正确名称。为了从系统上删除该用户账户,这个账户不能拥有任何当前处于运行中的进程。因此,下
一步就是查找并终止这些进程。
查找用户进程较为简单。这里脚本可以用 ps 命令和 -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 命令可以从 ps 命令输出中提取第一个字段,而这个字段恰好就是 PID。
gawk '{print $1}'
第三条命令是 xargs,该命令可以构建并执行来自标准输入 STDIN 的命令。它非常适合用在管道的末尾处。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 命令用于杀死用户账户的运行进程。另外还注意到kill命令使用了信号-9。
这三条命令通过管道串联在了一起。ps 命令生成了处于运行状态的用户进程列表,其中包括每个进程的 PID。gawk 命令将 ps 命令的标准输出(STDOUT)
作为自己的 STDIN,然后从中只提取出 PID。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."
;;
1.2.1.6 查找属于账户的文件
-----------------------------------------------------------------------------------------------------------------------------------------
在从系统上删除用户账户时,最好将属于该用户的所有文件归档。另外,还有一点比较重要的是,得删除这些文件或将文件的所属关系分配给其他账户。如果
要删除的账户的 UID 是 1003,而没有删除或修改它们的所属关系,那么下一个创建的 UID为 1003的账户会拥有这些文件。这种情况下显然会出现安全隐患。
脚本 delete_user.sh 不会大包大揽,但它会创建一个在daily_archive.sh 脚本中作为备份配置文件的报告。可以用这个报告帮助删除文件或重新分配文件
的所属关系。
要找到用户文件,可以用 find 命令。find 命令用 -u 选项查找整个文件系统,它能够准确查找到属于该用户的所有文件。该命令如下:
find / -user $USER_ACCOUNT > $REPORT_FILE
1.2.1.7 删除账户
-----------------------------------------------------------------------------------------------------------------------------------------
对删除系统中的用户账户慎之又慎总是好事。因此,应该再问一次脚本用户是否真的想删除该账户:
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 命令:
userdel $USER_ACCOUNT
1.2.2 创建脚本
-----------------------------------------------------------------------------------------------------------------------------------------
在脚本的顶部声明了两个函数,get_answer 和 process_answer。脚本通过四个步骤删除用户:获得并确认用户账户名,查找和终止用户的进程,创建一份
属于该用户账户的所有文件的报告,删除用户账户。
完整的脚本如下:
[devalone@devalone 24]$ cat delete_user.sh
#!/bin/bash
#
# delete_user - automates the 4 step to remove an account
#
###############################################################
#
# define functions
#
###############################################################
function get_answer {
#
unset ANSWER
ASK_COUNT=0
while [ -z "$ANSWER" ] # while no answer is given, keep asking.
do
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
echo $LINE1
echo -e $LINE2" \c"
else
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 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
#
############################ 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
# if user answers anything but "yes", exit the 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 ] # if the account is not found, exit script
then
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 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 process in variable, COMMAND_3
COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"
#
# kill processes via piping command together
$COMMAND_1 | gawk '{print $1}' | $COMMAND_3
#
echo
echo "Process(es) killed."
;;
*)
# if user answers anything bu "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
1.2.3 运行脚本
-----------------------------------------------------------------------------------------------------------------------------------------
由于被设计成了一个交互式脚本,delete_user.sh 脚本不应放入 cron 表中。但是,保证它能按期望工作仍然很重要。
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
要运行这种脚本,必须以 root 用户账户的身份登录,或者使用 sudo 命令以 root用户账户身份运行脚本。
测试脚本之前,赋予脚本执行权限:
[devalone@devalone 24]$ chmod u+x delete_user.sh
通过删除一个系统上临时设置的 test_user 账户来测试这个脚本:
[devalone@devalone 24]$ sudo ./delete_user.sh
step #1 - determine user account name to delete
Please enter the username of the user
account you wish to delete from system:
Please answer the question.
Please enter the username of the user
account you wish to delete from system: test
Is test the user account
you wish to delete from the system? [y|n] y
Account, test, not found.
Leaving the script...
[devalone@devalone 24]$ sudo ./delete_user.sh
step #1 - determine user account name to delete
Please enter the username of the user
account you wish to delete from system: user_test
Is user_test the user account
you wish to delete from the system? [y|n] y
I found this record:
user_test:x:1001:1001::/home/user_test:/bin/bash
Is this the correct user_test? [y|n] y
Step #2 - find process on system belonging to user account
there no processes for this account currently running.
Step 3 - Find files on system belonging to user account
creating a report of all files owned by user_test.
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: user_test_Files_180714.rpt
Location of report: /home/devalone/study/shell-script/24
Step #4 - remove user account
Remove user_test's account from system? [y|n] y
user account, user_test, has been removed
[devalone@devalone 24]$ grep user_test /etc/passwd
[devalone@devalone 24]$
账户 user_test 被删除。
查看 user_test 所属的文件报告:
[devalone@devalone 24]$ cat user_test_Files_180714.rpt
/home/user_test
/home/user_test/.bash_logout
/home/user_test/.bash_history
/home/user_test/testdir
/home/user_test/file
/home/user_test/.bashrc
/home/user_test/testfile
/home/user_test/.mozilla
/home/user_test/.mozilla/plugins
/home/user_test/.mozilla/extensions
/home/user_test/.zshrc
/home/user_test/.bash_profile
/home/user_test/.emacs
/var/spool/mail/user_test
1.3 监测磁盘空间
-----------------------------------------------------------------------------------------------------------------------------------------
这个 shell 脚本工具会找出指定目录中磁盘空间使用量位居前十名的用户。它会生成一个以日期命名的报告,使得磁盘空间使用量可以监测。
1.3.1 需要的功能
-----------------------------------------------------------------------------------------------------------------------------------------
要用到的第一个工具是 du 命令。该命令能够显示出单个文件和目录的磁盘使用情况。-s 选项用来总结目录一级的整体使用状况。这在计算单个用户使用的
总体磁盘空间时很方便。下面的例子是使用 du 命令总结 /home 目录下每个用户的 $HOME 目录的磁盘占用情况:
[devalone@devalone ~]$ sudo du -s /home/*
47523844 /home/devalone
16 /home/lost+found
437880 /home/user1
-s 选项能够很好地处理用户的 $HOME目录,但如果要查看系统目录(比如 /var/log)的磁盘使用情况:
[devalone@devalone ~]$ sudo du -s /var/log/*
6700 /var/log/anaconda
10576 /var/log/audit
8 /var/log/boot.log
0 /var/log/btmp
0 /var/log/btmp-20180702
4 /var/log/chrony
4 /var/log/cluster
4 /var/log/cups
964 /var/log/dnf.librepo.log
448 /var/log/dnf.librepo.log-20180506
384 /var/log/dnf.librepo.log-20180629
96 /var/log/dnf.librepo.log-20180702
708 /var/log/dnf.librepo.log-20180708
88 /var/log/dnf.log
...
这个列表很快就变得过于琐碎。-S(大写的S)选项能更适合这个目的,它为每个目录和子目录分别提供了总计信息。这样就能快速地定位问题的根源:
[devalone@devalone ~]$ sudo du -S /var/log/
4 /var/log/chrony
4 /var/log/ntpstats
6700 /var/log/anaconda
4 /var/log/cluster
4 /var/log/sssd
4 /var/log/httpd
655616 /var/log/journal/fd73c66c927142dda4afd46e8e2f53e7
8 /var/log/journal
4 /var/log/speech-dispatcher
8 /var/log/hudson
4 /var/log/glusterfs
4 /var/log/libvirt/qemu
4 /var/log/libvirt
10580 /var/log/audit
4 /var/log/cups
4 /var/log/gdm
4 /var/log/samba/old
4 /var/log/samba
4 /var/log/ppp
216 /var/log/vmware
3768 /var/log/
我们感兴趣的是占用磁盘空间最多的目录,所以需要使用 sort 命令对 du 产生的输出进行排序:
[devalone@devalone ~]$ sudo du -S /var/log/ | sort -rn
655616 /var/log/journal/fd73c66c927142dda4afd46e8e2f53e7
10584 /var/log/audit
6700 /var/log/anaconda
3768 /var/log/
216 /var/log/vmware
8 /var/log/journal
8 /var/log/hudson
4 /var/log/sssd
4 /var/log/speech-dispatcher
4 /var/log/samba/old
4 /var/log/samba
4 /var/log/ppp
4 /var/log/ntpstats
4 /var/log/libvirt/qemu
4 /var/log/libvirt
4 /var/log/httpd
4 /var/log/glusterfs
4 /var/log/gdm
4 /var/log/cups
4 /var/log/cluster
4 /var/log/chrony
-n 选项允许按数字排序。-r 选项会先列出最大数字(逆序)。这对于找出占用磁盘空间最多的用户很有用。
sed 编辑器可以让这个列表更容易读懂。我们要关注的是磁盘用量的前 10 名文件,所以当到了第 11 行时,sed 会删除列表的剩余部分。
下一步是给列表中的每行加一个行号。使用 sed 的等号命令(=)来加入行号。要让行号和磁盘空间文本位于同一行,可以用 N 命令将文本行合并在一起,
sed 命令如下:
sed '{11,$D; =}' |
sed 'N; s/\n/ /' |
现在可以用 gawk 命令清理输出了。sed 编辑器的输出会通过管道输出到 gawk 命令,然后用 printf 函数打印出来:
gawk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}'
在行号后面,加了一个冒号(:),还给输出的每行文本的字段间放了一个制表符。这样就能得到一个格式精致的磁盘空间用量前 10 名的文件列表:
[devalone@devalone ~]$ sudo du -S /var/log/ |
> sort -rn |
> sed '{11,$D; =}' |
> sed 'N; s/\n/ /' |
> gawk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}'
1: 655616 /var/log/journal/fd73c66c927142dda4afd46e8e2f53e7
2: 10584 /var/log/audit
3: 6700 /var/log/anaconda
4: 3244 /var/log/
5: 216 /var/log/vmware
6: 8 /var/log/journal
7: 8 /var/log/hudson
8: 4 /var/log/sssd
9: 4 /var/log/speech-dispatcher
10: 4 /var/log/samba/old
1.3.2 创建脚本
-----------------------------------------------------------------------------------------------------------------------------------------
这个脚本会为多个指定目录创建报告。用一个 CHECK_DIRECTORIES 的变量来完成这一任务。出于演示,该变量只设置为包含两个目录:
CHECK_DIRECTORIES="/var/log /home"
脚本使用 for 循环来对变量中列出的每个目录执行 du 命令。这个方法用来读取和处理列表中的值。每次 for 循环都会遍历变量 CHECK_DIRECTORIES 中的
值列表,它会将列表中的下一个值赋给 DIR_CHECK 变量。
for DIR_CHECK in $CHECK_DIRECTORIES
do
[...]
du -S $DIR_CHECK
[...]
done
为了方便识别,用 date 命令给报告的文件名加个日期戳。用 exec 命令将脚本的输出重定向到加带日期戳的报告文件中:
DATE=$(date '+%m%d%y')
exec > disk_space_$DATE.rpt
为了生成格式精致的报告,脚本会用 echo 命令来输出一些报告标题:
echo "Top Ten Disk Space Usage"
echo "for $CHECK_DIRECTORIES Directories"
完整的脚本代码如下:
[devalone@devalone 24]$ cat big_users.sh
#!/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 '+%Y%m%d') # 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 #loop to du directories
do
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
#
1.3.3 运行脚本
-----------------------------------------------------------------------------------------------------------------------------------------
[devalone@devalone 24]$ chmod a+x big_users.sh
[devalone@devalone 24]$ ./big_users.sh
[devalone@devalone 24]$ ll
总用量 28
-rwxrwxr-x. 1 devalone devalone 871 1月 15 13:53 big_users.sh
-rwxrwxr--. 1 devalone devalone 1842 7月 14 14:36 daily_archive.sh
-rwxrwxr-x. 1 devalone devalone 5747 1月 15 13:53 delete_user.sh
-rw-rw-r--. 1 devalone devalone 816 7月 16 10:35 disk_space_20180716.rpt
-rwxrwxr-x. 1 devalone devalone 2080 7月 14 15:11 hourly_archive.sh
-rw-r--r--. 1 root root 365 7月 14 17:46 user_test_Files_180714.rpt
[devalone@devalone 24]$ cat disk_space_20180716.rpt
Top ten disk space usage
for /var/log /home Directories
The /var/log directory:
1: 655616 /var/log/journal/fd73c66c927142dda4afd46e8e2f53e7
2: 6700 /var/log/anaconda
3: 3244 /var/log
4: 216 /var/log/vmware
5: 8 /var/log/journal
6: 4 /var/log/sssd
7: 4 /var/log/speech-dispatcher
8: 4 /var/log/samba
9: 4 /var/log/ppp
10: 4 /var/log/ntpstats
The /home directory:
1: 4580284 /home/devalone/software/eclipse/myeclipse
2: 1978496 /home/devalone/文档
3: 1650996 /home/devalone/workspaces/dearall/dearall/bakup
4: 1210888 /home/devalone/下载
5: 1073360 /home/devalone/repo/mysql-server/.git/objects/pack
6: 931816 /home/devalone/software/Android
7: 886600 /home/devalone/MyEclipse
8: 865168 /home/devalone/sources/dearall
9: 800076 /home/devalone/software/eclipse
10: 733588 /home/devalone/software/java/oracle_jdk8
OK.
系列目录:
Linux shell 脚本编程-实战篇(一)
Linux shell 脚本编程-实战篇(二)
Linux shell 脚本编程-实战篇(三)
-----------------------------------------------------------------------------------------------------------------------------------------
参考:
《Linux 命令行与 shell 脚本编程大全》 第 3 版 —— 2016.8(美)Richard Blum Cristine Bresnahan