Shell 工具和脚本
在这节课中,我们将会展示 bash 作为脚本语言的一些基础操作,以及几种最常用的 shell 工具。
到目前为止,我们已经学习了如何在 shell 中执行命令,并使用管道将命令组合使用。但是,很多情况下我们需要执行一系列的操作并使用条件或循环这样的控制流。
shell 脚本的复杂性进一步提高。
大多数 shell 都有自己的一套脚本语言,包括变量、控制流和自己的语法。shell 脚本与其他脚本语言不同之处在于,shell 脚本针对 shell 所从事的相关工作进行了优化。因此,创建命令流程(pipelines)、将结果保存到文件、从标准输入中读取输入,这些都是 shell 脚本中的原生操作,这让它比通用的脚本语言更易用。本节中,我们会专注于 bash 脚本,因为它最流行,应用更为广泛。
在 bash 中为变量赋值的语法是 foo=bar,访问变量中存储的数值,其语法为 $foo。 需要注意的是,foo = bar (使用空格隔开)是不能正确工作的,因为解释器会调用程序 foo 并将 = 和 bar 作为参数。 总的来说,在 shell 脚本中使用空格会起到分割参数的作用,有时候可能会造成混淆,请务必多加检查。
Bash 中的字符串通过 ' 和 " 分隔符来定义,但是它们的含义并不相同。以 ' 定义的字符串为原义字符串,其中的变量不会被转义,而 " 定义的字符串会将变量值进行替换。
foo=bar
echo "$foo"
# 打印 bar
echo '$foo'
# 打印 $foo
除了$,更加安全可靠扩展性强的做法是${}

和其他大多数的编程语言一样,bash 也支持 if, case, while 和 for 这些控制流关键字。同样地, bash 也支持函数,它可以接受参数并基于参数进行操作。下面这个函数是一个例子,它会创建一个文件夹并使用 cd 进入该文件夹。
mcd () {
mkdir -p "$1" # -p在创建目录时自动创建所有必要的父目录(如果它们不存在)
cd "$1"
}
这里 $1 是脚本的第一个参数(也就是说我们按照上面的指令创造了名为mcd的函数,然后之后就可以使用mcd $1的形式使用这个函数)。与其他脚本语言不同的是,bash 使用了很多特殊的变量来表示参数、错误代码和相关变量。下面列举了其中一些变量,更完整的列表可以参考 这里。
-
$0返回运行的时候脚本的名称或者路径或者shell的名称./script.sh:$0的值是./script.sh/home/user/script.sh:$0的值是/home/user/script.shbash script.sh:$0的值是script.shsource script.sh:$0的值是当前shell的名称
-
$1到$9- 脚本的参数。$1是第一个参数,依此类推。 -
$@和$*所有参数
![image]()
-
$#参数个数 -
$?前一个命令的返回值 -
$$当前脚本的PID -
!!完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用sudo !!再尝试一次。 -
$_- 上一条命令的最后一个参数。如果你正在使用的是交互式 shell,你可以通过按下Esc之后键入 . 来获取这个值。
命令通常使用 STDOUT(标准输出,即输出到终端) 来返回输出值,使用 STDERR(标准错误,也是将错误输出到终端) 来返回错误及错误码,便于脚本以更加友好的方式报告错误。 返回码或退出状态是脚本 / 命令之间交流执行状态的方式。返回值 0 表示正常执行,其他所有非 0 的返回值都表示有错误发生。
退出码可以搭配 &&(与操作符)和 ||(或操作符)使用,用来进行条件判断,决定是否执行其他程序。它们都属于短路 运算符(short-circuiting) 同一行的多个命令可以用 ; 分隔。程序 true 的返回码永远是 0,false 的返回码永远是 1。让我们看几个例子
false || echo "Oops, fail"
# Oops, fail
true || echo "Will not be printed"
#
true && echo "Things went well"
# Things went well
false && echo "Will not be printed"
#
false ; echo "This will always run"
# This will always run
另一个常见的模式是以变量的形式获取一个命令的输出,这可以通过命令替换实现。
当您通过 $( CMD ) 这样的方式来执行 CMD 这个命令时,它的输出结果会替换掉 $( CMD ) 。例如,如果执行 for file in $(ls) ,shell 首先将调用 ls ,然后遍历得到的这些返回值。还有一个冷门的类似特性是进程替换, <( CMD ) 会执行 CMD 并将结果输出到一个临时文件中,并将 <( CMD ) 替换成临时文件名。这在我们希望返回值通过文件而不是 STDIN 传递时很有用。例如, diff <(ls foo) <(ls bar) 会显示文件夹 foo 和 bar 中文件的区别。
说了很多,现在该看例子了,下面这个例子展示了一部分上面提到的特性。下面这段脚本(这段脚本不是直接在终端中运行的,而是先写在一个文件中,然后在终端中运行这个文件)会遍历我们提供的参数,使用 grep 搜索字符串 foobar,如果没有找到,则将其作为注释追加到文件中。
#!/bin/bash
echo "Starting program at $(date)"
echo "Running program $0 with $# arguments with pid $$"
for file in "$@"; do
grep foobar "$file" > /dev/null 2> /dev/null
if [[ $? -ne 0 ]]; then
echo "File $file does not have any foobar, adding one"
echo "# foobar" >> "$file"
fi
done
这段 Bash 脚本的功能是:
检查每个传入的文件中是否包含字符串
foobar,如果没有,则在文件末尾添加一行# foobar。
🧠 脚本逐行解释
#!/bin/bash
- 这是脚本的第一行,称为 shebang;
- 它告诉系统这个脚本使用
/bin/bash来执行。
grep foobar "$file" > /dev/null 2> /dev/null
- 在当前文件中查找字符串
foobar;- 当
$file存在且可读时,在文件中搜索包含foobar的行- 匹配成功:输出所有包含
foobar的行(整行文本) - 匹配失败:无输出(退出状态码
1)
- 匹配成功:输出所有包含
- 当
> /dev/null:将标准输出重定向到“黑洞”,即丢弃正常输出;2> /dev/null:将标准错误也丢弃;/dev/null是什么?/dev/null是 Linux 系统中的一个特殊文件(设备节点),俗称“黑洞”;- 写入到这个文件的内容都会被丢弃,不会保存,也不会产生任何输出;
2>又是什么?- 在 Shell 中,每个输入/输出都有一个编号(称为文件描述符):
- STDIN编号为0
- STDOUT编号为1
- STDERR编号为2
- 所以
> file或1> file表示重定向 标准输出(STDOUT),2> file表示重定向 标准错误(STDERR)
- 在 Shell 中,每个输入/输出都有一个编号(称为文件描述符):
- 目的是:我们不关心是否有匹配内容,只关心是否存在。
if [[ $? -ne 0 ]]; then
$?表示上一个命令的退出状态码;-ne:Bash 中的比较运算符,意思是 “not equal”,即“不等于”;;不是之前说的分割同一行的不同命令,这里就是bash中if-then的固定语法格式[[]]在bash中比[]更好,就是支持更现代地写法,所以都用[[]]- 所以这个判断的意思是:
“如果
grep没有找到foobar,那么执行下面的操作。”
echo "File $file does not have any foobar, adding one"
- 提示用户该文件中没有
foobar,即将添加一行注释。
echo "# foobar" >> "$file"
- 使用
>>将# foobar添加到文件末尾; - 如果文件不存在,会自动创建;
- 如果已经存在,则追加内容。
fi
done
fi结束if判断;done结束for循环。
在条件语句中,我们比较 $? 是否等于 0。 Bash 实现了许多类似的比较操作,您可以查看 test 手册。 在 bash 中进行比较时,尽量使用双方括号 [[ ]] 而不是单方括号 [ ],这样会降低犯错的几率,尽管这样并不能兼容 sh。 更详细的说明参见 这里。
当执行脚本时,我们经常需要提供形式类似的参数。bash 使我们可以轻松的实现这一操作,它可以基于文件扩展名展开表达式。这一技术被称为 shell 的通配(globbing)
- 通配符 - 当你想要利用通配符进行匹配时,你可以分别使用
?(一个或任意个) 和*(零个或任意个) 来匹配字符。例如,对于文件foo,foo1,foo2,foo10和bar,rm foo?这条命令会删除foo1和foo2,而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 文件
mkdir foo bar
# 下面命令会创建 foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h 这些文件
touch {foo,bar}/{a..h}
touch foo/x bar/y
# 比较文件夹 foo 和 bar 中包含文件的不同
diff <(ls foo) <(ls bar)
# 输出
# < x
# ---
# > y
编写 bash 脚本有时候会很别扭和反直觉。例如 shellcheck 这样的工具可以帮助你定位 sh/bash 脚本中的错误。
注意,脚本并不一定只有用 bash 写才能在终端里调用。比如说,这是一段 Python 脚本,作用是将输入的参数倒序输出:
#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
print(arg)
内核知道去用 python 解释器而不是 shell 命令来运行这段脚本,是因为脚本的开头第一行的 shebang。
在 shebang 行中使用 env 命令是一种好的实践,它会利用环境变量中的程序来解析该脚本,这样就提高了您的脚本的可移植性。env 会利用我们第一节讲座中介绍过的 PATH 环境变量来进行定位。 例如,使用了 env 的 shebang 看上去是这样的 #!/usr/bin/env python。
🔍 为什么建议使用 env?
✅ 示例对比
| 写法 | 含义 | 问题 |
|---|---|---|
#!/usr/bin/python |
指定使用 /usr/bin/python 这个路径下的 Python 解释器 |
不灵活,因为不同系统中 Python 可能不在这个位置 |
#!/usr/bin/env python |
使用 env 命令去 PATH 环境变量中找 Python |
更加通用、可移植 |
📌 举个例子说明
假设你在 Linux 上运行一个 Python 脚本:
$ cat hello.py
#!/usr/bin/env python3
print("Hello, world!")
然后你给它加上可执行权限并运行:
chmod +x hello.py
./hello.py
这时候发生了什么?
- 操作系统看到 shebang 行是
#!/usr/bin/env python3 - 它会先运行
/usr/bin/env程序; env会在你的环境变量PATH中查找python3;- 找到后就用那个 Python 来运行脚本。
shell 函数和脚本有如下一些不同点:
- 函数只能与 shell 使用相同的语言,脚本可以使用任意语言。因此在脚本中包含
shebang是很重要的。- 这里的函数说的就是直接在终端里面写一个函数,必须遵循shell的语法
- 脚本就是先写一个脚本文件,然后在首行包含shebang
- 函数仅在定义时被加载,脚本会在每次被执行时加载。这让函数的加载比脚本略快一些,但每次修改函数定义,都要重新加载一次。
- 函数会在当前的 shell 环境中执行,脚本会在单独的进程中执行。因此,函数可以对环境变量进行更改,比如改变当前工作目录,脚本则不行。脚本需要使用
export将环境变量导出,并将值传递给环境变量。- 如果不使用
export而是直接在终端中对环境变量进行更改,那么由当前终端发起的子进程是无法知道这个环境变量的修改的 - 如果使用了
export,那么当前终端及其子进程全部都知道了这个环境变量的修改 - 但是使用
export仍然只对当前终端有效,如果要对所有终端有效,需要修改配置文件,如下
# 写入 ~/.bashrc 或 ~/.bash_profile echo 'export JAVA_HOME="/usr/lib/jvm"' >> ~/.bashrc source ~/.bashrc # 重新加载 - 如果不使用
- 与其他程序语言一样,函数可以提高代码模块性、代码复用性并创建清晰性的结构。shell 脚本中往往也会包含它们自己的函数定义。
查看命令如何使用
看到这里,您可能会有疑问,我们应该如何为特定的命令找到合适的标记呢?例如 ls -l, mv -i 和 mkdir -p。更普遍的是,给您一个命令行,您应该怎样了解如何使用这个命令行并找出它的不同的选项呢? 一般来说,您可能会先去网上搜索答案,但是,UNIX 可比 StackOverflow 出现的早,因此我们的系统里其实早就包含了可以获取相关信息的方法。
在上一节中我们介绍过,最常用的方法是为对应的命令行添加 -h 或 --help 标记。另外一个更详细的方法则是使用 man 命令。man 命令是手册(manual)的缩写,它提供了命令的用户手册。
例如,man rm 会输出命令 rm 的说明,同时还有其标记列表,包括之前我们介绍过的 -i。 事实上,目前我们给出的所有命令的说明链接,都是网页版的 Linux 命令手册。即使是您安装的第三方命令,前提是开发者编写了手册并将其包含在了安装包中。在交互式的、基于字符处理的终端窗口中,一般也可以通过 :help 命令或键入 ? 来获取帮助。
有时候手册内容太过详实,让我们难以在其中查找哪些最常用的标记和语法。 TLDR pages 是一个很不错的替代品,它提供了一些案例,可以帮助您快速找到正确的选项。
例如,自己就常常在 tldr 上搜索 tar 和 ffmpeg 的用法。
查找文件
程序员们面对的最常见的重复任务就是查找文件或目录。所有的类 UNIX 系统都包含一个名为 find 的工具,它是 shell 上用于查找文件的绝佳工具。find 命令会递归地搜索符合条件的文件,例如:
# 查找所有名称为src的文件夹
find . -name src -type d # . 表示当前目录;-type d表示只匹配目录(而不是文件或其他类型)
# 查找所有文件夹路径中包含test的python文件
find . -path '*/test/*.py' -type f # -path '*/test/*.py'表示匹配路径中包含test子目录,并且以.py结尾的项
# 查找前一天修改的所有文件
find . -mtime -1
# 查找所有大小在500k至10M的tar.gz文件
find . -size +500k -size -10M -name '*.tar.gz'
除了列出所寻找的文件之外,find 还能对所有查找到的文件进行操作。这能极大地简化一些单调的任务。
# 删除全部扩展名为.tmp 的文件
find . -name '*.tmp' -exec rm {} \;
# 查找全部的 PNG 文件并将其转换为 JPG
find . -name '*.png' -exec convert {} {}.jpg \;
| 部分 | 含义 |
|---|---|
find |
查找命令,用于在文件系统中搜索文件或目录 |
. |
表示“当前目录”,即从当前所在路径开始查找 |
-name '*.tmp' |
匹配名字以 .tmp 结尾的文件(通过通配符 * 匹配任意前缀) |
-exec |
对每一个找到的文件执行一个指定的命令 |
rm {} |
实际要执行的命令,{} 是 find 的一个占位符,表示当前查找到的文件名 |
\; |
表示 -exec 命令的结束符号(注意前面有转义符 \,是为了防止 shell 特殊解释),-exec后一定要加 |
尽管 find 用途广泛,它的语法却比较难以记忆。例如,为了查找满足模式 PATTERN 的文件,您需要执行 find -name '*PATTERN*' (如果您希望模式匹配时是不区分大小写,可以使用 -iname 选项)
您当然可以使用 alias 设置别名来简化上述操作,但 shell 的哲学之一便是寻找(更好用的)替代方案。 记住,shell 最好的特性就是您只是在调用程序,因此您只要找到合适的替代程序即可(甚至自己编写)。
例如,fd 就是一个更简单、更快速、更友好的程序,它可以用来作为 find 的替代品。它有很多不错的默认设置,例如输出着色、默认支持正则匹配、支持 unicode 并且我认为它的语法更符合直觉。以模式 PATTERN 搜索的语法是 fd PATTERN。
大多数人都认为 find 和 fd 已经很好用了,但是有的人可能想知道,我们是不是可以有更高效的方法,例如不要每次都搜索文件而是通过编译索引或建立数据库的方式来实现更加快速地搜索。
这就要靠 locate 了。 locate 使用一个由 updatedb 负责更新的数据库,在大多数系统中 updatedb 都会通过 cron 每日更新。这便需要我们在速度和时效性之间作出权衡。而且,find 和类似的工具可以通过别的属性比如文件大小、修改时间或是权限来查找文件,locate 则只能通过文件名。 这里 有一个更详细的对比。
查找代码
查找文件是很有用的技能,但是很多时候您的目标其实是查看文件的内容。一个最常见的场景是您希望查找具有某种模式的全部文件,并找它们的位置。
为了实现这一点,很多类 UNIX 的系统都提供了 grep 命令,它是用于对输入文本进行匹配的通用工具。它是一个非常重要的 shell 工具,我们会在后续的数据清理课程中深入的探讨它。
-
grep的基本功能grep是类UNIX系统中用于对输入文本进行模式匹配的通用工具。- 它是Shell环境中非常重要的工具,尤其在数据清理等任务中会频繁使用。
-
常用选项
-
-C(Context)
获取匹配结果的上下文。例如,grep -C 5会输出匹配行及其前后各5行的内容。这在需要查看匹配内容周围信息时非常有用。 -
-v(Invert)
反选匹配结果,即输出所有不匹配的行。例如,grep -v "pattern"会输出所有不包含“pattern”的行。 -
-R(Recursive)
递归搜索子目录中的所有文本文件。例如,grep -R "pattern" /path/to/dir会在指定目录及其子目录中搜索所有包含“pattern”的文件。
-
-
改进
grep -R的建议- 默认情况下,
grep -R会搜索所有文件,包括版本控制目录(如.git)。可以通过忽略特定目录或文件来优化搜索效率。 - 使用多CPU可以加速搜索过程(例如,结合
parallel工具或其他并行化方法)。
- 默认情况下,
因此也出现了很多它的替代品,包括 ack, ag 和 rg。它们都特别好用,但是功能也都差不多,我比较常用的是 ripgrep (rg) ,因为它速度快,而且用法非常符合直觉。例子如下:
# 查找所有使用了 requests 库的文件
rg -t py 'import requests'
# 查找所有没有写 shebang 的文件(包含隐藏文件)
rg -u --files-without-match "^#!"
# 查找所有的foo字符串,并打印其之后的5行
rg foo -A 5
# 打印匹配的统计信息(匹配的行和文件的数量)
rg --stats PATTERN
- 查找所有使用了
requests库的 Python 文件
rg -t py 'import requests'
- 功能:在 Python 文件中搜索包含
import requests的行。 - 选项:
-t py:限定只搜索 Python 文件(扩展名为.py)。
- 查找所有没有 shebang 的文件(包含隐藏文件)
rg -u --files-without-match "^#!"
- 功能:列出所有文件中不包含 shebang(如
#!/bin/bash)的文件。 - 选项:
-u:搜索隐藏文件和忽略的目录(默认rg会跳过隐藏文件和.gitignore中的内容)。--files-without-match:只输出不匹配指定模式的文件路径。"^#!":正则匹配以#!开头的行(shebang 的格式)。^就表示行的开头
- 查找所有
foo字符串并打印其后的 5 行
rg foo -A 5
- 功能:搜索
foo并显示匹配行及其后 5 行的上下文。 - 选项:
-A 5:显示匹配行之后的 5 行(After)。
- 类似选项:
-B 5:显示匹配行之前的 5 行(Before)。-C 5:显示匹配行前后各 5 行(Context)。
- 打印匹配的统计信息(匹配的行和文件数量)
rg --stats PATTERN
- 功能:搜索
PATTERN并输出统计信息。 - 输出内容:
- 匹配的行数、文件数。
- 搜索的总文件数、耗时等。
与 find/fd 一样,重要的是你要知道有些问题使用合适的工具就会迎刃而解,而具体选择哪个工具则不是那么重要。
查找 shell 命令
目前为止,我们已经学习了如何查找文件和代码,但随着你使用 shell 的时间越来越久,您可能想要找到之前输入过的某条命令。首先,按向上的方向键会显示你使用过的上一条命令,继续按上键则会遍历整个历史记录。
history 命令允许您以程序员的方式来访问 shell 中输入的历史命令。这个命令会在标准输出中打印 shell 中的历史命令。如果我们要搜索历史记录,则可以利用管道将输出结果传递给 grep 进行模式搜索。 history | grep find 会打印包含 find 子串的命令。
对于大多数的 shell 来说,您可以使用 Ctrl+R 对命令历史记录进行回溯搜索。敲 Ctrl+R 后您可以输入子串来进行匹配,查找历史命令行。
反复按下就会在所有搜索结果中循环。在 zsh 中,使用方向键上或下也可以完成这项工作。
Ctrl+R 可以配合 fzf 使用。fzf 是一个通用的模糊查找工具,它可以和很多命令一起使用。这里我们可以对历史命令进行模糊查找并将结果以赏心悦目的格式输出。
另外一个和历史命令相关的技巧我喜欢称之为 基于历史的自动补全。 这一特性最初是由 fish shell 创建的,它可以根据您最近使用过的开头相同的命令,动态地对当前的 shell 命令进行补全。这一功能在 zsh 中也可以使用,它可以极大的提高用户体验。
你可以修改 shell history 的行为,例如,如果在命令的开头加上一个空格,它就不会被加进 shell 记录中。当你输入包含密码或是其他敏感信息的命令时会用到这一特性。 为此你需要在 .bashrc 中添加 HISTCONTROL=ignorespace 或者向 .zshrc 添加 setopt HIST_IGNORE_SPACE。 如果你不小心忘了在前面加空格,可以通过编辑 .bash_history 或 .zhistory 来手动地从历史记录中移除那一项。
文件夹导航
之前对所有操作我们都默认一个前提,即您已经位于想要执行命令的目录下,但是如何才能高效地在目录间随意切换呢?有很多简便的方法可以做到,比如设置 alias,使用 ln -s 创建符号连接等。而开发者们已经想到了很多更为精妙的解决方案。
- 核心功能
fasd和autojump的共同目标:
根据用户的历史行为(频率和时效)智能预测需要访问的目录,实现快速切换。
fasd工具
- 排序逻辑:
基于 频率(Frequency) 和 时效(Recency) 对文件和目录进行动态排序(简称 freency)。 - 常用命令:
z <关键词>:快速跳转到与关键词匹配的最常访问或最近使用的目录。- 例如:
z cool会跳转到类似/home/user/files/cool_project的目录(假设该目录被频繁访问)。
- 例如:
- 还支持其他命令(如
f快速打开文件、d快速进入目录等)。
autojump工具
- 与
fasd的区别:- 仅专注于目录跳转(不处理文件),但更轻量。
- 使用命令
j <关键词>代替z。- 例如:
j cool效果与z cool类似。
- 例如:
- 数据来源:
通过记录cd命令的历史来学习用户习惯。
- 使用场景示例
- 场景 1:频繁切换项目目录
- 传统方式:
cd /long/path/to/cool_project - 优化后:
z cool或j cool(无需记忆完整路径)。
- 传统方式:
- 场景 2:快速打开最近编辑的文件
- 使用
fasd的f命令:f report.txt直接打开最近修改过的report.txt。
- 使用
- 安装与配置
- 安装方法(以 Ubuntu 为例):
# 安装 fasd sudo apt install fasd # 安装 autojump sudo apt install autojump - 配置:
需将工具初始化脚本添加到 Shell 配置文件(如~/.bashrc或~/.zshrc),例如:eval "$(fasd --init auto)"
还有一些更复杂的工具可以用来概览目录结构,例如 tree, broot 或更加完整的文件管理器,例如 nnn 或 ranger。
习题解答(最后三道题目没有做)
-
阅读
man ls,然后使用ls命令进行如下操作:- 所有文件(包括隐藏文件)
- 文件打印以人类可以理解的格式输出 (例如,使用 454M 而不是 454279954)
- 文件以最近访问顺序排序
- 以彩色文本显示输出结果
典型输出如下:
-rw-r--r-- 1 user group 1.1M Jan 14 09:53 baz drwxr-xr-x 5 user group 160 Jan 14 09:53 . -rw-r--r-- 1 user group 514 Jan 14 06:42 bar -rw-r--r-- 1 user group 106M Jan 13 12:12 foo drwx------+ 47 user group 1.5K Jan 12 18:08 .. -
编写两个 bash 函数
marco和polo执行下面的操作。 每当你执行marco时,当前的工作目录应当以某种形式保存,当执行polo时,无论现在处在什么目录下,都应当cd回到当时执行marco的目录。 为了方便 debug,你可以把代码写在单独的文件marco.sh中,并通过source marco.sh命令,(重新)加载函数。source的作用是 在当前 Shell 环境中执行脚本,而不是在子 Shell 中运行,于是变量和函数对当前 Shell 可见,可以直接在终端中使用脚本中编写的函数 -
假设您有一个命令,它很少出错。因此为了在出错时能够对其进行调试,需要花费大量的时间重现错误并捕获输出。 编写一段 bash 脚本,运行如下的脚本直到它出错,将它的标准输出和标准错误流记录到文件,并在最后输出所有内容。 加分项:报告脚本在失败前共运行了多少次。
#!/usr/bin/env bash n=$(( RANDOM % 100 )) if [[ n -eq 42 ]]; then echo "Something went wrong" >&2 echo "The error was using magic numbers" exit 1 fi echo "Everything went according to plan"
-
为什么
RANDOM % 100要用两个括号$(( ))包裹?$(( ))是 Bash 的 算术扩展(Arithmetic Expansion)语法,用于计算数学表达式。RANDOM是 Bash 的内置变量(注意是变量而不是程序,所以没必要写成$(RANDOM)),每次调用会返回一个0-32767的随机数。RANDOM % 100表示对随机数取模 100,结果范围是0-99。- 必须用
$(( )):- 直接写
n=RANDOM % 100会被解析为字符串赋值,而非数学计算。 $(( ))告诉 Bash 先计算表达式,再将结果赋值给变量n。
- 直接写
-
>&2是什么?>&2是 将输出重定向到标准错误流(stderr) 的语法。注意这里是对The error was using magic numbers生效的- 在 Unix/Linux 中:
stdout(标准输出流,文件描述符 1):默认输出正常信息(如echo)。stderr(标准错误流,文件描述符 2):默认输出错误信息。
- 为什么用
>&2?&的用途是告诉 Shell,2是文件描述符(标准错误流),而非普通文件名- 脚本中一般将错误消息(如调试信息、警告)发送到
stderr,而非stdout。
-
本节课我们讲解的
find命令中的-exec参数非常强大,它可以对我们查找的文件进行操作。但是,如果我们要对所有文件进行操作呢?例如创建一个 zip 压缩文件?我们已经知道,命令行可以从参数或标准输入接受输入。在用管道连接命令时,我们将标准输出和标准输入连接起来,但是有些命令,例如tar则需要从参数接受输入。这里我们可以使用xargs命令,它可以使用标准输入中的内容作为参数。 例如ls | xargs rm会删除当前目录中的所有文件。您的任务是编写一个命令,它可以递归地查找文件夹中所有的 HTML 文件,并将它们压缩成 zip 文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行(提示:查看
xargs的参数-d,译注:MacOS 上的xargs没有-d,查看这个 issue)如果您使用的是 MacOS,请注意默认的 BSD
find与 GNU coreutils 中的是不一样的。你可以为find添加-print0选项,并为xargs添加-0选项。作为 Mac 用户,您需要注意 mac 系统自带的命令行工具和 GNU 中对应的工具是有区别的;如果你想使用 GNU 版本的工具,也可以使用 brew 来安装。 -
(进阶)编写一个命令或脚本递归的查找文件夹中最近使用的文件。更通用的做法,你可以按照最近的使用时间列出文件吗?


浙公网安备 33010602011771号