一起学习shell脚本:如何处理用户输入?

处理用户输入

bash shell提供了一些不同的方法来从用户 处获得数据,包括命令行参数(添加在命令后的数据)、命令行选项(可修改命令行为的单个字 母)以及直接从键盘读取输入的能力。

命令行参数

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

读取参数

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

factorial=1 
for (( number = 1; number <= $1 ; number++ )) 
do
    factorial=$[ $factorial * $number ] 
done
echo The factorial of $1 is $factorial 

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

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

# ./test 2 5

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

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

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

读取脚本名

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

echo The Zero parameter is set to: $0

但是这里存在一个潜在的问题。如果使用另一个命令来运行shell脚本,命令会和脚本名混在 一起,出现在$0参数中。

这还不是唯一的问题。当传给$0变量的实际字符串不仅仅是脚本名,而是完整的脚本路径时, 变量$0就会使用整个路径。

bash /home/test

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

basename命令会返回不包含路径的脚本名。

name=$(basename $0) 
echo 
echo The script name is: $name 

可以用这种方法来编写基于脚本名执行不同功能的脚本。

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 

测试参数

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

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

if [ -n "$1" ] 
then
    echo Hello $1, glad to meet you. 
else
    echo "Sorry, you did not identify yourself. " 
fi

在本例中,使用了-n测试来检查命令行参数$1中是否有数据。


特殊参数变量

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

参数统计

在脚本中使用命令行参数之前应该检查一下命令行参数。对于使用多 个命令行参数的脚本来说,这有点麻烦。

你可以统计一下命令行中输入了多少个参数,无需测试每个参数。bash shell为此提供了一个 特殊变量。

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

echo There were $# parameters supplied. 
if [ $# -ne 2 ] 
then
    echo 
    echo Usage: test9.sh a b 
    echo
else
    total=$[ $1 + $2 ] 
    echo
    echo The total is $total 
    echo
fi

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

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

最后一个参数

${!#}

不能在花括号内使用美元符。必须将美元符换成感叹号。

params=$# 
echo
echo The last parameter is $params 
echo The last parameter is ${!#} 
echo

当命令行上没有任何参数时,$#的值为0, params变量的值也一样,但${!#}变量会返回命令行用到的脚本名。

抓取所有的数据

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

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

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

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

echo
echo "Using the \$* method: $*" 
echo "Using the \$@ method: $@" 

从表面上看,两个变量产生的是同样的输出,都显示出了所有命令行参数。 下面的例子给出了二者的差异。

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

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


移动变量

bash shell工具箱中另一件工具是shift命令。bash shell的shift命令能够用来操作命令行参 数。跟字面上的意思一样,shift命令会根据它们的相对位置来移动命令行参数。

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

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

echo
count=1
while [ -n "$1" ]
do
    echo "Parameter #$count = $1" 
    count=$[ $count + 1 ]
    shift
done

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

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

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

shift 2 

处理选项

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

查找选项

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

处理简单选项

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

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

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

case语句在命令行参数中找到一个选项,就处理一个选项。如果命令行上还提供了其他参 数,你可以在case语句的通用情况处理部分中处理。

分离参数和选项

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

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

要检查双破折线,只要在case语句中加一项就行了。

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

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

处理带值的选项

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

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

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

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

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

使用getopt命令

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

命令的格式

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

getopt optstring parameters 

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

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

getopt命令有一个更高级的版本叫作getopts(注意这是复数形式)。getopts命令会在随后讲到。因为这两个命令的拼写几乎一模一样,所以很容易搞混。一定要小心!

$ 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命令会产生一条错误消息。

$ getopt ab:cd -a -b test1 -cde test2 test3
getopt: invalid option -- e 
 -a -b test1 -c -d -- test2 test3 

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

$ getopt -q ab:cd -a -b test1 -cde test2 test3 
 -a -b 'test1' -c -d -- 'test2' 'test3' 

注意,getopt命令选项必须出现在optstring之前。

在脚本中使用getopt

可以在脚本中使用getopt来格式化脚本所携带的任何命令行选项或参数,但用起来略微复杂。

方法是用getopt命令生成的格式化后的版本来替换已有的命令行选项和参数。用set命令能 够做到。

set命令能够处理shell中的各种变量。

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

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

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

现在原始的命令行参数变量的值会被getopt命令的输出替换,而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

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

使用更高级的getopts

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

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

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

getopts命令的格式如下:

getopts optstring variable 

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

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

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

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

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

getopts命令有几个好用的功能。对新手来说,可以在参数值中包含空格。

./test19.sh -b "test1 test2" -a 

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

./test19.sh -abtest1 

getopts命令能够从-b选项中正确解析出test1值。除此之外,getopts还能够将命令行上 找到的所有未定义的选项统一输出成问号。

$ ./test19.sh -d 
Unknown option: ? 

optstring中未定义的选项字母会以问号形式发送给代码。

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

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

现在你就拥有了一个能在所有shell脚本中使用的全功能命令行选项和参数处理工具。

将选项标准化

在创建shell脚本时,显然可以控制具体怎么做。你完全可以决定用哪些字母选项以及它们的 用法。

但有些字母选项在Linux世界里已经拥有了某种程度的标准含义。如果你能在shell脚本中支 持这些选项,脚本看起来能更友好一些。

获得用户输入

基本的读取

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

echo -n "Enter your name: " 
read name 
echo "Hello $name, welcome to my program. "

生成提示的echo命令使用了-n选项。该选项不会在字符串末尾输出换行 符,允许脚本用户紧跟其后输入数据,而不是下一行。这让脚本看起来更像表单。 实际上,read命令包含了-p选项,允许你直接在read命令行指定提示符。

read -p "Please enter your age: " age 

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

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

read -p "Enter your name: " 
echo
echo Hello $REPLY, welcome to my program. 

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

超时

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

if read -t 5 -p "Please enter your name: " name 
then
    echo "Hello $name, welcome to my script" 
else
    echo
    echo "Sorry, too slow! " 
fi

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

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

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 script" 

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

隐藏方式读取

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

-s选项可以避免在read命令中输入的数据出现在显示器上(实际上,数据会被显示,只是 read命令会将文本颜色设成跟背景色一样)。

read -s -p "Enter your password: " pass 
echo
echo "Is your password really $pass? "

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

从文件中读取

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

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

count=1
cat test | while read line
do
    echo "Line $count: $line" 
    count=$[ $count + 1] 
done
echo "Finished processing the file" 

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

posted @ 2020-06-15 17:20  trafalgar999  阅读(588)  评论(0编辑  收藏  举报