处理输入
- 命令行参数
- 读取参数
- 读取脚本名
- 测试参数
- 特殊参数变量
- 参数统计
- 获取所有参数
- 移动变量
- 处理选项
- 查找选项
- 处理简单选项
- 分离选项和参数
- 处理带值的选项
- getopt 命令
- 命令格式
- 在脚本中使用getopt
- getopts命令
- 脚本选项标准化
- 获取用户的输入
- 基本的读取
- 超时
- 隐藏式读取
- 文件中读取
在此之前我们已经学习了编写脚本,处理数据、变量和系统文件。有时,我们编写的脚本还得能够与使用者进行交互。bash shell提供了一些不同的方法来从用户处获得数据,包括 命令行参数(添加在命令后的数据)、命令行选项(可修改命令行为的单个字母)以及直接从键盘读取输入的能力,在下面我们都会一一学习
命令行参数
- 向shell脚本传递数据的最基本方法是使用命令行参数。命令行参数允许在运行脚本时向命令行添加参数,如下:
./test01 5 2
- 上例向脚本test01 传递了两个命令行参数(5和30)。脚本会通过特殊的变量来处理命令行参数
读取参数
- bash shell会将这些被称为位置参数(positional parameter)的特殊变量分配给输入到命令行中的所有参数。这也包括shell所执行的脚本名称。位置参数变量是标准的数字:$0是程序名,$1是第一个参数,$2是第二个参数,依次类推,直到第九个参数$9,如下:
#!/bin/bash
var1=1
for (( number = 1; number <= $1 ; number++ ))
do
var1=$[ $var1 * $number ]
done
echo "计算结果为:$var1"
- 可以在shell脚本中像使用其他变量一样使用$1变量。shell脚本会自动将命令行参数的值分配给变量,不需要再作任何处理
- 如果需要输入更多的命令行参数,则每个参数都必须用空格分开,如下:
#!/bin/bash
total=$[ $1 + $2 ]
echo "参数1为:$1"
echo "参数2为:$2"
echo "总值为:$total"
- 命令行上也可以传递文本字符串参数,但是碰到包含空格的字符串需要注意下述问题,如下:
#!/bin/bash
echo "您好!$1,欢迎登录系统!!"
- 每个参数都是用空格分隔的,所以shell会将空格当成两个值的分隔符。要在参数值中包含空格,必须要用引号(单引号或双引号均可),如下:
- 注意:将文本字符串作为参数传递时,引号并非数据的一部分。它们只是表明数据的起止位置
- 当脚本需要的命令行参数不止9个,我们还是可以处理的,但是需要稍微修改一下变量名。在第9个变量之后,你必须在变量数字周围加上花括号{},如下:
#!/bin/bash
total=$[ ${10} + ${11} ]
echo "第10个参数为:${10}"
echo "第11个参数为:${11}"
echo "合计值为:$total"
读取脚本名
- 可以用**$0参数**获取shell在命令行启动的脚本名。这在我们后续进行一些操作时是很方便的,如下:
#!/bin/bash
echo "正在运行的脚本为:$0!!"
- 如下是两种运行脚本的方法的运行结果:
- 上述存在一个潜在的问题。如果使用**./命令**来运行shell脚本,命令会和脚本名混在一起,出现在$0参数中,当传给$0变量的实际字符串不仅仅是脚本名,而是完整的脚本路径时, 变量$0就会使用整个路径,如下:
- 如果我们要编写一个根据脚本名来执行不同功能的脚本,就得做点额外工作。得把脚本的运行路径给剥离掉。另外,还要删除与脚本名混杂在一起的命令,幸好有个方便的小命令可以帮到我们。basename命令会返回不包含路径的脚本名,如下:
#!/bin/bash
name=$(basename $0)
echo "正在运行的脚本为:$name!!"
- 现在我们就可以使用这种方法来编写基于脚本名称执行不同功能的脚本,如下:
#!/bin/bash
name=$(basename $0)
if [ $name = "add_num" ]
then
total=$[ $1 + $2 ]
elif [ $name = "mult_num" ]
then
total=$[ $1 * $2 ]
fi
echo "计算结果为:$total"
- 将此脚本复制两份名字为:add_num与mult_num,如下:
- 测试结果如下:
- 上述脚本,会根据脚本的名称不同来执行不一样的计算逻辑!!
测试参数
- 在shell脚本中使用命令行参数需要注意。如果脚本不加参数运行,可能会出现问题,如下:
- 当脚本认为参数变量中会有数据而实际上并没有时,脚本很有可能会产生错误消息。这种写脚本的方法并不可取。所以,我们在使用参数前一定要检查其中是否存在数据,如下:
#!/bin/bash
if [ -n "$1" ]
then
echo "欢迎您,$1!!"
else
echo "请输入您的用户名!!"
fi
- 在本例中,我们使用了-n测试来检查命令行参数$1中是否有数据,后续我们会学习另一种检查命令行参数的方法
特殊参数变量
- 除上述的参数外,在bash shell中还有些特殊变量,它们会记录命令行参数,如下介绍
参数统计
- 如在上面所看到的,在脚本中使用命令行参数之前应该检查一下命令行参数。对于使用多个命令行参数的脚本来说,这有点麻烦,因此,我们可以统计命令行中一共输入了多少个参数,无需测试每个参数。bash shell为此提供了一个特殊变量
- **特殊变量$#**包含脚本运行时携带的命令行参数的个数。可以在脚本中任何地方使用这个特殊变量,就跟普通变量一样,如下:
#!/bin/bash
echo "你一共输入了$#个参数!!"
- 现在我们可以在使用参数之前测试参数的总数是否符合要求,如下:
#!/bin/bash
if [ $# -eq 2 ]
then
total=$[ $1 + $2 ]
echo "计算结果为:$total"
else
echo "请输入正确的参数!!"
fi
- if-then语句用-eq测试命令行参数数量。如果参数数量不对,会显示一条错误消息告知脚
本的正确用法。 - 除此之外,还可以使用这个变量简便的获取命令行中最后一个参数,完全不需要知道实际上到底用了多少个参数,
$#变量是输入参数的总数,那么变量${$#}就表示最后一个命令行参数
,我们实际操作一下,如下:
#!/bin/bash
echo "最后一个参数为:${$#}"
- 显然,上述的结果是不符合要求的。它表明我们不能在花括号内使用美元符。必须将美元符换成感叹号(!)。虽然奇怪,但确实有用,如下:
#!/bin/bash
var1=$#
echo "最后一个参数为:$var1"
echo "最后一个参数为:${!#}"
- 在上述的脚本中将
$#变量的值赋给了变量var1
,然后也按特殊命令行参数变量的格式使用了该变量。两种方法都没问题。重要的是要注意,当命令行上没有任何参数时,$#的值为0,var1变量的值也一样,但${!#}变量会返回命令行用到的脚本名
获取所有参数
- 有时候我们需要获取命令行上提供的所有参数。这时候就不需要先用$#变量来判断命令行上有多少参数,然后再进行遍历,你可以使用下述的一组其他的特殊变量来解决这个问题
- $*和$@变量可以用来获取所有的参数。这两个变量都能够在单个变量中存储所有的命令行参数,如下介绍:
- $*变量:
- $*变量会将命令行上提供的所有参数当作一个单词保存。这个单词包含了命令行中出现的每一个参数值。基本上$*变量会将这些参数视为一个整体,而不是多个个体
- $@变量:
- $@变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。这样
你就能够遍历所有的参数值,得到每个参数。可以使用for命令循环,如下举例:
- $@变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。这样
#!/bin/bash
echo
echo "\$* 获取的所有参数值为:$*"
echo
echo "\$@ 获取的所有参数值为:$@"
- 从上述的例子中,表面上看,两个变量产生的是同样的输出,都显示出了所有命令行参数,但是其实本质上是不一样的,如下:
#!/bin/bash
count=1
for val in "$*"
do
echo "\$*变量 #$count = $val"
count=$[ $count + 1 ]
done
echo "----------------------------"
count=1
for var in "$@"
do
echo "\$@变量 #$count = $var"
count=$[ $count + 1 ]
done
- 通过使用for命令遍历这两个特殊变量,可以看到它们是如何不同地处理命令行参数的。
$*变量会将所有参数当成单个参数,而$@变量会单独处理每个参数
移动变量
- bash shell还提供了一个工具命令shift。bash shell的shift命令能够用来操作命令行参数。跟字面上的意思一样,shift命令会根据它们的相对位置来移动命令行参数
- 在使用shift命令时,默认情况下它会将每个参数变量向左移动一个位置。即为变量$3的值会移到$2中,变量$2的值会移到$1中,而变量$1的值则会被删除(注意,变量$0的值,也就是程序名,不会改变)
- 这是遍历命令行参数的另一种方法,尤其是在你不知道到底有多少参数时。你可以只操作第一个参数,移动参数,然后继续操作第一个参数,非常方便,如下:
#!/bin/bash
count=1
while [ -n "$1" ]
do
echo "参数为:#$count = $1"
count=$[ $count + 1]
shift
done
- 脚本通过测试第一个参数值是否存在执行了一个while循环。当第一个参数不存在,循环结束。测试完第一个参数后,shift命令会将所有参数的位置向左移动一个位置
- 注意:使用shift命令的时候要小心。如果某个参数被移出,它的值就被丢弃了,无法再恢复
- 另外,还可以一次性移动多个位置,只需要给shift命令提供一个数字类型参数,指明要移动的位置数即可,如下:
#!/bin/bash
echo
echo "初始参数为:$*"
echo "移动之前的第一个参数:$1"
shift 3
echo "移动之后的第一个参数:$1"
处理选项
- 我们在使用命令时应该就见过了一些同时提供了参数和选项的bash命令,如
ls -l
l就是选项。选项是跟在单破折线后面的单个字母,它能改变命令的行为。下面会介绍在脚本中处理选项的方法,如下介绍
查找选项
- 表面上看,命令行选项也没什么特殊的。在命令行上,它们紧跟在脚本名之后,就跟命令行参数一样。实际上,我们可以像处理命令行参数一样处理命令行选项。
处理简单选项
- 我们在上面已经学习了如何使用shift命令来依次处理脚本程序携带的命令行参数。也可以用同样的方法来处理命令行选项,在提取每个单独参数时,用case语句来判断某个参数是否为选项,如下:
#!/bin/bash
while [ -n "$1" ]
do
case "$1" in
-a) echo "-a参数存在" ;;
-b) echo "-b参数存在" ;;
-c) echo "-c参数存在" ;;
*) echo "$1参数不存在" ;;
esac
shift
done
- case语句会检查每个参数是不是有效选项。如果是的话,就运行对应case语句中的命令。不管选项按什么顺序出现在命令行上,这种方法都适用,case语句在命令行参数中找到一个选项,就处理一个选项。如果命令行上还提供了其他参数,我们可以在case语句的通用情况处理部分中处理
分离选项和参数
- 我们还会在shell脚本中碰到同时使用选项和参数的情况。Linux中处理这个问题的标准方式是用特殊字符来将二者分开,该字符会告诉脚本何时选项结束以及普通参数何时开始
- 对Linux来说,这个特殊字符是双破折线(--)。shell会用双破折线来表明选项列表结束。在双破折线之后,脚本就可以放心地将剩下的命令行参数当作参数,而不是选项来处理了。要检查双破折线,只要在case语句中加一项即可,如下:
#!/bin/bash
while [ -n "$1" ]
do
case "$1" in
-a) echo "-a选项存在" ;;
-b) echo "-b选项存在" ;;
-c) echo "-c选项存在" ;;
--) shift
break ;;
*) echo "$1选项不存在" ;;
esac
shift
done
count=1
for var in $@
do
echo "参数为:#$count:$var"
count=$[ $count + 1 ]
done
- 当shell脚本遇到双破折线时,它会停止处理选项,并将剩下的参数都当作命令行参数
处理带值的选项
- 有些选项会带上一个额外的参数值。在这种情况下,命令行看起来像下面这样:
./test.sh -a wy -b -c -d wwy
- 当命令行选项要求额外的参数时,shell脚本必须能检测到并正确处理,如下:
#!/bin/bash
while [ -n "$1" ]
do
case "$1" in
-a) echo "-a选项存在" ;;
-b) var1="$2"
echo "-b选项存在,且参数为$var1"
shift ;;
-c) echo "-c选项存在" ;;
--) shift
break ;;
*) echo "$1选项不存在" ;;
esac
shift
done
count=1
for var in $@
do
echo "参数为:#$count:$var"
count=$[ $count + 1 ]
done
- 在上述例子中,case语句定义了三个它要处理的选项。-b选项还需要一个额外的参数值。由于要处理的参数是$1,额外的参数值就应该位于$2(因为所有的参数在处理完之后都会被移出)。只要将参数值从$2变量中提取出来就可以。当然,因为这个选项占用了两个参数位,所以你还需要使用shift命令多移动一个位置
- 使用上述的基本特性,整个shell脚本过程就能正常工作,不管按什么顺序放置选项(但要记住包含每个选项相应的选项参数),如下:
- 现在我们的shell脚本中已经有了处理命令行选项的基本能力,但还有一些限制。比如,如果你想将多个选项放进一个参数中时,它就会出现问题,如下:
- 在Linux中,合并选项是一个很常见的用法,而且如果脚本想要对用户使用更加方便友好,也要给用户提供这种特性,下述会介绍处理方法
getopt 命令
- getopt命令是一个在处理命令行选项和参数时非常方便的工具。它能够识别命令行参数,从而使脚本更加方便的处理命令行选项和参数
命令格式
- getopt命令可以接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。命令格式如下:
getopt optstring parameters
- optstring是这个过程的关键所在。它定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值,首先,在optstring中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。getopt命令会基于你定义的optstring解析提供的参数,如下简单举例:
getopt ab:cd -a -b wy -cd ww yy
- optstring定义了四个有效选项字母:a、b、c和d,冒号(:)被放在了字母b后面,因为b选项需要一个参数值。当getopt命令运行时,它会检查提供的参数列表(-a -b wy -cd ww yy),并基于提供的optstring进行解析。注意,它会自动将-cd选项分成两个单独的选项,并插入双破折线来分隔行中的额外参数,如果指定了一个不在optstring中的选项,默认情况下,getopt命令会产生一条错误消息,如下:
getopt ab:cd -a -b wy -cd ww yy -e
- 如果想要忽略这条错误消息,可以在命令后加-q选项,如下:
- 注意:getopt命令选项(-q)必须出现在optstring之前
在脚本中使用getopt
- 可以在脚本中使用getopt来格式化脚本所携带的任何命令行选项或参数,但用起来略微复杂。方法是用getopt命令生成的格式化后的版本,使用set命令来替换已有的命令行选项和参数,set命令能够处理shell中的各种变量,set命令的选项之一是双破折线(--),它会将命令行参数替换成set命令的命令行值
- 该方法会将原始脚本的命令行参数传给getopt命令,之后再将getopt命令的输出传给set命令,用getopt格式化后的命令行参数来替换原始的命令行参数,如下格式:
set -- $(getopt -q ab:cd "$@")
- 原始的命令行参数变量的值会被getopt命令的输出替换,而getopt已经为我们格式化
好了命令行参数,如下:
#!/bin/bash
set -- $(getopt -q ab:cd "$@")
while [ -n "$1" ]
do
case "$1" in
-a) echo "-a选项存在" ;;
-b) var1="$2"
echo "-b选项存在,且参数为$var1"
shift ;;
-c) echo "-c选项存在" ;;
--) shift
break ;;
*) echo "$1选项不存在" ;;
esac
shift
done
count=1
for var in $@
do
echo "参数为:#$count:$var"
count=$[ $count + 1 ]
done
- 上述运行结果显示,已经达到我们预期的结果,但是,在getopt命令中仍然隐藏着一个小问题,如下:
- getopt命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数,如下介绍解决方法
getopts命令
- 与getopt不同,getopt将命令行上选项和参数处理后只生成一个输出,而getopts命令能够和已有的shell参数变量配合默契。每次调用它时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于0的退出状态码,命令格式如下:
getopts optstring variable
- optstring值类似于getopt命令中的那个。有效的选项字母都会列在optstring中,如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,可以在optstring之前加一个冒号。getopts命令将当前参数保存在命令行中定义的variable中
- getopts命令会用到两个环境变量。如果选项需要跟一个参数值,OPTARG环境变量就会保存这个参数值。OPTIND环境变量保存了参数列表中getopts正在处理的参数位置。这样就能在处理完选项之后继续处理其他命令行参数,如下:
#!/bin/bash
while getopts :ab:c opt
do
case "$opt" in
a) echo "选项-a存在!!";;
b) echo "选项-b存在,且参数值为$OPTARG!!";;
c) echo "选项-c存在!!";;
*) echo "选项$opt不存在!!";;
esac
done
- while语句定义了getopts命令,指明了要查找哪些命令行选项,以及每次迭代中存储它们的变量名(opt),注意,在本例中case语句的用法有些不同。getopts命令解析命令行选项会移除开头的单破折线,所以在case定义中不用单破折线
- getopts命令有几个好用的功能,可以在参数值中包含空格,如下:
- 还可以将选项字母和参数值放在一起使用,而不用加空格,如下:
- getopts命令能够从-b选项中正确解析出参数wyyy值。除此之外,getopts还能够将命令行上找到的所有未定义的选项统一输出成问号,如下:
- optstring中未定义的选项字母会以问号形式发送给代码,getopts命令知道何时停止处理选项,并将参数留给你处理
- 在getopts处理每个选项时,它会将OPTIND环境变量值增一。在getopts完成处理时,你可以使用shift命令和OPTIND值来移动参数,如下:
#!/bin/bash
while getopts :ab:cd opt
do
case "$opt" in
a) echo "参数-a存在!!" ;;
b) echo "参数-b存在!!,且参数值为$OPTARG" ;;
c) echo "参数-c存在!!" ;;
d) echo "参数-d存在!!" ;;
*) echo "参数$opt不存在!!" ;;
esac
done
echo "---------------------------"
shift $[ $OPTIND - 1 ]
count=1
for var in "$@"
do
echo "第$count参数值为$var!!"
count=$[ $count + 1 ]
done
脚本选项标准化
- 在创建shell脚本时,我们完全可以决定用哪些字母选项以及它们的用法,但有些字母选项在Linux世界里已经拥有了某种程度的标准含义。如果你能在shell脚本中支持这些选项,会使得你的脚本更加方便,如下表所示:
选 项 | 描 述 |
---|---|
-a | 显示所有对象 |
-c | 生成一个计数 |
-d | 指定一个目录 |
-e | 扩展一个对象 |
-f | 指定读入数据的文件 |
-h | 显示命令的帮助信息 |
-i | 忽略文本大小写 |
-l | 产生输出的长格式版本 |
-n | 使用非交互模式(批处理) |
-o | 将所有输出重定向到的指定的输出文件 |
-q | 以安静模式运行 |
-r | 递归地处理目录和文件 |
-s | 以安静模式运行 |
-v | 生成详细输出 |
-x | 排除某个对象 |
-y | 对所有问题回答yes |
获取用户的输入
- 虽然命令行选项和参数是从脚本用户处获得输入的一种方式,但有时脚本的交互性还需要更强一些。比如我们想要在脚本运行时获取用户对某个问题的答案,因此bash shell提供了read命令
基本的读取
- read命令会从标准输入(键盘)或另一个文件描述符中接受输入。在收到输入后,read命令会将数据放进一个变量,如下:
#!/bin/bash
echo -n "请输入用户名:"
read name
echo "您好$name,欢迎登陆!!"
- 注意,生成提示的echo命令使用了-n选项。该选项不会在字符串末尾输出换行符,允许脚本用户紧跟其后输入数据,而不是下一行。这让脚本看起来更像表单
- 但是实际上,read命令包含了-p选项,允许你直接在read命令行指定提示符,如下:
#!/bin/bash
read -p "请入输入一个数字:" num
pf_num=$[ $num * $num ]
echo "它的平方为:$pf_num!!"
- read命令会将提示符后输入的所有数据分配给单个变量,要么指定多个变量。输入的每个数据值都会分配给变量列表中的下一个变量。如果变量数量不够,剩下的数据就全部分配给最后一个变量
- 也可以在read命令行中不指定变量。如果是这样,read命令会将它收到的任何数据都放进特殊环境变量REPLY中,如下:
#!/bin/bash
read -p "请入用户名:"
echo "--------------"
echo "欢迎您$REPLY,登录!!"
- REPLY环境变量会保存输入的所有数据,可以在shell脚本中像其他变量一样使用
超时
- 使用read命令时要注意。脚本可能会一直在等用户的输入。如果不管是否有数据输入,脚本都必须继续执行,你可以用**-t选项**来指定一个计时器。-t选项指定了read命令等待输入的秒数。当计时器过期后,read命令会返回一个非零退出状态码,如下:
#!/bin/bash
if read -t 3 -p "请入用户名:" name
then
echo "欢迎您$name,登录!!"
else
echo
echo "等待超时,请重试!!"
fi
- 如果计时器过期,read命令会以非零退出状态码退出,可以使用如if-then语句或while循环这种标准的结构化语句来处理所发生的具体情况。在上例中,计时器过期时,if语句不成立,shell会执行else部分的命令
- 也可以不对输入过程计时,而是让read命令来统计输入的字符数。当输入的字符达到预设的字符数时,就自动退出,将输入的数据赋给变量,如下:
#!/bin/bash
read -n1 -p "你是否要继续 [Y/N]" answer
case $answer in
Y | y)echo
echo "继续执行!!";;
N | n)echo
echo "再见!!"
exit;;
esac
echo "执行完毕!!"
- 上例中将**-n选项和值1**一起使用,告诉read命令在接受单个字符后退出。只要按下单个字符后,read命令就会接受输入并将它传给变量,无需按回车键
隐藏式读取
- 有时你需要从脚本用户处获取输入,但是不需要在屏幕上显示输入信息。其中典型的例子就是输入的密码,但除此之外还有很多其他需要隐藏的数据类型
- -s选项可以避免在read命令中输入的数据出现在显示器上(实际上,数据会被显示,只是read命令会将文本颜色设成跟背景色一样),如下:
#!/bin/bash
read -s -p "请输入您的密码:" password
echo
echo "您的密码是:$password"
文件中读取
- 也可以用read命令来读取Linux系统上文件里保存的数据。每次调用read命令,它都会从文件中读取一行文本。当文件中再没有内容时,read命令会退出并返回非零退出状态码
- 其中最主要的部分就是将文件中的数据传给read命令,最常见的方法是对文件使用cat命令,将结果通过管道直接传给含有read命令的while命令,如下:
#!/bin/bash
count=1
cat name | while read line
do
echo "第$count行为:$line"
count=$[ $count + 1 ]
done
echo "处理结束!!!"
- while循环会持续通过read命令处理文件中的行,直到read命令以非零退出状态码退出