本系列文章使用 Linux bash 的 read 命令逐步模拟一个简易的 shell 效果,并实现一个小游戏,从中说明 read 命令的常见用法。
本篇文章主要说明下面的内容:
- 从文件中逐行读取命令并执行
从文件中逐行读取命令并执行
既然是模拟一个简易的 shell 效果,当然要具有执行脚本文件的能力。
我们可以通过重定向用 read 命令逐行读取文件内容,然后执行每一行的命令。一段示例代码如下:
while read line; do echo $linedone < filename
这段代码会逐行读取 fliename 这个文件的内容,读取到最后一行 (EOF) 就会退出 while 循环。
参考这段代码,对 tinyshell.sh 脚本修改如下:
#!/bin/bash -iif [ $# -ne 0 ]; then filename="$1"else filename="/dev/stdin"fiwhile read -ep "tinyshell> " input; do if [ "$input" == "l" ]; then ls elif [ "$input" == "quit" ]; then break else bash -c "$input" fidone < "$filename"
这个脚本使用 $# 获取到传入脚本的参数个数,如果不等于 0,那么用 $1 获取到第一个参数值,赋值给 filename 变量,这个参数值用于指定要执行的脚本文件名。
如果没有提供任何参数,那么将 filename 赋值为 /dev/stdin,对应标准输入。
注意不能将 filename 赋值为空字符串,否则重定向会提示文件找不到。
重定向空字符串并不表示获取标准输入。
为了避免所给文件名带有空格导致异常,要用双引号把 $filename 括起来。
这里采用 bash 的 -i 选项来执行该脚本,所以要在 Linux 本地系统进行测试。
如果想要用 source 命令来执行,需要做一些修改,包括调整 $#、$1 的使用。这里不再提供使用 source 命令来执行的例子。
执行修改后的脚本,结果如下:
$ ./tinyshell.shtinyshell> lshfile tinyshell.shtinyshell> quit$ cat shfilelecho "This is in a test file."whoami$ ./tinyshell.sh shfileshfile tinyshell.shThis is in a test file.shy
这个例子先执行 ./tinyshell.sh 命令,不带参数时,脚本指定从 /dev/stdin 获取输入,可以正常获取到标准输入。
输入的是 l 字符,脚本执行 ls 命令,列出当前目录下的文件,可以看到有一个 shfile 文件。
这个 shfile 文件就是要被执行的脚本文件,用 cat shfile 命令列出它的内容,只有三行,每一行都是要执行的命令。
然后执行 ./tinyshell.sh shfile 命令,从打印结果来看,确实逐行读取到 shfile 文件的内容,并执行每一行的命令。
Bash 的 whoami 命令会打印当前登录的用户名,这里打印出来是 shy。
即,使用修改后的 ./tinyshell.sh 来模拟 shell 效果,具有执行脚本文件的能力,虽然功能还很弱,但基本框架已经搭好,后续可以根据实际需求进行扩展完善。
注意:使用上面的 “while read” 循环来逐行读取文件内容,有一个隐晦的异常:如果所给文件的最后一行不是以换行符结尾时,那么这个 “while read” 循环会处理不到最后一行。具体原因说明如下。
如果文件的最后一行以换行符结尾,那么 read 命令遇到换行符,会暂停获取输入,并把之前读取到的内容赋值给指定的变量,命令自身的返回值是 0,while 命令对这个值进行评估,0 对应 true,执行循环里面的语句,处理最后一行的内容。
然后再次执行 read 命令,遇到文件结尾 (EOF),read 命令返回非 0 值,对应 false,退出 while 循环。这是正常的流程。
如果文件的最后一行不是以换行符结尾,read 读取完这一行内容,遇到了 EOF,会把读取到的内容赋值给指定的变量,命令自身返回值是非 0 值(使用 $? 获取这个返回值,遇到 EOF 应该是返回 1),while 命令对这个非 0 值进行评估,就会退出 while 循环,没有执行循环里面的语句。
即,这种情况下,虽然 read 命令还是会把最后一行内容赋值给指定变量,但是退出了 while 循环,没有执行循环里面的语句,没有机会处理这一行的内容,除非在 while 循环外面再处理一次,但会造成代码冗余。
下面修改 shfile 文件的内容,最后一行不以换行符结尾,然后执行 ./tinyshell.sh shfile 命令,结果如下:
$ echo -ne "lwhoami" > shfile$ ./tinyshell.sh shfileshfile tinyshell.sh$ cat shfilelwhoami$
这里使用 echo 命令的 -n 选项指定不在行末追加换行符,那么写入文件的最后一行不以换行符结尾。
可以看到,执行 ./tinyshell.sh shfile 命令,只处理了第一行的 l 字符,第二行的 whoami 没有被执行。
用 cat shfile 命令查看该文件内容,whoami 跟命令行提示符打印在同一行,确实不以换行符结尾。
为了避免这个问题,可以在脚本中添加判断,如果所给文件的最后一行不以换行符结尾,则追加一个换行符到文件末尾。
要添加的代码如下,新增的代码前面用 + 来标识:
if [ $# -ne 0 ]; then filename="$1"+ if test -n "$(tail "$filename" -c 1)"; then+ echo >> "$filename"+ fielse filename="/dev/stdin"fi
新增的代码用 tail "$filename" -c 1 命令获取到 filename 文件的最后一个字符,"$(tail "$filename" -c 1)" 语句经过命令扩展后返回这个字符。
如果这个字符是换行符,由于 bash 在扩展后会自动丢弃字符串的最后一个换行符,获取到的内容为空,test -n 返回为 false,不做处理。
如果最后一个字符不是换行符,那么内容不为空,test -n 返回为 true,就会执行 echo >> "$filename" 命令追加一个换行符到文件末尾,echo 不带参数时,默认输出一个换行符, >> 表示追加内容到文件末尾。
添加这几个语句后,再执行 ./tinyshell.sh shfile 命令,就能处理到最后一行的 whoami,如下所示:
$ ./tinyshell.sh shfileshfile tinyshell.shshy$ cat shfilelwhoami$
可以看到,执行之后,shfile 文件的最后一行 whoami 后面被追加了一个换行符,输出该文件内容,命令行提示符会换行打印。
通常来说,在 Windows 下复制内容到新建文件,然后保存这个文件,文件的最后一行可能就不以换行符结尾。