Linux shell 脚本编程-实战篇(一)

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

你可能感兴趣的:(Linux,操作系统,CentOS7,Fedora)