世上本没有shell,命令多了,也就成了shell。
——Pope
要说shell,就不能不谈起Unix的Keep It Simple Stupid理念。Unix社区崇尚简洁、专注,鼓励一个工具只做一件事,做好这件事。当你需要完成一项复杂的工作时,分解,细化,再交给相应的小工具去执行。这样就需要提供一种机制把各个工具连接起来。所以,在我开来,学习shell重在掌握任务分解和命令间的互联。
当然,你还会发现,shell的用法很奇怪,毕竟它是个很古老的东西了,包容各种历史遗留也是无奈之选。
其实,本文是一篇Learning the Bash Shell (3rd)的读书笔记。
shell是一种用户与类Unix操作系统之间以文字方式进行交互的媒介。还有其他的交互形式,比如图形界面。
shell命令一般是祈使句 如:去 给老子 拿瓶水来。
shell命令先是一个动词,紧接着若干副词(option)和名词(argument)(也可能没有)。
操作的对象大多是文件(Unix里几乎所有东西都是文件)。文件分为普通文件(文本),可执行文件和目录。
目录可以包括若干子目录,包括本目录(.)和父目录(..)。目录被组织成树形结构,用路径来标示。
可以明确指定shell操作的对象的绝对路径,如果不说就默认是在当前目录下。当前目录虽然被称为目录却没有物理对应,是一个概念上的浮动地址,可以通过pwd
(print the working directory,你看多怪的名字,不知道的还以为是password呢)命令来查看,还可以用cd
命令(change the working directory)来修改。此外还有主目录(~
)和操作记录中的上一目录(-
)。
shell的操作对象可以是具体名词(如某个文件名),也可以是抽象名词(用来描述某一特征,需要用到通配符(wildcard))。
通配符有三种基本模式:
匹配任一字符、匹配任意字符、匹配字符集。
!, \*, […], [!…], {…}
Unix将I/O设备统一看作文件,并把I/O看作处理任意长的字节流。
Unix把I/O流简化为标准输入输入(standard input)、标准输出(standard output)和标准错误输出(standard error output)三个端口。输入流默认为键盘,输出和错误流默认为显示屏。如果需要,可以将这些端口映射到其他设备或文件上(重定向)。
Unix下的小工具往往只专注于某项处理。数据被不同的小工具一道道筛选、加工,最后得到想要的结果。正是因为专注,让它们更容易组合起来构建复杂的工具。
奇怪的命名
虽然shell命令的命名也不是无章可循,但总归是很奇怪的。以下是几个常用的工具:
但比起那些预设变量,这还算好的。
比如位置参数,人家名字直接叫1, 2, ⋯⋯要想取到它们的值,前面加$($var
其实是${var}
的简写),即$1
, $2
, ⋯⋯。还有0(脚本文件的名字)、*(所有位置参数构成的字符串,用IFS(Internal Field Separator)的首字符作分隔符)、@("$1" "$2" ... "$N")、#(位置参数的个数)⋯⋯
不带参数的cat就像while true { echo input }
有管道还不够吗
cat
是用来解释重定向的绝佳例子: cat < file1 > file2
等价于cp file1 file2
。command < filename
,表示command
从某个文件而不是标准输入拿到数据; command > filename
,表示command
将数据输出到某个文件而不是标准输出。
我觉得这个设计很好,但记法太屎。我宁愿多敲几个字,把从哪输入,输出到哪明确清楚了(比如: file1 > cat > file2
)而不是把<
和>
作为前缀贴到操作对象前面。
除了重定向输出到某个文件,你还可以把输出作为其他命令的输入直接导入(管道pipe,用|
表示。这个记法也很屎,体现不出方向来,虽然知道是从左到右。为什么不能和重定向统一起来呢?!比如,$ cut -d: -f1 < /etc/passwd | sort | lp
写成/etc/passwd > cut -d: -f1 > sort > lp
的形式。可能是于当初把命令设计成前缀形式有关吧,如果当初设计成中缀形式并把命令也看成文件(命令本来就是内存里的一个文件),也许会方便很多)。
文本编辑原来这么阳春白雪
bash的emacs-mode对文本编辑提供很精细的控制:对字符的操作(^B, ^F, ^D, ⌫)、对单词的操作(⎋B, ⎋F, ⎋D, ⎋⌫)、对整句的操作(^A, ^E, ^K, Y(找回前一次被删掉的东西)),甚至是对历史记录的操作(P(相当于↑), ^N(相当于↓), ^R)。
但emacs-mode最强大的功能还得是文本补全(⇥)。当你敲入几个字符并按下⇥,可能会出现四种情况:
- 没有以这些字符开头的词,shell会小叫一声beep;
- 找到一个唯一的匹配,shell会补全它,并附带一个空格方便你敲其他字符;
- 如果找到一个唯一匹配的目录,shell会补全文件名,并附上一个斜杠(/);
- 如果有多个匹配可能,shell会补上这些可能匹配的最长前缀。
如果你想知道都有哪些可能的选择,敲两下⇥(或者按⎋-?,它会显示所有可能选择)。
shell提供了很多快捷键来补全不同类型的名字(比如,⎋-/ 专门补文件名,⎋-~专补用户名,⎋-!专补命令名⋯⋯)
既然提到了emacs-mode,不用想也能知道肯定有个vi-mode。
vi最著名的就是用h, j, k, l作为←↓↑→。这得追溯到当初Bill Joy用ADM-3a开发vi,这种机器的h, j, k, l键上分别有四个←↓↑→小箭头,所以后面你知道的。
vi-mode和vi一样,也有两种模式(输入模式和控制模式)。在输入模式下,你可以键入、删除(⌫, ^W)。按下 ⎋ 可以进入控制模式。这个模式下,可以用单键来控制移动(h, l, b, w, 0, $)(很喜欢这种单键控制的方式,其实很多软件都可以借鉴。如果你不用编辑文字,干嘛要用control、shift这类的辅助键。这就好像有名字空间你不用,非得都搞成全家变量一样)。你不光可以移动一个字符(h, l)、一个单词(b, w),甚至还可以移动到某个特定的字符上(F-×, f-×,×表示任意字符)按下i(或a, I(等效于0i), A (等效于$a), R (与r略有不同,r只替换一个字符而且不会进入输入模式,即你不能再键入其他字符))切回输入模式。控制模式下还有个不得不提的命令——撤销(u, undo)。另外“.”也很有用,它反复执行上一次的操作。
vi也有文本补全,不过不是 ⇥,而是进入控制模式按反斜杠(/)。
一直觉得vi很有意思,设计很精巧,像一门微型语言。比如删除操作,按下d
(delete)之后不会有什么变化,还要指定删除的方向(向前还是向后)和长度(字符还是单词还是整句,是当前光标位置到行首还是到行尾)。这不是v. + adv. + n. 结构么!而且一个动作可以有多个说法(比如删除整句,可以用dd
,也可以用0d$
)。让你感觉它不是一堆互不相干的快捷键,而是一句意义明确的指示。
命令行工具一个总的印象就是太费脑子,这里指的是要记的东西太多,不如GUI直观。但好处就在于它能控制得很精细,不像GUI,界面设计成什么样就只有哪些功能。
到底0是真是假
在流程控制中,shell用返回状态作为逻辑判断的结果。这样,0(在[...]中)就表示真,其他返回码均表示假。但在算术表达式($((...))
或$[...]
)中,0却是假,1才是真。而算术式的另一种形式((...))
却用0作为真,1为假
丑陋的语法
如果说if...then...fi是bash的特色的话,bash的case这是巨丑陋:
case expression in
pattern1 )
statements ;;
pattern2 )
statements ;;
...
esac
你就不能正常一点?!至少对称一点,别弄出半边括号来好不。
还有,按理说shell的语法是不依赖空格或是换行的。可你看看赋值语句,多了空格它不干,再看看[ ... ]少了空格它也不干。而在for, while, select里do如果新起一行就不用打分号。
shell语法看似统一,其实很混乱。这货不是一门语言,而是一堆微型语言,虽然每个都很简单,但要想全都记住却有不小难度。
P.S. Learning the bash Shell似乎没有继承Learning系列简单明了的衣钵,看似很全,其实毫无系统。示例晦涩,讲解不清,比如对getopts里OPTSTRING(Mac下好像是这个,书中是OPTARG)的说明,OPTSTRING是一个由option及其所带参数构成的一个序列,而OPTIND是OPTSTRING里下一个要处理的option的下标(不敢举例,怕自己理解有误)。
我想要的shell
首先互联功能必须很强大;
灵活的语法(不依赖空格神马的,不限定格式);
统一的语法;
⋯⋯
(cont.)