在这节课中,我们将会展示bash作为脚本语言的一些基础操作,以及几种最常用的shell工具。我们已经学习来如何在shell中执行命令,并使用管道将命令组合使用。但是,很多情况下我们需要执行一系列的操作并使用条件或循环这样的控制流。本节中,我们会专注于bash脚本,因为它最流行,应用更为广泛。
在bash中为变量赋值的语法是foo=bar,访问变量中存储的数值,其语法为 $foo。 需要注意的是,foo = bar (使用空格隔开)是不能正确工作的,因为解释器会调用程序foo 并将 = 和 bar作为参数。 总的来说,在shell脚本中使用空格会起到分割参数的作用,有时候可能会造成混淆,请务必多加检查。
Bash中的字符串通过'
和 "
分隔符来定义,但是它们的含义并不相同。以'
定义的字符串为原义字符串,其中的变量不会被转义,而 "
定义的字符串会将变量值进行替换。
1 | foo=bar |
1.打印 bar
1 | echo '$foo'` |
2.打印 $foo
和其他大多数的编程语言一样,bash也支持if, case, while 和 for 这些控制流关键字。同样地, bash 也支持函数,它可以接受参数并基于参数进行操作。下面这个函数是一个例子,它会创建一个文件夹并使用cd进入该文件夹。
1 | #创建并打开一个文件夹 |
这里 $1 是脚本的第一个参数。与其他脚本语言不同的是,bash使用了很多特殊的变量来表示参数、错误代码和相关变量。
符号 | 含义 |
---|---|
$0 | 脚本名 |
$1 到$9 | 脚本的参数,其中$1 是第一个参数,依此类推 |
$@ | 所有参数 |
$# | 参数个数 |
$? | 前一个命令的返回值 |
$$ | 当前脚本的进程识别码 |
!! | 完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用 sudo !!再尝试一次。 |
$_ | 上一条命令的最后一个参数。如果你正在使用的是交互式shell,你可以通过按下 Esc 之后键入 . 来获取这个值。 |
- 命令返回值(类比C语言的退出码)
1 | echo hello |
命令通常使用 STDOUT
来返回输出值,使用STDERR
来返回错误及错误码,便于脚本以更加友好的方式报告错误。 返回码或退出状态是脚本/命令之间交流执行状态的方式。返回值0表示正常执行,其他所有非0的返回值都表示有错误发生。退出码可以搭配&&
(与操作符) 和 ||
(或操作符)使用,用来进行条件判断,决定是否执行其他程序。它们都属于短路运算符(short-circuiting) 同一行的多个命令可以用 ;
分隔。程序 true 的返回码永远是0
,false 的返回码永远是1
。让我们看几个例子:
1 | false || echo "Oops, fail" |
另一个常见的模式是以变量的形式获取一个命令的输出,这可以通过命令替换 (command substitution)实现。例如:
1 | foo=$(pwd) #foo存储当前工作目录 |
当您通过 $(CMD)
这样的方式来执行CMD
这个命令时,它的输出结果会替换掉 $(CMD)
。例如,如果执行 for file in $(ls)
,shell首先将调用ls
,然后遍历得到的这些返回值。还有一个冷门的类似特性是 进程替换(process substitution), <(CMD)
会执行 CMD
并将结果输出到一个临时文件中,并将 <(CMD)
替换成临时文件名。这在我们希望返回值通过文件而不是STDIN
传递时很有用。例如, diff <(ls foo) <(ls bar)
会显示文件夹 foo 和 bar 中文件的区别。
下面这个例子展示了一部分上面提到的特性。这段脚本会遍历我们提供的参数,使用grep 搜索字符串 foobar,如果没有找到,则将其作为注释追加到文件中。
1 |
|
在条件语句中,我们比较 $?
是否等于0。在bash中进行比较时,尽量使用双方括号 [[ ]]
而不是单方括号 [ ]
,这样会降低犯错的几率,尽管这样并不能兼容 sh。
注意:此时可能会遇到无权限执行,需要:
1 | chmod 777 <文件名> |
当执行脚本时,我们经常需要提供形式类似的参数。bash使我们可以轻松的实现这一操作,它可以基于文件扩展名展开表达式。
3.通配符
- 当你想要利用通配符进行匹配时,你可以分别使用
?
和*
来匹配一个或任意个字符。例如,对于文件foo, foo1, foo2, foo10 和 bar,rm foo?
这条命令会删除foo1 和 foo2 ,而rm foo*
则会删除除了bar之外的所有文件。 - 花括号{} :当你有一系列的指令,其中包含一段公共子串时,可以用花括号来自动展开这些命令。这在批量移动或转换文件时非常方便。
1 | convert image.{png,jpg |
编写 bash 脚本有时候会很别扭和反直觉。例如 shellcheck这样的工具可以帮助你定位sh/bash脚本中的错误。
4.shebang行
脚本并不一定只有用bash写才能在终端里调用。比如说,这是一段Python脚本,作用是将输入的参数倒序输出:
1 | #!/usr/local/bin/python |
内核知道去用python解释器而不是shell命令来运行这段脚本,是因为脚本的开头第一行的shebang行,这告诉内核解释器的位置。在 shebang 行中使用 env 命令是一种好的实践,它会利用环境变量中的程序来解析该脚本,这样就提高来您的脚本的可移植性。env 会利用我们第一节讲座中介绍过的PATH 环境变量来进行定位。 例如,使用了env的shebang看上去时这样的#!/usr/bin/env python
。
shell函数和脚本有如下一些不同点:
- 函数只能用与shell使用相同的语言,脚本可以使用任意语言。因此在脚本中包含 shebang 是很重要的。
- 函数仅在定义时被加载,脚本会在每次被执行时加载。这让函数的加载比脚本略快一些,但每次修改函数定义,都要重新加载一次。
- 函数会在当前的shell环境中执行,脚本会在单独的进程中执行。因此,函数可以对环境变量进行更改,比如改变当前工作目录,脚本则不行。脚本需要使用 export 将环境变量导出,并将值传递给环境变量。
- 与其他程序语言一样,函数可以提高代码模块性、代码复用性并创建清晰性的结构。shell脚本中往往也会包含它们自己的函数定义。
在上一节中我们介绍过,最常用的方法是为对应的命令行添加-h
或 --help
标记。另外一个更详细的方法则是使用man
命令。man
命令是手册(manual)的缩写,它提供了命令的用户手册。
例如,man rm 会输出命令 rm 的说明,同时还有其标记列表,包括之前我们介绍过的-i。 事实上,目前我们给出的所有命令的说明链接,都是网页版的Linux命令手册。即使是您安装的第三方命令,前提是开发者编写了手册并将其包含在了安装包中。在交互式的、基于字符处理的终端窗口中,一般也可以通过 help
命令或键入 ?
来获取帮助。
有时候手册内容太过详实,让我们难以在其中查找哪些最常用的标记和语法。 TLDRpages
是一个很不错的替代品,它提供了一些案例,可以帮助您快速找到正确的选项。
例如,自己就常常在tldr(too long don’t read)上搜索tar 和 ffmpeg 的用法。
1 | 查看tar用法 |
5.查找文件
5.1 find
程序员们面对的最常见的重复任务就是查找文件或目录。所有的类UNIX系统都包含一个名为 find
的工具,它是shell上用于查找文件的绝佳工具。find命令会递归地搜索符合条件的文件,例如:
1 | 查找所有名称为src的文件夹 |
尽管 find 用途广泛,它的语法却比较难以记忆。例如,为了查找满足模式 PATTERN 的文件,您需要执行 find -name '*PATTERN*'
(如果您希望模式匹配时是不区分大小写,可以使用-iname
选项)
您当然可以使用alias设置别名来简化上述操作,但shell的哲学之一便是寻找(更好用的)替代方案。 记住,shell最好的特性就是您只是在调用程序,因此您只要找到合适的替代程序即可(甚至自己编写)。
5.2 其他查找工具
例如, fd
就是一个更简单、更快速、更友好的程序,它可以用来作为find的替代品。它有很多不错的默认设置,例如输出着色、默认支持正则匹配、支持unicode并且我认为它的语法更符合直觉。以模式PATTERN
搜索的语法是 fd PATTERN
。
大多数人都认为 find 和 fd 已经很好用了,但是有的人可能想知道,我们是不是可以有更高效的方法,例如不要每次都搜索文件而是通过编译索引或建立数据库的方式来实现更加快速地搜索。
这就要靠 locate
了。 locate 使用一个由 updatedb负责更新的数据库,在大多数系统中 updatedb 都会通过 cron每日更新。这便需要我们在速度和时效性之间作出权衡。而且,find 和类似的工具可以通过别的属性比如文件大小、修改时间或是权限来查找文件,locate则只能通过文件名。 here有一个更详细的对比。
6.查找代码
查找文件是很有用的技能,但是很多时候您的目标其实是查看文件的内容。一个最常见的场景是您希望查找具有某种模式的全部文件,并找它们的位置。
为了实现这一点,很多类UNIX的系统都提供了grep
命令,它是用于对输入文本进行匹配的通用工具。它是一个非常重要的shell工具,我们会在后续的数据清理课程中深入的探讨它。
grep 有很多选项,这也使它成为一个非常全能的工具。其中我经常使用的有
- -C:获取查找结果的上下文(Context)
- -v:将对结果进行反选(Invert),也就是输出不匹配的结果
举例来说, grep -C 5 会输出匹配结果前后五行。当需要搜索大量文件的时候,使用 -R 会递归地进入子目录并搜索所有的文本文件。
但是,我们有很多办法可以对 grep -R 进行改进,例如使其忽略.git 文件夹,使用多CPU等等。替代品中常用的是 ripgrep (rg) ,因为它速度快,而且用法非常符合直觉。例子如下:
1 | 查找所有使用了 requests 库的文件 |
与 find/fd 一样,重要的是你要知道有些问题使用合适的工具就会迎刃而解,而具体选择哪个工具则不是那么重要。
7.查找已执行的shell命令
目前为止,我们已经学习了如何查找文件和代码,但随着你使用shell的时间越来越久,可能想要找到之前输入过的某条命令。首先,按向上的方向键会显示你使用过的上一条命令,继续按上键则会遍历整个历史记录。
7.1 history命令
history 命令允许您以程序员的方式来访问shell中输入的历史命令。这个命令会在标准输出中打印shell中的里面命令。如果我们要搜索历史记录,则可以利用管道将输出结果传递给 grep 进行模式搜索。 history | grep find
会打印包含find
子串的命令。
7.2 ctrl+R
命令
对于大多数的shell来说,您可以使用 Ctrl+R
对命令历史记录进行回溯搜索。敲 Ctrl+R
后可以输入子串来进行匹配,查找历史命令行。
反复按下就会在所有搜索结果中循环。在 zsh中,使用方向键上或下也可以完成这项工作。
Ctrl+R 可以配合 fzf
使用。fzf 是一个通用对模糊查找工具,它可以和很多命令一起使用。这里我们可以对历史命令进行模糊查找并将结果以赏心悦目的格式输出。
7.3 命令补全—fish
另外一个和历史命令相关的技巧我喜欢称之为基于历史的自动补全。 这一特性最初是由 fish shell
创建的,它可以根据您最近使用过的开头相同的命令,动态地对当前对shell命令进行补全。这一功能在 zsh 中也可以使用,它可以极大的提高用户体验。
你可以修改 shell history 的行为,例如,如果在命令的开头加上一个空格,它就不会被加进shell记录中。当你输入包含密码等命令时会用到这一特性。 为此你需要在.bashrc
中添加HISTCONTROL=ignorespace
或者向.zshrc
添加 setopt HIST_IGNORE_SPACE
。 如果你不小心忘了在前面加空格,可以通过编辑bash_history
或 .zhistory
来手动地从历史记录中移除那一项。
需要使用fish时,可以在shell中输入fish
8.文件夹导航
之前对所有操作我们都默认一个前提,即您已经位于想要执行命令的目录下,但是如何才能高效地在目录间随意切换呢?有很多简便的方法可以做到,比如设置alias,使用 ln -s创建符号连接等。而开发者们已经想到了很多更为精妙的解决方案。
由于本课程的目的是尽可能对你的日常习惯进行优化。因此,我们可以使用fasd和autojump这两个工具来查找最常用或最近使用的文件和目录。
Fasd 基于 frecency对文件和文件排序,也就是说它会同时针对频率(frequency )和时效( recency)进行排序。默认情况下,fasd使用命令 z 帮助我们快速切换到最常访问的目录。例如, 如果您经常访问/home/user/files/cool_project 目录,那么可以直接使用 z cool 跳转到该目录。对于 autojump,则使用j cool代替即可。
还有一些更复杂的工具可以用来概览目录结构,例如 tree, broot 或更加完整的文件管理器,例如 nnn 或 ranger。