禅道博客

分享专业技术知识,文章内容干货满满

《Linux命令行与shell脚本编程大全》第二十四章:编写简单的脚本实用工具

2022-12-02 14:26:38
李阳
原创 124
摘要:本文详细介绍了《Linux命令行与shell脚本编程大全》中,关于“如何编写简单的脚本实用工具”的内容。

一、归档

归档数据文件

如果你正在用Linux系统作为一个重要项目的平台,可以创建一个shell脚本来自动获取特定目录的快照。在配置文件中指定所涉及的目录,这样一来,在项目发生变化时,你就可以做出对应的修改。

1.需要的功能

tar命令可以将整个目录归档到单个文件中。


tar命令会显示一条警告消息,表明它删除了路径名开头的斜线,将路径从绝对路径名变成相对路径名。

你很可能不想在脚本中出现这条消息。这种情况可以通过将STDERR重定向到/dev/null文件


由于tar归档文件会消耗大量的磁盘空间,最好能够压缩一下该文件。这只需要加一个-z选项就行了。

它会将tar归档文件压缩成gzip格式的tar文件,这种文件也叫作tarball。

别忘了使用恰当的文件扩展名来表示这是个tarball,用.tar.gz或.tgz都行。


你不需要为待备份的新目录或文件修改或编写新的归档脚本,而是可以借助于配置文件。配置文件应该包含你希望进行归档的每个目录或文件。


$ cat Files_To_Backup
/home/Christine/Project
/home/Christine/Downloads
/home/Does_not_exist
/home/Christine/Documents
可以让脚本读取配置文件,然后将每个目录名加到归档列表中。使用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 [ -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语句就成立。

2.创建逐日归档文件的存放位置

如果要对多个目录进行备份,最好还是创建一个集中归档仓库目录。

可以通过sudo命令或者创建一个用户组的方式,为需要在集中归档目录中创建文件的用户授权。


sudo groupadd Archivers
sudo chgrp Archivers /archive
ls -ld /archive
sudo usermod -aG Archivers Christine
sudo chmod 775 /archive
ls -ld /archive
将用户添加到Archivers组后,用户必须先登出然后再登入,才能使组成员关系生效。现在只要是该组的成员,无需超级用户权限就可以在目录中创建文件了。

记住,Archivers组的所有用户都可以在归档目录中添加和删除文件。为了避免组用户删除他人的归档文件,最好还是把目录的粘滞位加上。



3.创建按日归档的脚本

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变量指向含有待归档目录信息的归档配置文件。如果需要,二者都可以很方便地改成备用目录和文件。

下面是完整的代码
#!/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 ] # Make sure the config file still exists.
then # 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 ] # 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 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

4.运行按日归档的脚本

必须赋予文件属主可执行权限(x)才能够运行脚本。
chmod u+x daily_archive.sh
./daily_archive.sh
这样就可以运行脚本了

5.创建按小时归档的脚本

不必将所有的归档文件都放到同一目录中,你可以为归档文件创建一个目录层级。

这个归档目录包含了与一年中的各个月份对应的目录,将月的序号作为目录名。而每月的目录中又包含与当月各天对应的目录(用天的序号作为目录名)。这样你只用给每个归档文件加上时间戳,然后将它们放到与月日对应的目录中就行了。


二、管理用户账户

管理用户账户绝不仅仅是添加、修改和删除账户,你还得考虑安全问题、保留工作的需求以及对账户的精确管理。

1.需要的功能

在删除账户时,至少需要4个步骤:
  1. 获得正确的待删除用户账户名;
  2. 杀死正在系统上运行的属于该账户的进程;
  3. 确认系统中属于该账户的所有文件;
  4. 删除该用户账户。

1.1.获取正确的账户名

账户删除过程中的第一步最重要:获取待删除的用户账户的正确名称。可以在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
通过给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

1.2. 创建函数获取正确的账户名

你要做的第一件事是声明函数名get_answer。下一步,用unset命令清除脚本用户之前给出的答案。
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
使用新函数让脚本代码清爽了许多。


LINE1="Is $USER_ACCOUNT the user account "
LINE2="you wish to delete from the system? [y/n]"

1.3.验证输入的用户名

可以用case语句来处理答案。case语句部分必须精心编码,这样它才会检查yes的多种输入方式。

这个脚本有时需要处理很多次用户的yes/no回答。因此,创建一个函数来处理这个任务是有意义的。


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


1.4.确定账户是否存在

现在最好核对一下这个用户账户在系统上是否真实存在。还有,最好将完整的账户记录显示给脚本用户,核对这是不是真的要删除的那个账户。

使用变量USER_ACCOUNT_RECORD,将它设成grep在/etc/passwd文件中查找该用户账户的输出。-w选项允许你对这个特定用户账户进行精确匹配。

USER_ACCOUNT_RECORD=$(cat /etc/passwd | grep -w $USER_ACCOUNT)

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.5.删除属于账户的进程

查找用户进程较为简单。这里脚本可以用ps命令(参见第4章)和-u选项来定位属于该账户的所有处于运行中的进程。

ps -u $USER_ACCOUNT >/dev/null

可以用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,那么系统上有属于该账户的进程在运行。

杀死用户进程,需要三条命令,收集pid

COMMAND_1="ps -u $USER_ACCOUNT --no-heading"

提取PID

gawk '{print $1}'

第三条命令是xargs,该命令可以构建并执行来自标准输入STDIN的命令。它非常适合用在管道的末尾处。xargs命令负责杀死PID所对应的进程。

COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"

选项-d指明使用什么样的分隔符。换句话说,既然

xargs命令接收多个项作为输入,那么各个项之间要怎么区分呢?在这里,\n(换行符)被作为各项的分隔符。

当每个PID发送给xargs时,它将PID作为单个项来处理。又因为xargs命令被赋给了一个变量,所以\n中的反斜杠(\)必须再加上另一个反斜杠(\)进行转义。

在处理PID时,xargs命令需要使用命令的完整路径名。sudo命令和kill命令用于杀死用户账户的运行进程。另外还注意到kill命令使用了信号-9。

通过管道符链接:

$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.6.查找属于账户的文件

要找到用户文件,你可以用find命令。find命令用-u选项查找整个文件系统,它能够准确查找到属于该用户的所有文件。该命令如下:

find / -user $USER_ACCOUNT > $REPORT_FILE

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

2.创建脚本

完整脚本:由于脚本太长,参见Linux命令行与shell脚本编程大全.第3版 (布鲁姆,布雷斯纳汉)的24.2.2章节

三、监测磁盘空间

这个shell脚本工具会帮你找出指定目录中磁盘空间使用量位居前十名的用户。它会生成一个以日期命名的报告,使得磁盘空间使用量可以监测。

1.需要的功能

你要用到的第一个工具是du命令,-s选项用来总结目录一级的整体使用状况。


-S(大写的S)选项,它为每个目录和子目录分别提供了总计信息。这样你就能快速地定位问题的根源。

由于我们感兴趣的是占用磁盘空间最多的目录,所以需要使用sort命令对du产生的输出进行排序

-n选项允许按数字排序。-r选项会先列出最大数字(逆序)。

sed编辑器可以让这个列表更容易读懂。我们要关注的是磁盘用量的前10名用户,所以当到了第11行时,sed会删除列表的剩余部分。

下一步是给列表中的每行加一个行号。

sed '{11,$D; =}' |
sed 'N; s/\n/ /' |
可以用gawk命令清理输出了,sed编辑器的输出会通过管道输出到gawk命令,然后用printf函数打印出来。

gawk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}'

完整的命令如下:



2.创建脚本

为 了 节 省 时 间 和 精 力 , 这 个 脚 本 会 为 多 个 指 定 目 录 创 建 报 告 。 我 们 用 一 个 叫 作CHECK_DIRECTORIES的变量来完成这一任务

脚本使用for循环来对变量中列出的每个目录执行du命令。

每次for循环都会遍历变量CHECK_DIRECTORIES中的值列表,它会将列表中的下一个值赋给DIR_CHECK变量

为了方便识别,我们用date命令给报告的文件名加个日期戳。脚本用exec命令将它的输出重定向到加带日期戳的报告文件中。

为了生成格式精致的报告,这个脚本会用echo命令来输出一些报告标题。

完整脚本如下:


#!/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 #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 #End of loop
#
exit
这个简单的shell脚本会为你选择的每个目录创建一个包含日期戳的磁盘空间用量前10名的用户报告。

3.运行脚本

报告内容

现在你可以让这个脚本在需要时自动运行了,可以用cron表来实现。

发表评论
评论通过审核后显示。