来点基础的--诡异的极客们的符号--流、管道和文件的耦合

来源:互联网 发布:制作婚礼视频软件 编辑:程序博客网 时间:2024/05/01 01:32

      在linux环境下,shell无疑是一件极其强大的武器。用shell写的代码(如果大家对称脚本为代码没有太大争议的话)具有简单快捷,极其容易整合其他脚本和命令的优点。并且支持基本的输入输出、条件、循环和数组(没错,shell有数组变量,大家自行查找资料),同时也支持函数、传参、嵌套调用,足以完成一个功能模块的逻辑。同时,linux下的小命令行工具,都保持了先贤们的设计思想,就是一个指令支持一个主要的应用功能,但可以通过参数进行不同方式精细的控制。 因此,shell 常常能够用简短到惊人的几行写出强悍的功能,当然强悍的背后,源于shell的设计思想:用文件和流的重定向,将各个功能命令串接起来,形成非常时效的战斗力。曾经有C++程序员说,linux环境下,如果不是特别复杂的应用,或者特别的算法,宁愿通过shell做更多的事。shell当然也有其显著的缺点,例如移植性,可读性,执行效率等等,因而它更多地成为一种“胶水语言”,用以将更多的专有,尤其是posix规范下的专有命令组合起来,而不是从头开始造轮子。

     大部分同学都应该了解,linux下的输入输出主要通过文件、管道、命令参数等方式将各种功能耦合起来。linux,或者说unix的先贤们完全的奉行了赫拉克利特两千年前的哲学:万物皆流: 各种设备都以文件的形式进行虚拟, /dev/ 目录下的各种文件,如果有足够的权限和解析能力,大部分都可以按照文件的方式,打开,读取/写入,关闭的方式进行处理; 文件之间,以及文件和命令操作之间,按照码流的方式传递信息。以下均为bash的举例:



基础篇

1.大家都了解的一个基础设计: 标准输入,标准输出和标准错误输出。这三个和控制台IO,都作为当前shell进程的文件,并且赋予了默认文件的FD(File Descriptor).

文件功能文件FD标准输入/dev/stdin 0标准输出/dev/stdout 1标准错误 /dev/stderr 2
尝试一下:

echo hello world > myFileecho hello world > /dev/stdoutecho hello world > &1
echo hello world
可见,通过 ">" 重定向,可以将命令的标准输出流定向到: a.一个普通文件, b. 标准输出文件,c. 文件描述符。    默认,所有的正确信息都是指向标准输出文件的,所以,有没有 >/dev/stdout 或者>&1,都是输出到标准输出。只是这里可以看到,重定向输出到文件,和标准输出是同一个方式。

既然有输出,自然也有输入的重定向。也就是将文件内容模拟标准输入(命令行的敲入内容),送入命令。

比如:

grep root </etc/passwd


2.管道“|"  通过管道,可以看做将一个命令的输出流,从标准输出指向管道的输入端,管道的另外一端流出,送入接下的另一个命令的标准输入。

例如大家都熟悉的:

cat /etc/passwd |grep root
如果单独运行grep root,那么系统会等待你从控制台(标准输入)输入各种字符串,当出现root字样时会打印出来。现在,cat file将file的输出,代替了敲入grep的输入的效果。

体会一下,它与上面的

grep root </etc/passwd
有何相同和不同。(提示:一个是把文件内容作为命令的输入,一个是把前一个命令,即输出文件内容命令的“输出内容”作为标准输入,好啰嗦对不对?)

当然,实质上,管道是一个:临时的FIFO(first in first out) 文件,也就是系统用FIFO文件做的一个临时buffer,写入前一个命令的输出,buffer存在数据则送给下一个命令的输入。可以试试:

mkfifo mypipegrep root <mypipe
这个时候,终端不动了??挂起了? 没错,因为管道文件mypipe中没有任何字符流,所以grep 由于什么输入都没有读入,就挂起了。

此时打开新的终端,输入以下命令,同时刚才那个别关掉等着看效果:

cat /etc/passwd>mypipe
好了,那个挂起的grep窗口输出,是不是和直接运行 
cat /etc/passwd |grep root
一样的效果呢?没错,都是从passwd文件中,抓取root信息的一行。 一个是通过自生自灭的临时管道|,一个是通过mkfifo命名的mypipe管道文件。好吧, 运行一下:
ll mypile
可以看到文件名后面也有一个“|”,它告诉我们,mypipe就是一个有名字的管道,对吧?

由此可见,本来的三行(创建管道,">"符号的命令输入到管道,以及“<”符号的命令从管道文件读取,三句话连在一起,一个临时管道 

cat /etc/passwd |grep root
就搞定了,简洁明了的把输入和输出串在一起。

好了,基础到此为止,没有瞌睡的继续。


提高篇


    有了重定向和管道,自然而然地,如上所述大家会不由的用最简便的方式将文件和命令行、命令输出和其他命令输入连在一起,达到“数据流过各个命令,”连续的处理得到结果的简单结构。但是,还有些新鲜的问题,例如,对于一个命令行,可能接受和依赖的输入是:

  • 标准输入
  • 命令有参数,参数代表指定文件名
  • 命令有参数,参数代表字符串

....是的,根据命令的设计(为何如此,要问作者或者规范) 往往是接受以上列举的某一种或者一两种,而恰好不巧,我们的数据是另外一种。

比如一个拧巴的例子,我们经常用sed 命令来编辑一行或者多行字符串,它能够提供基于正则表达式的查找和替换。看一下它的命令定义,嗯,复杂起来是这样的:

sed [OPTION]... {script-only-if-no-other-script} [input-file]...
通过帮助和测试,我们发现,sed 工作的时候,可以用 sed -e "s/source/dest/" 的方式,把之后指定文件名的内容中找到"source"进行替换成"dest"后输出,如果没有指定文件名,则会从标准输入流里的内容进行替换。但是,参数要求文件,或者标准输入,我们手头不需要把信息存入文件,可能仅仅放在一个变量中,那该如何处理呢?

嗯,上面列出的命令行,既然命令程序的作者规定了(从帮助中查看到)需要输入的方式,那我们为了利用,自然要把手头的数据扭转成命令需要的形式再送给它。 前面说了这么多,其实很多不熟悉shell和重定向的同学经常在此发蒙。


需要一些新鲜的符号来解决:


a. 命令行接受标准输入:

如果只是想将简单的直接字符串、变量作为标准输入,而不用新建一个文件的话,例如上面提到的sed命令,我们现在手头有一个字符串:

myStr='the source path is abc'
希望输出 'the dest path is abc' 那么如何做呢?

这个问题比较简单,既然有字符串存在,把他变成sed的标准输入啊!?那么

echo $myStr | sed -e "s/source/dest/"

这样把字符串打印到标准输出,然后通过管道送入sed的标准输入,完美了。没错,在此处用管道和标准输出最简单。但是管道有一个隐患:变量作用域。

来看一下这个例子,read  myVar 命令可以从标准输入内读入一行内容,或者分隔符隔开的多个字符串到变量中。那么预测一下,下面这个命令输出什么?

echo "test"|read myvarecho $myvar
很可惜,myvar中,居然没有"test",而是一个空的换行。为什么呢? 单独read myvar 试试?命令不是会等待标准输入吗?在控制台窗口标准输入中输入的信息之后,不是会赋值到myvar吗?  据此,有的同学提出read是不支持其他流的输入,只支持标准控制台输入。这个说法是不太准确的。 

因为shell 在处理管道时,必须两个命令协作,因此,上面的echo 和read两个命令,分别(管道左侧的程序)在当前shell进程和(管道右侧的程序)在当前shell的子进程中执行。  子进程中的变量赋值和改变,父进程中是没有影响和变化的。所以,myvar在子进程中有了“test”值,但是当前(父进程)中没有改变。read放在管道右侧,想通过管道读入一行现有文本,就失效了。

正确的处理是:

read myvar <<< "test"echo $myvar

没错,这里用了三个"<",表示shell 的“here document”,即,三个“<”右侧的字符串,将作为标准输入流的重定向。也就是说,如同read myvar后,从键盘送入"test"字符。(注意和一个“<”,即将文件内容作为输入流的区别!前一个送入的输入是“test”,后一个送入的,是名为test文件的内容)


b. 命令行接受字符参数

如果命令行不是通过读取文件或者标准输入,而是通过后续的参数字符串传递信息,而我们手上只有参数的字符串,或者记录着参数的文件,该如何处理呢?

举例来说,seq命令非常简单,生成起始到终止的一组数列,语法如下:

seq [OPTION]... FIRST LAST
可见,必须把起始和终止的数字,放入参数中。例如

@ubuntu:~$ seq 1 4#输出:1234
但是不巧,我们有一个之前生成的或是他人提供的文件,比如:

echo "1 4">myRange
cat myRange1 4
文件的内容是取值范围 1  4,中间用空格分割。现在要根据文件中的范围,seq生成1 2 3 4序列。文件作为流输出是不行的,因为seq只接受命令行参数。怎样才能把文件内容中的 "1  4"搬到seq 的后面作为参数呢?

此时,可以借助立即命令$()

seq $(cat myRange)#或者`反引号,在键盘~号下面seq `cat myRange`  
上面表示,立即执行myRange的输出命令,然后将输出内容作为展开的字符参数,追加在seq命令之后。

另一个好玩的命令是xargs, 作用也非常清楚,就是将任何输入流中的字符,作为后续命令的参数。 听起来有点绕,但是用起来很简单:

cat myRange | xrags seq
管道之前产生了字符串"1 4"作为xrags的标准输入流,xrags 立即将其与之后的命令拼成seq 1 4. 呵呵

当然,如果字符串不在文件里,而是在变量里,例如 myVar="1 4", 只要把上面的输出"1 4"的命令cat myRange换成echo myVar也是可行的。但是,如果是变量,这样做就多余了,因为直接用seq $myVar , seq会把$myvar展开作为2个参数的,而非将“1 4”作为一个整体(延伸:参考shell的展开规范,$*和$@的主要区别)


c.  命令只能将文件名作为参数,我们手上有文件的内容,但是不想把中间数据内容存为永久文件

这里会用比较好玩的一个功能,但大家一般了解和使用相对较少。举例来说,我们这次用

awk -F: "{print $1}" /etc/passwd

获得一个当前系统用户列表,然后希望用安全文件拷贝命令scp,传到其传到另一台机器存入文件.

正常的做法,我们可以将以上awk生成的用户列表存入一个本地文件srcFile (当然是用重定向写进去,对吧),然后scp命令发送这个文件。但是,scp只接受文件名,而不是文件内容字符串参数或者输入流,那么,一定需要一个用户列表临时文件吗?

不是的。如果想一口气通过一个命令完成,而不是先创建用户列表临时文件,发送然后删除的话(如果做某些安全相关的操作,不想因错误中断留下文件,或者不想操作创建文件后删除的话),可以用shell 的process substutition功能,即:

在需要一个文件名作为参数的地方,不传入文件名,而是采用"<(命令行)" 的方式替换文件名,系统(居然会其妙地!)以括号内命令行输出替代“此处该出现文件名”的文件的内容。

好,本来,scp 安全copy文件的命令如下:(请合理替换文件中passwd, loginname@127.0.0.1 为合适的登录密码,用户名和主机名)

<span style="font-family: Arial, Helvetica, sans-serif;"></span><pre name="code" class="plain"> sshpass -p "passwd" scp srcFile loginname@127.0.0.1:/~/destFile

在这里,srcFile必须是当前目录存在的一个文件的名字(而不是内容或者是流输入,对吧) 而我们却不想把awk命令产生一个用户列表文件,再把文件名填在这里,而是可以直接把awk产生的输出流作为文件内容,让scp发出去,对吧! 好的,那么就这样:

 sshpass -p "passwd" scp <(awk -F: '{print $1}' ) loginname@127.0.0.1:/~/destFile

体会一下,和先将awk命令输出的列表存为srcFile 再发出,后面这一句直接将awk 输出用户列表的命令用<() 包起来,放在文件名称的位置:

<(awk -F: '{print $1}' )
替换了文件名srcFile。"过程替换"就是此意:在需要文件名称的地方(scp 中的srcFile),用一个过程产生的输出(awk语句的输出用户列表)作为文件内容进行,即将过程命令包在<() 中,从而省略了存盘文件名。

注意的是,"<" 和()的左侧括号"("之间,必须没有空格。否则,嗯,好像<是重定向输入嘛... 所以加了空格就成了输入重定向到文件了。


基本上,通过上面的三种模式,所有的命令都可以组织起来,顺序对数据加工,直到产生结果。不得不一提的是,linux自带的文件表处理、抓取和替换、排序和格式输出等命令,如能正确合理结合文件、设备的输入输出流,将产生令人惊喜的简短而有效的处理过程。如果熟悉的话,一定要抽空看一下下列命令,也许会花费几天到一周,但是必将带来相当的收益,也许会深深感触“万物皆流”这一哲学吐舌头  

  • awk
  • sed
  • tr
  • sort
  • bc
  • grep
如果有兴趣,可以参考一下 https://github.com/xiongbinsh/httpsh   原来从一个url列表中获取网页反馈的连接状态信息,其实bash世界中,只是几个简单命令。



0 0