Bash Tutorial

对程序员而言,Shell是一种必备技能。目前,Bash是最流行的Shell程序之一,因此学会Bash的使用是很有必要的。

Step 01

什么是Bash?

Bash是“Bourne Again SHell”的简称,从名字来看,它是一个Shell程序,并且和Bourne(也是一种Shell)有着某种继承关系。

什么是Shell?

Shell是一种可执行的二进制文件,它接收你输入在终端的命令,并最终将这些命令翻译为系统调用,借助你的操作系统提供的API完成一系列工作。在中文里,Shell的意思是“外壳”:你可以把它想象成套在操作系统上的一层外壳,你所有的操作都在外壳上进行,但实际发挥作用的是外壳里的东西,就像是你通过外壳上的按钮来操作一台电视机一样。

除了Bash之外?

除了Bash之外当然还有很多其他的Shell程序,比如我自己使用的就是zsh。此外还有sh(就是刚才说的Bourne),ash,dash,ksh等等

你可以通过以下这张图片了解他们之间的关系:

MNtqNF.png

一件奇特的事情是你可以在某一个Shell里运行另一个Shell:不过这种特性是很自然的,因为“将运行其他程序统一为一体”正是Shell的一大特性,这里不过是把另一个Shell当作另一个程序罢了。

这个特色让我们得以轻松比较不同的Shell。比如在zsh,tcsh等中,$dirstack变量会存储最近访问过的路径列表,但这个变量在Bash中不存在:

MNG5X6.md.png

(我在tcsh和zsh中$dirstack的值不同是因为我在两个shell里访问过的路径不同)

尽管如此,它们在很多时候是相通的。

Step 02

Globbing:通配符

通配符的英文是wildcards,又叫做globbing patterns,它用来处理一组路径,可以被看做早期的正则表达式。之所以叫glob是因为Unix早期有一个/etc/glob文件来保存通配符模板。它现在被内置在Bash中,Python中也有一个glob模块。

一个简单的例子如下所示,和正则式有相通之处,这里不详细讲解:

Maixmt.md.png

但是你会发现,如果你输入的是ls '*'echo '*',这里的*并没有发生任何匹配,而是直接被当作这个字符来处理了。
当然正则式和glob的区别还是存在的并且不少,而且Bash本身也对glob做了一些拓展。这里不再细究。

Variables:变量

MYSTRING=astring
echo $MYSTRING

非常容易理解,值得注意的是取值的时候需要加上$,并且是不要乱加空格。如果变量值内部有空格,就需要引号,这里单引号双引号都可以:

MYSTRING="a sentence"

但单双引号的一大区别是,当你在引号里加入变量名的时候:

MYSENTENCE="This is $MYSTRING"
echo $MYSENTENCE  # OUTPUT: This is astring
MYSENTENCE='This is $MYSTRING'
echo $MYSENTENCE  # OUTPUT: This is $MYSTRING

前者会取变量的值,而后者不会。

事实上Bash对待变量和单双引号这个事情的效果还挺有趣的,而且和我通常使用的zsh有所不同:

MaEEXd.md.png

可以看到,无论MYGLOB被赋值*还是"*",存在MYGLOB里的都是一个单独的字符;并且在Bash中变量的值还可以进一步被当作通配符被解析,而zsh则不会。

Shell本身自带一些变量,比如PPID,它储存着当前Bash的父进程ID,这种变量是只读的。我们自己也可以设置只读变量:

readonly MYVAR=var
MYVAR=antohervar  # error

还有一个问题。当你设置了一个变量,又打开了另一个进程,在新的进程里你的变量就不存在了。但如果你使用export 命令,这个变量就会出现在你当前shell中运行的所有进程的环境中了:

export MYSTRING=astring

变量还可以是array或associate array的形式。这里不详细介绍。

Functions: 函数

像一个编程语言一样,Shell还可以编写函数:

function myfunc {
    echo $1
    echo $2
}

这个声明和普通的编程语言没什么区别,主要的不同就是它并不指出参数,并且它也并不在意你到底给它几个参数,因为多余的它会忽略,缺少的它会认为是空:

myfunc hello world haha  # output: hello world
myfunc hello world       # output: hello world
myfunc hello             # output: hello(and an empty line)

Shell函数中的变量也有其作用域。外部变量在函数中都可见,而想要函数内部的变量不被外部可见,可以在声明前加上local

Step 03

Commands: 命令

Bash中的命令主要是四种:

  • 内置(builtins)
  • 函数(functions)
  • 程序(programs)
  • 别名(Aliases)

内置命令是指直接在Bash里定义好的命令,最常见的是cdbuiltin自己也是一个builtin。当你输入builtin cd /dir 的时候它会和普通的cd一样运行;而如果是一个非builtin的命令,则会被提示没有这个命令。

一个它可能会发挥作用的场景是:假如你定义了一个函数也叫cd,那么它会覆盖原来builtin的cd,这时候输入builtin cd /dir 还是会唤起builtin的版本。

说到函数,要查看当前环境下的所有函数可以使用:

declare -f  # show all the functions in the environment

declare -F  # only show their names

对于已经定义的函数,可以通过unset -f来解除。

程序,在这里指可执行的文件。包括grepsedvi 等都是常见的程序。要判断一个命令到底是builtin还是program,可以通过刚才说的,在命令前加上builtin,也可以使用which <command>查看命令是否保存在文件系统中来确定。

which 自身是一个builtin。当你输入which which 时,可以看到这个结果。

别名很容易理解,就是一个字符串,你输入shell之后它会自动把你翻译给这个别名所指代的另一个字符串,这个字符串可能是一个命令。

用法是:

alias a=b  # make alias
unalias a  # delete this alias
alias      # show alias available

在我的zsh中,lsls -G的别名。ls -G中的ls是一个程序,这里被这个别名给覆盖了。

Pipes and Redirects:管道和重定向

重定向符号是>

echo "content of file1" > file1

它把前一个指令的输出发送到后面的文件中;如果文件不存在它会创建一个。默认的输出是标准输出流,如果要输出标准错误流,则需要在>前加上文件描述符(0是标准输入流,1是标准输出流,2是标准错误流),默认是1,标准输出流:

command 2> /dev/null

顺带一提,/dev/null是一个特殊的文件,传给它的所有东西都会被直接丢弃。

我们知道标准输出流和标准错误流是分别独立存在的。有时当我们写程序,并且程序有输出、又出现报错信息的时候,我们会发现它们是交替出现的,这正是这一特性的体现。但我们可以将标准错误流重定向到标准输出流指向的地方:

command_does_not_exist 2>&1

通常来说它们两个都是指向终端,所以没有什么区别,但是它们中有人被重定向到文件,情况就有所不同了:

command_does_not_exist 2>&1 > outfile
command_does_not_exist > outfile 2>&1

这两个命令看似没什么区别,但实际上第一行会把错误信息输出终端,而第二个输出错误,而是把错误信息存进了outfile中。区别就在于,第一行首先把标准错误流的输出指向了标准输出流指向的地方——终端,之后它再把标准输出流重定向到了文件。而第二行首先把command_does_not_exist 的标准输出流重定向到了outfile,之后再把标准错误流指向标准输出流指向的地方,而这时,是文件。

管道则是一个非常形象的名字。它将前一个命令的标准输出于是弄给下一个命令作为输入,好像通过管道连接一样。管道运算符是|

cat file1 | grep -c file

这个命令会找出文件file1 中含有“file”的行数。

但管道是只接收标准输出流的。假如上面这条命令中,file1这个文件并不存在, 则会输出以下结果:

MbeuLR.md.png

可见标准错误流并不能通过管道。但如果你把上面的 | 换成 |&结果就完全不同了:|& 可以同时传送标准输入和标准错误,因为这行错误信息中有一个“file”,因此输出结果会是1。

管道和重定向有相似之处,管道传送的对象是另一个命令,而重定向的对象则是文件。

到这里,我认为简单的Bash tutorial就暂时结束了。虽然没有涉及很多具体的命令,但我们建立了一个Bash的big picture。当然这里缺少了最关键的部分:shell脚本。不过我希望能有空再写一篇博客谈谈这个。

This blog is under a CC BY-NC-SA 3.0 Unported License
Link to this article: http://huangweiran.club/2019/11/14/Bash-Tutorial/