1. IO概念

程序是由指令+数据组成的。换言之 , 程序是对读入的数据进行处理 , 再输出数据。数据的输入(Input) , 输出(Output) , 简称为IO , 在没有指定输入输出的情况下 , 默认为标准输入和标准输出。

linux系统将每个对象当作文件处理 , 这包括输入输出进程。linux文件描述符(file descriptor)来标识每个文件对象。文件描述符是一个非负整数 , 可以唯一标识会话中打开的文件。每个进程一次最多可以有九个文件描述符。出于特殊目的,bash shell保留了前三个文件描述符(0,12)

Linux给程序提供三种IO设备

  • 标准输入(STDIN) : 0 。默认接受来自键盘的输入

  • 标准输出(STDOUT) :1。 默认输出到终端窗口

  • 标准错误(STDERR) : 2。 默认输出到终端窗口

[root@centos7 ~]# ll /dev/std*
lrwxrwxrwx 1 root root 15 Aug 23 15:15 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Aug 23 15:15 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Aug 23 15:15 /dev/stdout -> /proc/self/fd/1

默认情况下,上述的块设备为默认的输入输出设备

Linux中 , 一切皆文件 ,我们每打开文件 , 系统都会自动分配一个FD ( file description , 文件描述符 ) 。上面的0,1,2就是系统分配的文件描述符。

ll /proc/$$/fd    查看目前的文件描述符
exec 8<>/data/hosts    表示给/data/hosts文件指定一个文件描述符8,且8与/data/hosts之间是软链接
exec 8>&-    删除8号这个文件描述符
[root@centos7 ~]# ll /proc/$$/fd
total 0
lrwx------ 1 root root 64 Aug 23 21:55 0 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 1 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 2 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 255 -> /dev/pts/0
[root@centos7 ~]# cp /etc/hosts  /tmp/
[root@centos7 ~]# exec  8<> /tmp/hosts 
[root@centos7 ~]# ll /proc/$$/fd
total 0
lrwx------ 1 root root 64 Aug 23 21:55 0 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 1 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 2 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 255 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:58 8 -> /tmp/hosts
[root@centos7 ~]# cat /proc/$$/fd/8
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
[root@centos7 ~]# exec 8>&-
[root@centos7 ~]# ll /proc/$$/fd
total 0
lrwx------ 1 root root 64 Aug 23 21:55 0 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 1 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 2 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 23 21:55 255 -> /dev/pts/0
​

2. IO重定向

2.1 重定向基础用法

Linux可以通过特定的符号改变进程的默认IO设备。>是输出重定向符号 , <是输入重定向符号。

/dev/stdin/dev/stdout/dev/stderr等设备只是数据默认的流向目标(严格地说是文件描述符012的输出目标) , 它们不等价于"标准输入0、标准输出1、标准错误2"。之所以称为重定向 , 就是将数据的流向改变 , 不再输入到这些默认设备中。

Shell 文件描述符操作方法一览表

分类

用法

说明

输出

n>filename

以输出的方式打开文件 filename , 并绑定到文件描述符nn 可以不写 , 默认为 1 ,也即标准输出文件。

输出

n>&m

用文件描述符m 修改文件描述符n ,或者说用文件描述符 m 的内容覆盖文件描述符 n , 结果就是nm都代表了同一个文件 , 因为nm的文件指针都指向了同一个文件。 因为使用的是> , 所以nm 只能用作命令的输出文件。n 可以不写 , 默认为1

输出

n>&-

关闭文件描述符n 及其代表的文件。n 可以不写 , 默认为 1

输出

&>filename

将正确输出结果和错误信息全部重定向到filename

输入

n<filename

以输入的方式打开文件filename , 并绑定到文件描述符nn 可以不写 , 默认为 0,也即标准输入文件。

输入

n<&-

关闭文件描述符n 及其代表的文件。n 可以不写 , 默认为0

输入和输出

n<>filename

同时以输入和输出的方式打开文件filename , 并绑定到文件描述符 n , 相当于n>filenamen<filename 的总和。n 可以不写 , 默认为 0

根据上面表格所说的文件描述符操作方法可以归纳了以下输入输出用法

类型

符号

作用

标准输出重定向

command >file

以覆盖的方式 , 把 command 的正确输出结果输出到 file 文件中。

标准输出重定向

command >>file

以追加的方式 , 把 command 的正确输出结果输出到 file 文件中。

标准错误输出重定向

command 2>file

以覆盖的方式 , 把command的错误信息输出到 file 文件中。

标准错误输出重定向

command 2>>file

以追加的方式 , 把command的错误信息输出到 file 文件中。

正确输出和错误信息同时保存

command >file 2>&1

以覆盖的方式 , 把正确输出和错误信息同时保存到同一个文件(file)中。

正确输出和错误信息同时保存

command >>file 2>&1

以追加的方式 , 把正确输出和错误信息同时保存到同一个文件(file)中。

正确输出和错误信息同时保存

command >file1 2>file2

以覆盖的方式 , 把正确的输出结果输出到file1文件中 , 把错误信息输出到file2文件中。

正确输出和错误信息同时保存

command >>file1 2>>file2

以追加的方式 , 把正确的输出结果输出到file1文件中 , 把错误信息输出到file2文件中。

正确输出和错误信息同时保存

command >file 2>file

不推荐】这两种写法会导致file被打开两次 , 引起资源竞争 , 所以 stdoutstderr会互相覆盖

正确输出和错误信息同时保存

command >>file 2>>file

不推荐】这两种写法会导致file被打开两次 , 引起资源竞争 , 所以stdoutstderr会互相覆盖

command <file1 >file2

file1 作为command的输入 , 并将command的处理结果输出到 file2

() :合并多个程序的STDOUT。同样()有一个特殊的用法:(COMMAND 2>&1) >> /path/to/file.out,主要的效果与command >>file 2>&1一样,均是将标准输出内容与标准错误输出内容均追加至文件中。

(cal 2007;cal 2008) > all.txt

重定向的顺序很重要ls / >file1 2>&1 : 表示先打开file1作为标准输出(fd=1)的目的地 , 然后再将标准错误绑定到标准输出(已经是file1)上 , 这样无论是标准错误还是标准输出都重定向到file1中。它等价于ls / &>file1&符号表示描述符重用(fd 2 duplicate from fd 1)。将其理解为文件描述符2复制了文件描述符1 , 或者文件描述2重用文件描述符1 , 使得fd=2也指向了fd=1所指向的文件。

ls / 2>&1 >file1 表示先将标准错误指向到标准输出 , 此时标准输出还是/dev/sdtout(即屏幕) , 因此标准错误的输出目标是/dev/stdout(屏幕)。之后再打开file1作为标准输出的目标。因此 , 它最终将标准错误重定向到/dev/stdout , 将标准输出重定向到file1。可以让ls命令产生错误来测试 , ls dlfjasl 2>&1 >file1 ,结果将直接显示在屏幕上。

2.2 高级用法

stdoutstderr丢到/dev/null表示丢弃输出信息 , 反过来 , 将/dev/null重定向到某个文件则表示清空文件。

[root@centos7 ~]# cat /dev/null > ab.sh

除此 , 还有以下几种方法快速清空文件

[root@centos7 ~]# > ab.sh
[root@centos7 ~]# : > ab.sh             # 或"true >ab.sh" , 其实它们都等价于">ab.sh"
[root@centos7 ~]# echo '' > ab.sh
[root@centos7 ~]# truncate -s 0 ab.sh   # truncate命令用于收缩和扩展文件大小
[root@centos7 ~]# dd if=/dev/null of=ab.sh

> bigfile 效果是创建了一个bigfile的空文件。背后的原理是利用重定向标准输出的原理 , 重定向输出至bigfile文件 , 由于无任何输出结果 , 所以直接效果就是创建了一个空的bigfile文件。所以一个比较安全的创建空文件的方法是:>> file

原因:

  1. 假如file文件原来就存在 , 那么>> 不会覆盖原来的文件 , 只会在原文件基础上累加。

  2. >> 不会刷新时间 , touch命令会刷新时间。

  3. 假如abc_linkabc的软连接。那么我们>abc_link时,会直接覆盖源文件abc  

在有输出类的重定向(包括错误重定向)语句中 , 命令执行之前就已经将文件截断为0大小。所以如果正在编辑一个文件并将编辑的结果重定向回这个文件将出现异常 , 因为截断后就没有合适的内容用于编辑。

[root@centos7 ~]# head a.log > a.log

有些时候直接使用>覆盖输出是比较危险的。可以使用set -C来设置如果输出重定向文件已经存在则不覆盖。使用set +C来取消set -C的效果。如果在设置了set -C时仍然想强制覆盖 , 可以使用>|代替>来重定向输出。同理错误输出也有此特性。

[root@centos7 tmp]# set -C
​
[root@centos7 tmp]# cat flip >ttt.txt
-bash: ttt.txt: cannot overwrite existing file
​
[root@centos7 tmp]# cat flip >| ttt.txt
​
[root@centos7 tmp]# set +C

3. 特殊重定向符号

bash中 , <<<<<是特殊重定向符号。<<表示的是here document , <<<表示的是here string

3.1 here document

Here Document 是在Linux Shell 中的一种特殊的重定向方式 , 它的基本的形式如下

cmd << delimiter
  Here Document Content
delimiter

它的作用就是将两个 delimiter 之间的内容(Here Document Content部分) 传递给cmd 作为输入参数。

  • 注意

    • delimiter 可以使用任意合法字符

    • 结尾的delimiter 一定要顶格写 , 前不能有任何字符

    • 结尾的delimiter 前后不能有任何字符及空格 , 要和开始的delimiter 保持一致

    • 开头delimiter 前后的空格会被省略掉

    • 输出时加上双引号可以换行

    • delimiter 加上单引号或者双引号时变量将不生效

    • delimiter 前加上-可无视tab键

比如在终端中输入cat << EOF , 系统会提示继续进行输入 , 输入多行信息再输入EOF , 中间输入的信息将会显示在屏幕上。如下:

[root@centos7 ~]# cat << EOF
> First Line
> Second Line
> Third Line EOF
> EOF
First Line
Second Line
Third Line EOF

注: >这个符号是终端产生的提示输入信息的标识符

一方面 , eof部分都必须使用<<eof , 它表示here document , 此后输入的内容都作为一个document输入给cat。既然是document , 那就肯定有document结束符标记document到此结束 , 结束符使用的是here document后的字符 , 例如此处为eof。其实不使用eof , 使用其他字符也是一样的 , 但document的结束符也必须要随之改变。如:

[root@centos7 ~]# cat <<abcx
> 123
> 345
> abcx
123
345

另一方面 , >log1.txt表示将document的内容覆盖到log1.txt文件中 , 如果是要追加 , 则使用>>log1.txt。所以追加的方式如下:

[root@centos7 tmp]# cat >>log1.txt << eof 
> this is stdin character first!
> eof

[root@centos7 tmp]# cat << eof >>log1.txt 
> this is stdin character first!
> eof

实例

  • 免交互行数的统计

    [root@centos7 ~]# wc -l << EOF
    > 1
    > 2
    > EOF
    2
  • 免交互读取read命令并打印

    [root@centos7 ~]# read input <<EOF
    > hello
    > bor
    > EOF
    [root@centos7 ~]# echo $input
    hello
  • 免交互修改密码

    [root@centos7 ~]# passwd xiong << EOF
    > 123456
    > 123456
    > EOF
    Changing password for user xiong.
    New password: BAD PASSWORD: The password is shorter than 8 characters
    Retype new password: passwd: all authentication tokens updated successfully.
  • 免交互变量替换

    #将变量写入到某个文件中
    [root@centos7 ~]# cat demo.sh 
    #!/bin/bash
    catName=mimi
    cat > catHome <<EOF
    Cat name is $catName!
    EOF
    [root@centos7 ~]# bash demo.sh 
    [root@centos7 ~]# cat catHome 
    Cat name is mimi!
  • 变量整体免交互赋值

    [root@centos7 ~]# cat demo.sh 
    #!/bin/bash
    catName=mimi
    result=$(cat <<EOF
    time is "`date`"
    Cat name is $catName!
    EOF
    )
    echo $result
    [root@centos7 ~]# bash demo.sh 
    time is "Wed Aug 25 10:35:44 CST 2021" Cat name is mimi!
  • 多行注释

    Here Document的引入解决多行注释问题 使用:代表空指令 , 中间标记区域的内容不会被执行

    [root@centos7 ~]# cat demo.sh 
    #!/bin/bash
    catName=mimi
    : <<EOF
    time is "`date`"
    Cat name is $catName!
    EOF
    echo ">3"
    [root@centos7 ~]# bash demo.sh 
    >3

3.2 here string

对于here string , 表示将<<<后的字符串作为输入数据。

例如:

passwd --stdin user <<< password_value

等价于:

echo password_value | passwd --stdin user

4. 重定向相关命令

4.1 tee

可以使用tee双重定向。一般情况下 , 重定向要么将信息输入到文件中 , 要么输出到屏幕上 , 但是既想输出到屏幕又想输出到文件就比较麻烦。使用tee的双重定向功能可以实现该想法。

IO-redirect-tee-1

  • 语法 : tee [-a] file1 file2 file3

  • 选项说明

    • -a : 默认是将输出覆盖到文件中 , 使用该选项将变为追加行为。

    • file : 除了输出到标准输出中 , 还将输出到file中。如果file- , 则表示再输入一次到标准输出中。

tee的作用是将一份标准输入多重定向 , 一份重定向到标准输出/dev/stdout , 然后还将标准输入重定向到每个文件FILE中。

$ cat alpha.log | tee file1 file2 file3 | cat
$ cat alpha.log | tee file1 file2 file3 >/dev/null

上面第一个命令将alpha.log的文件内容重定向给file{1..3}和标准输出通过管道传递给cat; 上面第二个命令将alpha.log的文件内容重定向给file{1..3}/dev/null

示例

  1. ls的数据存一份到~/homefile , 同时屏幕也有输出信息。

    $ ls -l /home | tee ~/homefile | more
  2. a开头的文件内容全部保存到b.log , 同时把副本交给后面的的cat , 使用这个cat又将内容保存到了x.log。其中-代表前面的stdin

    [root@centos7 tmp]# cat a* | tee b.log | cat - >x.log

    直接输出到屏幕:

    [root@centos7 tmp]# cat a* | tee b.log | cat
  3. tee默认会使用覆盖的方式保存到文件 , 可以使用-a选项来追加到文件。如:

    [root@centos7 tmp]# cat a* | tee -a b.log | cat
  4. 现在就可以在使用cat和重定向创建文件或写入内容到文件的同时又可以在屏幕上显示一份。

    [root@centos7 tmp]# cat <<eof | tee ttt.txt
    > x y
    > z 1
    > eof
    ​
    x y
    z 1

4.1.1 tee重定向给多个命令

写多了脚本的人可能遇到过这样一种需求 : 将一份标准输入 m 重定向到多个命令中去。大概是这样的:

                      | CMD1
                    ↗
        INPUT | tee 
                    ↘
                      | CMD2

其实bash自身的特性就能实现这样的需求 , 通过重定向到子shell中 , 就能模拟一个文件重定向行为:

cat alpha.txt | tee >(grep -E "a|b") >(grep -E "d|b|c")

实际上这里的两个>(cmd_list)不是重定向 , 而是进程替换。命令行解析开始时 , 将首先进行进程替换 , 这两个grep将等待标准输入。然后启动cattee , 然后tee将标准输出交给两个进程的标准输入。

上面的命令将alpha.txt文件内容重定向为3份 : 一份给第一个grep命令 , 一份给第二个grep命令 , 一份给标准输出。假如alpha.txt的内容是a b c d e5个字母分别占用5行(每行一个字母) , 上面的输出结果如下:

[root@centos7 ~]# cat test | tee >(grep -E "a|b") >(grep -E "d|b|c")
a
b
c
d
e          # 前5行是重定向到/dev/stdout的
a
b          # 这2行是重定向给第一个grep后的执行结果
b
c
d          # 这3行是重定向给第二个grep后的执行结果

如果不想要给标准输出的那份重定向 , 加上>/dev/null:

[root@centos7 ~]# cat test | tee >(grep -E "a|b") >(grep -E "d|b|c") > /dev/null
b
c
d
[root@centos7 ~]# a
b
​

4.1.2 tee重定向给多个命令时的问题

tee将数据重定向给不同命令时 , 这些命令是独立执行的 , 它们都会各自打开一个属于自己的STDOUT , 如果它们都重定向到标准输出 , 由于涉及到多个不同的/dev/stdout , 它们的结果将出现两个问题 :

  1. 不保证有序性

  2. 因为跨了命令 , 交互式模式下(默认标准输出为屏幕)可能会出现命令行隔断的问题(非交互式下不会有问题)

$ cat alpha.txt | tee >(grep -E "a|b") >(grep -E "d|b|c") >/dev/null
$ a     # 结果直接出现在提示符所在行
b
b
c
d
​
$ cat alpha.txt | tee >(grep -E "a|b") >(grep -E "d|b|c") >/dev/null
b
c      # 这次的结果和上次的顺序不一样
d
a
b

这两个问题 , 在写脚本过程中必须解决。

对于第二个问题 : 不同/dev/stdout同时输出时在屏幕上交叉输出的问题 , 只需将它们再次重定向走即可 , 这样两份不同的/dev/stdout都再次同时作为一份标准输入

$ cat alpha.txt | tee >(grep -E "a|b") >(grep -E "d|b|c") >/dev/null | cat

对于第一个问题 : 不同/dev/stdout同时输出时 , 输出顺序的随机性 , 这个没有好方法 , 只能在各命令行中将各自的结果保存到文件中

$ cat alpha.txt | tee >(grep -E "a|b" >file1) >(grep -E "d|b|c" >file2) >/dev/null

所以 , tee在重定向到多个命令中是有缺陷的 , 或者说用起来非常不方便 , 只要将各命令的结果各自保存时, 才能一切按照自己的

4.2 pee

peemoreutils包中的一个小工具 , 先安装它(epel源中有):

yum -y install moreutils
  • 语法 : pee ["cmds"]

    [root@centos7 ~]# cat test | pee 'grep -E "a|b"' 'grep -E "b|c|d"' 
    a
    b
    b
    c
    d
    ​

tee有同样的问题 , 如果各命令都没有指定自己的标准输出重定向 , 它们将各自打开一个属于自己的/dev/stdout , 同样会有多个/dev/stdout同时输出时结果数据顺序随机性的问题 , 但是不会有多个/dev/stdout同时输出时交互式的隔断性问题 , 因为pee会收集各个命令的标准输出 ,然后将收集的结果作为自己的标准输出。

peetee最大的不同 , 在于pee将来自多个不同命令的结果作为pee自己的标准输出 ,所以下面的命令是可以像普通命令一样进行重定向的。

INPUT | pee CMD1 CMD2 >/FILE

tee则不同 , 是将cmd1cmd2的结果放进标准输出(假设各命令自身没有使用重定向) , 保存到FILE中的是tee读取的标准输入。

INPUT | tee >(cmd1) >(cmd2) >/FILE

所以 , 想要重定向`teecmd1cmd2的总结果 , 必须使用额外的管道 , 或者将整个tee放进子shell

INPUT | tee >(cmd1) >(cmd2) >/dev/null | cat >FILE1
INPUT | ( tee >(cmd1) >(cmd2) >/dev/null ) >/FILE1


参考链接

CSDN: 猫嗅花 :IO重定向及管道操作

四、IO重定向和管道以及基本文本处理工具 - 幻落之瞳 - 博客园 (cnblogs.com)

Linux重定向 ( 输入输出重定向 ) 详解 (biancheng.net)

结合Linux文件描述符谈重定向 , 彻底理解重定向的本质!_跳墙网 (tqwba.com)

Linux Shell重定向 ( 输入输出重定向 ) 精讲 (biancheng.net)

SHELL脚本--管道和重定向基础 - 骏马金龙 - 博客园 (cnblogs.com)

linux shell 的here document 用法 (cat << EOF) - GreatFish的个人空间 - OSCHINA - 中文开源技术交流社区

Shell脚本 - Here Document免交互与Expect自动化交互 | 实例操作 | 超详细_serendipity_cat的博客-CSDN博客

Linux Shell教程 ( 二 ) / tee双重重定向命令 - 汇智网 (hubwiz.com)

Linux tee的花式用法和pee - 骏马金龙 - 博客园 (cnblogs.com)

Linux Shell教程 ( 二 ) / 管线命令介绍 - 汇智网 (hubwiz.com)


熊熊