美文网首页Shell
Shell 脚本编程(中级篇)

Shell 脚本编程(中级篇)

作者: rollingstarky | 来源:发表于2018-11-15 02:24 被阅读27次

    中级篇

    一、结构化命令

    基础篇的示例中,Shell 大多数情况下都以顺序的方式依次执行脚本中的每一条指令。
    但是在实际情况中,很多程序都需要对脚本中命令的逻辑流进行控制。即根据某些条件和状态完成判断,再决定其后的程序该以怎样的规则执行。
    Shell 脚本支持一部分结构化指令,如 if-thencase 等,用于修改程序的执行流程。

    1. if-then

    最基本的结构化语句即 if-then,其格式如下:

    if command
    then
        commands
    fi
    

    Bash Shell 中的 if-then 语句和其他编程语言中的类似语句稍有些不同,其作用如下:

    • 首先执行 if 后面跟随的命令 command
    • 如果该命令 command结束状态(exit status)为 0(即该命令执行成功),则 then 后面的命令依次被执行
    • 如果该命令 command 的结束状态为任何非 0 的值(该命令非正常退出),则不执行 then 后面的命令,Bash Shell 移动到脚本中的下一条命令(即 fi 后面的内容)

    如下面的两个示例:

    $ cat if1.sh
    #!/bin/bash
    
    if pwd
    then
        echo "It worked"
    fi
    echo "We are outside the if statement"
    $ ./if1.sh
    /Users/starky/program/scripts/shell
    It worked
    We are outside the if statement
    $ cat if2.sh
    #!/bin/bash
    
    if IamNotaCommand
    then
        echo "It worked"
    fi
    echo "We are outside the if statement"
    $ ./if2.sh
    ./if2.sh: line 3: IamNotaCommand: command not found
    We are outside the if statement
    

    在上面的示例中,if1.sh 脚本里 if 后面的命令(pwd)成功执行,则 then 后面的 echo 命令也跟着执行。
    if2.sh 脚本里 if 后面的命令(IamNotaCommand)执行出错,则 then 后面的 echo 命令不会被执行。
    无论如何,判断结束后都会继续执行 fi 后面的语句。

    2. if-else-then

    if-else-then 语句的格式为:

    if command
    then
        commands
    else
        commands
    fi
    

    if 后面的命令执行后返回值为 0 时,则 then 后面的命令被执行。
    如果 if 后面的命令执行后返回值不为 0,则执行 else 后面的命令。

    示例(if3.sh)如下:

    #!/bin/bash
    
    testuser=starky
    if ls -d /Users/$testuser
    then
        echo "The bash files for user $testuser are:"
        ls -a /Users/$testuser/.b*
        echo
    else
        echo "The user $testuser does not exist on this system."
        echo
    fi
    

    运行结果:

    $ chmod +x if3.sh
    $ ./if3.sh
    /Users/starky
    The bash files for user starky are:
    /Users/starky/.bash_history /Users/starky/.bash_profile
    /Users/starky/.bash_sessions:
    ...
    

    但是将上述脚本中 testuser 变量的值改为系统中并不存在的用户时(如 NoSuchUser),则运行效果如下:

    $ cat if3.sh
    #!/bin/bash
    
    testuser=NoSuchUser
    if ls -d /Users/$testuser
    then
        echo "The bash files for user $testuser are:"
        ls -a /Users/$testuser/.b*
        echo
    else
        echo "The user $testuser does not exist on this system."
        echo
    fi
    $ ./if3.sh
    ls: /Users/NoSuchUser: No such file or directory
    The user NoSuchUser does not exist on this system.
    

    其他结构形式的 if-else-then 语句还包括:

    if command1
    then
        commands
    elif
        command2
    then
        more commands
    fi
    

    当然理论上讲可以使用足够的 if-then-elif-then 语句,只不过类似情况下多使用更为恰当的 case 语句。

    3. test 命令

    上面提到的 if-then 语句,大多是这样的形式:

    if command
    then
        commands
    ...
    

    if 后面跟的是一条普通的 Shell 命令,根据该命令是否成功执行,来确定 then 后面的命令是否执行。

    可以借助 test 命令,使用如下形式的 if-then 语句:

    if test condition
    then
        commands
    fi
    

    如果 test 命令后面的 condition 计算后为 TRUE,则 test 命令退出并返回一个值为 0 的状态值。then 后面的命令被执行。
    如果 test 命令后面的 condition 计算后为 FALSE,则 test 命令退出并返回一个非零的状态值。跳出当前的 if-then 并继续执行后面的内容。

    如下面的两个例子:

    $ cat test_condition.sh
    #!/bin/bash
    
    my_variable="Full"
    
    if test $my_variable
    then
        echo "The $my_variable expression returns a True"
    else
        echo "The $my_variable expression returns a False"
    fi
    $ ./test_condition.sh
    The Full expression returns a True
    $ cat test_condition2.sh
    #!/bin/bash
    
    my_variable=""
    
    if test $my_variable
    then
        echo "The $my_variable expression returns a True"
    else
        echo "The $my_variable expression returns a False"
    fi
    $ ./test_condition2.sh
    The  expression returns a False
    

    Bash Shell 还提供了另外一种不需要使用 test 命令来完成条件检查的 if-then 语句:

    if [ condition ]
    then
        commands
    fi
    

    方括号中的内容用来定义判断条件。注意方括号和 condition 之间的空格
    支持三种形式的判断条件:

    • 数字比较
    • 字符串比较
    • 文件比较
    数字比较
    操作 描述
    n1 -eq n2 检查 n1 是否等于 n2
    n1 -ge n2 检查 n1 是否大于或等于 n2
    n1 -gt n2 检查 n1 是否大于 n2
    n1 -le n2 检查 n1 是否小于或等于 n2
    n1 -lt n2 检查 n1 是否 小于 n2
    n1 -ne n2 检查 n1 是否不等于 n2

    示例如下:

    $ cat numeric_test.sh
    #!/bin/bash
    
    value1=10
    value2=11
    
    if [ $value1 -gt 5 ]
    then
        echo "The test value $value1 is greater than 5"
    fi
    
    if [ $value1 -eq $value2 ]
    then
        echo "The values are equal"
    else
        echo "The values are different"
    fi
    $ ./numeric_test.sh
    The test value 10 is greater than 5
    The values are different
    
    字符串比较
    操作 描述
    str1 = str2 检查 str1 和 str2 是否相同
    str1 != str2 检查 str1 和 str2 是否不相同
    str1 < str2 检查 str1 是否小于 str2
    str1 > str2 检查 str1 是否大于 str2
    -n str1 检查 str1 是否长度大于 0(不为空)
    -z str1 检查 str1 是否长度为 0(为空)

    示例1:

    $ cat welcome.sh
    #!/bin/bash
    
    testuser=starky
    
    if [ $USER = $testuser ]
    then
        echo "Welcome $testuser"
    else
        echo "This is not $testuser"
    fi
    $ ./welcome.sh
    Welcome starky
    

    PS:在使用 str1 > str2str1 < str2 这种类型的条件时,需加上转义符号。否则大于号或小于号会被当作重定向处理。

    示例2:

    $ cat compare.sh
    #!/bin/bash
    
    val1=Testing
    val2=testing
    
    if [ $val1 \> $val2 ]
    then
        echo "$val1 is greater than $val2"
    else
        echo "$val1 is less than $val2"
    fi
    $ ./compare.sh
    Testing is less than testing
    

    PSif 条件中的比较依据的是基本 ASCII 顺序,通过每个字符(从首字母开始)的 ASCII 值的比较来判断大小顺序。


    -n-z 常常用来确定指定字符串是否为空。
    示例3:

    $ cat empty.sh
    #!/bin/bash
    
    val1=testing
    val2=''
    
    if [ -n $val1 ]
    then
        echo "The string '$val1' is not empty"
    else
        echo "The string '$val1' is empty"
    fi
    
    if [ -z $val2 ]
    then
        echo "The string '$val2' is empty"
    else
        echo "The string '$val2' is not empty"
    fi
    
    if [ -z $val3 ]
    then
        echo "The string '$val3' is empty"
    else
        echo "The string '$val3' is not empty"
    fi
    $ ./empty.sh
    The string 'testing' is not empty
    The string '' is empty
    The string '' is empty
    

    上述脚本中 val3 变量自始至终没有被定义,被自动判断为空字符串。

    文件比较
    操作 描述
    -d file 检查 file 是否存在且是一个目录
    -e file 检查 file 是否存在
    -f file 检查 file 是否存在且是一个文件
    -r file 检查 file 是否存在且可读
    -s file 检查 file 是否存在且不为空
    -w file 检查 file 是否存在且可写
    -x file 检查 file 是否存在且可执行
    -O file 检查 file 是否存在且其属主为当前用户
    -G file 检查 file 是否存在且其默认数组和当前用户相同
    file1 -nt file2 检查 file1 是否比 file2 更新(newer than)
    file1 -ot file2 检查 file1 是否比 file2 更老(older than)

    示例1,目录检查:

    $ cat check_dir.sh
    #!/bin/bash
    
    directory=/Users/starky
    if [ -d $directory ]
    then
        echo "The $directory directory exists"
        cd $directory
        ls
    else
        echo "The $directory directory does not exist"
    fi
    $ ./check_dir.sh
    The /Users/starky directory exists
    Applications    Downloads   Music       extract.sh  program
    Desktop     Library     Pictures    id_rsa.pub  software
    Documents   Movies      Public      miniconda3  vim.tar.gz
    

    示例2,删除空文件:

    $ cat check_empty.sh
    #!/bin/bash
    
    file_name=empty
    
    # 检查该文件是否存在
    if [ -f $file_name ]
    then
        # 该文件存在,则继续检查其内容是否非空
        if [ -s $file_name ]
        then
            # 内容不为空
            echo "The $file_name file exists and has data in it."
            echo "Will not remove this file"
        else
            # 内容为空
            echo "The $file_name file exists, but is empty."
            echo "Deleting empty file..."
            rm $file_name
        fi
    else
        # 该文件不存在
        echo "File, $file_name, does not exist."
    fi
    $ ./check_empty.sh
    File, empty, does not exist.
    $ touch empty
    $ ls empty
    empty
    $ ./check_empty.sh
    The empty file exists, but is empty.
    Deleting empty file...
    $ ls empty
    ls: empty: No such file or directory
    
    4. 组合判断

    if-then 语句允许使用布尔逻辑来完成多个条件的组合判断。

    • [ condition1 ] && [ condition2 ] 逻辑与
    • [ condition1 ] || [ condition2 ] 逻辑或

    示例:

    cat compound_test.sh
    #!/bin/bash
    
    if [ -d $HOME ] && [ -w $HOME/testing ]
    then
        echo "The file exists and you can write to it"
    else
        echo "I cannot write to the file"
    fi
    $ ./compound_test.sh
    I cannot write to the file
    $ touch ~/testing
    $ ./compound_test.sh
    The file exists and you can write to it
    
    if-then 中的双括号

    Bash Shell 在 if-then 语句中还提供了一些更高级的特性:

    • 双小括号(数学表达式)
    • 双中括号(字符串处理函数)
    使用双小括号

    双小括号允许在条件测试时使用(相对于 test 命令)更高级的数学表达式。
    格式:(( expression ))
    其中 expression 除了支持前面提到过的基本的数学操作符外,还支持以下操作符:

    操作符 描述
    val++ Post-increment
    val-- Post-decrement
    ++val Pre-increment
    --val Pre-decrement
    ! 逻辑非
    ~ 按位否定
    **
    << 向左按位移位
    >> 向右按位移位
    & 按位逻辑与
    | 按位逻辑或
    && 逻辑与
    双管道符 逻辑或

    示例:

    $ cat parenthesis.sh
    #!/bin/bash
    
    val1=10
    
    if (( $val1 ** 2 > 90 ))
    then
        (( val2 = $val1 ** 2 ))
        echo "The square of $val1 is $val2"
    fi
    $ ./parenthesis.sh
    The square of 10 is 100
    
    使用双中括号

    双中括号为 test 命令中的字符串比较提供了更高级的特性,尤其是支持 模式匹配
    示例:

    cat pattern.sh
    #!/bin/bash
    
    if [[ $USER == s* ]]
    then
        echo "Hello $USER"
    else
        echo "Sorry, I do not know you"
    fi
    $ ./pattern.sh
    Hello starky
    
    4. case 命令

    很多时候,某个作为判断条件的变量有多个可能的取值,而你需要根据不同的取值指导程序去做对应的操作。
    如果使用 if-then-else 语句,代码免不了会拉得有些长。。。

    不同于 if 语句(通过 if、elif 依次检查同一个变量的所有取值),case 命令以类似列表的形式检查同一个变量的所有取值:

    case variable in
        pattern1 | pattern2) commands1;;
        pattern3) commands2;;
        *) default commands;;
    esac
    

    case 命令将指定的变量与多个不同的模式进行比对,如果匹配,则执行该模式后面的命令。
    可以在同一行中定义多个模式(使用 | 符号分隔)。
    星号表示当之前的所有模式都不能被匹配时,默认执行的命令。

    示例:

    $ cat case_user.sh
    #!/bin/bash
    
    case $USER in
        rich | barbara)
            echo "Welcome, $USER"
            echo "Please enjoy your visit";;
        testing)
            echo "Special testing account";;
        jessica)
            echo "Do not forget to logout when you're done";;
        *)
            echo "Sorry, you are not allowed here";;
    esac
    $ ./case_user.sh
    Sorry, you are not allowed here
    

    上面的脚本等同于使用如下的 if 语句:

    $ cat if_user.sh
    #!/bin/bash
    
    if [[ $USER = "rich" || $USER = "barbara" ]]
    then
        echo "Welcome $USER"
        echo "Please enjoy your visit"
    elif [ $USER = "testing" ]
    then
        echo "Special testing account"
    elif [ $USER = "jessica" ]
    then
        echo "Do not forget to logout when you're done"
    else
        echo "Sorry, you are not allowed here"
    fi
    $ ./if_user.sh
    Sorry, you are not allowed here
    

    二、更多结构化命令

    1. for 命令

    很多时候,需要重复地执行一系列命令直到某个特定的情况出现。
    for 命令允许创建一个循环来遍历某些值,并在循环过程中使用遍历到的值执行一系列特定的指令。语法格式:

    for var in list
    do
        commands
    done
    
    遍历列表
    $ cat for_list.sh
    #!/bin/bash
    for state in Alabama Alaska Arizona
    do
        echo The next state is $state
    done
    
    echo "The last state we visited was $state"
    $ ./for_list.sh
    The next state is Alabama
    The next state is Alaska
    The next state is Arizona
    The last state we visited was Arizona
    

    每次 for 命令遍历由后面列表(Alabama Alaska Arizona)提供的值时,会把列表中当前项目的值赋值给 $state 变量(即第一次遍历把 Alabama 赋值给 $state,第二次是 Alaska),该 $state 变量可以在后面的 do ... done 结构中使用。

    for 循环结束后 $state 变量依旧有效,即保留最后一次遍历时得到的值(列表的最后一项 Arizona)而不会被销毁。

    遍历字符串中的列表

    Bash Shell 中的for 循环可以直接接收单个字符串作为参数,并通过其中的空格将该字符串分割成多个遍历的项目。

    $ cat for_variable.sh
    #!/bin/bash
    
    states="Alabama Alaska Arizona"
    states=$states" Arkansas"
    
    for state in $states
    do
        echo "Have you ever visited $state?"
    done
    $ ./for_variable.sh
    Have you ever visited Alabama?
    Have you ever visited Alaska?
    Have you ever visited Arizona?
    Have you ever visited Arkansas?
    

    脚本中 state=$state" Arkansas" 可以用来在 $state 变量中的字符串末尾添加新的字符串,也等同于在列表末尾添加了新的项目。

    从命令中获取遍历的列表

    可以通过命令替换将命令的输出作为 for 命令循环遍历的列表:

    $ cat states.txt
    Alabama Alaska Arizona
    $ cat for_command.sh
    #!/bin/bash
    
    for state in $(cat states.txt)
    do
        echo "Visit beautiful $state"
    done
    $ ./for_command.sh
    Visit beautiful Alabama
    Visit beautiful Alaska
    Visit beautiful Arizona
    
    遍历文件目录

    可以使用 for 命令直接循环目录中的文件(结合 * 符号):

    $ cat for_dir.sh
    #!/bin/bash
    
    for file in /Users/starky/.vim* /Users/starky/badtest
    do
        if [ -d "$file" ]
        then
            echo "$file is a directory"
        elif [ -f "$file" ]
        then
            echo "$file is a file"
        else
            echo "$file doesn't exit"
        fi
    done
    $ ./for_dir.sh
    /Users/starky/.vim is a directory
    /Users/starky/.viminfo is a file
    /Users/starky/.vimrc is a file
    /Users/starky/badtest doesn't exit
    

    注意脚本中的 if [ -d "$file" ] 段代码, "$file" 被双引号包裹了起来。
    原因是文件名中包含空格是合法的,但是在 Shell 脚本中,如果 $file 变量的值包含空格,则不能直接在 if [ -d ... ] 中使用,所以这里使用了双引号。

    2. C 语法的 for 命令

    Bash Shell 也支持语法类似于 C 语言形式的 for 循环:
    for (( variable assignment ; condition ; iteration process ))

    $ cat for_c.sh
    #!/bin/bash
    
    for (( i=1; i <= 10; i++ ))
    do
        echo "The next number is $i"
    done
    $ ./for_c.sh
    The next number is 1
    The next number is 2
    The next number is 3
    The next number is 4
    The next number is 5
    The next number is 6
    The next number is 7
    The next number is 8
    The next number is 9
    The next number is 10
    
    3. while 命令

    while 命令允许用户定义一个判断指令,然后循环执行一系列命令,并在每次执行前检查判断指令的返回值(exit status)。直到判断指令的返回值不为 0,终止循环。

    while 命令的格式为:

    while test command
    do
        other commands
    done
    

    示例程序如下:

    $ cat while.sh
    #!/bin/bash
    
    var=5
    while [ $var -gt 0 ]
    do
        echo $var
        var=$[ $var -1 ]
    done
    $ ./while.sh
    5
    4
    3
    2
    1
    
    4. break 命令

    break 命令用于跳出循环

    $ cat break.sh
    #!/bin/bash
    
    for var in 1 2 3 4 5 6 7 8 9 10
    do
        if [ $var -eq 5 ]
        then
            break
        fi
        echo "Iteration number: $var"
    done
    echo "The for loop is completed"
    $ ./break.sh
    Iteration number: 1
    Iteration number: 2
    Iteration number: 3
    Iteration number: 4
    The for loop is completed
    
    5. 处理循环的输出

    可以使用管道重定向处理循环的输出信息,直接将管道或重定向命令加在循环语句块的末尾即可。

    $ cat output.sh
    #!/bin/bash
    
    for (( a = 1; a < 5; a++ ))
    do
        echo "The number is $a"
    done > numbers.txt
    
    echo "The command is finished"
    $ ./output.sh
    The command is finished
    $ cat numbers.txt
    The number is 1
    The number is 2
    The number is 3
    The number is 4
    

    参考资料

    Linux Command Line and Shell Scripting Bible 3rd Edition

    相关文章

      网友评论

        本文标题:Shell 脚本编程(中级篇)

        本文链接:https://www.haomeiwen.com/subject/neupxqtx.html