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等等
你可以通过以下这张图片了解他们之间的关系:
一件奇特的事情是你可以在某一个Shell里运行另一个Shell:不过这种特性是很自然的,因为“将运行其他程序统一为一体”正是Shell的一大特性,这里不过是把另一个Shell当作另一个程序罢了。
这个特色让我们得以轻松比较不同的Shell。比如在zsh,tcsh等中,$dirstack
变量会存储最近访问过的路径列表,但这个变量在Bash中不存在:
(我在tcsh和zsh中$dirstack
的值不同是因为我在两个shell里访问过的路径不同)
尽管如此,它们在很多时候是相通的。
Step 02
Globbing:通配符
通配符的英文是wildcards,又叫做globbing patterns,它用来处理一组路径,可以被看做早期的正则表达式。之所以叫glob是因为Unix早期有一个/etc/glob
文件来保存通配符模板。它现在被内置在Bash中,Python中也有一个glob模块。
一个简单的例子如下所示,和正则式有相通之处,这里不详细讲解:
但是你会发现,如果你输入的是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有所不同:
可以看到,无论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里定义好的命令,最常见的是cd
,builtin
自己也是一个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
来解除。
程序,在这里指可执行的文件。包括grep
,sed
,vi
等都是常见的程序。要判断一个命令到底是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中,ls
是ls -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
这个文件并不存在, 则会输出以下结果:
可见标准错误流并不能通过管道。但如果你把上面的 |
换成 |&
结果就完全不同了:|&
可以同时传送标准输入和标准错误,因为这行错误信息中有一个“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/