read命令

read命令的一般形式为:

  1. read variables

该命令执行时,Shell会从标准输入中读取一行,然后将第一个单词分配给variables中列出的第一个变量,第二个单词分配给第二个变量,以此类推。如果行中的单词多于列表中的变量,那么多出的单词全部分配给最后一个变量。例如,下列命令:

  1. read x y

会从标准输入中读入一行,将第一个单词分配给变量x,将行中余下的内容分配给变量y。按照这种处理逻辑,下列命令:

  1. read text

会读取并将一整行保存到Shell变量text中。

文件复制程序

下面来编写一个cp命令的简化版本,借此实践一下read命令。我们把这个程序叫做mycp,它可以接收两个参数:源文件和目标文件。如果目标文件已经存在,程序会发出警告并询问是否需要继续进行复制。如果回答是”yes”,则继续执行复制操作;否则,退出程序。

  1. #
  2. # 复制文件
  3. #
  4. if [ "$#" -ne 2 ] ; then
  5. echo "Usage: mycp from to"
  6. exit 1
  7. fi
  8. from="$1"
  9. to="$2"
  10. #
  11. # 检查目标文件是否已经存在
  12. #
  13. if [ -e "$to" ] ; then
  14. echo "$to already exists; overwrite (yes/no)?"
  15. read answer
  16. if [ "$answer" != yes ] ; then
  17. echo "Copy not performed"
  18. exit 0
  19. fi
  20. fi
  21. #
  22. # 如果目标文件不存在或用户输入了 yes
  23. #
  24. cp $from $to # 执行复制操作

特殊的echo转义字符

mycp命令有一个稍有点烦人的地方:echo命令运行过之后,由用户输入的响应出现在另一行上。这是因为echo命令会自动在最后一个参数后面加上一个用于终止的换行符。好在有办法阻止这种行为:只需要在echo命令的最后加上特殊的转义字符\c就可以了。这告诉echo 在显示完最后一个参数之后不输出换行符。如果你将mycp中的echo命令改成下面这样:

  1. echo "$to already exists; overwrite (yes/no)? \c"

这样用户输入就会出现在提示信息的右侧了。记住,\c是由echo解释的,而非Shell,这意味着必须将其引用起来交给echo

字符 输出
\b 退格
\c 忽略输出中最后的换行符
\f 换页
\n 回车换行
\r 回车
\t 制表符
\\ 反斜线
\0nnn ASCII 值为 nnn 的字符,其中 nnn 是 1~3 位的八进制数

read的退出状态

除非是碰到了文件结尾,read都会返回为0的退出状态码。如果是从终端读取数据,这意味着用户键入了Ctrl+d。如果是从文件读取数据,这意味着文件中已经没有数据可读了。很容易写出一个从文件或终端中读取数据行的循环。接下来名为addi的程序会读入包含了一对数字的行,这对数字会被相加并将之和写入标准输出:

  1. #
  2. # 将标准输入中的一对整数相加
  3. #
  4. while read n1 n2
  5. do
  6. echo $(( $n1 + $n2))
  7. done

basename命令

basename命令可以剥离参数的所有目录部分,得到其基础文件名,例如,basename /usr/bin/troff可以得到troff。basename troff也可以得到 troff。

变量$$

Shell会将替换成登录Shell的进程ID号。因为系统中每个进程都拥有唯一的进程ID,在文件名中使用就避免了不同的进程使用相同文件名的可能。

printf命令

对于简单的信息而言,echo已经足够了,但有时候你可能想输出格式化信息,如按列对齐的数据。printf命令的一般形式为:

  1. printf "format" arg1 arg2 ...

其中format是一个字符串,用于详细描述后续数值的显示格式。因为格式化字符串要作为单个参数,而且有可能其中会包含特殊字符和空格,最好是将其放入引号中。格式化字符串中那些前面没有百分号(%)的字符会被直接写入标准输出。按照最简单的用法,printf的效果和echo差不多(只要你记得每行末尾加上\n作为换行符,如下所示):

  1. $ printf "Hello world!\n"
  2. Hello world!

前面有百分号的字符叫做格式规范,用于告知printf如何显示对应的参数。格式化字符串中的每个百分号都应该有一个对应的参数,除了特殊规范%%,它会显示出一个百分号。来看一个printf的简单例子:

  1. $ printf "This is a number: %d\n" 10
  2. This is a number: 10

不同的格式规范字符。

字符 功能
%d 整数
%u 无符号整数
%o 八进制整数
%x 十六进制整数,使用 a~f
%X 十六进制整数,使用 A~F
%c 单个字符
%s 字符串字面量
%b 包含转义字符的字符串
%% 百分号

转换规范的一般格式为: sh %[flags][width][.precision]type type: 是上图中的转换规范字符。 flags: 有效的flags-、+、#和空格。 - -: 将输出的数值左对齐。 - +: 使得printf 在整数前面加上+-(默认只有负数才输出符号)。 - #: 使得printf在八进制数前加上0,在十六进制数前加上0x或0X(分别使用%#x%#X来指定)。 - 空格: 使得printf在正数前面加上一个空格,在负数前面加上-,以起到对齐的作用。 width: 是一个整数,用来指定输出参数时的最小字段宽度。对应的参数采用右对齐的形式,除非flags使用了- .precision: 是一个正数,指定了%d、%u、%o、%x 及%X所显示出的最小数位个数。在值的左侧,会使用0进行填充。对于字符串,修饰符.precision指定了所要显示出的字符串的最大字符数。如果字符串的长度大于precision,会在右侧被截断。

如果将widthprecision中的数字替换成*,那么待显示的值之前的参数必须是一个数字,该数字分别用作宽度或精度。如果两个位置上(widthprecision)用的全都是*,那么待显示的值之前必须有两个整数参数,作为宽度和精度:

  1. $ printf "%*s%*.*s\n" 12 "test one" 10 2 "test two"
  2. test one te
  3. $ printf "%12s%10.2s\n" "test one" "test two"
  4. test one te

可以看出,例子中两个 printf 的结果是一样的。在第一个printf中,12用作第一个字符串的宽度,10和2分别用作第二个字符串的宽度和精度。在第二个printf中,这些数字是作为格式化规范的一部分来指定的。

  1. #
  2. # 对齐两列数字
  3. # (最多支持包括符号在内的 12 位数)
  4. cat $* |
  5. while read number1 number2
  6. do
  7. printf "%12d %12d\n" $number1 $number2
  8. done
  1. $ cat data
  2. 1234 7960
  3. 593 -595
  4. 395 304
  5. 3234 999
  6. -394 -493
  7. $ align data
  8. 1234 7960
  9. 593 -595
  10. 395 304
  11. 3234 999
  12. -394 -493