Linux shell 脚本编程-基础篇 (四)

Linux shell 脚本编程-基础篇 (三)

4. 处理用户输入


脚本还得能够与使用者进行交互。bash shell 提供了一些不同的方法来从用户处获得数据,包括命令行参数(添加在命令后的数据)、命令行选项(可修改

命令行为的单个字母)以及直接从键盘读取输入的能力。

4.1 命令行参数
-----------------------------------------------------------------------------------------------------------------------------------------
向 shell 脚本传递数据的最基本方法是使用命令行参数。命令行参数允许在运行脚本时向命令行添加数据。

    ./addem 10 30

本例向脚本 addem 传递了两个命令行参数(10和30)。脚本会通过特殊的变量来处理命令行参数。


4.1.1 读取参数
-----------------------------------------------------------------------------------------------------------------------------------------
bash shell 会将一些称为位置参数(positional parameter)的特殊变量分配给输入到命令行中的所有参数。这也包括 shell 所执行的脚本名称。位置参数
变量是标准的数字:$0是程序名,$1是第一个参数,$2是第二个参数,依次类推,直到第九个参数$9。

示例:
    [devalone@devalone 14]$ cat test1.sh
    #!/bin/bash
    # using one command line parameter
    #
    factorial=1
    for (( number=1; number <= $1; number++ ))
    do
        factorial=$[ $factorial * $number ]
    done
    echo The factorial of $1 is $factorial

扫描二维码关注公众号,回复: 2551044 查看本文章

运行:
    [devalone@devalone 14]$ test1.sh 5
    The factorial of 5 is 120

可以在 shell 脚本中像使用其他变量一样使用 $1 变量。shell 脚本会自动将命令行参数的值分配给变量,不需要作任何处理。

如果需要输入更多的命令行参数,则每个参数都必须用空格分开。

示例:
    [devalone@devalone 14]$ cat test2.sh
    #!/bin/bash
    # testing two command line parameters
    #
    total=$[ $1 * $2 ]
    echo The first paramter is $1.
    echo The second parameter is $2.
    echo The total value is $total.

运行:
    [devalone@devalone 14]$ test2.sh 2 5
    The first paramter is 2.
    The second parameter is 5.
    The total value is 10.

shell 会将每个参数分配给对应的变量。

也可以在命令行上用文本字符串:

示例:
    [devalone@devalone 14]$ cat test3.sh
    #!/bin/bash
    # testing string parameters
    #
    echo Hello $1, glad to meet you

运行:
    [devalone@devalone 14]$ test3.sh deva
    Hello deva, glad to meet you

shell 将输入到命令行的字符串值传给脚本。但碰到含有空格的文本字符串时就会出现问题:

运行:
    [devalone@devalone 14]$ test3.sh deva Richard
    Hello deva, glad to meet you

每个参数都是用空格分隔的,所以 shell 会将空格当成两个值的分隔符。要在参数值中包含空格,必须要用引号(单引号或双引号均可)。

运行:
    [devalone@devalone 14]$ test3.sh 'deva Richard'
    Hello deva Richard, glad to meet you


    NOTE:
    -------------------------------------------------------------------------------------------------------------------------------------
    将文本字符串作为参数传递时,引号并非数据的一部分。它们只是表明数据的起止位置。

如果脚本需要的命令行参数不止 9 个,仍然可以处理,但是需要稍微修改一下变量名。在第 9 个变量之后,必须在变量数字周围加上花括号,比如${10}。

示例:
    [devalone@devalone 14]$ cat test4.sh
    #!/bin/bash
    # handling lots of parameters
    #
    total=$[ ${10} * ${11} ]
    echo The 10th parameter is ${10}
    echo The 11th parameter is ${11}
    echo The total is $total

运行:
    [devalone@devalone 14]$ test4.sh 1 2 3 4 5 6 7 8 9 10 12
    The 10th parameter is 10
    The 11th parameter is 12
    The total is 120

4.1.2 读取脚本名
-----------------------------------------------------------------------------------------------------------------------------------------
可以用 $0 参数获取 shell 在命令行启动的脚本名。这在编写多功能工具时很方便。

示例:
    [devalone@devalone 14]$ cat test5.sh
    #!/bin/bash
    # Testing the $0 parameter
    #
    echo The zero parameter is set to: $0
    #

运行:
    [devalone@devalone 14]$ test5.sh
    The zero parameter is set to: ./test5.sh

    [devalone@devalone 14]$ bash test5.sh
    The zero parameter is set to: test5.sh

运行方式不同,结果也不同。当传给 $0 变量的实际字符串不仅仅是脚本名,而是完整的脚本路径时,变量 $0 就会使用整个路径。

运行:

    [devalone@devalone 14]$ bash /home/devalone/study/shell-script/14/test5.sh
    The zero parameter is set to: /home/devalone/study/shell-script/14/test5.sh    

如果要编写一个根据脚本名来执行不同功能的脚本,就得做点额外工作。得把脚本的运行路径给剥离掉。另外,还要删除与脚本名混杂在一起的命令。

幸好有个方便的小命令可以帮到我们。basename 命令会返回不包含路径的脚本名。

示例:

    [devalone@devalone 14]$ cat test5b.sh
    #!/bin/bash
    # using basename with the $0 parameter
    #
    name=$(basename $0)
    echo The script name is: $name
    #

运行:
    [devalone@devalone 14]$ bash /home/devalone/study/shell-script/14/test5b.sh
    The script name is: test5b.sh

    [devalone@devalone 14]$ test5b.sh
    The script name is: test5b.sh

现在好多了。可以用这种方法来编写基于脚本名执行不同功能的脚本:

示例:
    [devalone@devalone 14]$ cat test6.sh
    #!/bin/bash
    # testing a multi-function script
    #
    name=$(basename $0)
    #
    if [ $name = "addem" ]
    then
        total=$[ $1 + $2 ]
    elif [ $name = "multem" ]
    then
        total=$[ $1 * $2 ]
    fi

    echo
    echo The calculated value is $total

运行:
    [devalone@devalone 14]$ cp test6.sh addem

    [devalone@devalone 14]$ ls -l *em
    -rwxrwxr-x. 1 devalone devalone 221 7月   6 16:24 addem
    lrwxrwxrwx. 1 devalone devalone   8 7月   6 16:25 multem -> test6.sh

    [devalone@devalone 14]$ addem 2 5

    The calculated value is 7
    [devalone@devalone 14]$ multem 2 5

    The calculated value is 10

4.1.3 测试参数
-----------------------------------------------------------------------------------------------------------------------------------------
在 shell 脚本中使用命令行参数时要小心些。如果脚本不加参数运行,可能会出问题。

运行:
    [devalone@devalone 14]$ addem 2
    ./addem:行8: 2 +  : 语法错误: 需要操作数 (错误符号是 "+  ")

    The calculated value is

当脚本认为参数变量中会有数据而实际上并没有时,脚本很有可能会产生错误消息。在使用参数前一定要检查其中是否存在数据。

示例:
    [devalone@devalone 14]$ cat test7.sh
    #!/bin/bash
    # testing parameters befor use
    #
    if [ -n "$1" ]
    then
        echo Hello $1, glad to meet you
    else
        echo Sorry, you did not identify yourself
    fi

运行:
    [devalone@devalone 14]$ test7.sh
    Sorry, you did not identify yourself
    
    [devalone@devalone 14]$ test7.sh Michael
    Hello Michael, glad to meet you
    


4.2 特殊参数变量
-----------------------------------------------------------------------------------------------------------------------------------------
在 bash shell 中有些特殊变量,它们会记录命令行参数。

4.2.1 参数统计
-----------------------------------------------------------------------------------------------------------------------------------------
特殊变量 $# 含有脚本运行时携带的命令行参数的个数。可以在脚本中任何地方使用这个特殊变量,就跟普通变量一样。

示例:
    [devalone@devalone 14]$ cat test8.sh
    #!/bin/bash
    # getting the number of parameters
    #
    echo there are $# parameters supplied

运行:
    [devalone@devalone 14]$ test8.sh
    there are 0 parameters supplied
    [devalone@devalone 14]$ test8.sh 1 2 3 4 5
    there are 5 parameters supplied
    [devalone@devalone 14]$ test8.sh 1 2 3 4 5 6  7 8 9 10
    there are 10 parameters supplied

    [devalone@devalone 14]$ test8.sh Michael Dan
    there are 2 parameters supplied
    [devalone@devalone 14]$ test8.sh "Michael Dan"
    there are 1 parameters supplied

在使用参数前测试参数的总数:
    [devalone@devalone 14]$ cat test9.sh
    #!/bin/bash
    # testing parameters
    #
    if [ $# -ne 2 ]
    then
        echo
        echo Usage: test9.sh a b
        echo
    else
        total=$[ $1 + $2 ]
        echo
        echo the total is $total
        echo
    fi

运行:
    [devalone@devalone 14]$ test9.sh

    Usage: test9.sh a b

    [devalone@devalone 14]$ test9.sh 10

    Usage: test9.sh a b

    [devalone@devalone 14]$ test9.sh 10 15

    the total is 25

if-then 语句用 -ne 测试命令行参数数量。如果参数数量不对,会显示一条错误消息告知脚本的正确用法。

这个变量还提供了一个简便方法来获取命令行中最后一个参数,完全不需要知道实际上到底用了多少个参数。不过要实现这一点,得稍微多花点工夫。

可能会觉得既然$#变量含有参数的总数,那么变量 ${$#} 就代表了最后一个命令行参数变量。试试看会发生什么:

    [devalone@devalone 14]$ cat badtest1.sh
    #!/bin/bash
    # testing grabbing last parameter
    #
    echo The last parameter was ${$#}
    #

运行:
    [devalone@devalone 14]$ ./badtest1.sh 10
    The last parameter was 3681

表明不能在花括号内使用美元符。必须将美元符换成感叹号。很奇怪,但的确管用。

    [devalone@devalone 14]$ cat test10.sh
    #!/bin/bash
    # grabbing the last parameter
    #
    parameter=$#
    echo
    echo the parameter count is $parameter
    echo the last parameter is ${!#}
    echo

运行:
    [devalone@devalone 14]$ test10.sh 1 2 3 4 5

    the parameter count is 5
    the last parameter is 5

这个测试将 $# 变量的值赋给了变量 params,然后也按特殊命令行参数变量的格式使用了该变量。两种方法都没问题。重要的是要注意,当命令行上没有
任何参数时,$# 的值为 0,params 变量的值也一样,但 ${!#} 变量会返回命令行用到的脚本名。


4.2.2 抓取所有的数据
-----------------------------------------------------------------------------------------------------------------------------------------
有时候需要抓取命令行上提供的所有参数。这时候不需要先用 $# 变量来判断命令行上有多少参数,然后再进行遍历,可以使用一组其他的特殊变量来解决
这个问题。

$* 和 $@ 变量可以用来轻松访问所有的参数。这两个变量都能够在单个变量中存储所有的命令行参数。

$* 变量会将命令行上提供的所有参数当作一个单词保存。这个单词包含了命令行中出现的每一个参数值。基本上 $* 变量会将这些参数视为一个整体,而
不是多个个体。

$@ 变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。这样就能够遍历所有的参数值,得到每个参数。这通常通过 for 命令完成。

示例:
    [devalone@devalone 14]$ cat test11.sh
    #!/bin/bash
    # testing $* and $@
    #
    echo
    echo "Using the \$* method: $*"
    echo "Using the \$@ method: $@"

运行:
    [devalone@devalone 14]$ test11.sh rich barbara katie jessica

    Using the $* method: rich barbara katie jessica
    Using the $@ method: rich barbara katie jessica

从表面上看,两个变量产生的是同样的输出,都显示出了所有命令行参数。

示例:
    [devalone@devalone 14]$ cat test12.sh
    #!/bin/bash
    # testing $* and $@
    #
    echo
    count=1
    #
    for param in "$*"
    do
        echo "\$* parameter #$count = $param"
        count=$[ $count + 1 ]
    done
    #
    echo
    count=1
    #
    for param in "$@"
    do
        echo "\$@ parameter #$count = $param"
        count=$[ $count + 1 ]
    done

运行:
    [devalone@devalone 14]$ test12.sh rich barbara katie jessica

    $* parameter #1 = rich barbara katie jessica

    $@ parameter #1 = rich
    $@ parameter #2 = barbara
    $@ parameter #3 = katie
    $@ parameter #4 = jessica

现在清楚多了。通过使用 for 命令遍历这两个特殊变量,能看到它们是如何不同地处理命令行参数的。$* 变量会将所有参数当成单个参数,而 $@ 变量会
单独处理每个参数。这是遍历命令行参数的一个绝妙方法。


4.3 移动变量
-----------------------------------------------------------------------------------------------------------------------------------------
bash shell 的 shift 命令能够用来操作命令行参数。跟字面上的意思一样,shift 命令会根据它们的相对位置来移动命令行参数。

在使用 shift 命令时,默认情况下它会将每个参数变量向左移动一个位置。所以,变量 $3 的值会移到 $2 中,变量 $2 的值会移到 $1 中,而变量 $1 的
值则会被删除(注意,变量 $0 的值,也就是程序名,不会改变)。

这是遍历命令行参数的另一个好方法,尤其是在不知道到底有多少参数时。可以只操作第一个参数,移动参数,然后继续操作第一个参数。

示例:
    [devalone@devalone 14]$ cat test13.sh
    #!/bin/bash
    # demonstrating the shift command
    echo
    count=1
    while [ -n "$1" ]
    do
        echo "Parameter #$count = $1"
        count=$[ $count + 1 ]
        shift
    done

运行:
    [devalone@devalone 14]$ test13.sh rich barbara katie jessica

    Parameter #1 = rich
    Parameter #2 = barbara
    Parameter #3 = katie
    Parameter #4 = jessica


这个脚本通过测试第一个参数值的长度执行了一个 while 循环。当第一个参数的长度为零时,循环结束。测试完第一个参数后,shift 命令会将所有参数的
位置移动一个位置。


    NOTE:
    -------------------------------------------------------------------------------------------------------------------------------------
    使用 shift 命令的时候要小心。如果某个参数被移出,它的值就被丢弃了,无法再恢复

    
也可以一次性移动多个位置,只需要给 shift 命令提供一个参数,指明要移动的位置数就行了。

示例:
    [devalone@devalone 14]$ cat test14.sh
    #!/bin/bash
    # demonstrating a multi-position shift
    #
    echo
    echo "The original parameters: $*"
    shift 2
    echo "Here's the new first parameter: $1"
    echo

运行:
    [devalone@devalone 14]$ test14.sh 1 2 3 4 5

    The original parameters: 1 2 3 4 5
    Here's the new first parameter: 3

通过使用 shift 命令的参数,就可以轻松地跳过不需要的参数。


4.4 处理选项
-----------------------------------------------------------------------------------------------------------------------------------------
选项是跟在单破折线后面的单个字母,它能改变命令的行为。


4.4.1 查找选项
-----------------------------------------------------------------------------------------------------------------------------------------
表面上看,命令行选项也没什么特殊的。在命令行上,它们紧跟在脚本名之后,就跟命令行参数一样。实际上,如果愿意,可以像处理命令行参数一样处理
命令行选项。


● 处理简单选项
-----------------------------------------------------------------------------------------------------------------------------------------
在前面的 test13.sh 脚本中,看到了如何使用 shift 命令来依次处理脚本程序携带的命令行参数。也可以用同样的方法来处理命令行选项。

在提取每个单独参数时,用 case 语句来判断某个参数是否为选项:

[devalone@devalone 14]$ cat test15.sh
#!/bin/bash
# extracting command line options as parameters
#
echo
while [ -n "$1" ]
do
    case "$1" in
        -a) echo "found the -a option";;
        -b) echo "found the -b option";;
        -c) echo "found the -c option";;
        *) echo "$1 is not an option";;
    esac
    shift
done

运行:
    [devalone@devalone 14]$ test15.sh -a -b -c -d

    found the -a option
    found the -b option
    found the -c option
    -d is not an option

case 语句会检查每个参数是不是有效选项。如果是的话,就运行对应 case 语句中的命令。不管选项按什么顺序出现在命令行上,这种方法都适用。

运行:
    [devalone@devalone 14]$ test15.sh -d -c -a

    -d is not an option
    found the -c option
    found the -a option


● 分离参数和选项
-----------------------------------------------------------------------------------------------------------------------------------------
经常遇到想在 shell 脚本中同时使用选项和参数的情况。Linux 中处理这个问题的标准方式是用特殊字符来将二者分开,该字符会告诉脚本何时选项结束
以及普通参数何时开始。

对 Linux 来说,这个特殊字符是双破折线(--)。shell会用双破折线来表明选项列表结束。在双破折线之后,脚本就可以放心地将剩下的命令行参数当作
参数,而不是选项来处理了。

示例:
    [devalone@devalone 14]$ cat test16.sh
    #!/bin/bash
    # extracting options and parameters
    #
    echo
    while [ -n "$1" ]
    do
        case "$1" in
            -a) echo "found the -a option";;
            -b) echo "found the -b option";;
            -c) echo "found the -c option";;
            --) shift
                break;;
            *) echo "$1 is not an option";;
        esac
        shift
    done
    #
    count=1
    for param in $@
    do
        echo "parameter #$count: $param"
        count=$[ $count + 1 ]
    done

在遇到双破折线时,脚本用 break 命令来跳出 while 循环。由于过早地跳出了循环,我们需要再加一条 shift 命令来将双破折线移出参数变量。

运行:
    [devalone@devalone 14]$ test16.sh -c -a -b -- test1 test2 test3

    found the -c option
    found the -a option
    found the -b option
    parameter #1: test1
    parameter #2: test2
    parameter #3: test3

当脚本遇到双破折线时,它会停止处理选项,并将剩下的参数都当作命令行参数。


● 处理带值的选项
-----------------------------------------------------------------------------------------------------------------------------------------
有些选项会带上一个额外的参数值。在这种情况下,命令行看起来像下面这样:

    $ ./testing.sh -a test1 -b -c -d test2

当命令行选项要求额外的参数时,脚本必须能检测到并正确处理。下面是如何处理的示例:

    [devalone@devalone 14]$ cat test17.sh
    #!/bin/bash
    # extracting options and values
    #
    echo
    while [ -n "$1" ]
    do
        case "$1" in
            -a) echo "found the -a option";;
            -b) param="$2"
                echo "found the -b option, with parameter value $param"
                shift ;;
            -c) echo "found the -c option";;
            --) shift
                break;;
            *) echo "$1 is not an option";;
        esac
        shift
    done
    #
    count=1
    for param in $@
    do
        echo "parameter #$count: $param"
        count=$[ $count + 1 ]
    done

运行:
    [devalone@devalone 14]$ test17.sh -a -b test1 -d

    found the -a option
    found the -b option, with parameter value test1
    -d is not an option

在这个例子中,case 语句定义了三个它要处理的选项。-b 选项还需要一个额外的参数值。由于要处理的参数是 $1,额外的参数值就应该位于$2(因为所有
的参数在处理完之后都会被移出)。只要将参数值从 $2 变量中提取出来就可以了。当然,因为这个选项占用了两个参数位,所以还需要使用 shift 命令
多移动一个位置。

只用这些基本的特性,整个过程就能正常工作,不管按什么顺序放置选项(但要记住包含每个选项相应的选项参数)。

运行:
    [devalone@devalone 14]$ test17.sh -b test1 -a -d

    found the -b option, with parameter value test1
    found the -a option
    -d is not an option

现在 shell 脚本中已经有了处理命令行选项的基本能力,但还有一些限制。比如,如果想将多个选项放进一个参数中时,它就不能工作了。

    [devalone@devalone 14]$ test17.sh -ac
    -ac is not an option

在 Linux中,合并选项是一个很常见的用法,而且如果脚本想要对用户更友好一些,也要给用户提供这种特性。幸好,有另外一种处理选项的方法能够帮忙。


4.4.2 使用getopt 命令
-----------------------------------------------------------------------------------------------------------------------------------------
getopt 命令是一个在处理命令行选项和参数时非常方便的工具。它能够识别命令行参数,从而在脚本中解析它们时更方便。

● 命令的格式
-----------------------------------------------------------------------------------------------------------------------------------------
getopt 命令可以接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。它的命令格式如下:

    getopt optstring parameters

optstring 是这个过程的关键所在。它定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值。

首先,在 optstring 中列出要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。getopt 命令会基于此定义的
optstring 解析提供的参数。

下面是个 getopt 如何工作的简单例子:

    [devalone@devalone 14]$ getopt ab:cd -a -b test1 -cd test2 test3
     -a -b test1 -c -d -- test2 test3

optstring 定义了四个有效选项字母:a、b、c和d。冒号(:)被放在了字母 b 后面,因为 b 选项需要一个参数值。当 getopt 命令运行时,它会检查提供
的参数列表(-a -b test1 -cd test2 test3),并基于提供的 optstring 进行解析。注意,它会自动将 -cd 选项分成两个单独的选项,并插入双破折线来
分隔行中的额外参数。

如果指定了一个不在 optstring 中的选项,默认情况下,getopt 命令会产生一条错误消息:

    [devalone@devalone 14]$ getopt ab:cd -a -b test1 -cde test2 test3
    getopt: invalid option -- 'e'
     -a -b test1 -c -d -- test2 test3

如果想忽略这条错误消息,可以在命令后加 -q 选项:

    [devalone@devalone 14]$ getopt -q ab:cd -a -b test1 -cde test2 test3
     -a -b 'test1' -c -d -- 'test2' 'test3'
 
注意,getopt 命令选项必须出现在 optstring 之前。

● 在脚本中使用 getopt
-----------------------------------------------------------------------------------------------------------------------------------------
可以在脚本中使用 getopt 来格式化脚本所携带的任何命令行选项或参数,但用起来略微复杂。方法是用 getopt 命令生成的格式化后的版本来替换已有的
命令行选项和参数。用 set 命令能够做到。

set 命令的选项之一是双破折线(--),它会将命令行参数替换成 set 命令的命令行值。

然后,该方法会将原始脚本的命令行参数传给 getopt 命令,之后再将 getopt 命令的输出传给 set 命令,用 getopt 格式化后的命令行参数来替换原始的
命令行参数,看起来如下所示:

    set -- $(getopt -q ab:cd "$@")

现在原始的命令行参数变量的值会被 getopt 命令的输出替换,而 getopt 已经为我们格式化好了命令行参数。

利用该方法,现在就可以写出能帮我们处理命令行参数的脚本。

示例:
    [devalone@devalone 14]$ cat test18.sh
    #!/bin/bash
    # extract command line options and values with getopt
    #
    set -- $(getopt -q ab:cd "$@")
    #
    echo
    while [ -n "$1" ]
    do
        case "$1" in
            -a) echo "found the -a option";;
            -b) param="$2"
                echo "found the -b option, with parameter value $param"
                shift ;;
            -c) echo "found the -c option";;
            --) shift
                break;;
            *) echo "$1 is not an option";;
        esac
        shift
    done
    #
    count=1
    for param in $@
    do
        echo "parameter #$count: $param"
        count=$[ $count + 1 ]
    done

运行:
    [devalone@devalone 14]$ test18.sh -ac

    found the -a option
    found the -c option    
    
    [devalone@devalone 14]$ test18.sh -a -b test1 -cd test2 test3 test4

    found the -a option
    found the -b option, with parameter value 'test1'
    found the -c option
    -d is not an option
    parameter #1: 'test2'
    parameter #2: 'test3'
    parameter #3: 'test4'

现在看起来相当不错了。但是,在 getopt 命令中仍然隐藏着一个小问题。看看这个例子:

运行:
    [devalone@devalone 14]$ test18.sh -a -b test1 -cd "test2 test3" test4

    found the -a option
    found the -b option, with parameter value 'test1'
    found the -c option
    -d is not an option
    parameter #1: 'test2
    parameter #2: test3'
    parameter #3: 'test4'

getopt 命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。还有另外一个办法能解决这个问题。

● 使用更高级的 getopts
-----------------------------------------------------------------------------------------------------------------------------------------
getopts 命令(注意是复数)内建于 bash shell。它跟近亲 getopt 看起来很像,但多了一些扩展功能。

与 getopt 不同,前者将命令行上选项和参数处理后只生成一个输出,而 getopts 命令能够和已有的 shell 参数变量配合默契。

每次调用它时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于 0 的退出状态码。这让它非常适合用解析命令行
所有参数的循环中。

getopts 命令的格式如下:

    getopts optstring variable

optstring 值类似于 getopt 命令中的那个。有效的选项字母都会列在 optstring 中,如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,
可以在 optstring 之前加一个冒号。getopts 命令将当前参数保存在命令行中定义的 variable 中。

getopts 命令会用到两个环境变量。如果选项需要跟一个参数值,OPTARG 环境变量就会保存这个值。OPTIND 环境变量保存了参数列表中 getopts 正在处理
的参数位置。这样就能在处理完选项之后继续处理其他命令行参数了。

getopts 命令的简单示例:

    [devalone@devalone 14]$ cat test19.sh
    #!/bin/bash
    # simple demonstration of the getopts command
    #
    echo
    while getopts :ab:c opt
    do
        case "$opt" in
            a) echo "Found the -a option";;
            b) echo "Found the -b option, with value $OPTARG";;
            c) echo "Found the -c option";;
            *) echo "Unknown option: $opt";;
        esac
    done

运行:
    [devalone@devalone 14]$ test19.sh -ab test1 -c

    Found the -a option
    Found the -b option, with value test1
    Found the -c option

while语句定义了 getopts 命令,指明了要查找哪些命令行选项,以及每次迭代中存储它们的变量名(opt)。

注意到在本例中case语句的用法有些不同。getopts 命令解析命令行选项时会移除开头的单破折线,所以在 case 定义中不用单破折线。

getopts 命令有几个好用的功能。可以在参数值中包含空格。

运行:
    [devalone@devalone 14]$ test19.sh -ab "test1 test2" -c

    Found the -a option
    Found the -b option, with value test1 test2
    Found the -c option

另一个好用的功能是将选项字母和参数值放在一起使用,而不用加空格。

运行:    
    [devalone@devalone 14]$ test19.sh -abtest1

    Found the -a option
    Found the -b option, with value test1
    
getopts 命令能够从 -b 选项中正确解析出 test1 值。

除此之外,getopts 还能够将命令行上找到的所有未定义的选项统一输出成问号。

运行:
    [devalone@devalone 14]$ test19.sh -d

    Unknown option: ?

    [devalone@devalone 14]$ test19.sh -acde

    Found the -a option
    Found the -c option
    Unknown option: ?
    Unknown option: ?

getopts 命令知道何时停止处理选项,并将参数留给处理。在 getopts 处理每个选项时,它会将 OPTIND 环境变量值增一。在 getopts 完成处理时,
可以使用 shift 命令和 OPTIND 值来移动参数。

示例:

    [devalone@devalone 14]$ cat test20.sh
    #!/bin/bash
    # processing options & parameters with getopts
    #
    echo
    while getopts :ab:cd opt
    do
        case "$opt" in
            a) echo "Found the -a option";;
            b) echo "Found the -b option, with value $OPTARG";;
            c) echo "Found the -c option";;
            d) echo "Found the -d option";;
            *) echo "Unknown option: $opt";;
        esac
    done
    #
    shift $[ $OPTIND - 1 ]
    #
    echo
    count=1
    for param in "$@"
    do
        echo "Parameter $count: $param"
        count=$[ $count + 1 ]
    done

运行:
    [devalone@devalone 14]$ test20.sh -a -b test1 -d test2 test3 test4

    Found the -a option
    Found the -b option, with value test1
    Found the -d option

    Parameter 1: test2
    Parameter 2: test3
    Parameter 3: test4

    
4.5 将选项标准化
-----------------------------------------------------------------------------------------------------------------------------------------
在创建 shell 脚本时,显然可以控制具体怎么做。程序员完全可以决定用哪些字母选项以及它们的用法。但有些字母选项在 Linux 世界里已经拥有了某种
程度的标准含义。如果能在 shell 脚本中支持这些选项,脚本看起来能更友好一些。

下表显示了 Linux 中用到的一些命令行选项的常用含义。

    常用的Linux命令选项
    +-------+---------------------------------------------------------------------------------
    | 选 项    | 描 述
    +-------+---------------------------------------------------------------------------------
    | -a    | 显示所有对象
    +-------+---------------------------------------------------------------------------------
    | -c    | 生成一个计数
    +-------+---------------------------------------------------------------------------------
    | -d    | 指定一个目录
    +-------+---------------------------------------------------------------------------------
    | -e    | 扩展一个对象
    +-------+---------------------------------------------------------------------------------
    | -f    | 指定读入数据的文件
    +-------+---------------------------------------------------------------------------------
    | -h    | 显示命令的帮助信息
    +-------+---------------------------------------------------------------------------------
    | -i    | 忽略文本大小写
    +-------+---------------------------------------------------------------------------------
    | -l    | 产生输出的长格式版本
    +-------+---------------------------------------------------------------------------------
    | -n    | 使用非交互模式(批处理)
    +-------+---------------------------------------------------------------------------------
    | -o    | 将所有输出重定向到的指定的输出文件
    +-------+---------------------------------------------------------------------------------
    | -q    | 以安静模式运行
    +-------+---------------------------------------------------------------------------------
    | -r    | 递归地处理目录和文件
    +-------+---------------------------------------------------------------------------------
    | -s    | 以安静模式运行
    +-------+---------------------------------------------------------------------------------
    | -v    | 生成详细输出
    +-------+---------------------------------------------------------------------------------
    | -x    | 排除某个对象
    +-------+---------------------------------------------------------------------------------
    | -y    | 对所有问题回答yes
    +-------+---------------------------------------------------------------------------------

如果自己的选项也采用同样的含义,这样用户在使用这样的脚本时就不用去查手册了。


4.6 获得用户输入
-----------------------------------------------------------------------------------------------------------------------------------------
尽管命令行选项和参数是从脚本用户处获得输入的一种重要方式,但有时脚本的交互性还需要更强一些。比如想要在脚本运行时问个问题,并等待运行脚本的
用户来回答。bash shell 为此提供了 read 命令。


4.6.1 基本的读取
-----------------------------------------------------------------------------------------------------------------------------------------
read 命令从标准输入(键盘)或另一个文件描述符中接受输入。在收到输入后,read 命令会将数据放进一个变量。下面是 read 命令的最简单用法。

示例:
    [devalone@devalone 14]$ cat test21.sh
    #!/bin/bash
    # testing the read command
    #
    echo -n "Enter your name: "
    read name
    echo "Hello $name, welcome to my program"

运行:
    [devalone@devalone 14]$ test21.sh
    Enter your name: Michael
    Hello Michael, welcome to my program

注意,生成提示的 echo 命令使用了 -n 选项。该选项不会在字符串末尾输出换行符,允许脚本用户紧跟其后输入数据,而不是下一行。这让脚本看起来更
像表单。

实际上,read 命令包含了 -p 选项,允许直接在 read 命令行指定提示符。

示例:
    [devalone@devalone 14]$ cat test22.sh
    #!/bin/bash
    #
    # testing the read -p option
    #
    read -p "Please enter your age: " age
    days=$[ $age * 365 ]
    echo "That makes you over $days days old!"

运行:
    [devalone@devalone 14]$ test22.sh
    Please enter your age: 22
    That makes you over 8030 days old!

再次运行第一个例子,输入名和姓氏两个单词:
    [devalone@devalone 14]$ test21.sh
    Enter your name: Michael Y.
    Hello Michael Y., welcome to my program

在例示例中当有名字输入时,read 命令会将姓和名保存在同一个变量中。read 命令会将提示符后输入的所有数据分配给单个变量,要么就指定多个变量。
输入的每个数据值都会分配给变量列表中的下一个变量。如果变量数量不够,剩下的数据就全部分配给最后一个变量。

示例:
    [devalone@devalone 14]$ cat test23.sh
    #!/bin/bash
    # enter multiple variablse
    #
    read -p "Enter your name: " first last
    echo "Checking data for $last, $first"

运行:
    [devalone@devalone 14]$ test23.sh
    Enter your name: Michael Ya
    Checking data for Ya, Michael

也可以在 read 命令行中不指定变量。如果是这样,read 命令会将它收到的任何数据都放进特殊环境变量 REPLY 中。

示例:
    [devalone@devalone 14]$ cat test24.sh
    #!/bin/bash
    # testing the REPLY environment variable
    #
    read -p "Enter you name: "
    echo
    echo Hello $REPLY, welcome to my program

运行:
    [devalone@devalone 14]$ test24.sh
    Enter you name: Michael

    Hello Michael, welcome to my program

REPLY 环境变量会保存输入的所有数据,可以在 shell 脚本中像其他变量一样使用。


4.6.2 超时
-----------------------------------------------------------------------------------------------------------------------------------------
使用 read 命令时要当心。脚本很可能会一直等着脚本用户的输入。如果不管是否有数据输入,脚本都必须继续执行,可以用 -t 选项来指定一个计时器。
-t 选项指定了 read 命令等待输入的秒数。当计时器过期后,read 命令会返回一个非零退出状态码。

示例:
    [devalone@devalone 14]$ cat test25.sh
    #!/bin/bash
    # enter multiple variablse
    #
    if read -t 5 -p "Enter your name: " name
    then
        echo "Hello $name, welcome to my script"
    else
        echo "Sorry, too slow"
    fi

运行:
    [devalone@devalone 14]$ test25.sh
    Enter your name: Sorry, too slow

如果计时器过期,read 命令会以非零退出状态码退出,可以使用如 if-then 语句或 while 循环这种标准的结构化语句来理清所发生的具体情况。在本例中,
计时器过期时,if 语句不成立,shell 会执行 else 部分的命令。

也可以不对输入过程计时,而是让 read 命令来统计输入的字符数。当输入的字符达到预设的字符数时,就自动退出,将输入的数据赋给变量。

示例:
    [devalone@devalone 14]$ cat test26.sh
    #!/bin/bash
    # getting just one character of input
    #
    read -n1 -p "Do you want to continue [Y/N]?" answer
    case $answer in
        Y | y) echo
               echo "fine, continue on...";;
        N | n) echo
               echo OK, goodbye
               exit;;
    esac
    echo "This is the end of the scripty"

运行:
    [devalone@devalone 14]$ test26.sh
    Do you want to continue [Y/N]?y
    fine, continue on...
    This is the end of the scripty
    [devalone@devalone 14]$ test26.sh
    Do you want to continue [Y/N]?n
    OK, goodbye

本例中将 -n 选项和值 1 一起使用,告诉 read 命令在接受单个字符后退出。只要按下单个字符回答后,read 命令就会接受输入并将它传给变量,无需按
回车键。


4.6.3 隐藏方式读取
-----------------------------------------------------------------------------------------------------------------------------------------
有时需要从脚本用户处得到输入,但又在屏幕上显示输入信息。其中典型的例子就是输入的密码,但除此之外还有很多其他需要隐藏的数据类型。

-s 选项可以避免在 read 命令中输入的数据出现在显示器上。

示例:
    [devalone@devalone 14]$ cat test27.sh
    #!/bin/bash
    # hiding input data from the monitor
    #
    read -s -p "Enter your password: " pass
    echo
    echo "Is your password really $pass ?"

运行:
    [devalone@devalone 14]$ test27.sh
    Enter your password:
    Is your password really 12345 ?

输入提示符输入的数据不会出现在屏幕上,但会赋给变量,以便在脚本中使用。


4.6.4 从文件中读取
-----------------------------------------------------------------------------------------------------------------------------------------
也可以用 read命令来读取 Linux 统上文件里保存的数据。每次调用 read 命令,它都会从文件中读取一行文本。当文件中再没有内容时,read 命令会退出
并返回非零退出状态码。

其中最难的部分是将文件中的数据传给 read 命令。最常见的方法是对文件使用 cat 命令,将结果通过管道直接传给含有 read 命令的 while 命令。

示例:
    [devalone@devalone 14]$ cat test28.sh
    #!/bin/bash
    # reading data from a file
    #
    count=1
    cat test | while read line
    do
        echo "Line $count: $line"
        count=$[ $count + 1 ]
    done

    echo "finished processing the file"

运行:
    [devalone@devalone 14]$ cat test
    The quick brown dog jumps over the lazy fox.
    This is a test, this only a test.
    Romeo,Romeo! wherefore art thou Romeo?

    [devalone@devalone 14]$ test28.sh
    Line 1: The quick brown dog jumps over the lazy fox.
    Line 2: This is a test, this only a test.
    Line 3: Romeo,Romeo! wherefore art thou Romeo?
    finished processing the file


while 循环会持续通过 read 命令处理文件中的行,直到 read 命令以非零退出状态码退出。

系列目录:

    Linux shell 脚本编程-基础篇 (一)

    Linux shell 脚本编程-基础篇 (二)

    Linux shell 脚本编程-基础篇 (三)

    Linux shell 脚本编程-基础篇 (四)

    Linux shell 脚本编程-基础篇 (五)

    Linux shell 脚本编程-基础篇 (六)

-----------------------------------------------------------------------------------------------------------------------------------------


参考:

    《Linux 命令行与 shell 脚本编程大全》 第 3 版 —— 2016.8(美)Richard Blum  Cristine Bresnahan

猜你喜欢

转载自blog.csdn.net/devalone/article/details/81302118