Linux折腾记十:Bash脚本编程语言中的美学与哲学linux bash编程与脚本应用实战




Linux折腾记十:Bash脚本编程语言中的美学与哲学linux bash编程与脚本应用实战

2022-07-20 20:28:08 网络知识 官方管理员

  我承认,我再一次地当了标题党。但是不可否认,这一定是一篇精华随笔。在这一篇中,我将探讨Bash脚本语言中的美学与哲学。这不是一篇Bash脚本编程的教程,但是却能让人更加深入地了解Bash脚本编程,更加快速地学习Bash脚本编程。阅读这篇随笔,不需要你有Bash编程的经验,但一定要和我一样热衷于探索各种编程语言的本质,感悟它们的魅力。

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

一、一切皆是字符串

  Bash是一个Shell,Shell出现的初衷是为了将系统中的各种工具粘合在一起,所以它最根本的功能是调用各种命令。但是Bash又提供了丰富的编程功能。我们经常对编程语言进行分类,比如面向过程的语言、面向对象的语言、面向函数的语言等等。可以把Bash脚本语言看成是一个面向字符串的语言。Bash语言的本质就是:一切都是字符串。看看下图中的这些变量:

Linux折腾记,Bash,脚本编程语言,美学,哲学

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

二、引用和元字符

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

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

Linux折腾记,Bash,脚本编程语言,美学,哲学

  在上图中,我展示了Bash中生成字符串的7种方法:大括号展开、波浪符展开、参数展开、命令替换、算术展开、单词分割和文件路径展开。还有两种生成字符串的方式没有讲(Processsubstitution和历史命令展开)。在使用Bash脚本编程的时候,了解以上7种字符串生成的方式就够了。在交互式使用Bash命令行的时候,还需要了解历史命令展开,熟练使用历史命令展开可以让人事半功倍。

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

  如果想让任何一个字符都不具有特殊含义,可以使用单引号将字符串包围。比如使用正则表达式的时候,还比如使用sed、awk等工具的时候,由于sed和awk自己执行的命令中往往包含有很多特殊字符,所以它们的命令最好用单引号包围。比如使用awk命令显示/etc/passwd文件中的每个用户的用户名和全名,可以使用这个命令awk-e'{print$1,$5}',其中,传递给awk的命令用单引号包围,说明bash不执行其中的任何替换或展开。

  另外一个特殊的字符是\,它也是引用的一种。它可以解除紧跟在它后面的一个特殊字符的特殊含义(引用)。之所以需要\的存在,是因为在Bash中,有些字符称为元字符,这些字符一旦出现,就会将一个字符串分割为多个子串。如果需要在一个字符串中包含这些元字符本身,就必须对它们进行引用。如下图:

Linux折腾记,Bash,脚本编程语言,美学,哲学

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

三、字符串从哪里来、到哪里去

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

  字符串展开的流程如下:

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

    2.如果字符串是用来给变量赋值,则不管它是否被双引号包围,都认为它被双引号包围;

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

以上三个流程可以通过下图证明:

Linux折腾记,Bash,脚本编程语言,美学,哲学

    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个等号会被当成赋值运算符。

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

Linux折腾记,Bash,脚本编程语言,美学,哲学

四、再加上一点点的定义,就可以推导出整个Bash脚本语言的语法了

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

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

    定义2:关键字(ReservedWords) 我没有将其翻译成保留字,很显然,作为编程语言来说,它们应该叫做关键字。一门编程语言肯定必须得提供选择、循环等流程控制语句,还得提供定义函数的功能。这些功能只能通过关键字实现。在Bash中,只有22个关键字,它们是!casecoprocdodoneelifelseesacfiforfunctionifinselectthenuntilwhile{}time[[]]。这其中有不少的特别之处,比如!{}[[]]等符号都是关键字,也就是说它们当关键字使用时相当于一个单词,也就是说它们和别的单词必须以元字符分开(否则无法成为独立的单词)。这也是为什么在Bash中使用!{}[[]]时经常要在它们周围留空格的原因。(再一次证明=是一个很变态的特例,因为它既不是元字符,也不是控制操作符,更加不是关键字,它到底是什么?)

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

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

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

    推导3:命令序列(List) 如果多个简单命令或多个管道放在一起,它们之间以;&<newline>||&&等控制操作符分开,就称之为一个命令序列。关于;&<newline>前面已经讲过了,无需重复。关于||和&&,熟悉C、C++、Java等编程语言的朋友们肯定也不会陌生,它们遵循同样的短路求值的思想。比如command1||command2只有当command1执行不成功的时候才执行command2,而command1&&command2只有当command1执行成功的时候才执行command2。

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

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

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

    1.forname[[in[word...]];]dolist;done;

    2.for((expr1;expr2;expr3));dolist;done;

    3.selectname[inword];dolist;done;

    4.casewordin[[(]pattern[|pattern]...)list;;]...esac;

    5.iflist;thenlist;[eliflist;thenlist;]...[elselist;]fi;

    6.whilelist-1;dolist-2;done;

    7.untillist-1;dolist-2;done。

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

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

    name()compound-command[redirection]

    或者

    functionname[()]compound-command[redirection]

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

Linux折腾记,Bash,脚本编程语言,美学,哲学

  That'sall。这就是Bash脚本语言的全部语法。就这么简单。

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

五、输入输出重定向

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

  ><&数字exec-,这就是输入输出重定向的全部。下面的公式中,我使用n代表数字,如果是两个不同的数字,则使用n1、n2,使用[...]代表可选参数。输入输出重定向的语法如下:

复制代码

发表评论:

最近发表
网站分类
标签列表