- 论坛徽章:
- 11
|
所以,我们要引入一个新的范式:
STDIN-PROG-STDOUT(-STDERR)
简写作:
I-P-O(-E)
这个范式跟之前(关于用户与系统之间关系)的V(O,A)范式不同,它是用来概念化为PROGrams(程序)与Shell为其所提供的标准I/O环境及其要素的。在每个要素之间,川行的是数据流,数据流透过Stdio句柄被接纳或释出。
现在,我们考虑一个问题:既然程序可以由Shell为其创建的stdin(源头通常是键盘)来获取输入信息(流),那么,是否可以选择其他的信息输入源头呢?比如bar程序不再从用户的键盘获取输入信息,而是以foo的输出作为自己的输入源头,即:foo程序的stdout流与bar程序的stdin流“对接”(信息如水流自上而下地行进)?这样的“对接”完全是可以实现的,那就是利用Shell提供的“管道”机制!命令的形式如下:我们回到一开始的例子:ls命令程序的输出信息是当前一级目录里的所有文件,这样的信息应该是:-
- bar1
- bar2
- bar3
- ……
- bar9998
- bar9999
- foo1
- foo2
- foo3
- ……
- foo9998
- foo9999
复制代码 一共20,000行。※寻常状况下,我们所看到的ls命令(无参数)的反馈,各个文件名都是按照表格式样排列呈现的,但当这些信息成为stdio流的时候,其实是按照“行”呈现的,例如我们使用命令:就可以看到文件名以“行”排列呈现。less命令是一个用来查看文本类型文件的内容的程序,使用时按[PageUp]与[PageDown]键可以实现翻页,按[↑]与[↓]键可以实现滚动,按[q]键盘退出程序。
这20,000行的信息并没有被反馈到终端屏幕(即/dev/ttyvx文件)上,而是经由管道哺馈给(流向)了管道符后面的grep命令程序,即这10,000行信息成为了grep命令的stdin的内容。
grep命令程序,全称是“Global Regular Expression Print”,这个全称揭示了这个工具的特性:“Global”指它的工作对象是“全局”也就是全部的行,“Regular Expression”就是“正则表达式”,“Print”就是“打印”,统合起来说,就是:在stdin的全部行中,找到跟给定正则表达式匹配的内容,并将该内容所在的行打印出来(该内容本身通常会被高亮凸显)。
说到“正则表达式”(简称RegExp),它又是一个极具UNIX STYLE的超级武器。RegExp的内容极为广泛与庞杂,若要精通它,恐怕要跟精通C语言的难度相当。许多讲述RegExp的教材的作者自己就对它一知半解、不得真要,所以写出来的教材也让学生看得云里雾里、稀里糊涂,更是完全无法发挥这套武器的强大威力。本人根据自己多年在UNIX¬-like系统上的琢磨和捣鼓,或许可以用最简洁、最切中要害的方法,向你们解明RegExp的基本精义。
※在当前的FreeBSD系统中,虽然/usr/bin/grep与/usr/bin/egrep两个程序完全一致,但要用到egrep(扩充语法的grep),还是必须显式地使用egrep命令,或用“grep -E”。下面的内容,是针对egrep的(更简单、更强大)。
—— 只要八个汉字:
转义、数量、位置、逻辑
(0)正则表达式的目的,在于用它自己寻找到目标文本中符合某种特征的字符串,这种“寻找”的过程,也叫“扫描”,一旦寻找到,则就叫“匹配”。这跟之前介绍的“通配符”很相似,但正则表达式机制远比后者更复杂、更强大。正则表达式犹如一个模板一样,去圈套一切符合它样式的内容,所以,它也叫Pattern(模式、模板)。
(1)转义 —— 用元字符指代一类具有特性的字符的机制。一般通过中括号、小数点、反斜杠这些特殊符号(就是所谓的Meta Character,元字符)的序列来实现。
中括号 —— 类似于之前介绍的通配符中的。比如:模板“[a-z]”可以匹配26个小写英文字母中的任何单一字符;模板“[^A-D]”可以匹配大写英文字母A-D之外的任何单一字符。
小数点 —— 除了“换行”(\n)之外的任何单一字符。比如:模板“^.{5}$”可以匹配任何正好有5个字符的行。
反斜杠序 —— 这是典型的转义序列:序列“\w”等效于“[a-zA-Z0-9_]”;序列“\W”则等效于“[^a-zA-Z0-9_]”。
(2)数量 —— 用来指定被匹配的字符(串)的数量。一般通过花括号、星号、加号和问号这些元字符来实现:
花括号—— 描述字符出现多少次将会被匹配。{m,n}表示m次或n次或两者之间的任意次数;{m,}表示大于等于m的次数;{,n}表示小于等于n的次数;{m}表示有且仅有m次。比如:模板“s{2,3}”可以匹配字符串“ss”、“sss”。
星号 —— 意同{0,},即0次或任意正次数。比如:模板“As*”可以匹配字符串“A”、“As”、“Ass”。
加号 —— 意同{1,},即1次或其他任意正次数。比如:模板“As+” 可以匹配字符串“As”、“Ass”但不能匹配“A”。
问号 —— 要么有1次,要么有0次。比如:模板“as?\>” 可以匹配文本“a man as an ass”中的字符串“a”、“as”,但不能匹配“ass”或“an”。
※ 所有表示数量的元字符,作用且仅作用在紧挨在它左边的第一个(或第一个分组)字符的身上,比如:“foo.*”里的“*”仅仅作用在“.”身上,而不是“foo.”身上。
(3)位置 —— 用来匹配到指定位置的字符串。一般通过以下两对元字符来实现:
补脱符(^,Caret,[Shift]+[6]) —— 表示BOL(Begin of Line),比如:模板“^Hello”只能匹配位于行首的字符串“Hello”,在非行首出现的字符串“Hello”将不能被匹配。美元符 —— 与补脱符相反,表示EOL(End of Line)。
\<(反斜杠加一个小于号)——描述了这么一种位置:若有一个字符串处于它左边的空白或标点之右,该字符串将被“\<字符串”这样的模板匹配。比如:模板“\<BSD”将匹配“I love BSD”中的字符串“BSD”,但 “I love FreeBSD”中的字符串“BSD”却不能被匹配。而“\>”的意义正相反,比如:模板“Windows\>” 将匹配“Windows 98”中的字符串“Windows”,但“Windows98”中的字符串“Windows”却不能被匹配。
其实,关于“位置”的模板表达,有一种非常易于识记的方法:将一行的行首、行尾这些“看不见字符”的东东,以及用于分割单词的某种空间,想象为可以某种“摸得着”的特殊字符,那么,这些特殊字符就是“^”、“$”、“\<”、“\>”,把那些原本难以表达的概念,转化为实体字符,然后将这种字符,安插在它们的对应位置,就没错了!
(4)逻辑。主要内容只有两个:
小括号 —— 用来分组,即将被它括起来的字符串视为一个整体。
管道符 —— 两项被管道符(|)黏结的字符串组成的模板,表示要么用左边一项字符串来匹配,要么用右边一项字符串来匹配。
比如:可以用模板“^[0-9]{6}(1970|1972)[0-9]{8}$”匹配文本中所有其出生年份信息是1970年或1972年的身份证号码行,当然可以可以模板“^[0-9]{6}197[02][0-9]{8}$”。
在grep命令行中,如果模板中的某些字符(比如小于号、大于号等)会引起Shell出现我们所不期望的解读(发生了歧义),我们就应该用双引号将Pattern括起来。然而,在csh中,用双引号括起某些特殊符号(比如美元符)反而又会引起Shell的解读问题,倒是用单引号却不会有问题。所以,当我们怀疑Shell误解了我们向grep程序提供的Pattern的时候,我们应当用其他的方案反复测试与验证,以确认是否发生了“歧义”。
用起csh来,是不是让人觉得有点闹心?或许你的感觉正是如此。那么,我向你强烈推荐另一个Shell —— Z Shell,简称zsh,它是由大黑客Paul Falstad在学生时代(于普林斯顿大学)创建的。※ zsh的许可协议属于MIT-like(类麻省理工学院的)。
我们面前的这个FreeBSD上,有zsh吗?我们使用命令:得到的反馈是:- zsh: /usr/ports/shells/zsh
复制代码 ※ 一个具有专门搜索文件的命令是“locate”,该程序将搜索数据库中符合关键词的文件,不过这个数据库是需要适时更新的,在FreeBSD上,这个更新的命令是“/usr/libexec/locate.updatedb”。
再对比一下命令:其反馈结果则是:- tcsh: /bin/tcsh /usr/share/man/man1/tcsh.1.gz
复制代码 这至少可以说明,tcsh已经是一个被安装在系统上的可执行程序,但zsh不是,关于zsh的位置只有“/usr/ports/shells/zsh”这么一项信息。
之前介绍过,“/usr/ports/”目录是Ports Collection的位置。Ports Collection是什么?Ports又是什么?
我们必须了解,在为UNIX-like系统上安装软件的某种“经典”过程:
(1)下载软件的源代码,通常是一个扩展名为.tar.gz的文件(原始文件被tar程序打包再由gzip程序压缩得到),解开它的命令通常是:(※ 这里的参数z代表关于gZip的压缩或解压缩,x代表解包,v代表滔滔不绝地说话意即信息馈出,f代表后面隔着空格紧跟的是解压缩的对象或压缩的目的文件。各参数前的连字符可以省略,且可以黏合在一块儿。)
通常会得到一个被释放出来的目录。进入目录,可以看到一堆文件。
接下来是经典三部曲:—— 根据当前的架构特征,配置和生成Makefile文件。—— 根据当前目录中的Makefile,开始将源代码编译成可执行程序。—— 将程序安装到系统文件层次结构中的特定位置。
※ 卫生习惯好的用户,还会:—— 清除上述过程中的中间件,为今后可能再次进行的编译工作提供一个干净的环境。
看到这里,你可能会想,如果这几样步骤(包括下载源代码、解压缩、配置编译、编译、安装等等)能够一次性自动运行,那该多好呀!
FreeBSD的Ports机制,就是为了满足这样的要求而生的。一个特定软件的port就是可以自动实现上述步骤的文件的集合。而Ports Collection就是这样的集合的集合。我们进入“/usr/ports/”目录,可以看到这种集合内容的排布,这些内容分门别类、层次分明、井然有序,比如“/usr/ports/shells/zsh”就告诉我们,zsh软件的port正位于“shells”这个分类当中。
现在,我们进入“/usr/ports/shells/zsh”目录,使用命令:(※ Port的Makefile文件提供了足够的自动化机能,使得这行命令可以将make、make install、 make clean这些工作“一站式”完成。)
接着,一个蓝底安装界面出现,我们尽可以选上全部项目,然后按<Ok>。
经过“漫长”的下载、配置、编译、安装(其间还会有分支安装的交互式界面,所以最好别走开)之后,整个安装程序以这么一行结束:- ===> Cleaning for zsh-4.3.14
复制代码 这标志着zsh程序已经完全安装完毕。
此时,我们需要使用命令:来刷新Shell缓存(这并非是启用新的Shell。在任何安装过程结束后,都必须立即执行这个命令),令我们可以执行新近被安装好的程序。
此后,我们再使用命令:就会发现zsh已经安装在特定的目录中了(/usr/local/bin/zsh),而且还多了一项:- /usr/local/man/man1/zsh.1.gz
复制代码 这说明,我们可以使用命令:来查看zsh的man文档(Manual,手册,使用说明书)。这里的“1”代表,我们将这里的“zsh”视为“命令程序”。“1”是“程序命令”在man文档里的章编号。这种章编号可以反映出一个专有术语的属性:
1 命令程序。
2 系统调用。
3 C库函数。
4 设备的驱动程序。
5 文件格式。
6 游戏或娱乐项目。
7 杂项资讯。
8 系统维护。
9 内核开发情况。
比如,我们可以分别用:和来查看命令程序printf与C库函数printf(两者同名却是不同的东东)的手册文档。
查看man文档的方法与使用less程序查看一般文档的方法是类似的。
现在,我们要将当前用户的Shell(即tsch)更新为zsh。我们使用Change Shell命令:此时,一个编辑器打开了,我们需要修改其中内容的第10行。
其实,chsh命令只是一个将某个编辑器打开某个文档的操作“打包”起来。而这时chsh用到的编辑器正是vi!
哇,终于讲到本节课的正题了!好,我们快快使用vi来更新文件内容吧!
鼠标指针当然是没有的,所以,我们只能用方向键[↓]将光标移动到第10行,再用方向键[→]将光标移动到“/bin/csh”中的字符“b”位置(因为第一个字符“/”我们并不需要修改),然后用[Delete]键将“bin/csh”这些字符逐个消灭掉(只能用[Delete]键,若使用[Backspace]键则无效,在有些虚拟终端中,[Backspace]键会被解释为“^?”,这会让vi报错。) ……当我们消灭掉最后一个字符“h”时,光标向后退了一个字符,真是觉得有点怪怪的 ……让用惯了GUI软件的屌丝们更加感到意外的事情发生了 —— 当我们准备输入zsh程序的具体位置(应该是准备输入“usr/local/bin/zsh”)的时候,按下路径的第一个字符“u”时,居然在光标的位置上出现了一个莫名其妙的字符“h”!……好吧,你可以就此打住了,因为你完全不了解vi应该如何使用!
vi的使用,是Microsoft Windows或DOS用户转学UNIX-like系统的重大难点之一,更是让用惯了GUI软件的屌丝们极感手足无措的一件事情。没有一本UNIX-like系统基础教材会回避教授关于vi或Vim的使用方法,因为它们是UNIX-like系统必备的标准编辑器。但是,正如在讲授正则表达式这个难点时表现得非常无力一样,许多教材对于vi或Vim的讲授,也是格外地给学生添堵,这恐怕也是因为那些教材的编写者自己根本就不熟悉vi或Vim,更不要说以深刻的见解为学生解惑了。
对vi或Vim的熟悉程度,基本上与用户使用UNIX-like的深刻程度成正比。现在我教授大家如何使用vi,其中的思路,是在我多年来所累计的技术经验的一种反映,比如说:许多教材(尤其是国内的)都宣称vi或Vim有三种模式(命令模式、末行模式、插入模式),但我的技术经验给予我的强大观念是:vi或Vim只有两种模式,所谓“末行模式”其实也是命令模式。下面的解说,将完全按照这样的思路进行。
当面前的这个vi程序使我们感到混乱的时候,请不要试图继续录入或编辑操作,而是应该立即按下键盘最左上角的[Esc]键(该键的意思是“Escape”,即逃生、逃逸 —— 一旦你在vi中迷失方向,就应该使用这样的逃生方法,它是最最重要的安全通道),多按它几下也无妨。在任何状况下,按下[Esc]都将把vi拉回到“命令模式” 注8 。
脚注
(8)基本上只有这么一种例外:在vi中按下[Esc]键会执行“:”开头的命令,所以在已经输入“:q”这类命令(但还没有按[Enter]键)的情况下,按下[Esc]键会让vi退出。
将vi拉回到命令模式之后,输入命令:※ 上面的“√”表示须按下[Enter]键,这种提示对于学习vi或Vim非常重要,所以就不再忽略。
结果是我们退出了vi程序。“q”的意思是“Quit”。在vi的命令中,感叹号“!”的意思是忽略一切诸如“内容已经改动但还未保存”、“文件将被覆盖”之类的警告,直到你的意图被执着地完成。使用命令“:q!”,使得在上一次文件被更改之后的任何对文件Buffer(文件内容在内存中的一个副本,即你在vi程序所直观的文本内容)的更改都不会写入到文件中,且vi程序直接被退出 —— 利用这个命令,你尽可以找来各种又大又复杂的文件来演练vi的使用。
我们退出vi之后,重新使用命令:进入vi。面对这个文件,我们最好能让vi显示行号。我们现在已经命令模式下了,那么,输入命令:此时,我们可以比较直观地看到:“Shell: /bin/csh”在第10行。那么,我们要将光标移动到第10行,我们使用命令(如果你码不准是否处于命令模式,请多按[Esc]键,此时你会听到“Beep-Beep”或“Ding-Ding”声响。):这次没有“√”,表示你不需要按下[Enter]。凡是不以“:”打头的命令,都不需要再命令结尾键入[Enter],除此之外,这时你甚至都看不到你到底输入了什么!确实,除了“:”打头的命令之外,vi对于你输入的其他一切命令,都不会显示 —— 这的确是屌丝们难以驾驭vi的原因之一。
“10G”这个命令代表了“Goto the line #10”的意思,你或许会想,为什么不是“G10”呢?再仔细想想,如果按照这样的思路:当你输入了“G10”之后,只有你按下[Enter]键vi才知道你的意图是“G10”,如果没有按下[Enter]键而是继续输入一个“0”,那么再按下[Enter]键vi才知道你的意图是“G100”,也就是说,再你按下[Enter]键之前,vi无法确定你的意图是什么。但vi的设计哲学是:最大限度地节省你的键入量,包括削减一切可以削减的[Enter]键入,这样,就必须要求有一个键入的字符触发了vi立即完整地领会你的意图 —— 命令中字符“G”的键入就是如此,它一定是命令的结尾(在字符G之后不应该有任何参数附赘)。所以,“Goto(转到……行)”的命令格式必然是:
nG
n不能等于0,因为没有第0行(vi从1开始为行计数),事实上,“0”是一个命令,这意味着当你键入“0”的时候,命令的输入就终结了,所以,也不可能有“0G”这样的命令。命令“0”是将光标拉回到当前文本行首个字符的位置。(※ 命令“^”则是将光标拉回到当前文本行首个非空格字符的位置;命令“$”是将光标拉回到当前文本行的最末一个字符的位置。)一个无参数的命令“G”,会将光标拉到文件的末行。
现在,光标被拉到了第10行的起头,现在我们要按键将光标逼近“bin/csh”位置,此时,你可以用方向键,但我强烈建议你使用命令:命令“l”将光标右移一个字符,“h”则是左移一个字符,“k”则是上移一行(是文本的行,即逻辑行,而非屏幕直观的一行),“j” 则是下移一行 —— 键盘上的连续排列小写字母hjkl分别对应了方向←↓↑→(容易记错为“←↑↓→”,因为我们可能会习惯于有next意味的↓和→紧挨在一起);命令“L”与“H”分别会将光标拉到当前页面的顶部或底部。
当我们将光标移动到“bin/csh”的第一个字符时,我们的确可以使用[Delete]键来删除当前字符,但我建议你不要使用这类非字符按键,而是使用字符按键所输入的命令,删除光标当前字符的命令是“x”。如果你持续使用命令“x”的话,你会发现,它最终将消灭掉当前行上的全部字符,但一个空行将被保留,不会被消灭。如果你希望彻底消灭这一行,你应该使用命令“dd”。
不过,我仍然不建议你在这里使用命令“x”,因为一旦你不留神地多按下几次,那些你不想更改的文本内容也可能被破坏掉,而且重复地输入同一个命令,实在不符合UNIX的Style。我们必须认定一种更为高效的思路:将“bin/csh”中的字符一次Cut掉(没错,是剪切而不是删除,因为剪切可以保存被剪切的内容以待将来粘贴,而如果你以后不想粘贴的话,就当是删除它们了,这很符合UNIX把雷同的事情合并在一种方法里的设计哲学)。Cut的命令是:这里的“目的地”是这么一个意思:想Cut掉光从标当前位置到某个位置的全部字符,那么“某个位置”就是这个命令中的“目的地”。那么,这个“目的地”该如何表达呢?难道用行列坐标吗?不是。要表达这个“目的地”,应该用“从当前光标如何到达目的地”的“命令”来表达,比如说:
我希望Cut掉“bin/csh”的全部字符,也就是说,从字符u(当前光标所在字符)到当前的行尾,全部要Cut掉,那么,“光标移动到当前的行尾”的命令是“$”,这样,这个Cut命令就应该是:当命令被输入后,“bin/csh”并没有如我们预期的那样消失(跟GUI软件的那种Cut不一样),而是行尾的字符“h”变成了“$”—— 这说明,Cut命令生效了:最关键的一点是:Cut命令生效后,vi从“命令模式”转变到了“插入模式”。所谓“插入模式”,就是:
光标所处在的位置,是等待你输入(插入、填入)字符的位置,当你键入一个字符后,光标从这个位置上跳到下一个字符位置,即继续等待你在该位置上填入字符,……
这跟GUI软件里的编辑文本模式,是一致的,只不过在GUI软件里,光标通常是一个闪烁的细竖线,而在vi这样的TUI软件里,光标则是一个不会闪的实心方块儿。但不论是细竖线还是方块儿,它们总是出现在你已经输入的字符的右边。
如果想从让vi从命令模式直接进入插入模式,可以键入命令“i”(表示Insert,插入),这样,光标所在位置的字符以及后面的字符,将会被“插入”的(键入的新的)字符往后“驱逐”;也可以键入命令“a”(表示Append,追加),这样,光标会先 “跳到”后面一个字符上,然后再让你“插入”字符 —— 这等同于:在光标原来(跳动之前)所在的字符之后“追加”新的内容,故有此名。※ 在插入模式下,有时候(比如使用某些虚拟终端)不能使用方向键来操纵光标的移动。
有Cut就应该有Copy(复制)和Paste(粘贴),复制的命令是“y”(Yank,拉扯);粘贴的命令是“p”(粘贴到当前光标所在位置之后)或“P” (粘贴到当前光标所在位置之前)。如同命令“dd”是删除当前整行,命令“yy”是复制当前整行。
现在,在插入模式下,你可以直接逐字键入“usr/local/bin/zsh”了。由于这个字符串比原来“bin/cs$”的要长,所以我们可以发现原本的字符串被逐字覆盖的过程,但如果新插入的字符串的长度不足以覆盖原来的内容,那么,就会有“……$”样的字符串总是残留在那里,为了让这些残留的“影子”消失,我们可以按下[Esc]键,那么Buffer将被更新,同时,vi又回到了命令模式。
之前,我们已经学习了RegExp,如果能把RegExp的技巧运用在vi中,那就更加符合vi的设计哲学了(不做呆板而重复的工作、找到直接切入问题核心的途径)!
我们可以这样:
使用命令:※ 命令“/”是运用RegExp,从当前光标位置向下寻找到第一个匹配的字符串。(若是向上(往回),则用命令“?”。)后续键入“n”或“N”则可以寻找“nEXT(下一个)”或“上一个”。
于将光标拉到“Shell”所在行的首段。
然后,再用命令:- :s/bin.*$/usr\/local\/bin\/zsh√
复制代码 这个命令的格式是:- :区域s/模板/被匹配的字符串将被更新为什么/参数√
复制代码 这里的“区域”是指查找的范围,用“m,n”表示从第m行到第n行,用“%”整个Buffer;这里的“参数”有:“g”表示全行每处匹配处均进行替换(否则仅在当前行第一处匹配处进行替换);“c”表示替换时需要Confirm(确认)。
若要在命令里的正斜杠之间表达正斜杠,必须使用转义序列“\/”。
至此,文件的Buffer被更新好了,我们需要将Buffer写入文件,我们可以使用命令:或以及或来实现写入以及写入并退出vi(也退出了chsh程序)。
※ 必须确认上述操作完全无误,否则破坏了用户配置文件,很可能会导致无法登录Shell,那可就麻烦了!
※ 现在,我们可以领略到,vi有这么一个优点:用户的手指不需要离开键盘(甚至仅仅是主键盘,而不要方向键),就可以完成一切操作。
现在我们通过chsh命令(藉着vi)改变了当前用户的Shell设置,这当然需要系统再一次进行某种初始化后才能生效。于是,我们登出当前用户,使用命令:再登录。
现在,我们使用命令:反馈的结果是新的Shell(即zsh)的位置,这说明我们Change Shell的操作成功了。 |
|