10.5 数据流重导向

数据流重导向 (redirect) 由字面上的意思来看,好像就是将“数据给他传导到其他地方去”的样子? 没错~数据流重导向就是将某个指令执行后应该要出现在屏幕上的数据, 给他传输到其他的地方,例如文件或者是设备 (例如打印机之类的)!这玩意儿在 Linux 的文字模式下面可重要的! 尤其是如果我们想要将某些数据储存下来时,就更有用了!

10.5.1 什么是数据流重导向

什么是数据流重导向啊?这得要由指令的执行结果谈起!一般来说,如果你要执行一个指令,通常他会是这样的:

指令执行过程的数据传输情况图10.5.1、指令执行过程的数据传输情况

我们执行一个指令的时候,这个指令可能会由文件读入数据,经过处理之后,再将数据输出到屏幕上。 在上图当中, standard output 与 standard error output 分别代表“标准输出 (STDOUT)”与“标准错误输出 (STDERR)”, 这两个玩意儿默认都是输出到屏幕上面来的啊!那么什么是标准输出与标准错误输出呢?

  • standard output 与 standard error output

简单的说,标准输出指的是“指令执行所回传的正确的讯息”,而标准错误输出可理解为“ 指令执行失败后,所回传的错误讯息”。举个简单例子来说,我们的系统默认有 /etc/crontab 但却无 /etc/vbirdsay, 此时若下达“ cat /etc/crontab /etc/vbirdsay ”这个指令时,cat 会进行:

  • 标准输出:读取 /etc/crontab 后,将该文件内容显示到屏幕上;
  • 标准错误输出:因为无法找到 /etc/vbirdsay,因此在屏幕上显示错误讯息

不管正确或错误的数据都是默认输出到屏幕上,所以屏幕当然是乱乱的!那能不能通过某些机制将这两股数据分开呢? 当然可以啊!那就是数据流重导向的功能啊!数据流重导向可以将 standard output (简称 stdout) 与 standard error output (简称 stderr) 分别传送到其他的文件或设备去,而分别传送所用的特殊字符则如下所示:

  1. 标准输入  (stdin) :代码为 0 ,使用 < 或 << ;
  2. 标准输出  (stdout):代码为 1 ,使用 > 或 >> ;
  3. 标准错误输出(stderr):代码为 2 ,使用 2> 或 2>> ;

为了理解 stdout 与 stderr ,我们先来进行一个范例的练习:

范例一:观察你的系统根目录 (/) 下各目录的文件名、权限与属性,并记录下来
[[email protected] ~]$ ll /  <==此时屏幕会显示出文件名信息
[[email protected] ~]$ ll / > ~/rootfile <==屏幕并无任何信息
[[email protected] ~]$ ll  ~/rootfile <==有个新文件被创建了!
-rw-rw-r--. 1 dmtsai dmtsai 1078 Jul  9 18:51 /home/dmtsai/rootfile

怪了!屏幕怎么会完全没有数据呢?这是因为原本“ ll / ”所显示的数据已经被重新导向到 ~/rootfile 文件中了! 那个 ~/rootfile 的文件名可以随便你取。如果你下达“ cat ~/rootfile ”那就可以看到原本应该在屏幕上面的数据啰。 如果我再次下达:“ ll /home > ~/rootfile ”后,那个 ~/rootfile 文件的内容变成什么? 他将变成“仅有 ll /home 的数据”而已!咦!原本的“ ll / ”数据就不见了吗?是的!因为该文件的创建方式是:

  1. 该文件 (本例中是 ~/rootfile) 若不存在,系统会自动的将他创建起来,但是
  2. 当这个文件存在的时候,那么系统就会先将这个文件内容清空,然后再将数据写入!
  3. 也就是若以 > 输出到一个已存在的文件中,那个文件就会被覆盖掉啰!

那如果我想要将数据累加而不想要将旧的数据删除,那该如何是好?利用两个大于的符号 (>>) 就好啦!以上面的范例来说,你应该要改成“ ll / >> ~/rootfile ”即可。 如此一来,当 (1) ~/rootfile 不存在时系统会主动创建这个文件;(2)若该文件已存在, 则数据会在该文件的最下方累加进去!

上面谈到的是 standard output 的正确数据,那如果是 standard error output 的错误数据呢?那就通过 2> 及 2>> 啰!同样是覆盖 (2>) 与累加 (2>>) 的特性!我们在刚刚才谈到 stdout 代码是 1 而 stderr 代码是 2 , 所以这个 2> 是很容易理解的,而如果仅存在 > 时,则代表默认的代码 1 啰!也就是说:

  • 1> :以覆盖的方法将“正确的数据”输出到指定的文件或设备上;
  • 1>>:以累加的方法将“正确的数据”输出到指定的文件或设备上;
  • 2> :以覆盖的方法将“错误的数据”输出到指定的文件或设备上;
  • 2>>:以累加的方法将“错误的数据”输出到指定的文件或设备上;

要注意喔,“ 1>> ”以及“ 2>> ”中间是没有空格的!OK!有些概念之后让我们继续聊一聊这家伙怎么应用吧! 当你以一般身份执行 find 这个指令的时候,由于权限的问题可能会产生一些错误信息。例如执行“ find / -name testing ”时,可能会产生类似“ find: /root: Permission denied ”之类的讯息。 例如下面这个范例:

范例二:利用一般身份帐号搜寻 /home 下面是否有名为 .bashrc 的文件存在
[[email protected] ~]$ find /home -name .bashrc <==身份是 dmtsai 喔!
find: '/home/arod': Permission denied    <== Standard error output
find: '/home/alex': Permission denied    <== Standard error output
/home/dmtsai/.bashrc                     <== Standard output

由于 /home 下面还有我们之前创建的帐号存在,那些帐号的主文件夹你当然不能进入啊!所以就会有错误及正确数据了。 好了,那么假如我想要将数据输出到 list 这个文件中呢?执行“ find /home -name .bashrc > list ” 会有什么结果?呵呵,你会发现 list 里面存了刚刚那个“正确”的输出数据, 至于屏幕上还是会有错误的讯息出现呢!伤脑筋!如果想要将正确的与错误的数据分别存入不同的文件中需要怎么做?

范例三:承范例二,将 stdout 与 stderr 分存到不同的文件去
[[email protected] ~]$ find /home -name .bashrc > list_right 2> list_error

注意喔,此时“屏幕上不会出现任何讯息”!因为刚刚执行的结果中,有 Permission 的那几行错误信息都会跑到 list_error 这个文件中,至于正确的输出数据则会存到 list_right 这个文件中啰!这样可以了解了吗? 如果有点混乱的话,去休息一下再回来看看吧!

  • /dev/null 垃圾桶黑洞设备与特殊写法

想像一下,如果我知道错误讯息会发生,所以要将错误讯息忽略掉而不显示或储存呢? 这个时候黑洞设备 /dev/null 就很重要了!这个 /dev/null 可以吃掉任何导向这个设备的信息喔!将上述的范例修订一下:

范例四:承范例三,将错误的数据丢弃,屏幕上显示正确的数据
[[email protected] ~]$ find /home -name .bashrc 2> /dev/null
/home/dmtsai/.bashrc  <==只有 stdout 会显示到屏幕上, stderr 被丢弃了

再想像一下,如果我要将正确与错误数据通通写入同一个文件去呢?这个时候就得要使用特殊的写法了! 我们同样用下面的案例来说明:

范例五:将指令的数据全部写入名为 list 的文件中
[[email protected] ~]$ find /home -name .bashrc > list 2> list  <==错误
[[email protected] ~]$ find /home -name .bashrc > list 2>&1     <==正确
[[email protected] ~]$ find /home -name .bashrc &> list         <==正确

上述表格第一行错误的原因是,由于两股数据同时写入一个文件,又没有使用特殊的语法, 此时两股数据可能会交叉写入该文件内,造成次序的错乱。所以虽然最终 list 文件还是会产生,但是里面的数据排列就会怪怪的,而不是原本屏幕上的输出排序。 至于写入同一个文件的特殊语法如上表所示,你可以使用 2>&1 也可以使用 &> ! 一般来说,鸟哥比较习惯使用 2>&1 的语法啦!

  • standard input : < 与 <<

了解了 stderr 与 stdout 后,那么那个 < 又是什么呀?呵呵!以最简单的说法来说, 那就是“将原本需要由键盘输入的数据,改由文件内容来取代”的意思。 我们先由下面的 cat 指令操作来了解一下什么叫做“键盘输入”吧!

范例六:利用 cat 指令来创建一个文件的简单流程
[[email protected] ~]$ cat > catfile
testing
cat file test
<==这里按下 [ctrl]+d 来离开
[[email protected] ~]$ cat catfile
testing
cat file test

由于加入 > 在 cat 后,所以那个 catfile 会被主动的创建,而内容就是刚刚键盘上面输入的那两行数据了。 唔!那我能不能用纯文本文件取代键盘的输入,也就是说,用某个文件的内容来取代键盘的敲击呢? 可以的!如下所示:

范例七:用 stdin 取代键盘的输入以创建新文件的简单流程
[[email protected] ~]$ cat > catfile < ~/.bashrc
[[email protected] ~]$ ll catfile ~/.bashrc
-rw-r--r--. 1 dmtsai dmtsai 231 Mar  6 06:06 /home/dmtsai/.bashrc
-rw-rw-r--. 1 dmtsai dmtsai 231 Jul  9 18:58 catfile
# 注意看,这两个文件的大小会一模一样!几乎像是使用 cp 来复制一般!

这东西非常的有帮助!尤其是用在类似 mail 这种指令的使用上。 理解 < 之后,再来则是怪可怕一把的 << 这个连续两个小于的符号了。 他代表的是“结束的输入字符”的意思!举例来讲:“我要用 cat 直接将输入的讯息输出到 catfile 中, 且当由键盘输入 eof 时,该次输入就结束”,那我可以这样做:

[[email protected] ~]$ cat > catfile << "eof"
> This is a test.
> OK now stop
> eof  <==输入这关键字,立刻就结束而不需要输入 [ctrl]+d
[[email protected] ~]$ cat catfile
This is a test.
OK now stop     <==只有这两行,不会存在关键字那一行!

看到了吗?利用 << 右侧的控制字符,我们可以终止一次输入, 而不必输入 [crtl]+d 来结束哩!这对程序写作很有帮助喔!好了,那么为何要使用命令输出重导向呢?我们来说一说吧!

  • 屏幕输出的信息很重要,而且我们需要将他存下来的时候;
  • 背景执行中的程序,不希望他干扰屏幕正常的输出结果时;
  • 一些系统的例行命令 (例如写在 /etc/crontab 中的文件) 的执行结果,希望他可以存下来时;
  • 一些执行命令的可能已知错误讯息时,想以“ 2> /dev/null ”将他丢掉时;
  • 错误讯息与正确讯息需要分别输出时。

当然还有很多的功能的,最简单的就是网友们常常问到的:“为何我的 root 都会收到系统 crontab 寄来的错误讯息呢”这个咚咚是常见的错误, 而如果我们已经知道这个错误讯息是可以忽略的时候,嗯!“ 2> errorfile ”这个功能就很重要了吧! 了解了吗?

问:假设我要将 echo "error message" 以 standard error output 的格式来输出,该如何处置?答:既然有 2>&1 来将 2> 转到 1> 去,那么应该也会有 1>&2 吧?没错!就是这个概念!因此你可以这样作:

[[email protected] ~]$ echo "error message" 1>&2
[[email protected] ~]$ echo "error message" 2> /dev/null 1>&2

你会发现第一条有讯息输出到屏幕上,第二条则没有讯息!这表示该讯息已经是通过 2> /dev/null 丢到垃圾桶去了! 可以肯定是错误讯息啰! ^_^

10.5.2 命令执行的判断依据: ; , &&, ||

在某些情况下,很多指令我想要一次输入去执行,而不想要分次执行时,该如何是好?基本上你有两个选择, 一个是通过 第十二章、学习 Shell Scripts 撰写脚本去执行,一种则是通过下面的介绍来一次输入多重指令喔!

  • cmd ; cmd (不考虑指令相关性的连续指令下达)

在某些时候,我们希望可以一次执行多个指令,例如在关机的时候我希望可以先执行两次 sync 同步化写入磁盘后才 shutdown 计算机,那么可以怎么作呢?这样做呀:

[[email protected] ~]# sync; sync; shutdown -h now

在指令与指令中间利用分号 (;) 来隔开,这样一来,分号前的指令执行完后就会立刻接着执行后面的指令了。 这真是方便啊~再来,换个角度来想,万一我想要在某个目录下面创建一个文件,也就是说,如果该目录存在的话, 那我才创建这个文件,如果不存在,那就算了。也就是说这两个指令彼此之间是有相关性的, 前一个指令是否成功的执行与后一个指令是否要执行有关!那就得动用到 && 或 || 啰!

  • $? (指令回传值) 与 &&||

如同上面谈到的,两个指令之间有相依性,而这个相依性主要判断的地方就在于前一个指令执行的结果是否正确。 还记得本章之前我们曾介绍过指令回传值吧!嘿嘿!没错,您真聪明!就是通过这个回传值啦! 再复习一次“若前一个指令执行的结果为正确,在 Linux 下面会回传一个 $? = 0 的值”。 那么我们怎么通过这个回传值来判断后续的指令是否要执行呢?这就得要借由“ && ”及“ || ”的帮忙了! 注意喔,两个 & 之间是没有空格的!那个 | 则是 [Shift]+[] 的按键结果。

指令下达情况 说明
cmd1 && cmd2 1. 若 cmd1 执行完毕且正确执行($?=0),则开始执行 cmd2。 2. 若 cmd1 执行完毕且为错误 ($?≠0),则 cmd2 不执行。
cmd1||cmd2 1. 若 cmd1 执行完毕且正确执行($?=0),则 cmd2 不执行。 2. 若 cmd1 执行完毕且为错误 ($?≠0),则开始执行 cmd2。

上述的 cmd1 及 cmd2 都是指令。好了,回到我们刚刚假想的情况,就是想要: (1)先判断一个目录是否存在; (2)若存在才在该目录下面创建一个文件。由于我们尚未介绍如何判断式 (test) 的使用,在这里我们使用 ls 以及回传值来判断目录是否存在啦! 让我们进行下面这个练习看看:

范例一:使用 ls 查阅目录 /tmp/abc 是否存在,若存在则用 touch 创建 /tmp/abc/hehe
[[email protected] ~]$ ls /tmp/abc && touch /tmp/abc/hehe
ls: cannot access /tmp/abc: No such file or directory
# ls 很干脆的说明找不到该目录,但并没有 touch 的错误,表示 touch 并没有执行
[[email protected] ~]$ mkdir /tmp/abc
[[email protected] ~]$ ls /tmp/abc && touch /tmp/abc/hehe
[[email protected] ~]$ ll /tmp/abc
-rw-rw-r--. 1 dmtsai dmtsai 0 Jul  9 19:16 hehe

看到了吧?如果 /tmp/abc 不存在时,touch 就不会被执行,若 /tmp/abc 存在的话,那么 touch 就会开始执行啰! 很不错用吧!不过,我们还得手动自行创建目录,伤脑筋~能不能自动判断,如果没有该目录就给予创建呢? 参考一下下面的例子先:

范例二:测试 /tmp/abc 是否存在,若不存在则予以创建,若存在就不作任何事情
[[email protected] ~]$ rm -r /tmp/abc                <==先删除此目录以方便测试
[[email protected] ~]$ ls /tmp/abc || mkdir /tmp/abc
ls: cannot access /tmp/abc: No such file or directory  <==真的不存在喔!
[[email protected] ~]$ ll -d /tmp/abc
drwxrwxr-x. 2 dmtsai dmtsai 6 Jul  9 19:17 /tmp/abca   <==结果出现了!有进行 mkdir

如果你一再重复“ ls /tmp/abc || mkdir /tmp/abc ”画面也不会出现重复 mkdir 的错误!这是因为 /tmp/abc 已经存在, 所以后续的 mkdir 就不会进行!这样理解否?好了,让我们再次的讨论一下,如果我想要创建 /tmp/abc/hehe 这个文件, 但我并不知道 /tmp/abc 是否存在,那该如何是好?试看看:

范例三:我不清楚 /tmp/abc 是否存在,但就是要创建 /tmp/abc/hehe 文件
[[email protected] ~]$ ls /tmp/abc || mkdir /tmp/abc && touch /tmp/abc/hehe

上面这个范例三总是会尝试创建 /tmp/abc/hehe 的喔!不论 /tmp/abc 是否存在。那么范例三应该如何解释呢? 由于Linux 下面的指令都是由左往右执行的,所以范例三有几种结果我们来分析一下:

  • (1)若 /tmp/abc 不存在故回传 $?≠0,则 (2)因为 || 遇到非为 0 的 $? 故开始 mkdir /tmp/abc,由于 mkdir /tmp/abc 会成功进行,所以回传 $?=0 (3)因为 &&遇到$?=0 故会执行 touch /tmp/abc/hehe,最终 hehe 就被创建了;
  • (1)若 /tmp/abc 存在故回传 $?=0,则 (2)因为 || 遇到 0 的 $? 不会进行,此时 $?=0 继续向后传,故 (3)因为 && 遇到 $?=0 就开始创建 /tmp/abc/hehe 了!最终 /tmp/abc/hehe 被创建起来。

整个流程图示如下:

指令依序执行的关系示意图图10.5.2、指令依序执行的关系示意图

上面这张图显示的两股数据中,上方的线段为不存在 /tmp/abc 时所进行的指令行为,下方的线段则是存在 /tmp/abc 所在的指令行为。如上所述,下方线段由于存在 /tmp/abc 所以导致 $?=0 ,让中间的 mkdir 就不执行了! 并将 $?=0 继续往后传给后续的 touch 去利用啦!瞭乎?在任何时刻你都可以拿上面这张图作为示意! 让我们来想想下面这个例题吧!

例题:以 ls 测试 /tmp/vbirding 是否存在,若存在则显示 "exist" ,若不存在,则显示 "not exist"!答:这又牵涉到逻辑判断的问题,如果存在就显示某个数据,若不存在就显示其他数据,那我可以这样做:

ls /tmp/vbirding && echo "exist" || echo "not exist"

意思是说,当 ls /tmp/vbirding 执行后,若正确,就执行 echo "exist" ,若有问题,就执行 echo "not exist" !那如果写成如下的状况会出现什么?

ls /tmp/vbirding || echo "not exist" && echo "exist"

这其实是有问题的,为什么呢?由图 10.5.2 的流程介绍我们知道指令是一个一个往后执行, 因此在上面的例子当中,如果 /tmp/vbirding 不存在时,他会进行如下动作:

  1. 若 ls /tmp/vbirding 不存在,因此回传一个非为 0 的数值;
  2. 接下来经过 || 的判断,发现前一个指令回传非为 0 的数值,因此,程序开始执行 echo "not exist" ,而 echo "not exist" 程序肯定可以执行成功,因此会回传一个 0 值给后面的指令;
  3. 经过 && 的判断,咦!是 0 啊!所以就开始执行 echo "exist" 。

所以啊,嘿嘿!第二个例子里面竟然会同时出现 not exist 与 exist 呢!真神奇~

经过这个例题的练习,你应该会了解,由于指令是一个接着一个去执行的,因此,如果真要使用判断, 那么这个 && 与 || 的顺序就不能搞错。一般来说,假设判断式有三个,也就是:

command1 && command2 || command3

而且顺序通常不会变,因为一般来说, command2 与 command3 会放置肯定可以执行成功的指令, 因此,依据上面例题的逻辑分析,您就会晓得为何要如此放置啰~这很有用的啦!而且.....考试也很常考~

下一节:就如同前面所说的, bash 命令执行的时候有输出的数据会出现! 那么如果这群数据必需要经过几道手续之后才能得到我们所想要的格式,应该如何来设置? 这就牵涉到管线命令的问题了 (pipe) ,管线命令使用的是“ | ”这个界定符号! 另外,管线命令与“连续下达命令”是不一样的呦! 这点下面我们会再说明。下面我们先举一个例子来说明一下简单的管线命令。