Linux 桌面玩家指南:06. 优雅地使用命令行及 Bash 脚本编程语言中的美学与哲学

原文: Linux 桌面玩家指南:06. 优雅地使用命令行及 Bash 脚本编程语言中的美学与哲学

特别说明:要在个人随笔后写评论的小伙伴们请注意了,个人博客开启了 MathJax 数学公式支持,MathJax 使用$标记数学公式的开始和结束。若是某条评论中出现了两个$,MathJax 会将两个$之间的内容按照数学公式进行排版,从而致使评论区格式混乱。若是你们的评论中用到了$,可是又不是为了使用数学公式,就请使用\$转义一下,谢谢。html

想从头阅读该系列吗?下面是传送门:linux

前言

虽然咱们玩的是 Linux 桌面系统,可是不少时候咱们仍然离不开命令行。有时候,是由于某些工具只有命令行版本,要解决某些问题必须使用命令行,特别是对于咱们程序猿和系统管理员来讲更是这样。有时候,是由于使用命令行解决问题确实比使用图形界面更加高效。还有些时候,为了自动化、批量化运行程序,咱们也不得不使用命令行。得益于 Unix 系统的传统,在命令行中使用管道和文件重定向以及 Shell 脚本语言做为粘合剂,能够将许多简单的工具组合到一块儿完成更加复杂的任务。总之,Linux 系统中的命令行是至关舒服和优雅的。程序员

我这里使用的终端程序就是 Gnome 3 桌面自带的 gnome-terminal,而我使用的 Shell 就是 Bash。网上有不少人推崇 Z Shell,可是我并无改弦易辙,而是坚持使用 Bash。我认为,Bash 的功能也是很强大的,只是我本身水平有限,不能发挥出它所有的威力而已。关于高效使用命令行这个话题,在网上已是老生常谈了。我这里主要的参考资料是 Bash 的官方文档,使用man bash便可以阅读,固然也能够到 Bash 的官网上下载 pdf 版的文档,放到手机上有空的时候慢慢看。在本文中,也有很多我本身的观点和体会,我会提到有些快捷键要熟记,有些则彻底不须要记,毕竟咱们的记忆力也是有限的,我还会提到一些助记的方法。因此,本文绝对不是照本宣科,值得你们拥有,请你们必定记得点赞。正则表达式

四年前,我脑子一抽,写了一篇 Bash 脚本编程语言中的美学与哲学,还很是洋洋得意。如今回看起来,以为仍是幼稚了一些。可是我以为我写的这些也不是彻底没有帮助,相比于长达 171 页的详细的 Bash 官方文档,也许我对 Bash 脚本编程语言的定位——面向字符串的编程语言——更能让你们理解记住并熟练使用命令行呢。shell

使用 tmux 复用控制台窗口

高效使用命令行的首要原则就是要尽可能避免干扰,什么意思呢?就是说一但开启了一个控制台窗口,就尽可能不要再在桌面上切换来切换去了,不要一下子被别的窗口挡住控制台,一下子又让别的窗口破坏了控制台的背景,最好是把控制台最大化或全屏,甚至连鼠标都不要用。可是在实际工做中,咱们又常常须要同时在多个控制台窗口中进行工做,例如:在一个控制台窗口中运行录制屏幕的命令,在另一个控制台窗口中工做;或者在一个控制台窗口中工做,在另一个控制台窗口中阅读文档。若是既想在多个控制台窗口中工做,又不想一大堆窗口挡来挡去、换来换去的话,就能够考虑试试 tmux 了。以下图:
express

tmux 的功能不少,什么 Session 啊、Atach 啊、Detach 啊等功能都很是强大。可是咱们暂时不用去关心这些,只把重点放在它的控制台窗口复用功能上就好了。tmux 中有 window 和 pane 的概念,tmux 能够建立多个 window,这些 window 是不会互相遮挡的,每次只显示一个 window,其它的 window 会自动隐藏,可使用快捷键在 window 之间切换。同时,能够把一个 window 切分红多个 pane,这些 pane 同时显示在屏幕上,可使用快捷键在 pane 之间切换。编程

tmux 的快捷键不少,要想全面了解 tmux 的最好办法固然是使用man tmux命令阅读 tmux 的文档。可是咱们只须要记住少数几个重要的快捷键就能够了,以下表:vim

快捷键 功能
Ctrl+B c 建立一个 window
Ctrl+B [n][p] 切换到下一个窗口或上一个窗口
Ctrl+B & 关闭当前窗口
Ctrl+B " 将当前 window 或 pane 切分红两个 pane,上下排列
Ctrl+B % 将当前 window 或 pane 切分红两个 pane,左右排列
Ctrl+B x 关闭当前 pane
Ctrl+B [↑][↓][←][→] 在 pane 之间移动
Ctrl+[↑][↓][←][→] 调整当前 pane 的大小,一次调整一格
Alt+[↑][↓][←][→] 调整当前 pane 的大小,一次调整五格

tmux 的快捷键比较特殊,除了调整 pane 大小的快捷键以外,其它的都是先按 Ctrl+B,再按一个字符。先按 Ctrl+B,再按 c,就会建立一个 window,这里 c 就是 create window。先按 Ctrl+B,再按 n 或者 p,就能够在窗口之间切换,它们是 next window 和 previous window 的意思。关闭窗口是先按 Ctrl+B,再按 &,这个只能死记。先按 Ctrl+B,再按 " ,表示上下拆分窗口,能够想象成单引号和双引号在键盘上是上下铺关系。先按 Ctrl+B,再按 % 表示左右拆分窗口,大概是由于百分数都是左右书写的吧。至于在 pane 之间移动和调整 pane 大小的方向键,就不用多说了吧。数组

在命令行中快速移动光标

在命令行中输入命令时,常常要在命令行中移动光标。这个很简单嘛,使用左右方向键就能够了,可是有时候咱们输入了很长一串命令,却忽然要修改这个命令最开头的内容,若是使用向左的方向键一个字符一个字符地把光标移到命令的开头,是否太慢了呢?有时咱们须要直接在命令的开头和结尾之间切换,有时又须要可以一个单词一个单词地移动光标,在命令行中,其实这都不是事儿。以下图:
bash

这几种移动方式都是有快捷键的。其实一个字符一个字符地移动光标也有快捷键 Ctrl+B 和 Ctrl+F,可是这两个快捷键咱们不须要记,有什么能比左右方向键更方便的呢?咱们真正要记的是下面这几个:

快捷键 功能
Ctrl + A 将光标移动到命令行的开头
Ctrl + E 将光标移动到命令行的结尾
Alt + B 将光标向左移动一个单词
Alt + F 将光标向右移动一个单词

这几个快捷键太好记了,A 表明 ahead,E 表明 end,B 表明 back,F 表明 forward。为何按单词移动光标的快捷键都是以 Alt 开头呢?那是由于按字符移动光标的快捷键把 Ctrl 占用了。可是按字符移动光标的快捷键咱们用不到啊,由于咱们有左右方向键啊。

在命令行中快速删除文本

对输入的内容进行修改也是咱们常常要干的事情,对命令行进行修改就涉及到先删除一部份内容,再输入新内容。咱们碰到的状况是有时候只须要修改个别字符,有时候须要修改个别单词,而有时候,输入了半天的很长的一段命令,咱们说不要就全都不要了,要整行删除。经常使用的删除键固然是 BackSpace 和 Delete 啦,不过一次删除一个字符,仍是太慢了些。要在命令行中快速删除文本,请熟记如下几个快捷键吧:

快捷键 功能
Ctrl + U 删除从光标到行首的全部内容,若是光标在行尾,天然就整行都删除了啊
Ctrl + K 删除从光标到行尾的全部内容,若是光标在行首,天然也是整行都删除了啊
Ctrl + W 删除光标前的一个单词
Alt + D 删除光标后的一个单词
Ctrl + Y 将刚删除的内容粘贴到光标处,有时候删错了能够用这个快捷键恢复删除的内容

效果请看下图:

这几个快捷键也是蛮好记的,U 表明 undo,K 表明 kill,W 表明 word,D 表明 delete, Y 表明 yank。其中比较奇怪的是 Alt+D 又是以 Alt 开头的,那是由于 Ctrl+D 又被占用了。Ctrl+D 有两个意思,一是在编辑命令行的时候它表明删除一个字符,固然,这个快捷键其实咱们用不到,由于 BackSpace 和 Delete 方便多了;二是在某些程序从 stdin 读取数据的时候,Ctrl+D 表明 EOF,这个咱们偶尔会用到。

快速查看和搜索历史命令

对于曾经运行过的命令,除非特别短,咱们通常不会重复输入,从历史记录中找出来用天然要快得多。咱们用得最多的就是 ↑ 和 ↓,特别是不久前才刚刚输入过的命令,使用 ↑ 向上翻几行就找到了,按一下 Enter 就执行,多舒服。可是有时候,明明记得是不久前才用过的命令,可是向上翻了半天也没找到,怎么办?那只好使用history命令来查看全部的历史记录了。历史记录又特别长,怎么办?可使用 history | lesshistory | grep '...'。除此以外,还有终极大杀招,那就是按 Ctrl+R 从历史记录中进行搜索。按了 Ctrl+R 以后,每输入一个字符,都会和历史记录中进行增量匹配,输入得越多,匹配越精确。固然,有时候含有相同搜索字符串的命令特别多,怎么办?继续按 Ctrl+R,就会继续搜索下一条匹配的历史记录。以下图:

这里,须要记住的命令和快捷键以下表:

命令或快捷键 功能
history 查看历史记录
history | less 分页查看历史记录
history | grep '...' 在历史记录中搜索匹配的命令,并显示
Ctrl + R 逆向搜索历史记录,和输入的字符进行增量匹配
Esc 中止搜索历史记录,并将当前匹配的结果放到当前输入的命令行上
Enter 中止搜索历史记录,并将当前匹配的结果当即执行
Ctrl + G 中止搜索历史记录,并放弃当前匹配的结果
Alt + > 将历史记录中的位置标记移动到历史记录的尾部

这里须要注意的是,当咱们在历史记录中搜索的时候,是有位置标记的,Ctrl+R 是指从当前位置开始,逆向搜索,R 表明的是 reverse,每搜索一条记录,位置标记都会向历史记录的头部移动,下次搜索又从这里开始继续向头部搜索。因此,咱们必定要记住快捷键 Alt+>,它能够把历史记录的位置标记还原。另外须要注意的是中止搜索历史记录的快捷键有三个,若是按 Enter 键,匹配的命令就当即执行了,若是你还想有修改这条命令的机会的话,必定不要按 Enter,而要按 Esc。若是什么都不想要,就按 Ctrl+G,它会还你一个空白的命令行。

快速引用和修饰历史命令

除了查看和搜索历史记录,咱们还能够以更灵活的方式引用历史记录中的命令。常见的简单的例子有!!表明引用上一条命令,!$表明引用上一条命令的最后一个参数,^oldstring^newstring^表明将上一条命令中的 oldstring 替换成 newstring。这些操做是咱们平时使用命令行的时候的一些经常使用技巧,其实它们的本质,是由 history 库提供的 history expansion 功能。Bash 使用了 history 库,因此也能使用这些功能。其完整的文档能够查看man history手册页。知道了 history expansion 的理论,咱们还能够作一些更加复杂的操做,以下图:

引用和修饰历史命令的完整格式是这样的:

![!|[?]string|[-]number]:[n|x-y|^|$|*|n*|%]:[h|t|r|e|p|s|g]

能够看到,一个对历史命令的引用被 : 分为了三个部分,第一个部分决定了引用哪一条历史命令;第二部分决定了选取该历史命令中的第几个单词,单词是从0开始编号的,也就是说第0个单词表明命令自己,第1个到最后一个单词表明命令的参数;第三部分决定了对选取的单词如何修饰。下面我列出完整表格:

表格1、引用哪一条历史命令:

操做符 功能
! 全部对历史命令的引用都以 ! 开始,除了 ^oldstring^newstring^ 形式的快速替换
!n 引用第 n 条历史命令
!-n 引用倒数第 n 条历史命令
!! 引用上一条命令,等于 !-1
!string 逆向搜索历史记录,第一条以 string 开头的命令
!?string[?] 逆向搜索历史记录,第一条包含 string 的命令
^oldstring^newstring^ 对上一条命令进行快速替换,将 oldstring 替换为 newstring
!# 引用当前输入的命令

表格2、选取哪个单词:

操做符 功能
0 第0个单词,在 shell 中就是命令自己
n 第n个单词
^ 第1个单词,使用 ^ 时能够省略前面的冒号
$ 最后一个单词,使用 $ 时能够省略前面的冒号
% 和 ?string? 匹配的单词,能够省略前面的冒号
x-y 从第 x 个单词到第 y 个单词,-y 表明 0-y
* 除第 0 个单词外的全部单词,等于 1-$
x* 从第 x 个单词到最后一个单词,等于 x-$,能够省略前面的冒号
x- 从第 x 个单词到倒数第二个单词

表格3、对选取的单词作什么修饰:

操做符 功能
h 选取路径开头,不要文件名
t 选取路径结尾,只要文件名
r 选取文件名,不要扩展名
e 选取扩展名,不要文件名
s/oldstring/newstring/ 将 oldstring 替换为 newstring
g 全局替换,和 s 配合使用
p 只打印修饰后的命令,不执行

这几个命令其实挺好记的,h 表明 head,只要路径开头不要文件名,t 表明 tail,只要路径结尾的文件名,r 表明 realname,只要文件名不要扩展名,e 表明 extension,只要扩展名不要文件名,s 表明 substitute,执行替换功能,g 表明 global,全局替换,p 表明 print,只打印不执行。有时候光使用 :p 还不够,咱们还能够把这个通过引用修饰后的命令直接在当前命令行上展开而不当即执行,它的快捷键是:

操做符 功能
Ctrl + Alt + E 在当前命令行上展开历史命令引用,展开后不当即执行,能够修改,按 Enter 后才会执行
Alt + ^ 和上面的功能同样

这两个快捷键,记住一个就行。这样,当咱们对历史命令的引用修饰完成后,能够先展开来看一看,若是正确再执行。眼见为实嘛,反正我是每次都展开看看才放心。

使用 Tab 键进行补全

在使用命令行的时候,可使用 Tab 键对命令和文件名进行补全。通常若是你输入一条命令的前面几个字符后,按 Tab 键两次,将会提示全部可用的命令。输入命令后,在输入参数的位置,若是输入了一个文件名的前几个字符,按 Tab 键,Shell 会查找当前目录下的文件,对文件名进行补全。或者在输入参数的位置直接按两次 Tab 键,将提示全部可用的文件名。效果以下:

快速切换当前目录

在使用命令行时,可使用cd命令切换当前目录,可是,若是每次都输入一个超长的目录名,则会严重影响效率,特别是在多个目录之间快速切换的时候。例如,在我前面几篇中,常常须要进入/usr/share/backgrounds/contest目录和/etc/fonts/conf.d目录查看配置文件,也会进入/usr/src/linux-source-4.15.0目录查看内核源代码,这些目录名都比较长,若是每次都本身输入,效率低不说,还容易出错。这时,能够经过 Bash 提供的pushd命令和popd命令维护一个目录堆栈,并使用dirs命令查看目录堆栈,使用pushd命令在目录之间切换。效果以下图:

这三个命令的具体参数以下:

一、dirs——显示当前目录栈中的全部记录(不带参数的dirs命令显示当前目录栈中的记录)

格式:dirs  [-clpv]  [+n]  [-n]
选项
-c    删除目录栈中的全部记录
-l     以完整格式显示
-p    一个目录一行的方式显示
-v    每行一个目录来显示目录栈的内容,每一个目录前加上的编号
+N  显示从左到右的第n个目录,数字从0开始
-N   显示从右到左的第n个日录,数字从0开始

二、pushd——pushd命令经常使用于将目录加入到栈中,加入记录到目录栈顶部,并切换到该目录;若pushd命令不加任何参数,则会将位于记录栈最上面的2个目录对换位置

格式:pushd  [目录 | -N | +N]   [-n]
选项
目录   将该目录加入到栈顶,并执行"cd 目录",切换到该目录
+N   将第N个目录移至栈顶(从左边数起,数字从0开始)
-N    将第N个目录移至栈顶(从右边数起,数字从0开始)
-n    将目录入栈时,不切换目录

三、popd——popd用于删除目录栈中的记录;若是popd命令不加任何参数,则会先删除目录栈最上面的记录,而后切换到删除事后的目录栈中的最上面的目录

格式:popd  [-N | +N]   [-n]
选项
+N   将第N个目录删除(从左边数起,数字从0开始)
-N    将第N个目录删除(从右边数起,数字从0开始)
-n    将目录出栈时,不切换目录

Bash 脚本编程语言的本质:一切都是字符串

下面,我将探讨 Bash 脚本语言中的美学与哲学。 这不是一篇 Bash 脚本编程的教程,可是却能让人更加深刻地了解 Bash 脚本编程,更加快速地学习 Bash 脚本编程。 阅读如下内容,不须要你有 Bash 编程的经验,但必定要和我同样热衷于探索各类编程语言的本质,感悟它们的魅力。

咱们平时喜欢对编程语言进行分类,把编程语言分为面向过程的编程语言、面向对象的编程语言、函数式编程语言等等。在我心中,我认为 Bash 就是一个面向字符串的编程语言。Bash 脚本语言的本质:一切皆是字符串。 Bash 脚本语言的一切哲学都围绕着字符串:它们从哪里来?到哪里去?使命是什么? Bash 脚本语言的一切美学都源自字符串: 由键盘上几乎全部的符号 “$ ~ ! # & ( ) [ ] { } | > < - . , ; * @ ' " ` \ ^” 排列组合而成的极富视觉冲击力的、功能极其复杂的字符串。

Bash 是一个 Shell,Shell 出现的初衷是为了将系统中的各类工具粘合在一块儿,因此它最根本的功能是调用各类命令。而命令以及命令的参数都是由字符串组成的,因此 Bash 脚本语言最终进化成一个面向字符串的语言。 Bash 语言的本质就是:一切都是字符串。 看看下图中的这些变量:

上图是我在交互式的 Bash 命令行中作的一些演示。在上图中,我对变量分别赋值,无论等号右边是一个不带引号的字符串,仍是带有引号的字符串,甚至数字,或者数学表达式,最终的结果,变量里面存储的都是字符串。我使用一个 for 循环显示全部的变量,能够看到数学表达式也只是以字符串的形式储存,没有被求值。

Bash 脚本编程语言中的引号、元字符和反斜杠

若是一切都是没有特殊功能的平凡的字符串,那就没法构成一门编程语言。在 Bash 中,有不少符号具备特殊含义,如$符号被用于字符串展开,&符号用于让命令在后台执行, |用做管道,> <用于输入输出重定向等等。因此在 Bash 中,虽然一样是字符串,可是被引号包围的字符串和不被引号包围的字符串使用起来是不同的,被单引号包围的字符串和被双引号包围起来的字符串也是不同的。

究竟带引号的字符串和不带引号的字符串使用起来有什么不同呢?下图是我构建的一些比较典型的例子:

在上图中,我展现了 Bash 中生成字符串的 7 种方法:大括号展开、波浪符展开、参数展开、命令替换、算术展开、单词分割和文件路径展开。还有历史命令展开没有在上图展现,可是历史命令展开在前面快速引用和修饰历史命名那一节有展现,能够看到历史命令展开都是使用!开头的。在使用 Bash 脚本编程的时候,了解以上 7 种字符串生成的方式就够了。在交互式使用 Bash 命令行的时候,才须要了解历史命令展开,熟练使用历史命令展开可让人事半功倍。

在上面的图片中能够看到,有一些展开方式在被双引号包围的字符串中是不起做用的,如大括号展开、波浪符展开、单词分割、文件路径展开,而只有参数展开、命令替换和算术展开是起做用的。从图片中还能够看出,字符串中的参数展开、命令替换和算术展开都是由$符号引导,命令替换还能够由` 引导。因此,能够进一步总结为,在双引号包围的字符串中,只有$ \ `这三个字符具备特殊含义。

若是想让任何一个字符都不具备特殊含义,可使用单引号将字符串包围,例如使用正则表达式的时候。还有就是在使用 sed、awk 等工具的时候,因为 sed 和 awk 本身执行的命令中每每包含有不少特殊字符,因此它们的命令最好用单引号包围。 例如使用 awk 命令显示/etc/passwd文件中的每一个用户的用户名和全名,可使用这个命令awk -e '{print $1,$5}',其中,传递给 awk 的命令用单引号包围,说明 bash 不执行其中的任何替换或展开。

另一个特殊的字符是\,它也是引用的一种。它能够解除紧跟在它后面的一个特殊字符的特殊含义(引用)。之因此须要\的存在,是由于在 Bash 中,有些字符称为元字符,这些字符一旦出现,就会将一个字符串分割为多个子串。若是须要在一个字符串中包含这些元字符自己,就必须对它们进行引用。以下图:

最多见的元字符就是空格。 从上面几张图片能够看出,若是要将一个含有空格的字符串赋值给一个变量,要么把这个字符串用双引号包围,要么使用\对空格进行引用。 从上图中能够看出,Bash 中只有9个元字符,它们分别是| & ( ) ; < > space tab,而在其它编程语言中常常出现的元字符. { } [ ]以及做为数学运算的加减乘除,在 Bash 中都不是元字符。

字符串从哪里来,到哪里去

介绍完字符串、介绍完引用和元字符,下一个目标就是来探讨这一个哲学问题:字符串从哪里来、到哪里去?经过该哲学问题的探讨,能够推导出 Bash 脚本语言的整个语法。字符串从哪里来?很显然,其中一个很直接的来源就是咱们从键盘上敲上去的。除此以外,就是我前面提到的七八九种字符串展开的方法了。

字符串展开的流程以下:

1.先用元字符将一个字符串分割为多个子串;

2.若是字符串是用来给变量赋值,则无论它是否被双引号包围,都认为它被双引号包围;

3.若是字符串不被单引号和双引号包围,则进行大括号展开,即将 {a,b}c 展开为 ab ac;

以上三个流程能够经过下图证实:

4.若是字符串不被单引号或双引号包围,则进行波浪符展开,即将 ~/ 展开为用户的主目录,将 ~+/ 展开为当前工做目录(PWD),将 ~-/ 展开为上一个工做目录(OLDPWD);

5.若是字符串不被单引号包围,则进行参数和变量展开;这一类的展开全都以$开头,这是整个 Bash 字符串展开中最复杂的,其中包括用户定义的变量,包括全部的环境变量,以上两种展开方式都是$后跟变量名,还包括位置变量$1 $2 ... $9,其它特殊变量:$@ $* $# $- $! $0 $? $_,甚至还有数组:${var[i]}, 还能够在展开的过程当中对字符串进行各类复杂的操做,如:${parameter:-word} ${parameter:=word} ${parameter:+word} ${parameter:?word} ${parameter:offset} ${parameter:offset:length} ${!prefix*} ${!prefix@} ${name[@]} ${!name[*]} ${#parameter} ${parameter#word} ${parameter##word} ${parameter%word} ${parameter%%word} ${parameter/pattern/string} ${parameter^pattern} ${parameter^^pattern} ${parameter,pattern} ${parameter,,pattern}

6.若是字符串不被单引号包围,则进行命令替换;命令替换有两种格式,一种是 $(...),一种是 `...`;也就是将命令的输出做为字符串的内容;

7.若是字符串不被单引号包围,则进行算术展开;算术展开的格式为 $((...))

8.若是字符串不被单引号或双引号包围,则进行单词分割;

9.若是字符串不被单引号或双引号包围,则进行文件路径展开;

10.以上流程所有完成后,最后去掉字符串外面的引号(若是有的话)。以上流程只按以上顺序进行一遍。不会在变量展开后再进行大括号展开,更不会在第 10 步去除引用后执行前面的任何一步。若是须要将流程再走一遍,请使用 eval。

探讨完了字符串从哪里来,下面来看看字符串到哪里去。也就是怎么使用这些字符串。使用字符串有如下几种方式:

1.把它当命令执行;这是 Bash 中的最根本的用法,毕竟 Shell 的存在就是为了粘合各类命令。若是一个字符串出如今本该命令出现的地方(一行的开头,或者关键字 then、do 等的后面),它将会被当成命令执行,若是它不是个合法的命令,就会报错;

2.把它当成表达式;Bash 中本没有表达式,可是有了 ((...))[[...]],就有了表达式;((...)) 能够把它里面的字符串当成算术表达式,而 [[...]] 会把它里面的字符串当逻辑表达式,仅此两个特例;

3.给变量赋值;这也是一个特例,有点破坏 Bash 编程语言语法哲学的完整性。为何这么说呢?由于=即不是一个元字符,也不容许两边有空格,并且只有第 1 个等号会被当成赋值运算符。

下面图片为以上观点给出证据:

再加上一点点的定义,就能够推导出整个 Bash 脚本编程语言的语法了

前面我已经展现了我对字符串从哪里来、到哪里去这个问题的理解。关于字符串的去向,除了两个表达式和一个为变量赋值这三个特例,剩下的就只有当命令来执行了。在前面,我提到了元字符和引用的概念,这里,还得再增长一点点定义:

定义1:控制操做符(Control Operator) 前面提到元字符是为了把一个字符串分割为多个子串,而控制操做符就是为了把一系列的字符串分割成多个命令。举例说明,在 Bash中,一个字符串 cat /etc/passwd 就是一个命令,第一个单词 cat 是命令,第 2 个单词 /etc/passwd 是命令的参数,而字符串 cat /etc/passwd | grep youxia 就是两个命令,这两个命令分别是 catgrep,它们之间经过|分割,因此这里的|是控制操做符。熟悉 Shell 的朋友确定知道|表明的是管道,因此它的做用是:1.把一个字符串分割为两个命令,2.将第一个命令的输出做为第二个命令的输入。在 Bash 中,总共只有 10 个控制操做符,它们分别是|| & && | ; ;; () |& <newline>。只要看到这些控制操做符,就能够认为它前面的字符串是一个完整的命令。

定义2:关键字(Reserved Words) 我没有将其翻译成保留字,很显然,做为编程语言来讲,它们应该叫作关键字。一门编程语言确定必须得提供选择、循环等流程控制语句,还得提供定义函数的功能。这些功能只能经过关键字实现。在 Bash 中,只有 22 个关键字,它们是“! case coproc do done elif else esac fi for function if in select then until while { } time [[ ]]”。这其中有很多的特别之处,好比“! { } [[ ]]”等符号都是关键字,也就是说它们当关键字使用时至关于一个单词,也就是说它们和别的单词必须以元字符分开(不然没法成为独立的单词)。这也是为何在 Bash 中使用“! { } [[ ]]”时常常要在它们周围留空格的缘由。(再一次证实=是一个很变态的特例,由于它既不是元字符,也不是控制操做符,更加不是关键字,它究竟是什么?)

下面开始推导 Bash 脚本语言的语法:

推导1:简单命令(Simple command) 就是一条简单的命令,它能够是一个以上述控制操做符结尾的字符串。好比单独放在一行的 uname -r 命令(单独放在一行的命令实际上是以<newline>结尾,<newline>是控制操做符),或者虽然不单独放在一行,可是以;&结尾,好比 uname -r; who; pwd; gvim& 其中每个命令都是一个简单命令(固然,这四个命令放在一块儿的这行代码不叫简单命令),;就是简单地分割命令,而&还有让命令在后台执行的功能。这里比较特殊的是双分号;;,它只用在 case 语句中。

推导2:管道(Pipe Line) 管道是 Shell 中的精髓,就是让前一个命令的输出成为后一个命令的输入。管道的完整语法是这样 [time [-p]] [ ! ] command1 | command2 或这样 [time [-p]] [ ! ] command1 |& command2 的。其中 time 关键字和 ! 关键字都是可选的(使用[...]指出哪些部分是可选的),time 关键字能够计算命令运行的时间,而 ! 关键字是将命令的返回状态取反。看清楚 ! 关键字周围的空格哦。若是使用|,就是把第一个命令的标准输出做为第二个命令的标准输入,若是使用|&,则将第一个命令的标准输出和标准错误输出都当成第二个命令的输入。

推导3:命令序列(List) 若是多个简单命令或多个管道放在一块儿,它们之间以; & <newline> || &&等控制操做符分开,就称之为一个命令序列。关于||&&,熟悉 C、C++、Java 等编程语言的朋友们确定也不会陌生,它们遵循一样的短路求值的思想。好比 command1 || command2 只有当 command1 执行不成功的时候才执行 command2,而 command1 && command2 只有当 command1 执行成功的时候才执行 command2。

推导4:复合命令(Compound Commands) 若是将前面的简单命令、管道或者命令序列以更复杂的方式组合在一块儿,就能够构成复合命令。在 Bash 中,有 4 种形式的复合命令,它们分别是 (list){ list; }((expression))[[ expression ]] 。请注意第 2 种形式和第 4 种形式大括号和中括号周围的空格,也请注意第 2 种形式中 list 后面的;,不过若是}另起一行,则不须要;,由于<newline>;是起一样做用的。在以上4种复合命令中, (list) 是在一个新的Shell中执行命令序列,这些命令的执行不会影响当前Shell的环境变量,而 { list; } 只是简单地将命令序列分组。后面两种表达式求值前面已经讲过,这里就不讲了。后面可能会详细列出逻辑表达式求值的选项。

上面的4步推导是一步更进一步的,是由简单逐渐到复杂的,最简单的命令能够组合成稍复杂的管道,再组合成更复杂的命令序列,最后组成最复杂的复合命令。

下面是 Bash 脚本语言的流程控制语句,以下:

  1. for name [ [ in [ word ... ] ] ; ] do list ; done

  2. for (( expr1 ; expr2 ; expr3 )) ; do list ; done

  3. select name [ in word ] ; do list ; done

  4. case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac

  5. if list; then list; [ elif list; then list; ] ... [ else list; ] fi

  6. while list-1; do list-2; done

  7. until list-1; do list-2; done

上面的公式你们看得懂吧,我相信你们确定看得懂。其中的 [...] 表明的是能够有也能够真没有的部分。在以上公式中,请注意第 2 个公式 for 循环中的双括号,它执行的是其中的表达式的算术运算,这是和其它高级语言的 for 循环最像的,可是很遗憾,Bash 中的算术表达式目前只能计算整数。再请注意第 3 个公式,select 语法,和 for...in... 循环的语法比较相似,可是它能够在屏幕上显示一个菜单。若是我没有记错的话,Basic 语言中应该有这个功能。其它的控制结构在别的高级语言中都很常见,就不须要我在这里啰嗦了。

最后,再来展现一下如何定义函数:

   name () compound-command [redirection]

  或者

   function name [()] compound-command [redirection]

能够看出,若是有 function 关键字,则()是可选的,若是没有 function 关键字,则()是必须的。这里须要特别指出的是:函数体只要求是 compound-command,我前面总结过 compound-command 有四种形式,因此有时候定义一个函数并不会出现{ }哦。以下图,这样的函数也是合法的:

That's all。这就是 Bash 脚本语言的所有语法。就这么简单。

好像忘了点什么?对了,还有输入输出重定向没有讲。输入输出重定向是 Shell 中又一个伟大的发明,它的存在有着它独特的哲学意义。这个请看下一节。

输入输出重定向

Unix 世界有一个伟大的哲学:一切皆是文件。(这个扯得有点远。) Unix 世界还有一个伟大的哲学:建立进程比较方便。(这个扯得也有点远。)并且,每个进程一建立,就会自动打开三个文件,它们分别是标准输入、标准输出、标准错误输出,普通状况下,它们链接到用户的控制台。在 Shell 中,使用数字来标识一个打开的文件,称为文件描述符,并且数字 0、 一、 2 分别表明标准输入、标准输出和标准错误输出。在 Shell 中,能够经过><将命令的输入、输出进行重定向。结合 exec 命令,能够很是方便地打开和关闭文件。须要注意的是,当文件描述符出如今><右边的时候,前面要使用&符号,这多是为了和数学表达式中的大于和小于进行区别吧。使用&-能够关闭文件描述符。

> < & 数字 exec -,这就是输入输出重定向的所有。下面的公式中,我使用 n 表明数字,若是是两个不一样的数字,则使用 n一、n2,使用 [...] 表明可选参数。输入输出重定向的语法以下:

[n]> file        #重定向标准输出(或 n)到file。
[n]>> file       #重定向标准输出(或 n)到file,追加到file末尾。
[n]< file        #将file重定向到标准输入(或 n)。
[n1]>&n2         #重定向标准输出(或 n1)到n2。
2> file >&2      #重定向标准输出和错误输出到file。
| command        #将标准输出经过管道传递给command。
2>&1 | command   #将标准输出和错误输出一块儿经过管道传递给command,等同于|&。

请注意,数字和><符号之间是没有空格的。结合 exec,能够很是方便地使用一个文件描述符来打开、关闭文件,以下:

echo Hello >file1
exec 3<file1 4>file2  #打开文件
cat <&3 >&4           #重定向标准输入到 3,标准输出到 4,至关于读取file1的内容而后写入file2
exec 3<&- 4>&-        #关闭文件
cat file2
#显示结果为 Hello
 
#还能够暂存和恢复文件描述符,以下:
exec 5>&2            #把原来的标准错误输出保存到文件描述符5上
exec 2> /tmp/$0.log  #重定向标准错误输出
...
exec 2>&5            #恢复标准错误输出
exec 5>&-            #关闭文件描述符5,由于不须要了

还能够将<>一块儿使用,表示打开一个文件进行读写。

除了 exec,输入输出重定向和 read 命令配合也很好用,read 命令每次读取文件的一行。可是要注意的是,输入输出重定向放到 for、while 等循环的循环体和循环外,效果是不同的。以下图:

另外,输入输出重定向符号><还能够和()一块儿使用,表示进程替换(Process substitution),如>(list)<(list)。结合前面提到的<>(list)的含义,进程替换的做用是很容易猜到的哦。

Bash 脚本编程语言的美学:大道至简

若是你问我 Bash 脚本语言哪里美?我会回答:简洁就是美。请看下面逐条论述:

1.使用了简洁的抽象的符号。Bash 脚本语言几乎使用到了键盘上可以找到的全部符号,$用做字符串展开,|用做管道,<>用做输入输出重定向,一点都不浪费;

2.只使用了 9 个元字符、10 个控制操做符和 22 个关键字,就构建了一个完整的、面向字符串编程的语言;

3.概念上具备很好的一致性;例如 (list) 复合命令的功能是执行括号内的命令序列,而$用于引导字符串展开,因此 $(list) 用于命令替换(因此我前面说$()形式的命令替换比`...`形式的命令替换更加具备一致性)。再例如 ((expresion)) 用于数学表达式求值,因此 $((expression)) 表明算术展开。再例如{},配合使用,且中间没有空格时,表明大括号展开,可是当须要使用{ }来定义复合命令时,必须把{ }当关键字,它们和它里面的内容必须以空格隔开,并且}和它前面的一条命令之间必须有一个;或者<newline>。这些概念上的一致性设计得很是精妙,使用起来天然而然可让人体会到一种美感;

4.完美解决了一个命令执行时的输出和运行状态的分离。有其它编程语言经历的人也常常会遇到这样的问题:当咱们调用一个函数的时候,函数可能会产生两个结果,一个是函数的返回值,一个是函数调用是否成功。在 C# 和 Java 等高级语言中,每每使用 try...catch 等捕获异常的方式来判断函数调用是否成功,但仍然有程序员让函数返回 null 表明失败,而 C 语言这种没有异常机制的语言,实在是难以判断一个函数的返回值究竟如何表示该函数调用是否成功(好比就有不少 API 让函数返回 -1 表明失败,而有的函数运行失败是会设置 errno 全局变量)。在 Bash 中,命令运行的状态和命令的标准输出区分很明确,若是你须要命令的标准输出,使用命令替换来生成字符串,若是你只须要命令的运行状态,直接将命令写在 if 语句之中便可,或者使用 $? 特殊变量来检查上一条命令的运行状态。若是不想在检查命令运行状态的时候让命令的标准输出影响用户,能够把它重定向到 /dev/null,像这样:

if cat /etc/passwd | grep youxia > /dev/null; then echo 'youxia is exist'; fi

5.使用管道和输入输出重定向让文件的读写变得简单。想想在 C 语言中怎么读文件吧,除了麻烦的 open、close 不说,每读一个字符串还得先准备一个 buffer,准备长了怕浪费空间,准备短了怕缓冲区溢出,虐心啦。使用 Bash,那真的是太方便了。

6.它还有邪恶的 eval 哦,eval 命令实在是太强大了,请看下图,模拟指针进行查表:

固然,自从 Bash 3 以后,Bash 自己就提供了间接引用的功能(使用“${!var}”)。

例外:

  Bash 语言也并非在全部的方面都是完美的,还存在几个特别的例外,像前面说的=。除了=以外,()也有一个使用不一致的地方,那就是对数组的初始化,例如 array=(a b c d e f) ,这和前面讲的()用于在子 Shell 中执行命令序列还真的是不一致。

总结

以上内容是个人胡言乱语,由于以上内容即没法教会你们完整的 Bash 语法,也没法教会你们用 Bash 作任何一点有意义的工做。若是想用 Bash 干点实事,建议你们阅读 O'Reilly 出的《Shell脚本学习指南》。

求打赏

我对此次写的这个系列要求是很是高的:首先内容要有意义、够充实,信息量要足够丰富;其次是每个知识点要讲透彻,不能模棱两可含糊不清;最后是包含丰富的截图,让那些不想装 Linux 系统的朋友们也能够领略到 Linux 桌面的风采。若是个人努力获得你们的承认,能够扫下面的二维码打赏一下:

版权申明

该随笔由京山游侠在2018年10月04日发布于博客园,引用请注明出处,转载或出版请联系博主。QQ邮箱:1841079@qq.com

相关文章
相关标签/搜索