Chinaunix首页 | 论坛 | 博客
  • 博客访问: 356945
  • 博文数量: 158
  • 博客积分: 52
  • 博客等级: 民兵
  • 技术积分: 613
  • 用 户 组: 普通用户
  • 注册时间: 2011-10-27 11:58
文章分类

全部博文(158)

文章存档

2017年(1)

2016年(5)

2015年(19)

2014年(8)

2013年(13)

2012年(80)

2011年(32)

分类:

2011-11-27 18:13:31

原文地址:bash 进阶之I/O重定向 作者:leibniz_zsu

摘自《高级bash脚本编程指南》

默认情况下始终有3个”文件”处于打开状态, stdin(键盘), stdout(屏幕), 和stderr(错误消息输出到屏幕上). 这3个文件和其他打开的文件都可以被重定向. 对于重定向简单的解释就是捕捉一个文件, 命令, 程序, 脚本, 或者是脚本中的代码块的输出, 然后将这些输出作为输入发送到另一个文件, 命令, 程序, 或脚本中.

每个打开的文件都会被分配一个文件描述符. (一个文件描述符说白了就是文件系统为了跟踪这个打开的文件而分配给它的一个数字. 也可以的将其理解为文件指针的一个简单版本. 与C语言中文件句柄的概念很相似.) stdin, stdout, 和stderr的文件描述符分别是0, 1, 和 2. 除了这3个文件, 对于其他那些需要打开的文件, 保留了文件描述符3到9. 在某些情况下, 将这些额外的文件描述符分配给stdin, stdout, 或stderr作为临时的副本链接是非常有用的. (使用文件描述符5可能会引起问题. 当Bash使用exec创建一个子进程的时候, 子进程会继承fd5,最好还是不要去招惹这个特定的fd. ) 在经过复杂的重定向和刷新之后需要把它们恢复成正常状态。

  1    COMMAND_OUTPUT >
2 # 将stdout重定向到一个文件.
3 # 如果这个文件不存在, 那就创建, 否则就覆盖.
4
5 ls -lR > dir-tree.list
6 # 创建一个包含目录树列表的文件.
7
8 : > filename
9 # >操作, 将会把文件"filename"变为一个空文件(就是size为0).
10 # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
11 # :是一个占位符, 不产生任何输出.
12
13 > filename
14 # >操作, 将会把文件"filename"变为一个空文件(就是size为0).
15 # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
16 # (与上边的": >"效果相同, 但是某些shell可能不支持这种形式.)
17
18 COMMAND_OUTPUT >>
19 # 将stdout重定向到一个文件.
20 # 如果文件不存在, 那么就创建它, 如果存在, 那么就追加到文件后边.
21
22
23 # 单行重定向命令(只会影响它们所在的行):
24 # --------------------------------------------------------------------
25
26 1>filename
27 # 重定向stdout到文件"filename".
28 1>>filename
29 # 重定向并追加stdout到文件"filename".
30 2>filename
31 # 重定向stderr到文件"filename".
32 2>>filename
33 # 重定向并追加stderr到文件"filename".
34 &>filename
35 # 将stdout和stderr都重定向到文件"filename".
36
37 M>N
38 # "M"是一个文件描述符, 如果没有明确指定的话默认为1.
39 # "N"是一个文件名.
40 # 文件描述符"M"被重定向到文件"N".
41 M>&N
42 # "M"是一个文件描述符, 如果没有明确指定的话默认为1.
43 # "N"是另一个文件描述符.
44
45 #==============================================================================
46
47 # 重定向stdout, 一次一行.
48 LOGFILE=script.log
49
50 echo "This statement is sent to the log file, \"$LOGFILE\"." 1>$LOGFILE
51 echo "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILE
52 echo "This statement is also appended to \"$LOGFILE\"." 1>>$LOGFILE
53 echo "This statement is echoed to stdout, and will not appear in \"$LOGFILE\"."
54 # 每行过后, 这些重定向命令会自动"reset".
55
56
57
58 # 重定向stderr, 一次一行.
59 ERRORFILE=script.errors
60
61 bad_command1 2>$ERRORFILE # Error message sent to $ERRORFILE.
62 bad_command2 2>>$ERRORFILE # Error message appended to $ERRORFILE.
63 bad_command3 # Error message echoed to stderr,
64 #+ and does not appear in $ERRORFILE.
65 # 每行过后, 这些重定向命令也会自动"reset".
66 #==============================================================================
67
68
69
70 2>&1
71 # 重定向stderr到stdout.
72 # 将错误消息的输出, 发送到与标准输出所指向的地方.
73
74 i>&j
75 # 重定向文件描述符i到j.
76 # 指向i文件的所有输出都发送到j.
77
78 >&j
79 # 默认的, 重定向文件描述符1(stdout)到j.
80 # 所有传递到stdout的输出都送到j中去.
81
82 0< FILENAME
83 < FILENAME
84 # 从文件中接受输入.
85 # 与">"是成对命令, 并且通常都是结合使用.
86 #
87 # grep search-word <>filename
91 # 为了读写"filename", 把文件"filename"打开, 并且将文件描述符"j"分配给它.
92 # 如果文件"filename"不存在, 那么就创建它.
93 # 如果文件描述符"j"没指定, 那默认是fd 0, stdin.
94 #
95 # 这种应用通常是为了写到一个文件中指定的地方.
96 echo 1234567890 > File # 写字符串到"File".
97 exec 3<> File # 打开"File"并且将fd 3分配给它.
98 read -n 4 <&3 # 只读取4个字符.
99 echo -n . >&3 # 写一个小数点.
100 exec 3>&- # 关闭fd 3.
101 cat File # ==> 1234.67890
102 # 随机访问.
103
104
105
106 |
107 # 管道.
108 # 通用目的处理和命令链工具.
109 # 与">", 很相似, 但是实际上更通用.
110 # 对于想将命令, 脚本, 文件和程序串连起来的时候很有用.
111 cat *.txt | sort | uniq > result-file
112 # 对所有.txt文件的输出进行排序, 并且删除重复行.
113 # 最后将结果保存到"result-file"中.

可以将输入输出重定向和(或)管道的多个实例结合到一起写在同一行上.

  1 command < input-file > output-file
2
3 command1 | command2 | command3 > output-file

(ps: command2 的输入输出都经过管道重定向了)

  1 ls -yz >> command.log 2>&1
2 # 将错误选项"yz"的结果放到文件"command.log"中.
3 # 因为stderr被重定向到这个文件中,
4 #+ 所有的错误消息也就都指向那里了.
5
6 # 注意, 下边这个例子就不会给出相同的结果.
7 ls -yz 2>&1 >> command.log
8 # 输出一个错误消息, 但是并不写到文件中.
9
10 # 如果将stdout和stderr都重定向,
11 #+ 命令的顺序会有些不同.

关闭文件描述符

n<&-

关闭输入文件描述符n.
0<&-, <&-

关闭stdin.
n>&-

关闭输出文件描述符n.
1>&-, >&-

关闭stdout.

子进程继承了打开的文件描述符. 这就是为什么管道可以工作. 如果想阻止fd被继承, 那么可以关掉它.

使用exec

exec 按行读取文件的方法, 并且可以使用sed和/或awk来对每一行进行分析.

例子 1. 使用exec重定向stdin

  1 #!/bin/bash
2 # 使用'exec'重定向stdin.
3
4
5 exec 6<&0 # 将文件描述符#6与stdin链接起来.
6 # 保存stdin.

7
8 exec < data-file # stdin被文件"data-file"所代替.
9
10 read a1 # 读取文件"data-file"的第一行.
11 read a2 # 读取文件"data-file"的第二行.
12
13 echo
14 echo "Following lines read from file."
15 echo "-------------------------------"
16 echo $a1
17 echo $a2
18
19 echo; echo; echo
20
21 exec 0<&6 6<&-
22 # 现在将stdin从fd #6中恢复, 因为刚才我们把stdin重定向到#6了,
23 #+ 然后关闭fd #6 ( 6<&- ), 好让这个描述符继续被其他进程所使用.

24 #
25 # <&6 6<&- 这么做也可以.
26
27 echo -n "Enter data "
28 read b1 # 现在"read"已经恢复正常了, 就是能够正常的从stdin中读取.
29 echo "Input read from stdin."
30 echo "----------------------"
31 echo "b1 = $b1"
32
33 echo
34
35 exit 0

同样的, exec >filename命令将会把stdout重定向到一个指定的文件中. 这样所有命令的输出就都会发送到那个指定的文件, 而不是stdout.

注:exec N > filename会影响整个脚本或当前shell. 对于这个指定PID的脚本或shell来说, 从这句命令执行之后, 就会重定向到这个文件中, 然而 . . .

N > filename只会影响新fork出来的进程, 而不会影响整个脚本或shell. not the entire script or shell.

感谢你, Ahmed Darwish, 指出这个问题.

例子 2. 使用exec来重定向stdout

  1 #!/bin/bash
2 # reassign-stdout.sh
3
4 LOGFILE=logfile.txt
5
6 exec 6>&1 # 将fd #6与stdout链接起来.
7 # 保存stdout.

8
9 exec > $LOGFILE # stdout就被文件"logfile.txt"所代替了.
10
11 # ----------------------------------------------------------- #
12 # 在这块中所有命令的输出都会发送到文件$LOGFILE中.
13
14 echo -n "Logfile: "
15 date
16 echo "-------------------------------------"
17 echo
18
19 echo "Output of \"ls -al\" command"
20 echo
21 ls -al
22 echo; echo
23 echo "Output of \"df\" command"
24 echo
25 df
26
27 # ----------------------------------------------------------- #
28
29 exec 1>&6 6>&- # 恢复stdout, 然后关闭文件描述符#6.
30
31 echo
32 echo "== stdout now restored to default == "
33 echo
34 ls -al
35 echo
36
37 exit 0

例子 3. 使用exec在同一个脚本中重定向stdin和stdout

  1 #!/bin/bash
2 # upperconv.sh
3 # 将一个指定的输入文件转换为大写.
4
5 E_FILE_ACCESS=70
6 E_WRONG_ARGS=71
7
8 if [ ! -r "$1" ] # 判断指定的输入文件是否可读?
9 then
10 echo "Can't read from input file!"
11 echo "Usage: $0 input-file output-file"
12 exit $E_FILE_ACCESS
13 fi # 即使输入文件($1)没被指定
14 #+ 也还是会以相同的错误退出(为什么?).
15
16 if [ -z "$2" ]
17 then
18 echo "Need to specify output file."
19 echo "Usage: $0 input-file output-file"
20 exit $E_WRONG_ARGS
21 fi
22
23
24 exec 4<&0
25 exec < $1 # 将会从输入文件中读取.
26
27 exec 7>&1
28 exec > $2 # 将写到输出文件中.
29 # 假设输出文件是可写的(添加检查?).
30
31 # -----------------------------------------------
32 cat - | tr a-z A-Z # 转换为大写.
33 # ^^^^^ # 从stdin中读取.
34 # ^^^^^^^^^^ # 写到stdout上.
35 # 然而, stdin和stdout都被重定向了.
36 # -----------------------------------------------
37
38 exec 1>&7 7>&- # 恢复stout.
39 exec 0<&4 4<&- # 恢复stdin.
40
41 # 恢复之后, 下边这行代码将会如预期的一样打印到stdout上.
42 echo "File \"$1\" written to \"$2\" as uppercase conversion."
43
44 exit 0

I/O重定向是一种避免可怕的子shell中不可访问变量问题的方法.

例子 4. 避免子shell

  1 #!/bin/bash
2 # avoid-subshell.sh
3 # 由Matthew Walker所提出的建议.
4
5 Lines=0
6
7 echo
8
9 cat myfile.txt | while read line; # (译者注: 管道会产生子shell)
10 do {
11 echo $line
12 (( Lines++ )); # 增加这个变量的值
13 #+ 但是外部循环却不能访问.
14 # 子shell问题.
15 }
16 done
17
18 echo "Number of lines read = $Lines" # 0
19 # 错误!
20
21 echo "------------------------"
22
23
24 exec 3<> myfile.txt
25 while read line <&3

26 do {
27 echo "$line"
28 (( Lines++ )); # 增加这个变量的值
29 #+ 现在外部循环就可以访问了.
30 # 没有子shell, 现在就没问题了.
31 }
32 done
33 exec 3>&-
34
35 echo "Number of lines read = $Lines" # 8
36
37 echo
38
39 exit 0
40
41 # 下边这些行是这个脚本的结果, 脚本是不会走到这里的.
42
43 $ cat myfile.txt
44
45 Line 1.
46 Line 2.
47 Line 3.
48 Line 4.
49 Line 5.
50 Line 6.
51 Line 7.
52 Line 8.

代码块重定向

象while, until, 和for循环代码块, 甚至if/then测试结构的代码块, 都可以对stdin进行重定向. 即使函数也可以使用这种重定向方式. 要想做到这些, 都要依靠代码块结尾的 < 操作符.

例子 5. while循环的重定向

  1 #!/bin/bash
2 # redir2.sh
3
4 if [ -z "$1" ]
5 then
6 Filename=names.data # 如果没有指定文件名, 则使用这个默认值.
7 else
8 Filename=$1
9 fi
10 #+ Filename=${1:-names.data}
11 # 这句可代替上面的测试(参数替换).
12
13 count=0
14
15 echo
16
17 while [ "$name" != Smith ] # 为什么变量$name要用引号?
18 do
19 read name # 从$Filename文件中读取输入, 而不是在stdin中读取输入.
20 echo $name
21 let "count += 1"
22 done <"$Filename" # 重定向stdin到文件$Filename.
23 # ^^^^^^^^^^^^
24
25 echo; echo "$count names read"; echo
26
27 exit 0
28
29 # 注意在一些比较老的shell脚本编程语言中,
30 #+ 重定向的循环是放在子shell里运行的.
31 # 因此, $count 值返回后会是 0, 此值是在循环开始前的初始值.
32 # *如果可能的话*, 尽量避免在Bash或ksh中使用子shell,
33 #+ 所以这个脚本能够正确的运行.
34 # (多谢Heiner Steven指出这个问题.)
35
36 # 然而 . . .
37 # Bash有时还是*会*在一个使用管道的"while-read"循环中启动一个子shell,
38 #+ 与重定向的"while"循环还是有区别的.
39
40 abc=hi
41 echo -e "1\n2\n3" | while read l
42 do abc="$l"
43 echo $abc
44 done
45 echo $abc
46
47 # 感谢, Bruno de Oliveira Schneider
48 #+ 给出上面的代码片段来演示此问题.
49 # 同时, 感谢, Brian Onn, 修正了一个注释错误.

例子 6. 重定向while循环的另一种形式

  1 #!/bin/bash
2
3 # 这是上个脚本的另一个版本.
4
5 # Heiner Steven建议,
6 #+ 为了避免重定向循环运行在子shell中(老版本的shell会这么做), 最好让重定向循环运行在当前工作区内,
7 #+ 这样的话, 需要提前进行文件描述符重定向,
8 #+ 因为变量如果在(子shell上运行的)循环中被修改的话, 循环结束后并不会保存修改后的值.
9
10
11 if [ -z "$1" ]
12 then
13 Filename=names.data # 如果没有指定文件名则使用默认值.
14 else
15 Filename=$1
16 fi
17
18
19 exec 3<&0 # 将stdin保存到文件描述符3.
20 exec 0<"$Filename" # 重定向标准输入.
21
22 count=0
23 echo
24
25
26 while [ "$name" != Smith ]
27 do
28 read name # 从stdin(现在已经是$Filename了)中读取.
29 echo $name
30 let "count += 1"
31 done # 从文件$Filename中循环读取
32 #+ 因为文件(译者注:指默认文件, 在本节最后)有20行.
33
34 # 这个脚本原先在"while"循环的结尾还有一句:
35 #+ done <"$Filename"
36 # 练习:
37 # 为什么不需要这句了?
38
39
40 exec 0<&3 # 恢复保存的stdin.
41 exec 3<&- # 关闭临时文件描述符3.
42
43 echo; echo "$count names read"; echo
44
45 exit 0

例子 7. 重定向until循环

  1 #!/bin/bash
2 # 和前面的例子相同, 但使用的是"until"循环.
3
4 if [ -z "$1" ]
5 then
6 Filename=names.data # 如果没有指定文件名那就使用默认值.
7 else
8 Filename=$1
9 fi
10
11 # while [ "$name" != Smith ]
12 until [ "$name" = Smith ] # 把!=改为=.
13 do
14 read name # 从$Filename中读取, 而不是从stdin中读取.
15 echo $name
16 done <"$Filename" # 重定向stdin到文件$Filename.
17 # ^^^^^^^^^^^^
18
19 # 结果和前面例子的"while"循环相同.
20
21 exit 0

例子 8. 重定向for循环

  1 #!/bin/bash
2
3 if [ -z "$1" ]
4 then
5 Filename=names.data # 如果没有指定文件名就使用默认值.
6 else
7 Filename=$1
8 fi
9
10 line_count=`wc $Filename | awk '{ print $1 }'`
11 # 目标文件的行数.
12 #
13 # 此处的代码太过做作, 并且写得很难看,
14 #+ 但至少展示了"for"循环的stdin可以重定向...
15 #+ 当然, 你得足够聪明, 才能看得出来.
16 #
17 # 更简洁的写法是 line_count=$(wc -l < "$Filename")
18
19
20 for name in `seq $line_count` # "seq"打印出数字序列.
21 # while [ "$name" != Smith ] -- 比"while"循环更复杂 --
22 do
23 read name # 从$Filename中, 而非从stdin中读取.
24 echo $name
25 if [ "$name" = Smith ] # 因为用for循环, 所以需要这个多余测试.
26 then
27 break
28 fi
29 done <"$Filename" # 重定向stdin到文件$Filename.
30 # ^^^^^^^^^^^^
31
32 exit 0

我们也可以修改前面的例子使其能重定向循环的标准输出.

例子 9. 重定向for循环(stdin和stdout都进行重定向)

  1 #!/bin/bash
2
3 if [ -z "$1" ]
4 then
5 Filename=names.data # 如果没有指定文件名, 则使用默认值.
6 else
7 Filename=$1
8 fi
9
10 Savefile=$Filename.new # 保存最终结果的文件名.
11 FinalName=Jonah # 终止"read"时的名称.
12
13 line_count=`wc $Filename | awk '{ print $1 }'` # 目标文件的行数.
14
15
16 for name in `seq $line_count`
17 do
18 read name
19 echo "$name"
20 if [ "$name" = "$FinalName" ]
21 then
22 break
23 fi
24 done < "$Filename" > "$Savefile" # 重定向stdin到文件$Filename,
25 # ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 并且将它保存到备份文件中.
26
27 exit 0

例子 10. 重定向if/then测试结构

  1 #!/bin/bash
2
3 if [ -z "$1" ]
4 then
5 Filename=names.data # 如果文件名没有指定, 使用默认值.
6 else
7 Filename=$1
8 fi
9
10 TRUE=1
11
12 if [ "$TRUE" ] # if true 和 if : 都可以.
13 then
14 read name
15 echo $name
16 fi <"$Filename"
17 # ^^^^^^^^^^^^
18
19 # 只读取了文件的第一行.
20 # An "if/then"测试结构不能自动地反复地执行, 除非把它们嵌到循环里.
21
22 exit 0

例子 11. 用于上面例子的"names.data"数据文件

  1 Aristotle
2 Belisarius
3 Capablanca
4 Euler
5 Goethe
6 Hamurabi
7 Jonah
8 Laplace
9 Maroczy
10 Purcell
11 Schmidt
12 Semmelweiss
13 Smith
14 Turing
15 Venn
16 Wilson
17 Znosko-Borowski

重定向代码块的stdout, 与"将代码块的输出保存到文件中"具有相同的效果.

阅读(593) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~