missing-semester(2)-shell工具和脚本

shell脚本

在shell中,空格十分重要。

$ foo=bar %可以执行
$ foo = bar %无法执行

注意单引号和双引号的区别。

$ echo "Value is $foo" %$foo会转变为其对应的变量值
$ echo 'Value is $foo' %$foo不会转换

下面是一个bash脚本的例子mcd.sh(创建以第一个参数为名的文件夹并进入):

mcd() {
ppmkdir -p "$1"
ppcd "$1"
}

其中$0表示脚本自身的名字,$1$9依次表示各个输入参数,$_是上一个指令的最后一个参数,$?可以获得上一个指令的错误代码,!!可以指代上一条指令。可以通过如下指令来加载新定义的函数:

$ source mcd.sh %脚本中包含的函数会在当前shell中加载

逻辑符号:

$ false || echo "Oops fail" %如果第一条指令出错,则执行第二条指令
$ true && echo "Things went well" %如果第一条指令正确,才会执行第二条指令
$ false ; echo "This will alwanys print" %分号只是将两条指令分割开来

将某个指令的输出赋值给一个变量:

$ foo=$(pwd)
$ echo "We are in $(pwd)"

冷门特性——进程替换:

$ cat <(ls) <(ls ..) %<(命令)相当于把该命令的输出作为一个临时文件

下面展示一个具体的例子(使用 grep 搜索字符串 foobar,如果没有找到,则将其作为注释追加到文件中):

#!/bin/bash

echo "Starting program at $(date)" # date会被替换成日期和时间

echo "Running program $0 with $# arguments with pid $$"
# $#表示输入参数的数目,$$表示当前进程的id

for file in "$@"; do # $@表示所有的参数
    grep foobar "$file" > /dev/null 2> /dev/null
    # 如果模式没有找到,则grep退出状态为 1
    # 我们将标准输出流(第一个>)和标准错误流(2>)重定向到Null,因为我们并不关心这些信息
    if [[ $? -ne 0 ]]; then
        echo "File $file does not have any foobar, adding one"
        echo "# foobar" >> "$file"
    fi
done

直接使用如下方式执行上述脚本:

$ ./example.sh [文件1] [文件2]

通配符

shell的通配(globbing)允许我们方便地进行文件匹配:

  • 通配符 - 当你想要利用通配符进行匹配时,你可以分别使用 ?* 来匹配一个或任意个字符。例如,对于文件 foo, foo1, foo2, foo10bar, rm foo? 这条命令会删除 foo1foo2 ,而 rm foo* 则会删除除了 bar 之外的所有文件。
  • 花括号 {} - 当你有一系列的指令,其中包含一段公共子串时,可以用花括号来自动展开这些命令。这在批量移动或转换文件时非常方便。
$ convert image.{png,jpg} %会展开为convert image.png image.jpg
$ cp /path/to/project/{foo,bar,baz}.sh /newpath %会展开为cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath
$ mv *{.py,.sh} folder %移动所有以.py和.sh结尾的文件
$ touch {foo,bar}/{a..j} %展开为foo/a, foo/b, ...

脚本并不是一定只有用bash写才能在终端中调用,如下的Python脚本也可以直接在终端中调用:

#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)

脚本中第一行的shebang使得终端知道应该调用python解释器。(也可以写为#!/usr/bin/env python

编写 bash 脚本有时候会很别扭和反直觉,shellcheck 这样的工具可以帮助你定位 sh/bash 脚本中的错误。

$ shellcheck mcd.sh

shell 函数和脚本有如下一些不同点:

  • 函数只能与 shell 使用相同的语言,脚本可以使用任意语言。因此在脚本中包含 shebang 是很重要的。
  • 函数仅在定义时被加载,脚本会在每次被执行时加载。这让函数的加载比脚本略快一些,但每次修改函数定义,都要重新加载一次。
  • 函数会在当前的 shell 环境中执行,脚本会在单独的进程中执行。因此,函数可以对环境变量进行更改,比如改变当前工作目录,脚本则不行。脚本需要使用 export 将环境变量导出,并将值传递给环境变量。
  • 与其他程序语言一样,函数可以提高代码模块性、代码复用性并创建清晰性的结构。shell 脚本中往往也会包含它们自己的函数定义。

shell工具

查看命令实例

convertffmpeg这两个命令可以分别用于处理图片和视频。可以使用如下指令获得一些命令的简单例子:

$ tldr convert 

查找文件

通过如下指令可以找到所需的文件:

$ find . -name src -type d %在当前文件夹中迭代找到名称为src的文件夹
$ find . -path '**/test/*.py' -type f %在当前文件夹中找到某些文件夹下包含在test子目录中的py文件
$ find . -mtime -1 %在昨天被修改过的文件,mtime表示修改时间
$ find . -name "*.tmp" -exec rm {} \; %找到这些文件并移除它们
$ find . -name '*.png' -exec convert {} {}.jpg \; %查找全部的 PNG 文件并将其转换为 JPG
$ find . -size +500k -size -10M -name '*.tar.gz' %查找所有大小在500k至10M的tar.gz文件
$ fd "*.py" %具有和find相似的作用

也可以检索整个文件系统:

$ updatedb %在执行查找前需要更新数据库
$ locate tmp %找到整个文件系统中路径带有tmp的文件,注意是是路径中带有

查找内容

如下方式可以查找文件中特定内容:

$ grep foobar mcd.sh %查找mcd.sh文件中的foobar
$ grep -R foobar %在当前文件夹下查找文件中所有出现位置
$ rg "import requests" -t py ~/scratch %在该文件夹下迭代搜索所有出现该内容的py文件
$ rg "import requests" -t py -C 5 ~/scratch %输出时展示附近的5行内容
$ rg -u --files-without-match "^#\!" -t sh %找到没有出现"#!"的文件,即没有shebang的文件,-u表示不要忽略隐藏文件
$ rg "import requests" -t py -C 5 --stats PATTERN ~/scratch %打印更详细的检索信息
$ 

grep 有很多选项,-C :获取查找结果的上下文(Context);-v 将对结果进行反选(Invert),也就是输出不匹配的结果。举例来说, grep -C 5 会输出匹配结果前后五行。当需要搜索大量文件的时候,使用 -R 会递归地进入子目录并搜索所有的文本文件。也出现了很多它的替代品,包括 ack, agrg

查找shell命令

可以直接显示过往命令历史:

$ history
$ history | grep convert %先显示历史,再匹配对应的命令

也可以使用ctrl+Rfzf可以进行模糊的检索(可以将其与ctrl+R绑定):

$ cat example.sh | fzf %进行可交互的查找

其他

之前对所有操作我们都默认一个前提,即您已经位于想要执行命令的目录下,但是如何才能高效地在目录间随意切换呢?有很多简便的方法可以做到,比如设置 alias,使用 ln -s 创建符号连接等。而开发者们已经想到了很多更为精妙的解决方案。

由于本课程的目的是尽可能对你的日常习惯进行优化。因此,我们可以使用 fasdautojump 这两个工具来查找最常用或最近使用的文件和目录。

Fasd 基于 frecency 对文件和文件排序,也就是说它会同时针对频率(frequency)和时效(recency)进行排序。默认情况下,fasd 使用命令 z 帮助我们快速切换到最常访问的目录。例如, 如果您经常访问 /home/user/files/cool_project 目录,那么可以直接使用 z cool 跳转到该目录。对于 autojump,则使用 j cool 代替即可。

还有一些更复杂的工具可以用来概览目录结构,例如 tree, broot 或更加完整的文件管理器,例如 nnnranger

$ tree %树状图显示文件结构
$ broot %可交互的树状图

命令行可以从参数或标准输入接受输入,在用管道连接命令时,我们将标准输出和标准输入连接起来,但是有些命令,例如 tar 则需要从参数接受输入,而xargs可以使用标准输入中的内容作为参数,比如

$ ls | xargs rm %删除当前文件夹下的所有文件