shell 操作之 read、cat 和 here document

jopen 9年前

整理和总结网络上 shell 操作中常用的读取和写入内容:read 、cat 和 here document 。
本文主要学习总结一下三方面问题:

  1. 通过 read 进行行读
  2. here document
  3. here document 的应用

【read】

在 linux 下执行 man read 能看到如下内容
</span>
read [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]       One line is read from the standard input, or from the file descriptor fd supplied as an argument to the -u option,   and the first word is assigned to the first name, the second word to the second name, and so on, with leftover words and   their intervening separators assigned to the last name. If there are fewer words read from the input stream than names,   the remaining names are assigned empty values. The characters in IFS are used to split the line into words. The backslash   character (\) may be used to remove any special meaning for the next character read and for line continuation. Options,   if supplied, have the following meanings:       从标准输入上读取一行数据,或者从通过 -u 选项指定的文件描述符 fd 上读取,并且按照顺序,将读取的第一个 word 赋值给第一个 name ,第二个   word 赋值给 name ,以此类推,对于剩余的 words 和 word 之间的分隔符都被赋值到最后一个 name 上。如果从输入流上读取的 word 的数量少于给出  的 name 数量,则多出来的 names 将被自动赋值为空值。IFS 中所包含的字符用于将整行字符拆分成单独的 word (换句话说也就是将 IFS 中包含的字符  从行数据中去除,IFS 中默认包含的字符为空格、制表符和回车)。反斜线字符(\)可以用于移除紧随其后读到的字符的任何特殊含义,还可用于行接续。  若提供了选项,则具有如下意义:       -a aname           The words are assigned to sequential indices of the array variable aname, starting at 0. aname is unset before           any new values are assigned. Other name  arguments are ignored.           -d delim           The first character of delim is used to terminate the input line, rather than newline.           delim 的首字符被用于作为输入行数据的终止符,而不是换行符。       -e           If the standard input is coming from a terminal, readline (see READLINE above) is used to obtain the line.           Readline uses the current (or default, if line editing was not previously active) editing settings.       -i text           If readline is being used to read the line, text is placed into the editing buffer before editing begins.       -n nchars           read returns after reading nchars characters rather than waiting for a complete line of input, but honor a delimiter           if fewer than nchars characters are read  before the delimiter.       -N nchars           read returns after reading exactly nchars characters rather than waiting for a complete line of input, unless EOF           is encountered or read times out.           Delimiter characters encountered in the input are not treated specially and do not cause read to return until           nchars characters are read.       -p prompt           Display prompt on standard error, without a trailing newline, before attempting to read any input. The prompt is           displayed only if input is coming from a  terminal.           在开始读取任何输入前,向标准出错上显示提示信息,并且不带尾部换行符。该提示信息仅在输入数据来自终端的时候才被显示。       -r  Backslash does not act as an escape character. The backslash is considered to be part of the line.            In particular, a backslash-newline pair may not be used as a line continuation.           反斜线不作为转义字符起作用。反斜线被当做行数据的一部分。           特别值得注意的是,反斜线-换行 组合将不能作为行接续来使用。       -s  Silent mode. If input is coming from a terminal, characters are not echoed.           安静模式。如果输入来自终端,字符将不会被 echo 。       -t timeout           Cause read to time out and return failure if a complete line of input is not read within timeout seconds. timeout           may be a decimal number with a fractional  portion following the decimal point. This option is only effective if read           is reading input from a terminal, pipe, or other special file; it has no effect when reading from regular files. If           timeout is 0, read returns success if input is available on the specified file descriptor, failure otherwise. The exit           status is greater than 128 if the timeout is exceeded.       -u fd          Read input from file descriptor fd.           从文件 fd 读取输入数据。         If no names are supplied, the line read is assigned to the variable REPLY. The return code is zero, unless end-of-file   is encountered, read times out (in which case the return code is greater than 128), or an invalid file descriptor is supplied   as the argument to -u.       如果没有 name 变量被指定,所读取的行数据将被赋值给变量 REPLY 。除非遇到了文件结束符(EOF),或者发生读取超时(此时返回值将大于 128),或者  通过 -u 指定了无效的文件描述符,其他情况返回值均为 0 。 

【read 测试】

测试文件如下
[root@Betty Shell]# vi file     -module( unique_name_test      )    .   -compile(export_all).     %% @spec (Nibble::integer()) -> char()   %% @doc Returns the character code corresponding to Nibble.   %%   %% Nibble must be >=0 and =&lt;16.   hex_digit(0) -> $0;   hex_digit(1) -> $1;   hex_digit(2) -> $2;   hex_digit(3) -> $3;   hex_digit(4) -> $4;   hex_digit(5) -> $5;   hex_digit(6) -> $6;   hex_digit(7) -> $7;   hex_digit(8) -> $8;   hex_digit(9) -> $9;   hex_digit(10) -> $A;   hex_digit(11) -> $B;   hex_digit(12) -> $C;   hex_digit(13) -> $D;   hex_digit(14) -> $E;   hex_digit(15) -> $F. 

测试一:读取文件的首行并赋值给变量

[root@Betty Shell]# read -r line < file        [root@Betty Shell]# echo $line            -module( unique_name_test ) . 
      这一行命令用到了 Bash 的内置命令 read,和输入重定向操作符 < 。read 命令从标准输入中读取一行,并将内容保存到变量 line 中。在这里,-r 选项保证读入的内容是原始的内容,意味着反斜杠转义的行为不会发生。输入重定向操作符 < file 打开并读取文件 file ,然后将它作为 read 命令的标准输入。</span>

      记住,read 命令会删除包含在 IFS 变量中出现的所有字符(这个说法似乎不够准确),IFS 的全称是 Internal Field Separator,Bash 根据 IFS 中定义的字符来分隔单词。在这里,read 命令读入的行被分隔成多个单词。默认情况下,IFS 包含空格,制表符和回车,这意味着开头和结尾的空格和制表符都会被删除。如果你想保留这些符号,可以通过设置 IFS 为空来完成:</span>
[root@Betty Shell]# IFS= read -r line < file   [root@Betty Shell]# echo $line                 -module( unique_name_test ) . 
IFS 的变化仅会影响当前的命令,这行命令可以保证读入原始的首行内容到变量 line 中,同时行首与行尾的空白字符被保留。

测试二:依次读入文件每一行

[root@Betty Shell]# while read -r line; do  > echo "test $line";  > done < file  test -module( unique_name_test  )    .  test  test -compile(export_all).  test  test  test %% @spec (Nibble::integer()) -> char()  test %% @doc Returns the character code corresponding to Nibble.  test %%  test %% Nibble must be >=0 and =&lt;16.  test hex_digit(0) -> $0;  test hex_digit(1) -> $1;  test hex_digit(2) -> $2;  test hex_digit(3) -> $3;  test hex_digit(4) -> $4;  test hex_digit(5) -> $5;  test hex_digit(6) -> $6;  test hex_digit(7) -> $7;  test hex_digit(8) -> $8;  test hex_digit(9) -> $9;  test hex_digit(10) -> $A;  test hex_digit(11) -> $B;  test hex_digit(12) -> $C;  test hex_digit(13) -> $D;  test hex_digit(14) -> $E;  test hex_digit(15) -> $F.  test  test  [root@Betty Shell]#
      这是一种正确的读取文件内容的做法,read 命令放在 while 循环中。当 read 命令遇到文件结尾时(EOF),它会返回一个正值,导致循环判断失败终止。

=== 我是火影终结的分隔线 ===</strong>

关于 read 命令遇到文件结尾返回一个正值的结论,之前我一直持怀疑态度。因为经常会遇到这样的用法:
while read -r line; do echo $line; done < file
而按照常规编程思维,while 循环的判定条件应该是不为 0 则循环,似乎这里就出现了矛盾。
于是进行如下实验进行验证
[root@Betty workspace]# touch abc.txt  [root@Betty workspace]# cat abc.txt   [root@Betty workspace]# read -r line < abc.txt    [root@Betty workspace]# echo $?  1  [root@Betty workspace]# echo "1" >> abc.txt  [root@Betty workspace]# cat abc.txt              1  [root@Betty workspace]# read -r line < abc.txt   [root@Betty workspace]# echo $?  0
</span></span>结果证明,read 读到文件结束时确实返回 1 ,而读到内容时返回 0 。
最后再确认一下 while 的判定规则
while list; do list; done      The while command continuously executes the do list as long as the last command in list returns an exit status   of zero. The exit status of the while commands is the exit status of the last do list command executed, or zero if   none was executed.
哈哈,套用工藤新一的话“真相只有一个”~~

=== 我是火影终结的分隔线 ===</span>

记住,read 命令会删除首尾多余的空白字符,所以如果你想保留,请设置 IFS 为空值:</span>
[root@Betty Shell]# while IFS= read -r line; do    > echo "test $line";   > done < file   test  -module( unique_name_test )    .      test    test -compile(export_all).   test    test    test %% @spec (Nibble::integer()) -> char()   test %% @doc Returns the character code corresponding to Nibble.   test %%   test %% Nibble must be >=0 and =&lt;16.   test hex_digit(0) -> $0;   test hex_digit(1) -> $1;   test hex_digit(2) -> $2;   test hex_digit(3) -> $3;   test hex_digit(4) -> $4;   test hex_digit(5) -> $5;   test hex_digit(6) -> $6;   test hex_digit(7) -> $7;   test hex_digit(8) -> $8;   test hex_digit(9) -> $9;   test hex_digit(10) -> $A;   test hex_digit(11) -> $B;   test hex_digit(12) -> $C;   test hex_digit(13) -> $D;   test hex_digit(14) -> $E;   test hex_digit(15) -> $F.   test    test    [root@Betty Shell]#  
      从上面可以看出 < file 永远是放在最后的,如果你不想将 < file 放在最后,可以通过管道将文件的内容输入到 while 循环中:
[root@Betty Shell]# cat file | while IFS= read -r line; do    > echo "test $line";   > done   test  -module( unique_name_test )    .      test    test -compile(export_all).   test    test    test %% @spec (Nibble::integer()) -> char()   test %% @doc Returns the character code corresponding to Nibble.   test %%   test %% Nibble must be >=0 and =&lt;16.   test hex_digit(0) -> $0;   test hex_digit(1) -> $1;   test hex_digit(2) -> $2;   test hex_digit(3) -> $3;   test hex_digit(4) -> $4;   test hex_digit(5) -> $5;   test hex_digit(6) -> $6;   test hex_digit(7) -> $7;   test hex_digit(8) -> $8;   test hex_digit(9) -> $9;   test hex_digit(10) -> $A;   test hex_digit(11) -> $B;   test hex_digit(12) -> $C;   test hex_digit(13) -> $D;   test hex_digit(14) -> $E;   test hex_digit(15) -> $F.   test    test    [root@Betty Shell]#  

测试三:读取文件首行前三个字段并赋值给变量

[root@Betty Shell]# head -1 file | while read -r field1 field2 field3 throwaway; do echo "filed1 = $field1";echo "field2 = $field2";echo "field3 = $field3"; done      filed1 = -module(   field2 = unique_name_test   field3 = ) 
      如果在 read 命令中指定多个变量名,它会将读入的内容分隔成多个字段,然后依次赋值给对应的变量,第一个字段赋值给第一个变量,第二个字段赋值给第二个变量,等等,最后将剩余的所有字段赋值给最后一个变量。这也是为什么,在上面的例子中,我们加了一个 throwaway 变量,否则的话,当文件的一行大于三个字段时,第三个变量的内容会包含所有剩余的字段。
      有时候,为了书写方便,可以简单地用 _ 来替换 throwaway 变量:
[root@Betty Shell]# head -1 file | while read -r field1 field2 field3 _; do echo "filed1 = $field1";echo "field2 = $field2";echo "field3 = $field3"; done   filed1 = -module(   field2 = unique_name_test   field3 = ) 
又或者,如果你的文件确实只有三个字段,可以忽略 _ 。

【cat 与 <<】

  • cat 命令是 linux 下的一个文本输出命令,通常是用于观看某个文件的内容的; 命令 cat >file 可以用于将键盘上的输入写到文件中。
  • EOF 为 "end of file" 的缩写,从语义上代表文本的结束符。
  • 通过 cat <<EOF 将两者结合使用(EOF 和 << 中间是否有空格没有关系),即可避免使用多行 echo 命令的方式,并实现多行输出的结果。原则上讲,此处的 EOF 可以使用任何其他字符替代。

测试 - 1

cat > test.cfg <<EOF   log_facility=daemon   pid_file=/var/run/nrpe.pid   EOF 

测试 - 2

cat > test.cfg <<ABC   log_facility=daemon   pid_file=/var/run/nrpe.pid   ABC 

测试 - 3

cat <<ABC > test.cfg   log_facility=daemon   pid_file=/var/run/nrpe.pid   ABC 

【Here document】

  • 有写书籍将 here document 翻译为内嵌文档。
  • here document 的别名有 here-document 、heredoc 、hereis 、 here-string 或 here-script 。
  • here document 原本指 file literal 或者 input stream literal ;后来也指 multiline string literals 。
  • here document 会保留 text 中的 line break 和其他 whitespace (including indentation) 的含义。
  • here document 起始于 Unix shell ,后在各种其他 shell 中被使用。
  • here document 风格的字符串在很多高级语言中存在,尤其是 Perl 语言和其他受 Perl 影响的语言,如 PHP 和 Ruby 。
  • 对于 here document 而言,无论指代的是文件还是字符串,一些语言都将其看做格式化字符串,并允许在其内部进行变量替换和命令替代。
  • here document 的最通用语法起源于 Unix shell ,使用 "<< delimiter" 的形式(delimiter 通常为 EOF 或 END)标识多行字串的开始,之后新起一行包含相应的文本,最后以相同的 delimiter 独占一行标识多行字串的结束。这种语法形式是因为 here documents 主要用于 stream literal ,且 document 的内容被重定向到前面 command 的标准输入,即 here document 的语法模拟了输入重定向的语法,也就是 < 所表示的“从后续 command 的输出获取输入”。
  • 其它语言通常使用了非常相似的语法,但是语法的细节和实际的功能可能非常的不同。

Unix shell 中的应用

In the following example, text is passed to the tr command using a here document. This could be in a shell file, or entered interactively at a prompt.
[root@Betty Shell]# tr a-z A-Z << END_TEXT   > one two three   > uno dos tres   > END_TEXT 
This yields the output:
ONE TWO THREE   UNO DOS TRES 
此处使用 </span> <<END_TEXT 或 << END_TEXT 都正确。

在 << 后添加 - 符号的作用是可以忽略掉前置的 tab</strong> 。这将允许在 shell 命令行上直接对包含缩进的 here document 进行操作,而不用变更脚本的内容。</span>
注意:要想在 shell 命令行上输入 TAB ,需要连续输入 CTRL-V 、TAB 才行。
[root@Betty Shell]# tr a-z A-Z <<- END_TEXT   >(Ctrl-V + TAB)one two three   >(Ctrl-V + TAB)uno dos tres   >(Ctrl-V + TAB)END_TEXT 
This yields the same output, notably not indented:
ONE TWO THREE   UNO DOS TRES 

补充测试:
[root@Betty Shell]# tr a-z A-Z << END_TEXT    >(Ctrl-V + TAB)one two three   >(Ctrl-V + TAB)uno dos tres   >(Ctrl-V + TAB)END_TEXT   > END_TEXT 
将得到
(Ctrl-V + TAB)ONE TWO THREE   (Ctrl-V + TAB)UNO DOS TRES   (Ctrl-V + TAB)END_TEXT 

在默认情况下,变量将被内插替换,包含在 `` 中的命令将被求值。
backtick 即传说中的反引号。
[root@Betty Shell]# cat << EOF   > Working dir $PWD   > EOF 
yields:
Working dir /root/workspace/CODE_TEST/Shell 
上述行为可以通过引号引用标签的任何部分进行取消。例如,将 EOF 使用单引号或者双引号进行包含:
[root@Betty Shell]# cat << "EOF"   > Working dir $PWD   > EOF 
yields:
Working dir $PWD 

补充测试:
[root@Betty Shell]# cat << "E"OF   Working dir $PWD   EOF 
将得到
Working dir $PWD 

(后面还有对 here string 的介绍,此处略过)

man 手册上的说明

Here Documents
      This type of redirection instructs the shell to read input from the current source until a line containing only delimiter (with no trailing blanks) is seen. All of the lines read up to that point are then used as the standard input for a command.
      这种类型的重定向会使得 shell 从当前源读取输入,直到遇到仅包含 delimiter 的行(尾部没有任何空白符)。此时读取到的全部行将被作为 command 的标准输入。

       The format of here-documents is:

              <<[-]word
                      here-document
              delimiter

       No parameter expansion, command substitution, arithmetic expansion, or pathname expansion is performed on word. If any characters in word are quoted, the delimiter is the result of quote removal on word, and the lines in the here-document are not expanded. If word is unquoted, all lines of the here-document are subjected to parameter expansion, command substitution, and arithmetic expansion. In the latter case, the character sequence \<newline> is ignored, and \ must be used to quote the characters \, $, and `.
      针对 word 不会执行任何参数扩展、命令替代、算数扩展,或路径扩展。如果 word 中有任何字符是被引号括起来的,那么 delimiter 将是 word 移除引用部分后的内容,此时位于 here-document 中的行将不会被扩展,如果 word 没有被引号括起来,here-document 中的所有行都要受到参数扩展、命令替换和算数扩展的影响。在后者的情况下,字符序列 \<newline> 会被忽略,并且只要存在 \,$ 和 ` 字符都要使用 \ 进行转义(如果你确实打算输出未被转义的字符)。

       If the redirection operator is <<-, then all leading tab characters are stripped from input lines and the line containing delimiter.  This allows here-documents  within shell scripts to be indented in a natural fashion.
      如果重定向操作符为 <<- ,那么所有前置 tab 字符都将被从输入的行数据和仅包含 delimiter 的行中移除。这将使得 here-document 用于 shell 脚本时能够以自然方式进行缩进。

Here Strings
       A variant of here documents, the format is:

              <<<word

       The word is expanded and supplied to the command on its standard input.


=== 我是 7 代的分隔线 ===

stackoverflow 上的讨论

在  stackoverflow 上有如下针对 bash 中使用   cat << EOF 的讨论。

bash 语法 cat <<EOF 在你遇到 Bash 上使用多行字符串的时候是非常有用的,例如,当传递多行字串到一个变量、文件,或者管道中的情况。
例子一: 将多行字符串传递给一个变量 (原文中的测试此处被我加强了)
[root@Betty Shell]# sql=$(cat <<EOF   > SELECT foo, bar FROM db   > WHERE foo='baz'   > EOF   > )   [root@Betty Shell]#    [root@Betty Shell]# echo $sql       SELECT foo, bar FROM db WHERE foo='baz'   [root@Betty Shell]#    [root@Betty Shell]# echo -e $sql                     -e     enable interpretation of backslash escapes   SELECT foo, bar FROM db WHERE foo='baz'   [root@Betty Shell]#    [root@Betty Shell]# echo -E $sql                     -E     disable interpretation of backslash escapes (default)   SELECT foo, bar FROM db WHERE foo='baz'   [root@Betty Shell]#    [root@Betty Shell]# echo "$sql"   SELECT foo, bar FROM db   WHERE foo='baz'   [root@Betty Shell]#    [root@Betty Shell]# echo -e "$sql"                   -e     enable interpretation of backslash escapes   SELECT foo, bar FROM db   WHERE foo='baz'   [root@Betty Shell]#    [root@Betty Shell]# echo -E "$sql"                   -E     disable interpretation of backslash escapes (default)   SELECT foo, bar FROM db   WHERE foo='baz'   [root@Betty Shell]# 
      执行后,$sql 变量中将会包含带换行符的字串,你可以通过 echo -e "$sql" 命令进行查看。
(上面的结论和我自己的实验结果有出入,按照原文的说法,shell 命令输入时是带有 \n 字符的,并且只有在 -e 选项下能被解析,然而实验结果表明,只要将 $sql 用双引号括起来,结果一定是带有换行符的;而不用双引号括起来的 $sql 则被显示为单行。这里引出一个问题,"$sql" 和 $sql 在 shell 中的区别是什么?)

例子二:将多行字符串传递给一个文件
[root@Betty Shell]# cat <<EOF > print.sh   > #!/bin/bash   > echo \$PWD   > echo $PWD   > EOF 
The print.sh file now contains:
[root@Betty Shell]# cat print.sh    #!/bin/bash   echo $PWD                              -- 未被命令替换   echo /root/workspace/CODE_TEST/Shell   -- 被命令替换 
例子三:将多行字符串传递给一个 command/pipe</span>
[root@Betty Shell]# cat <<EOF | grep 'b' | tee b.txt | grep 'r'   > foo   > bar   > baz   > EOF   bar 
上述命令只将 bar 打印到标准输出,但会创建 b.txt 文件,其中包含了 bar 和 baz 两行字符串。

===

      在上面的例子中,"EOF" 被用作 "Here Tag" 。简单的讲,"<< Here" 的作用就在于告诉 shell 下面将开始一段多行字符串输入,并且该多行字符串以 "Here" 作为终止。你也可以将 "Here" 替换成任何你想要的内容,但通常会使用 EOF 或者 STOP 。

一些关于 Here 标签的规则:
  • 标签可以是任何字符串,大小写字母均可,但通常人们习惯使用大写字母。
  • 若(尾部)标签所在行还存在其他字符,则该标签将不作为标签起作用。在这种情况下,该标签将仅作为字符串的一部分。标签必须自身独占一行才会被判定为正确的标签。
  • 标签所在行中的标签不应该具有前置或后置的空白符,只有这样才被判定为正确的标签。否则会被认为是字符串的一部分。
[root@Betty Shell]# cat >> test <<HERE   > Hello world HERE               <--- Not the end of string   >   HERE                         <-- Leading space, so not end of string   > HERE                           <-- Now we have the end of the string   [root@Betty Shell]# cat test    Hello world HERE     HERE 

参考

1. Linux man 手册
2. bash read 背后的故事二:read -r
3. 关于 cat > file 和 cat > file <<EOF
4. Here document 
5. how does ` cat << EOF` work in bash? 

来自:http://my.oschina.net/moooofly/blog/341769