黑客第二课:脱屌第一步(主要讲unix-like系统的初步知识)

来源:互联网 发布:阿里云网站空间购买 编辑:程序博客网 时间:2024/05/09 08:17

http://netsecurity.51cto.com/art/201211/365224_12.htm


在众多环境变量当中,还有一个环境变量,对Shell的工作方式具有直接的、举足轻重的影响作用,那就是名为“PATH”的变量。这个变量的名字的含义是“Search Path(搜索路径)”。这个变量的实际的值,是由一个以“:(Colon,冒号)”黏结的若干个目录名所组成的序列。UNIX-like中的PATH变量的含义,与Microsoft Windows或DOS中的PATH环境变量的,基本上是一致的(但后者的PATH变量值是由“;(Semicolon,分号)”黏结起若干个路径)。

我们已经知道,Shell承担着解释用户在CLI方式下所输入的命令的重要任务。当用户输入一个概念V的表达(即命令名)之后,Shell如何去找到系统中关于概念V的实现(即用来实现命令的程序)的呢?

Shell的做法,通常分为两个步骤:

(1)判断用户的输入,是否为Shell的内置命令(即这样的命令是Shell的一部分,这类似于Microsoft DOS中的COMMAND.COM所提供的“内部命令”)。若判断为是,则执行;若判断为否,则:

(2)在PATH环境变量所提供的路径(目录名)中,逐个路径地去寻找符合输入的命令。若找到,则执行;若找不到,则反馈“Command not found.”信息,即报错。

当然,如果用户显式地、完整地向Shell输入了概念V的实现(即可执行程序)所处的位置(即完整的路径/目录名称),那么Shell将直接执行那个可执行程序。

跟Microsoft DOS不同的是,UNIX-like系统不会认为用户的当前工作目录是PATH变量“理所应当的”一部分,也就是说,它不会在当前工作目录(与PATH所提供的目录不同)中寻找概念V的实现。

如果要执行当前工作目录中的可执行文件,应该使用这样的命令形式:

代码:

./foo

即:在可执行文件名的前面,顶上“./”(一个小数点加一个正斜杠)—— 这有强调当前工作目录的意味,这是出于对系统安全考虑的强制规定。

通过学习上面的知识,我们可以理解这麽一个技巧:

我们可以特地地将一些专门的路径(某些经常要用到的程序所处的位置),放置到PATH环境变量中,这样我们就不必每次输入完整的路径或切换工作目录了。

更新一个环境变量的实际的值,在csh/tcsh这样的Shell中,应当使用命令:

代码:

setenv VARNAME value

而在sh/bash这样的Shell中,应当使用命令:

代码:

export VARNAME=”value”

请注意上述二者在命令名、等号、双引号等方面的区别。

举个例子。如果我们希望我们的家目录成为PATH环境变量的一部分,那么,在csh/tcsh下,就可以使用这样的命令:

代码:

setenv PATH $PATH\:$HOME

请注意,在这个命令中,PATH环境变量中用来黏合路径用的“:”被写成了“\:”,这是因为,如果csh看到了一个单独的“:”紧跟在字符串后面,会将它解读为一个Built-In Command(内置命令)从而导致反馈“Bad: modifier in $ ($)”即第二个“$”解读错误,所以,为了让csh能在概念A的表达中正确地解读到一个“:”,我们就必须在“:”前面顶上一个“\” —— 这称为“转义序列”机制,学过C语言的学员,对此一定不会感到陌生。又如,我们希望系统反馈这麽一行字符:

代码:

$SHELL

那么我们可以使用命令:

代码:

echo \$SHELL

除了利用转义序列规避csh对概念A的表示解读失败,我们还可以使用命令:

代码:

setenv PATH ${PATH}:$HOME

实现相同的效果 ——  可见,一对花括号可以拦阻csh将单独的“:”与之前的字符串相黏合,从而规避将其解读为一个命令。


我们使用命令:

代码:

w

可以观察到当前整个系统上,有哪几位用户登录,他们分别从那个终端接入的。比如,反馈是这样的:

代码:

USER    TTY    FROM    LOGIN@    IDLE    WHAT

root        v0    -    3:31PM        2    -csh (csh)

USER一栏给出了用户名;TTY一栏给出了该用户所接驳的终端名称;FROM一栏给出了该用户的登录地点(一般远程登录者以网络地址如IP地址形式给出);LOGIN@一栏给出了该用户的(最近一次)登录时刻;IDLE一栏给出了该用户的“发呆时间”(即从该用户最后一次敲键盘至当前时刻所间隔的分钟数);WHAT一栏给出了该用户当前的“正在干什么”(即当前的进程名及其参数)。


Shell有一种必需的本领,即向它所启动的程序提供Stdio(标准I/O)环境。标准I/O环境,这个抽象的概念是什么意思呢?

我们知道,一个概念V的实现(命令程序)是由Shell找到并启动的。当Shell启动一个程序之后,就会向这个程序提供三个File Handle(文件句柄),这三个文件句柄即构成了属于每个程序自己的标准I/O环境,它们分别是:

(1)stdin —— 标准输入,也叫“FD0(FD是File Descriptor的缩写,意同File Handle)”,这是一种关于字符的Input Stream(输入流),通常,这个流的源头,就是我们手头的键盘。

(2)stdout —— 标准输出,“FD1”,这是一种关于字符的Output Stream,通常,这个流的出口,就是我们面前的终端屏幕(即/dev/ttyvx文件,因此,该流以文件句柄的形式存在),当然你也可以使用前述的“重定向”方法将改流转向输出其他文件(犹如黄河改道,从江苏省流向黄海)。

(3)stderr —— 标准错误信息输出,“FD2”,从流与流的输出形式上说,它与stdout一致,只是程序主体会选择性令错误信息经由这个途径流出。为什么要这么做?这其实是一个很巧妙、很合理的设计,目的在于:使程序的正常输出(它的“本分工作”的结果)与它在遇到错误时所报出的错误信息可以分开,泾渭分明,这样,那些期望攫获该程序正常结果的另一个程序,就可以仅仅收到期望的信息,而不受报错信息的干扰。而且,我们可以通过“重定向”机制,将stdout与stderr流分别导向到不同的文件中去,具体方法是:

代码:

(foo > bar) >& qux

将foo程序的stdout即FD1流写入文件bar,且将foo程序的stderr即FD2流写入文件qux。※ 由于FreeBSD的默认Shell即tcsh并没有提供单纯而直接的方法,将stdout流与stderr流分别重定向,所以,tsch的用户必须使用上述技巧实现stdout与stderr的分别重定向。

而对于GNU/Linux普遍采用的bash,分流的方法则十分清晰明了:

代码:

foo > bar

代码:

foo 1> bar

是将foo程序的stdout即FD1流写入文件bar。

代码:

foo 2> qux

是将foo程序的stderr即FD2流写入文件qux。

※请注意,数字“1”或“2”都必须紧贴“>”,否则数字将被视作概念O的表达。

所以,我们要引入一个新的范式:

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提供的“管道”机制!命令的形式如下:

代码:

foo | bar

我们回到一开始的例子:

代码:

ls | grep "^foo.*"

ls命令程序的输出信息是当前一级目录里的所有文件,这样的信息应该是:

代码:

bar1

bar2

bar3

……

bar9998

bar9999

foo1

foo2

foo3

……

foo9998

foo9999

一共20,000行。

※寻常状况下,我们所看到的ls命令(无参数)的反馈,各个文件名都是按照表格式样排列呈现的,但当这些信息成为stdio流的时候,其实是按照“行”呈现的,例如我们使用命令:

代码:ls | less

就可以看到文件名以“行”排列呈现。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吗?我们使用命令:

代码:

whereis zsh

得到的反馈是:

代码:

zsh: /usr/ports/shells/zsh

※ 一个具有专门搜索文件的命令是“locate”,该程序将搜索数据库中符合关键词的文件,不过这个数据库是需要适时更新的,在FreeBSD上,这个更新的命令是“/usr/libexec/locate.updatedb”。

再对比一下命令:

代码:

whereis tcsh

其反馈结果则是:

代码:

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程序压缩得到),解开它的命令通常是:

代码:

tar zxvf foo.tar.gz

(※ 这里的参数z代表关于gZip的压缩或解压缩,x代表解包,v代表滔滔不绝地说话意即信息馈出,f代表后面隔着空格紧跟的是解压缩的对象或压缩的目的文件。各参数前的连字符可以省略,且可以黏合在一块儿。)

通常会得到一个被释放出来的目录。进入目录,可以看到一堆文件。

接下来是经典三部曲:

代码:

./configure

—— 根据当前的架构特征,配置和生成Makefile文件。

代码:

make

—— 根据当前目录中的Makefile,开始将源代码编译成可执行程序。

代码:

make install

—— 将程序安装到系统文件层次结构中的特定位置。

※ 卫生习惯好的用户,还会:

代码:

make clean

—— 清除上述过程中的中间件,为今后可能再次进行的编译工作提供一个干净的环境。

看到这里,你可能会想,如果这几样步骤(包括下载源代码、解压缩、配置编译、编译、安装等等)能够一次性自动运行,那该多好呀!

FreeBSD的Ports机制,就是为了满足这样的要求而生的。一个特定软件的port就是可以自动实现上述步骤的文件的集合。而Ports Collection就是这样的集合的集合。我们进入“/usr/ports/”目录,可以看到这种集合内容的排布,这些内容分门别类、层次分明、井然有序,比如“/usr/ports/shells/zsh”就告诉我们,zsh软件的port正位于“shells”这个分类当中。

现在,我们进入“/usr/ports/shells/zsh”目录,使用命令:

代码:

make install clean

(※ Port的Makefile文件提供了足够的自动化机能,使得这行命令可以将make、make install、 make clean这些工作“一站式”完成。)

接着,一个蓝底安装界面出现,我们尽可以选上全部项目,然后按<Ok>。

经过“漫长”的下载、配置、编译、安装(其间还会有分支安装的交互式界面,所以最好别走开)之后,整个安装程序以这么一行结束:

代码:

===>  Cleaning for zsh-4.3.14

这标志着zsh程序已经完全安装完毕。

此时,我们需要使用命令:

代码:

rehash

来刷新Shell缓存(这并非是启用新的Shell。在任何安装过程结束后,都必须立即执行这个命令),令我们可以执行新近被安装好的程序。

此后,我们再使用命令:

代码:

whereis zsh

就会发现zsh已经安装在特定的目录中了(/usr/local/bin/zsh),而且还多了一项:

代码:

/usr/local/man/man1/zsh.1.gz

这说明,我们可以使用命令:

代码:

man 1 zsh

来查看zsh的man文档(Manual,手册,使用说明书)。这里的“1”代表,我们将这里的“zsh”视为“命令程序”。“1”是“程序命令”在man文档里的章编号。这种章编号可以反映出一个专有术语的属性:

1    命令程序。

2    系统调用。

3    C库函数。

4    设备的驱动程序。

5    文件格式。

6    游戏或娱乐项目。

7    杂项资讯。

8    系统维护。

9    内核开发情况。

比如,我们可以分别用:

代码:

man 1 printf

代码:

man 3 printf

来查看命令程序printf与C库函数printf(两者同名却是不同的东东)的手册文档。

查看man文档的方法与使用less程序查看一般文档的方法是类似的。

现在,我们要将当前用户的Shell(即tsch)更新为zsh。我们使用Change Shell命令:

代码:

chsh

此时,一个编辑器打开了,我们需要修改其中内容的第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拉回到“命令模式” [backcolor=Red]  注8 [/backcolor] 。

[hr]

[backcolor=Red]   脚注   [/backcolor]

(8)基本上只有这么一种例外:在vi中按下[Esc]键会执行“:”开头的命令,所以在已经输入“:q”这类命令(但还没有按[Enter]键)的情况下,按下[Esc]键会让vi退出。

[hr]

将vi拉回到命令模式之后,输入命令:

代码:

:q! √

※ 上面的“√”表示须按下[Enter]键,这种提示对于学习vi或Vim非常重要,所以就不再忽略。

结果是我们退出了vi程序。“q”的意思是“Quit”。在vi的命令中,感叹号“!”的意思是忽略一切诸如“内容已经改动但还未保存”、“文件将被覆盖”之类的警告,直到你的意图被执着地完成。使用命令“:q!”,使得在上一次文件被更改之后的任何对文件Buffer(文件内容在内存中的一个副本,即你在vi程序所直观的文本内容)的更改都不会写入到文件中,且vi程序直接被退出 —— 利用这个命令,你尽可以找来各种又大又复杂的文件来演练vi的使用。

我们退出vi之后,重新使用命令:

代码:

chsh

进入vi。面对这个文件,我们最好能让vi显示行号。我们现在已经命令模式下了,那么,输入命令:

代码:

:set number√

此时,我们可以比较直观地看到:“Shell: /bin/csh”在第10行。那么,我们要将光标移动到第10行,我们使用命令(如果你码不准是否处于命令模式,请多按[Esc]键,此时你会听到“Beep-Beep”或“Ding-Ding”声响。):

代码:

10G

这次没有“√”,表示你不需要按下[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的命令是:

代码:

c目的地

这里的“目的地”是这么一个意思:想Cut掉光从标当前位置到某个位置的全部字符,那么“某个位置”就是这个命令中的“目的地”。那么,这个“目的地”该如何表达呢?难道用行列坐标吗?不是。要表达这个“目的地”,应该用“从当前光标如何到达目的地”的“命令”来表达,比如说:

我希望Cut掉“bin/csh”的全部字符,也就是说,从字符u(当前光标所在字符)到当前的行尾,全部要Cut掉,那么,“光标移动到当前的行尾”的命令是“$”,这样,这个Cut命令就应该是:

代码:

c$

当命令被输入后,“bin/csh”并没有如我们预期的那样消失(跟GUI软件的那种Cut不一样),而是行尾的字符“h”变成了“$”—— 这说明,Cut命令生效了:

代码:

bin/cs$

最关键的一点是: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的设计哲学了(不做呆板而重复的工作、找到直接切入问题核心的途径)!

我们可以这样:

使用命令:

代码:

/Shell.*csh√

※ 命令“/”是运用RegExp,从当前光标位置向下寻找到第一个匹配的字符串。(若是向上(往回),则用命令“?”。)后续键入“n”或“N”则可以寻找“nEXT(下一个)”或“上一个”。

于将光标拉到“Shell”所在行的首段。

然后,再用命令:

代码:

:s/bin.*$/usr\/local\/bin\/zsh√

这个命令的格式是:

代码:

:区域s/模板/被匹配的字符串将被更新为什么/参数√

这里的“区域”是指查找的范围,用“m,n”表示从第m行到第n行,用“%”整个Buffer;这里的“参数”有:“g”表示全行每处匹配处均进行替换(否则仅在当前行第一处匹配处进行替换);“c”表示替换时需要Confirm(确认)。

若要在命令里的正斜杠之间表达正斜杠,必须使用转义序列“\/”。

至此,文件的Buffer被更新好了,我们需要将Buffer写入文件,我们可以使用命令:代码::w√或代码::w! √以及代码::wq√或代码::wq! √

来实现写入以及写入并退出vi(也退出了chsh程序)。

※ 必须确认上述操作完全无误,否则破坏了用户配置文件,很可能会导致无法登录Shell,那可就麻烦了!

※ 现在,我们可以领略到,vi有这么一个优点:用户的手指不需要离开键盘(甚至仅仅是主键盘,而不要方向键),就可以完成一切操作。

现在我们通过chsh命令(藉着vi)改变了当前用户的Shell设置,这当然需要系统再一次进行某种初始化后才能生效。于是,我们登出当前用户,使用命令:

代码:exit再登录。

现在,我们使用命令:

代码:echo $SHELL

反馈的结果是新的Shell(即zsh)的位置,这说明我们Change Shell的操作成功了。

不过 …… 你会觉得,这个zsh跟原来的tcsh根本没有什么区别嘛……

的确是的,目前的Shell的Prompt(提示符,就是提示用户可以输入命令的特殊符号,由Shell提供)仍然是“主机名”加一个“#”(如果是root账户登录,就是“#”,此外就是“$”)。

现在我们掌握了vi的基本用法,就可以Customize(由客户定制)自己的zsh了。

我们进入自己的家目录,使用命令:

代码:cd ~

然后创建一个名为“.zshrc”的文件:

代码:vi .zshrc

撰写文件内容如下:

case $TERM in

xterm*) TERM=xterm-256color ;;

screen*) TERM=screen-256color ;;

esac

alias grep='grep -E --color=auto' \

fgrep='fgrep --color=auto' \

egrep='egrep --color=auto' \

ll='ls -lh' \

la='ls -A' \

l='ls -CF' \

tree='tree -C' \

v='vi'

HISTFILE=~/.zsh_history

HISTSIZE=1000

SAVEHIST=1000

bindkey -e

bindkey "^[OH" beginning-of-line

bindkey "^[OF" end-of-line

bindkey "^[[3~" delete-char

autoload compinit && compinit

zstyle ':completion:*' menu select

setopt autocd

setopt autopushd

setopt cdablevars

setopt correct

setopt globdots

setopt extendedglob

setopt interactivecomments

setopt completeinword

setopt nobeep

setopt multios

PS1=$'%{\e[1;32m%}%n@%M%{\e[0m%}%{\e[1;33m%}:%{\e[0m%}%{\e[1;32m%}%~%{\e[0m%}%{\e[1;33m%}\n$%{\e[0m%} '

alias cp="cp -i"

alias mv="mv -i"

alias rm="rm -i"

※ 这段.zshrc文件内容,参考并引用了CPFN.org(中国程序员自由网络)的站长wallee的作品,特此致谢:)

这是一段关于当前用户的zsh的配置(准确地说,是当前用户登录后,用来初始化zsh的Script(脚本)),如果你看不懂其中的某些命令或变量名,请你用man来学习!

保存文件,登出,再登录。怎么样?TUI界面是不是漂亮了许多?!


让我们休息一下,回顾一下这段历程:我们从ls命令中出现的通配符讲到了管道,“顺带着”学习了“一切皆是文件”、设备节点、终端、TTY、换行与回车、I/O重定向、STDIO(重点)、grep、RegExp(重点)、FreeBSD Ports、.tar.gz、make install、man、chsh、vi(重点)、zsh。

现在,该是我们回到这个支点的时候了:

代码:

ls | grep "^foo.*"

当ls将当前一级目录下的所有文件名以“逐行”形式的stdout,透过管道哺馈到grep程序的stdin之后,grep程序就对它所获取的stdin流进行处理:凡是匹配于模板的行,就被反馈到终端屏幕上。请特别注意:stdio流是的存在方式是“流”——“流动”的“流”,这就意味着:这里的数据流以类似“逐行逐行”的“川行”方式,源源不断地被哺馈给grep程序,当grep获得到流中的“一行”,它就做出处理,之后继续处理“川流不息”而来的数据中的“下一行”。—— 整个过程是动态的(有当前一步不能预料将来一步的意味,这也叫Asynchronous(异步的))。

下面这个例子,将让屌丝们体会一下 UNIX Styled 命令行、正则表达式、Shell、管道流等等的强大威力:

假设当前一级目录下、以及其子目录下、以及其子目录的子目录下、以及其子目录的子目录的子目录下、……(有多少级目录都没关系)存放着许多文件(全都是文本类型的文件),这些文件中有大量的手机号码,且有大量重复号码,每个号码都是独立占一行,但号码顶头或尾后可能有空格。现在,我们只要一行命令,就将这些号码中的以“136”开头的中国移动的号码的数量统计出来(重复存在的号码只计一次),显示在终端屏幕上,并保存到一个文件中。

只要一行命令哦:

代码:

grep "^\s*136[0-9]{8}\s*[        DISCUZ_CODE_106        ]quot; * -rho | sed 's/[^0-9]//g' | sort -nr | uniq -c | wc -l | sed 'i\Qty of 136-numbers:' | tee stat.txt

我们逐项来解释一下:

(1)grep命令程序中的 RegExp 模板我们应该已经很容易理解它了,这里还有两个参数:“r”表示递归查找,“h”表示to Hide filename(不显示被匹配的文本所在的文件的名字),“o”表示Only(仅仅)显示被匹配的字符串而不是显示所在行的全部内容。

(2)sed命令程序是一个Strean Editor(流编辑器),它好像一个过滤/整流器,可以利用其中的编辑命令“s”对流中的内容进行更新,这里,命令“s”将一切非数字的字符全部替换为空 —— 这是为了除去之前grep匹配所获取到的空白字符(\s*)。(※ 标记“g”的意思是:全局替换而非只替换第一次所匹配到的。)

(3)sort命令程序,用来对grep的stdout(匹配结果)排序,参数“n”表示Number(以数值大小排序),“r”表示Reverse(逆向排序,这只是为了讲解这个选项,此例完全可以不用它)。

(4)因为sort将号码排序,所以相同的号码一定是聚集着连续排列在一起的,此时,uniq命令程序将这样的结果进行“消除重复行(使文本行内容Unique)”,参数“c”表示在每行结果前面打印出该行出现的次数 —— 这在本例中也是完全可以不用的,但对于那些接着需要用到“sort -n”的场合就十分必要了。

(5)wc命令程序是一个Word Count(计数)程序,参数“l”令它反馈出stdin中的行数。

(6)sed命令程序中的“i”命令,使它在流中Insert(插入)文本。

(7)tee命令程序,则如同一个Tee(T形三通水管)一样,使得本来单单反馈到终端屏幕的数据流,能被复制到制定的文件中去。

—— 是不是超强大?如果按照屌丝们使用GUI软件的方法 …… 在Microsoft Windows环境下,要多少个软件,拖泥带水地组合在一起,鼠标要拉扯点击多少次,才能实现这个需求呢?!


现在,我们安装Vim —— 它将是本节课的终点。

使用命令:

代码:

cd /usr/ports/editors/vim/

make install clean

如同给zsh配备初始化脚本(一个dot file)一样,我们也需在Vim安装完成之后,为其配备初始化脚本,这样的脚本,在系统中已经有现成的了,我们直接使用命令copy到当前用户的家目录即可:

代码:

cp /usr/local/share/vim/vim73/vimrc_example.vim ~/.vimrc

此后,使用命令:

代码:

vim foo.txt

就可以打开Vim,编辑你所需要的文件了。你将会发现,Vim比vi要好用得多。

现在,我们专门介绍一下Vim较vi的一些新加特性:

(一)窗口(Window)

在命令模式下,按[Ctrl]+[w]之后,Vim界面的末行,将显示一个“^W”,表示你已经按下了这个组合键,此时,你可以再按以下的键:

【n】 —— 在Vim界面上开启一个新窗口,用来编辑新文件(New file)。

【s】 —— 在Vim界面上开启一个新窗口,用来编辑相同的文件(Same file)。

【w】 —— 编辑的焦点(Focus),在不同窗口之间顺次切换。

(二)拆分(Split)

使用命令:

代码:

:split 文件名

代码:

:vsplit 文件名

可以水平或垂直拆分出新的窗口,以编辑指定的文件,否不指定文件名,则是开启空Buffer。

在Vim的多窗口模式下,编辑工作一如单窗口模式,没有什么区别。除了可以用命令“:q”关闭当前焦点所在的窗口之外,还可以使用命令:

代码:

:qall

—— 关闭全部窗口。

代码:

:only

—— 关闭其他窗口。

最后,我们利用Vim编写一个汇编程序,使得我们可以有属于自己的Boot-able软盘:

代码:

; Verse 1

org 0x7c00

; Verse 2

mov ax, cs

mov ds, ax

mov es, ax

call PrintStr

jmp fin

; Verse 3

fin:

hlt

jmp fin

; Verse 4

PrintStr:

mov bp, MyBoot

mov cx, 80

mov ax, 0x1301

mov bh, 0x00

mov bl, 0x09

mov dl, 35

mov dh, 12

int 0x10

jmp fin

ret

; Verse 5

MyBoot:

db "Just A Tiny Tiny Boot!"

times 510-($-$) db 0

dw 0xaa55

※ 类似上述代码示例,在网上的许多地方、在许多书中都有,令人无法稽考最初的原创者是谁,无论如何,向相关原创者致谢!

为了使得程序线索清晰,我将代码分为五段(Verses),大家可以练习用Vim将这段代码又快又准地录入到一个文本文档,至于代码各部分的含义,大家如果不理解,可以先猜一猜。尽量猜,能猜到哪里算哪里。※ 可以参考之前介绍过的关于BIOS读取Bootstrap以及BIOS中断调用的知识。

我们需要使用Netwide 汇编器(NASM),将上述代码翻译为CPU可以执行的(二进制)机器代码。如何安装nasm,就不用我多说了吧:)

使用nasm翻译汇编程序的命令格式如下:

代码:

nasm 源程序名 -o 二进文件名

我们应该将二进文件的扩展名设为“.img”(映像),然后我们将这个映像文件复制到一台有Virtual Machine(虚拟机软件,如Virtual Box)的电脑上,对于如何使用虚拟机软件,屌丝们应该十分熟稔吧!但对于如何将文件从这台FreeBSD电脑上复制出去,恐怕有点无措,其实很简单:

找来一个你在Microsoft Windows下使用的USB硬盘或闪存设备,插上机器。

FreeBSD系统随即会有提示,有新设备加入了。

此时你会发现在/dev/目录下多了一个诸如da0这样的文件(设备节点)。

现在,我们需要将这个设备挂载到我们的文件系统上,运用之前已经学到的知识,你应该不难理解以下操作:

代码:

mount -t msdosfs /dev/da0 /media

(※ 参数t后面跟着的,是所要挂载的文件系统的类型。)

当然,你可以在/media/目录下新建一个目录,然后将da0设备挂载到那里。

将之前生成好的二进制映像文件复制到/media/目录。

此后,你可以随时卸载设备,可以使用命令:

代码:

umount /media

这样,你就把文件从FreeBSD电脑上导出去了。

回到你再熟悉不过的Microsoft Windows电脑上,使用虚拟机软件,新建一个虚拟电脑,并为其配置一个软盘控制器,再将刚才的二进制映像文件作为软盘“插入”到这个“软盘驱动器”上,此外,别忘了设置从这个软盘Boot,最后,开启这台虚拟电脑。

怎么样,是不是很有趣?

这节课,就到此为止了。对于电脑屌丝们来说,这节课已经为你们完成了脱屌的第一步!

我们下节课再见!

祝你们平安!

Larry Ageratum Westernwall

2012年11月15日凌晨




原创粉丝点击