一、Shell 脚本
以bash脚本为例
1、变量赋值语法:foo=bar(shell脚本中使用空格会起到分割参数的作用,有时会造成混淆)
2、访问变量中储存的数值:$foo
3、以'
定义的字符串为原义字符串,其中的变量不会被转义,而 "
定义的字符串会将变量值进行替换。
4、bash
也支持if
, case
, while
和 for
这些控制流关键字。同样地, bash
也支持函数,它可以接受参数并基于参数进行操作。
mcd () {
mkdir -p "$1"
cd "$1"
}
这里 $1
是脚本的第一个参数。与其他脚本语言不同的是,bash使用了很多特殊的变量来表示参数、错误代码和相关变量。下面列举了其中一些变量
$0
- 脚本名$1
到$9
- 脚本的参数。$1
是第一个参数,依此类推。$@
- 所有参数$#
- 参数个数$?
- 前一个命令的返回值$$
- 当前脚本的进程识别码!!
- 完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用sudo !!
再尝试一次。$_
- 上一条命令的最后一个参数。
5、命令通常使用 STDOUT
来返回输出值,使用STDERR
来返回错误及错误码,返回码或退出状态是脚本/命令之间交流执行状态的方式。返回值0表示正常执行,其他所有非0的返回值都表示有错误发生。
退出码可以搭配 &&
(与操作符)和 ||
(或操作符)使用,用来进行条件判断,决定是否执行其他程序。它们都属于短路运算符(short-circuiting) 同一行的多个命令可以用 ;
分隔。程序 true
的返回码永远是0
,false
的返回码永远是1
。
6、命令替换(command substitution) :以变量的形式获取一个命令的输出。$(cmd):例如,如果执行 for file in $(ls)
,shell首先将调用ls
,然后遍历得到的这些返回值。
7、进程替换(process substitution):<(cmd)会执行CMD并将结果输出到一个临时文件夹中,并将<(CMD)替换成临时文件名。例如,diff <(ls foo) <(ls bar)会显示文件夹foo和bar中文件的区别。
8、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 写才能在终端里调用。内核知道去用 python 解释器而不是 shell 命令来运行这段脚本,是因为脚本的开头第一行的 shebang。在Shebang之后,可以有一个或数个空白字符,后接解释器的绝对路径,用于指明执行这个脚本文件的解释器。
使用 #!/usr/bin/env+脚本解释器名称
是一种常见的在不同平台上都能正确找到解释器的办法。因为 env
会利用我们第一节讲座中介绍过的PATH
环境变量来进行定位。
二、Shell 工具
1、查看命令如何使用:
man 命令是手册(manual)的缩写,它提供了命令的用户手册。
TLDR pages 是一个很不错的替代品,它提供了一些案例,可以帮助您快速找到正确的选项。
2、查找文件:
find是 shell 上用于查找文件的绝佳工具。find
命令会递归地搜索符合条件的文件
# 查找所有名称为src的文件夹
find . -name src -type d
# 查找所有文件夹路径中包含test的python文件
find . -path '*/test/*.py' -type f
# 查找前一天修改的所有文件
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 \;
fd,locate也是比较好的工具。
3、查找代码:
grep命令,它是用于对输入文本进行匹配的通用工具。
grep
有很多选项,这也使它成为一个非常全能的工具。其中我经常使用的有 -C
:获取查找结果的上下文(Context);-v
将对结果进行反选(Invert),也就是输出不匹配的结果。举例来说, grep -C 5
会输出匹配结果前后五行。当需要搜索大量文件的时候,使用 -R
会递归地进入子目录并搜索所有的文本文件。
因此也出现了很多它的替代品,包括 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
三、课后练习
1、编写两个bash函数 marco
和 polo
执行下面的操作。 每当你执行 marco
时,当前的工作目录应当以某种形式保存,当执行 polo
时,无论现在处在什么目录下,都应当 cd
回到当时执行 marco
的目录。 为了方便debug,你可以把代码写在单独的文件 marco.sh
中,并通过 source marco.sh
命令,(重新)加载函数。
#!/bin/bash
marco(){
# $(pwd)获取当前工作目录的绝对路径,并保存
echo "$(pwd)" > $HOME/marco_history.log
# 输出日志信息,显示保存路径
echo "save pwd $(pwd)"
}
polo(){
# 通过cat函数获取被保存的路径
cd "$(cat "$HOME/marco_history.log")"
}
2、 假设您有一个命令,它很少出错。因此为了在出错时能够对其进行调试,需要花费大量的时间重现错误并捕获输出。 编写一段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"
解:用while来解答
#!/usr/bin/env bash
count=0
# 创建一个空的out.log文件
echo > out.log
while true
do
# 运行buggy.sh脚本将标准输出和错误追加到out.log文件中
./buggy.sh &>> out.log
if [[ $? -ne 0 ]]; then
cat out.log
echo "failed after $count times"
break
fi
((count++))
done
ps:&>>
是一种用来重定向输出的特殊符号,它将标准输出和标准错误都追加到指定的文件中。/
具体来说:
>
:用于将标准输出重定向到文件,会覆盖文件中的内容。>>
:用于将标准输出追加到文件末尾,不会覆盖文件中的现有内容。2>
:用于将标准错误重定向到文件,会覆盖文件中的内容。2>>
:用于将标准错误追加到文件末尾,不会覆盖文件中的现有内容。
但是,&>
和 &>>
的使用方式略有不同:
&>
:将标准输出和标准错误都重定向到同一个文件,会覆盖文件中的内容。&>>
:将标准输出和标准错误都追加到同一个文件的末尾,不会覆盖文件中的现有内容。
3、编写一个命令,它可以递归地查找文件夹中所有的HTML文件,并将它们压缩成zip文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行。
find . -type f -name "*.html" | xargs -d '\n' tar -cvzf html.zip
-
.
的含义:.
表示当前目录。当你在命令行中运行find .
时,find
命令会从当前目录开始查找。 -
-type f
参数:-type f
参数告诉find
命令只找普通文件(regular files)。这样做的目的是排除目录、符号链接等其他文件类型。 -
-name "*.html"
参数:-name "*.html"
参数用于指定查找文件名以.html
结尾的文件。这是一个模式匹配,会找到所有以.html
结尾的文件,无论它们在哪个子目录里。 -
递归搜索:
find
命令默认情况下是递归搜索的。也就是说,它会从当前目录开始,深入进入每个子目录,直到找到所有符合条件的文件为止。 -
使用
-print0
参数来打印符合条件的文件名,每个文件名以 NULL 字符分隔。这确保了即使文件名中包含空格或特殊字符,也能正确地传递给下一个命令。使用-0
参数来接收find
命令输出的文件列表(每个文件名以 NULL 字符分隔),并将它们作为参数传递给tar
命令。tar
命令创建一个名为html.zip
的压缩文件,其中包含所有符合条件的.html
文件。选项-cvzf
分别表示创建压缩文件 (c
),显示详细信息 (v
),使用 gzip 压缩算法 (z
),并指定压缩文件名为html.zip
(f html.zip
)。
4、(进阶)编写一个命令或脚本递归的查找文件夹中最近使用的文件。更通用的做法,你可以按照最近的使用时间列出文件吗?
find . -type f -mmin -60 -print0 | xargs -0 ls -lt | head -10