不知道你是否还记得你的第一次计算机编程。我对“第一次”记忆犹新,那是一个很神奇的时刻。当我把有生以来编写的第一个 C 程序交给计算机执行时,黑色的屏幕上显示出了一行字“please input your name:”。
我怀着无比激动的心情写下我的名字“roc”,然后按下回车。美妙的时刻就此凝固了,计算机毫无差错地显示出:
welcome !!! roc.
如果现在让你用
Shell 来实现同样效果的程序,不知道你是不是能很快就写出来呢?
本文就来帮你实现这个程序,在实现该程序之前,我们还是先来学习一下
Linux 系统中最擅长倾听的命令——read 吧。
倾听键盘的声音
read 命令用来倾听标准输入或文件输入,并把信息存放到变量中。从 read 命令的定义可以看出,倾听键盘是 read 的主要职责之一。
现在,我们用 Shell 来模拟一下上面的 C 程序吧:
#!/bin/bash echo -n "please input your name:" read name echo "welcome !!! $name" exit 0
就这么简单,使用 read 命令来倾听用户的输入,并把用户的输入内容自动保存到指定的 name 变量中,最后使用 echo 实现输入内容的显示。
上面的
Shell 脚本是不是已经是最精简的了?当然不是,还可以继续精简,我们可以直接使用 read 自带的显示提示语功能,省略程序中的 echo 语句:
#!/bin/bash read -p "please input your name:" name echo "welcome !!! $name" exit 0
上面脚本中的 read 的
-p
选项就是用来显示提示语的。
在这个例子中,我们只是让 read 来接收 name 变量的值,那么 read 可以同时接收两个甚至更多的变量吗?答案是可以的。
#!/bin/bash read -p "please input your name and place:" name place echo "welcome $name to $place" exit 0
看到了吗?read 后面可以指定两个变量 name 和 place,当用户输入完成时,read 会以空格来分割用户的输入内容,并把输入的内容分别存放到后面的变量中。
按照这个规律,read 完全可以指定 3 个、4 个……N 个变量。需要注意的是,当用户的实际输入和程序期望输入的变量个数不等时,又会出现什么情况呢?
这是一个好问题,比如上面的脚本期望用户输入 name 和 place 两个变量的值,而我们却故意输入一个数据或三个数据,这时来看看脚本的执行情况:
示例一:只输入一个数据
[roc@roclinux ~]$ ./test.sh please input your name and place: roc welcome roc to
示例二:输入多个数据
[roc@roclinux ~]$ ./test.sh please input your name and place:roc beijing shanghai tianjin welcome roc to beijing shanghai tianjin
通过上面脚本的执行,我们可以得出以下的结论:
- 如果输入的数据数量少于变量的个数,那么多余的变量不会获取到数据,即变量值为空。
- 如果输入的数据数量多于变量的个数,那么超出的数据将都赋值给最后一个变量。
还有一个非常特殊的情况,就是在编写脚本时,如果 read 命令后面没有写任何变量,脚本执行时,我们输入数据,那输入的数据会存放到哪里呢?
对于这种极端情况,Shell 的设计者早就预料到了:用户的数据会存放到一个叫作 $REPLY 的环境变量中去。
#!/bin/bash ## read -p "please input your name and place:" echo "welcome $REPLY" exit 0 [roc@roclinux ~]$ bash test.sh please input your name and place:roc beijing welcome roc beijing
read 非常乐于倾听用户的声音,但有的时候,用户长时间不输入,那 read 也可以有自己的后手。read 命令提供了
-t
选项,可以用来设置一个倾听的时限。如果超过所设置的时限的话,那么 read 的耐心也就到此为止喽。
#!/bin/bash if read -t 5 -p "please input your name within 5s:" name then echo "welcome !!! $name" else echo "sorry, too slow" fi exit 0
上面的脚本就使用了
-t
选项,用来指定等待输入的时长(秒)。
上述脚本表示 read 会等待用户的输入,但丑话说在前,如果 5 秒内用户没有响应,那么 read 就会自动结束,并显示“sorry,too slow”来抱怨一下用户。可见,read 命令也可以有自己的态度的。
密码输入场景
还有一种使用场景非常特殊,就是密码输入,我们都不希望密码明文显示在屏幕上,万一被别人看到了,可就不妙了。对于这种场景,我们希望实现隐藏输入的效果,read 命令使用
-s
选项即可实现这样的效果:
#!/bin/bash read -s -p "please input your code:" password echo "hehe, your password is $password"
上面的程序使用了
-s
选项,它实现的效果是用户在输入的时候,屏幕上不显示任何信息。这样,是不是再也不用担心密码被别人看到了?
内容来自文件
前面我们说过,read 命令不仅能监听键盘输入,还能读取文件内容。上面的示例都在说键盘,下面就来说文件。
首先,我们准备一个素材文件,其内容如下:
[roc@roclinux ~]$ cat -n test.txt 1 19248 2 19247 3 19246
下面,我们为大家展示 read 命令的三种读取文件的方法。
第一种方法:使用
-u
选项
#! /bin/bash # assign the file descriptor to file for input fd # 3 is Input file exec 3< test.txt # read the file using fd # 3 count=0 while read -u 3 var do let count=$count+1 echo "Line $count:$var" done echo "finished" echo "Line no is $count" # Close fd # 3 exec 3<&-
上面的脚本通过“exec 3<test.txt”生成了编号为 3 的文件描述符,接着通过“read-u 3 var”来读取文件内容。最后通过“exec 3<&-”关闭了 3 号文件描述符。
我们来看一下脚本的执行效果:
# 执行效果 [roc@roclinux ~]$ ./test.sh Line 1:19248 Line 2:19247 Line 3:19246 finished Line no is 3
第二种方法:使用管道
#!/bin/bash count=1 cat test.txt | while read line do echo "Line $count:$line" let count=$count+1 done echo "finished" echo "Line no is $count" exit 0
请注意上面的用法“cat test.txt|while read line”,通过这样的方式来遍历 test.txt 文件的内容,并按行赋值给变量 line。
# 执行效果 [roc@roclinux ~]$ ./test.sh Line 1:19248 Line 2:19247 Line 3:19246 finished Line no is 1
咦,有个问题。脚本最后输出的是 Line no is 1,怎么行数是 1 呢,明明是 3 才对啊?
上面这个问题,是由于管道导致的。我们知道,管道的两边一般需要新建进程,当执行完 while 语句后,新进程也就结束了。而脚本中 count 是新进程中的自定义变量,进程结束后该变量也就消失了(自定义变量的生命周期结束)。当脚本执行 echo 时,显示的 count 变量是脚本中第一行定义的变量的值,而不是 while 语句中的那个 count 变量了,因而输出的结果当然就是 1 了。
第三种方法:使用重定向
#!/bin/bash count=0 while read line do let count=$count+1 echo "Line $count:$line" done < test.txt echo "finished" echo "Line no is $count" exit 0
由于第二种方法中的 count 变量计数有误,所以我们才为大家介绍第三种方法,这种方法可以有效规避因为新建进程而导致的变量值无法保留的问题。
运行脚本,看看执行结果是否符合预期:
[roc@roclinux ~]$ bash test.sh Line 1:19248 Line 2:19247 Line 3:19246 finished Line no is 3
分析上面的第二种方法和第三种方法可以发现,它们最核心的区别是“管道技术”变成了“重定向技术”,从而成功规避了进程中新生成自定义变量的问题。
神奇的换行符
我们接着上面重定向的例子来说,我们把 test.txt 文件的内容稍微变动一下。
[roc@roclinux ~]$ cat -n test.txt 1 19248 \ 2 19247 \ 3 19246
可以看到,我们在前两行的后面分别加上了一个反斜线(\),这个符号表示续行符,它实现的效果是“肉眼看上去是换行了,但是 Linux 认为并没有换行”。
如果我们把这个文件作为上面例子的输入,那么会输出什么呢?我们来看一下实际效果:
[roc@roclinux ~]$ ./test.sh Line 1:19248 19247 19246 finished Line no is 1
是不是和你想的一样?这就是续行符(\)的作用,read 命令在读取数据时,当读取到续行符(\)时,它不认为这是一行的结束,而是会继续读取下一行,直到遇到真正的换行符(\n)为止。
这样一解释,大家就应该知道为什么脚本执行结果里只有 Line 1 了吧。
其实在 read 读取到的数据中,所有的转义符表示的特殊含义都是起作用的,如果你不想让它们起作用的话,请使用
-r
选项吧。
#! /bin/bash # assign the file descriptor to file for input fd # 3 is Input file exec 3< test.txt # read the file using fd # 3 count=0 while read -u 3 -r var do let count=$count+1 echo "Line $count:$var" done echo "finished" echo "Line no is $count" # Close fd # 3 exec 3<&-
上面的代码中,我们会发现“read-u 3-r var”这一行增加了
-r
选项,如果我们仍以含有续行符的那个文件为输入文件的话,输出的结果就变成:
[roc@roclinux ~]$ ./test.sh Line 1:19248 \ Line 2:19247 \ Line 3:19246 finished Line no is 3
可以看到,续行符的作用被完全忽略了,原本的续行符(\)被当作原始的反斜线(\)符号了。这就是
-r
选项的威力,它的眼里不揉沙子,任何符号都没有特殊身份。
最后,我们要特别说明的一点是,
-r
选项不仅对读取的文件内容有效,而且对键盘的输入也是有效的。