处理用户输入: getopt, getopts, read

作者:jicanmeng

时间:2016年05月14日


  1. bash shell的命令行参数变量: $0,$1,$2...
  2. 三个特殊的命令行参数相关的变量: $#,$*,$@
  3. 命令行参数的移动: shift
  4. 命令行参数选项
    1. 简单的命令行参数选项
    2. 分离命令行参数中的选项和实际参数
    3. 选项的参数
    4. getopt的引入
    5. getopts的引入
  5. 获取用户输入:read命令

1. bash shell的命令行参数变量: $0,$1,$2...

shell scipt的每个参数都是用空格分开的。要在参数中包含空格,必须要用引号(单引号和双引号均可)。

$1表示shell scipt的第一个参数,$2表示第二个参数,依次类推,直到第九个参数$9。从第十个参数开始,必须要加上{}。例如${10},${11}等。

$0表示shell script程序的名字。其中还包含了脚本的路径。举例如下:

[jicanmeng@andy tmp]$ cat 1-command-parameter.sh
                        #!/bin/bash

                        echo "\$0 is $0"
                    [jicanmeng@andy tmp]$ bash 1-command-parameter.sh
                        $0 is 1-command-parameter.sh
                    [jicanmeng@andy tmp]$ ./1-command-parameter.sh
                        $0 is 1-command-parameter.sh
                    [jicanmeng@andy tmp]$ bash /home/jicanmeng/Desktop/tmp/1-command-parameter.sh
                        $0 is /home/jicanmeng/Desktop/tmp/1-command-parameter.sh
                    [jicanmeng@andy tmp]$ 

可以看到,bash 1-command-parameter.sh1-command-parameter.sh在执行时是完全等价的,无论哪种形式,执行的都是1-command-paramter.sh,所以$0都是1-command-parameter.sh。

在很多时候,我们只需要知道脚本的名称,而不需要知道脚本所在的路径。我们可以通过basename命令来解决这个问题,它只返回程序名。举例如下:

[jicanmeng@andy tmp]$ cat 2-command-parameter.sh
                        #!/bin/bash

                        echo "\$0 is $0"
                        name=`basename $0`
                        echo "program name is $name"
                    [jicanmeng@andy tmp]$ ./2-command-parameter.sh
                        $0 is ./2-command-parameter.sh
                        program name is 2-command-parameter.sh
                    [jicanmeng@andy tmp]$ ./home/jicanmeng/Desktop/tmp/2-command-parameter.sh
                        $0 is /home/jicanmeng/Desktop/tmp/2-command-parameter.sh
                        program name is 2-command-parameter.sh
                    [jicanmeng@andy tmp]$ 

2. 三个特殊的命令行参数相关的变量: $#,$*,$@

有三个特殊的变量:$#表示脚本运行时命令行参数的个数; $*$@都表示从$1开始的所有参数。$*$@的区别如下:

举例如下:

[jicanmeng@andy tmp]$ cat 3-command-parameter.sh
                        #!/bin/bash

                        echo "There are $# pamameters supplied"

                        count=1
                        for param in "$*"
                        do
	                            echo "\$* parameter #$count = $param"
	                            count=$[ $count + 1 ]
                        done

                        count=1
                        for param in "$@"
                        do
	                            echo "\$@ parameter #$count = $param"
	                            count=$[ $count + 1 ]
                        done
                    [jicanmeng@andy tmp]$ ./3-command-parameter.sh aa bb cc
                        There are 3 pamameters supplied
                        $* parameter #1 = aa bb cc
                        $@ parameter #1 = aa
                        $@ parameter #2 = bb
                        $@ parameter #3 = cc
                    [jicanmeng@andy tmp]$ 

虽然$#表示所有参数的个数,但是${$#}并不表示最后一个参数。最后一个参数用${!#}来表示。而且重要的是,当命令行没有参数时,$#的值为零,但${!#}变量会返回命令行用到的脚本名,即等于$0的值。举例如下:

[jicanmeng@andy tmp]$ cat 4-command-parameter.sh
                        #!/bin/bash

                        echo The last parameter is ${!#}
                    [jicanmeng@andy tmp]$ ./4-command-parameter.sh aa bb cc dd
                        The last parameter is dd
                    [jicanmeng@andy tmp]$ ./4-command-parameter.sh
                        The last parameter is 4-command-parameter.sh
                    [jicanmeng@andy tmp]$ 

3. 命令行参数的移动: shift

bash shell使用shift命令移动命令行参数。

默认情况下,shift命令会将每个命令行参数变量向前移动一位。所以,$3的值会移动到$2, $2的值会移动到$1, 而$1的值则会被删除(注意,变量$0的值不会改变)。同时参数个数也会发生变化,我们可以通过$#的值看出来。

另外,你也可以给shift命令一个参数,表示移动几位。

举例如下:

[jicanmeng@andy tmp]$ cat 5-shift.sh
                        #!/bin/bash

                        count=1
                        while [ -n "$1" ]
                        do
	                            echo "Parameter #$count = $1"
	                            count=$[$count+1]
	                            shift
                        done
                    [jicanmeng@andy tmp]$ ./5-shift.sh aa bb cc dd
                        Parameter #1 = aa
                        Parameter #2 = bb
                        Parameter #3 = cc
                        Parameter #4 = dd
                    [jicanmeng@andy tmp]$ 

4. 命令行参数选项

在实际应用中,我们常常遇到同时提供了参数和和选项的bash命令,例如ls -l, cat -A test.txt。选项(option)是跟在单破折线后面的单个字母,能改变命令的行为。

其实选项也没有什么特殊的,就和前面遇到的普通的命令行参数一样。下面讨论三种处理选项的方法,最后引出getopt命令和getopts命令。

4.1 简单的命令行参数选项

我们只看命令行参数中有什么选项,有符合条件的就打印出来。程序如下:

[jicanmeng@andy tmp]$ cat 6-options-1-simple.sh
                        #!/bin/bash

                        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
                    [jicanmeng@andy tmp]$ ./6-options-1-simple.sh -a -b -c -d
                        found the -a option
                        found the -b option
                        found the -c option
                        -d is not an option
                    [jicanmeng@andy tmp]$ 

4.2 分离命令行参数中的选项和实际参数

shell脚本运行时常常会遇到同时使用选项和参数的情况(注意,这里的参数不是选项的参数)。在这个脚本中,我们看命令行参数中有什么选项,有符合条件的就打印出来,同时也将脚本的实际参数打印出来。

但是如何判断已经到了选项末尾,下一个就是实际参数呢? linux中处理这个问题的标准方法是用特殊字符--将二者分开,该字符告诉脚本选项到此结束,下一个就是实际参数了。

本脚本程序中存在的问题是假设实际参数都放在了命令行参数的最后,选项都放在了前面。程序如下:

[jicanmeng@andy tmp]$ cat 7-options-2-separate-option-and-param.sh
                        #!/bin/bash

                        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
                    [jicanmeng@andy tmp]$ ./7-options-2-separate-option-and-param.sh -c -b -a -- test1 test2 test3
                        found the -c option
                        found the -b option
                        found the -a option
                        parameter #1: test1
                        parameter #2: test2
                        parameter #3: test3
                    [jicanmeng@andy tmp]$ 

4.3 选项的参数

有时选项会带上一个额外的参数值。那我们还要继续修改我们的脚本程序:

[jicanmeng@andy tmp]$ cat 8-options-3-option-with-value.sh
                        #!/bin/bash

                        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
                    [jicanmeng@andy tmp]$ ./8-options-3-option-with-value.sh -c -b test1 -a -- test2 test3
                        found the -c option
                        found the -b option, with parameter value: test1
                        found the -a option
                        parameter #1: test2
                        parameter #2: test3
                    [jicanmeng@andy tmp]$ 

现在的问题是:如果将两个选项合并,例如./8-options-3-option-with-value.sh -ac,shell script就不能正常运行了。我们可以用getopt命令来解决这个问题。

4.4 getopt的引入

getopt命令是一个在处理命令行选项和参数时非常方便的工具。它可以接受一系列任意形式的命令行选项和参数,将它们转换成适当的格式并返回。命令格式如下:
getopt optstring options parameters

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

[jicanmeng@andy tmp]$ getopt ab:cd -a -b test1 -cd test2 test3
                         -a -b test1 -c -d -- test2 test3
                    [jicanmeng@andy tmp]$ 

在这个例子中,optstring中定义了4个有效的选项字母:a、b、c和d。它还定义了选项字母b后面需要一个参数值。这里需要注意,getopt会自动将-cd选项分成两个单独的选项,并插入双破折线--来分开行中的额外参数。我们在4.2 分离命令行参数中的选项和实际参数中提到使用双破折线--来分隔选项和实际参数是linux的标准方法。

看一个在脚本中使用getopt命令的实例:

[jicanmeng@andy tmp]$ cat 9-option-getopt.sh
                        #!/bin/bash

                        set -- `getopt ab:c "$@"`

                        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
                    [jicanmeng@andy tmp]$ ./9-option-getopt.sh -b test1 -ac test2 test3 test4
                        found the -b option, with parameter value: test1
                        found the -a option
                        found the -c option
                        parameter #1: test2
                        parameter #2: test3
                        parameter #3: test4
                    [jicanmeng@andy tmp]$ ./9-option-getopt.sh test2 -b test1 -ac test3 test4
                        found the -b option, with parameter value: test1
                        found the -a option
                        found the -c option
                        parameter #1: test2
                        parameter #2: test3
                        parameter #3: test4
                    [jicanmeng@andy tmp]$ 

首先说明一下set命令。set命令的选项之一是双破折线,它会将命令行参数替换成set命令的命令行的值。对于本例,getopt命令首先得到的原始的脚本的命令行参数并解析,将解析的结果返回给set命令,set命令则用getopt返回的格式化的参数替换原始的命令行参数。

可以看出,getopt解决了4.3中提到的问题。两个选项合并在一起,getopt也能正确解析。而且getopt还有一个优点:在命令行参数中,可以先写实际参数,再写选项和参数。即实际参数可以和选项以及选项参数混合写,最终也会解析成为--前面是选项和选项参数,后面是实际参数的标准形式

但是shell还有一个问题:不能处理包含空格的命令行参数。不但命令行实际参数中不能包含空格,选项参数中也不能包含空格。例如下面的例子:

[jicanmeng@andy tmp]$ ./9-option-getopt.sh -a -b hello -c test1 "test2 test3"
                        found the -a option
                        found the -b option, with parameter value: hello
                        found the -c option
                        parameter #1: test1
                        parameter #2: test2
                        parameter #3: test3
                    [jicanmeng@andy tmp]$ ./9-option-getopt.sh -a -b "hello world" -c test1 test2 test3
                        found the -a option
                        found the -b option, with parameter value: hello
                        world is not an option
                        found the -c option
                        parameter #1: test1
                        parameter #2: test2
                        parameter #3: test3
                    [jicanmeng@andy tmp]$ 

可以看到,getopt命令会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。我们可以通过getopts命令来解决这个问题。

4.5 getopts的引入

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

    相对于getopt,getopts的优点是:
  1. 可以在选项参数中包含空格;
  2. 可以将选项字母和选项的参数值放在一起使用,而不用加空格;(其实getopt也有这个特点。)
  3. 它能够将命令行上找到的所有未定义的选项统一输出为问号。

但是getopts的缺点也有,它不能把命令行实际参数放到前面。例如:对于命令行参数-a -b hello -c -d para1 para2 para3就不能写成para3 -a -b hello -c -d para1 para2。而getopt就可以正常解析-上面刚刚提到了getopt的这个优点。不过在实际的应用中,我们一般都会先写命令行选项和选项参数,再写实际参数

getopts每次执行循环,getopts 就检查下一个命令行参数,并判断它是否合法。即检查参数是否以 - 开头,后面跟一个包含在 options 中的字母。如果是,就把匹配的选项字母存在指定的变量 variable 中,并返回退出状态0;如果 - 后面的字母没有包含在 options 中,就在 variable 中存入一个 ?,并返回退出状态0;如果命令行中已经没有参数,或者下一个参数不以 - 开头,就返回不为0的退出状态。

我们也可以用info getopts来查看。要重点注意OPTIND和OPTARG的意义。

格式:getopts optstring name
OPTIND is initialized to 1 each time the shell or a shell script is invoked. When an option requires an argument, getopts places that argument into the variable OPTARG. When the end of options is encountered, getopts exits with a return value greater than zero. OPTIND is set to the index of the first non-option argument, and name is set to ?.

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

看一个简单的使用getopts的例子:

[jicanmeng@andy tmp]$ cat 10-option-getopts-1.sh
                        #!/bin/bash

                        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
                    [jicanmeng@andy tmp]$ ./10-option-getopts-1.sh -ab "test1 test2" -c

                        found the -a option
                        found the -b option. with value test1 test2
                        found the -c option
                    [jicanmeng@andy tmp]$ 

可以看到,getopts可以正常解析包含空格的选项参数了,当然了,也可以正常解析包含空格的实际参数了。另外,你可能会注意到本例中case语句的用法有些不同。原因是getopts命令解析命令行选项时,它会移除开头的单破折线,所以在case语句中不用但破折线。

在看一个使用getopts命令的例子。在这个例子中,将OPTIND值和shift命令一起使用来移动参数:

[jicanmeng@andy tmp]$ cat 11-option-getopts-2.sh
                        #!/bin/bash

                        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 ]

                        count=1
                        for param in "$@"
                        do
	                            echo "parameter $count: $param"
	                            count=$[ $count + 1 ]
                        done
                    [jicanmeng@andy tmp]$ ./11-option-getopts-2.sh -ab test1 -d test2 test3
                        found the -a option
                        found the -b option. with value test1
                        found the -d option
                        parameter 1: test2
                        parameter 2: test3
                    [jicanmeng@andy tmp]$ 

5. 获取用户输入:read命令

read命令接受从标准输入(键盘)或另一个文件描述符的输入。在收到输入后,read命令会将数据放进一个变量中。常用的命令行选项有-p,-n,-s,-t。

从文件中读取数据时,每次read命令都会从文件中读取一行文本,就像c语言中的read()一样,然后移动读指针。例如下面的例子:

[jicanmeng@andy tmp]$ cat test.txt
                        hello
                        world
                        this is a test
                    [jicanmeng@andy tmp]$ cat 13-read-file.sh
                        #!/bin/bash

                        count=1
                        cat test.txt | while read line
                        do
	                            echo "Line $count: $line"
	                            count=$[ $count + 1 ]
                        done
                    [jicanmeng@andy tmp]$ ./13-read-file.sh
                        line 1: hello
                        line 2: world
                        line 3: this is a test
                    [jicanmeng@andy tmp]$ 

参考资料

  1. Lnux命令行与shell脚本编程大全
  2. 鸟哥的linux私房菜
  3. http://www.cnblogs.com/xiangzi888/archive/2012/04/03/2430736.html