Scripting language 脚本语言
接下来介绍一些使用 bash 作为脚本语言的基础知识,已经一些 Shell tools,帮助解决在命令行中经常执行的几个常见任务,例如查找一些代码,或者一些藏得很深的文件
接下来将会以 bash 为例
变量
定义变量是学习编程的第一步
在 bash 中,如果你想要创建变量,使用 {变量名}={值}
的语法进行创建,使用该变量时,只需要在变量名前加上 $
字符
1 | ekko@Ling:~/桌面$ foo=bar |
一个需要注意的问题是,在 bash 中,空格非常关键,主要是因为空格是用于分隔参数的字符。
所以,错误使用空格,会导致出错
1 | ekko@Ling:~/桌面$ foo = bar |
Shell 会告诉你为什么出错,是因为 “foo” 命令没有执行,比如这里提示 “foo” 不存在
我们并没有成功的定义变量(联系 “foo” 和 “bar” )
上面这个错误的语句被 bash 认为是调用了 “foo” 程序,第一个参数是 “=” ,第二个参数是 “bar” 。
所以对于空格,要注意处理,使用引号(也要注意单双引号区别)
流程控制
bash 同样可以进行流程控制,例如 for 循环,while 循环,或者是定义函数
接下来给出一个函数的例子
我们经常会创建一个新的文件夹并进入这个文件夹
1 | ekko@Ling:~/桌面$ vim mcd.sh |
这里使用 Vim 编写 mcd.sh 文件,文件内容如 cat
所示
我们可以在 Shell
中直接输入这个命令,他会定义该函数并起作用。但是有时候将这些内容写入文件会更好,使用
source
在 Shell 中加载并执行这个脚本,这时候我们的 Shell
中已经定义了 “mcd” 函数
1 | ekko@Ling:~/桌面$ source mcd.sh |
此时我们通过使用该命令,创建并进入一个 “test” 的目录,发现是可以正常运行的
此处的 $1
代表脚本的第一个参数,bash
使用各种特殊变量来引用参数,错误码和其他相关变量。下面是一些变量的列表。
$0
脚本名称$1
到$9
bash 脚本接受到的第一到第九个参数$@
获取执行脚本传入的所有参数$#
执行脚本传入参数的个数$?
可以获取上一个命令的错误代码1
2
3
4ekko@Ling:~/桌面$ echo "hello"
hello
ekko@Ling:~/桌面$ echo $?
0比如这里获取
echo "hello"
的错误代码,错误代码 “0” 和 C 语言一样,表示一切顺利,没有错误1
2
3ekko@Ling:~/桌面$ grep foobar mcd.sh
ekko@Ling:~/桌面$ echo $?
1再比如,这里错误代码 “1” ,因为无法在 mcd.sh 中搜索到 “foobar” 并打印,所以
grep
不会有任何输出,但是会返回 “1” 的错误代码,告诉我们发生了错误1
2
3
4
5
6ekko@Ling:~/桌面$ true
ekko@Ling:~/桌面$ echo $?
0
ekko@Ling:~/桌面$ false
ekko@Ling:~/桌面$ echo $?
1true
的错误代码为 0 ,false
的错误代码为 1通过使用这些逻辑运算符,我们可以执行某些条件语句
1
2
3
4
5
6
7ekko@Ling:~/桌面$ false || echo "Oops fail"
Oops fail
ekko@Ling:~/桌面$ true || echo "Will be not be printed"
ekko@Ling:~/桌面$ true && echo "Things went well"
Things went well
ekko@Ling:~/桌面$ false && echo "This will not print"
ekko@Ling:~/桌面$ false ; echo "This will always print"和 C 语言一样,短路现象
同样也可以把任何语句,使用
;
连接在同一行$$
当前脚本的 PID(Process identification number)!!
完整的最后一个命令,包括他的参数。比如有时候某些命令,没有足够权限,需要我们使用
sudo
1
2
3
4ekko@Ling:~/桌面$ mkdir /mnt/new
mkdir: 无法创建目录 “/mnt/new”: 权限不够
ekko@Ling:~/桌面$ sudo mkdir /mnt/new
ekko@Ling:~/桌面$ sudo !!这里的 3 ,4 行命令就是等效的
$_
可以获取上一个命令的最后一个参数1
2
3ekko@Ling:~/桌面$ mkdir foo
ekko@Ling:~/桌面$ cd $_
ekko@Ling:~/桌面/foo$创建一个目录后,进入该目录,就可以使用
$_
来代替目录名
在会创建变量后,我们同样希望将一个命令的输出存储到一个变量中
1 | ekko@Ling:~/桌面$ foo=$(pwd) |
这样就将 pwd
的输出赋值给 foo 变量
同样可以进行命令替换
1 | ekko@Ling:~/桌面$ echo "we are in $(pwd)" |
这样扩展为一个字符串
还有一个小众的工具称为进程替换,和命令替换有点类似
1 | ekko@Ling:~/桌面$ cat <(ls) <(ls ..) |
它会内部执行,然后将输出放到一个类似临时文件的东西中,并将文件标识符提供给最左边命令
简单脚本示例
1 | !/bin/bash |
- line 3:打印脚本运行日期
- line 5:打印脚本运行名称,运行的参数数量,脚本 PID
- line 7:for 循环,从所有参数中,依次传递给 file 变量
- line 8:
$file
展开 file 变量,在该文件中搜索 “foobar”,>
将结果写入某个文件,这里写入 null 文件,也就是丢弃了结果,2>
重定向标准错误流 - line 11:如果错误代码不为 0 ,
-ne
比较运算符,不等于 - line 12:打印 “文件{扩展文件名}中不含有任何 foobar,adding one”
- line 13:添加 “# foobar” 到 {文件拓展名},
>>
追加
1 | ekko@Ling:~/桌面$ ./example.sh 1.txt example.sh |
运行该脚本,并给定两个文件作为参数,其中 1.txt 为空白文件,是的他是可以把自己作为参数传进去的
line 4 给出缺失 foobar 并添加的信息
line 5,6 成功添加
通配符
当你在执行脚本的时候,还有一件事需要知道,你不需要一个一个输入文件名。
1 | ekko@Ling:~/桌面$ ls |
这里想要引用所有的 .sh 文件,可以使用 ls *.sh
的命令,其中 *
被称为通配符 globbing
1 | ekko@Ling:~/桌面$ ls project* |
有时候,你或许你希望获取 project1 ,project2,可以使用 ?
做为通配符,它只会拓展为一个字符
通常情况下,通配符非常强大,你也可以结合使用
1 | ekko@Ling:~/桌面$ convert image.png image.jpg |
line 2 和 1 等效
line 3 效果等同于
touch foo foo1 foo2 foo10
line 4 对于多层操作,他们将做笛卡尔积
结合 *
和 {}
,甚至可以使用类似于区间的东西
1 | ekko@Ling:~/桌面$ mkdir foo bar |
可以在 foo 和 bar 目录中,创建文件 a 到文件 j
再次演示一下进程替换
1 | ekko@Ling:~/桌面$ touch foo/x bar/y |
x 文件仅存在于第一个文件夹,y 文件仅存在于第二个文件夹
其他语言和 Shell 的交互
实际上也可以用许多不同语言来实现与 Shell 交互的脚本
1 | #!/usr/bin/python3 |
line 1 import sys
Python 默认情况下不会与 Shell
交互,所以需要导入库
这个脚本做一件非常简单的事情,遍历 “sys.argv[1:]” ,然后逆向输出他们
这里的 “sys.argv[1:]” 有点类似于在 bash 中我们得到的 “$0” ,“$1”,就像参数向量一样,将其逆序打印
开头这样一个魔法行,叫做解释行 Shebang,是让 Shell 知道如何运行这个脚本
1 | ekko@Ling:~/桌面$ python3 script.py a b c |
可以这样运行这个脚本
但是,如果我们想让它在 Shell 中直接执行呢?
Shell 知道它必须使用 Python 作为解释器,这样才能通过 Shebang 的方式来运行此文件
Shebang 展示的就是运行这个脚本的程序所在路径(不同的机器会将 Python
放在不同的位置,使用命令 which python3
获取)
或者在 Shebang 中给出参数
1 | #!/usr/bin/env python3 |
对于绝大多数系统来说,可以在 /usr/bin/
中调用
env
命令,以 python3
为参数,然后通过路径环境变量,在该路径中搜索 Python
二进制文件,然后使用它来解释该文件,这将使得它的可移植性变强了,因此这个脚本可以在我的电脑,你的电脑和其他电脑上运行
但是 bash 并不现代化,有时调试起来可能会很棘手,默认情况下,有些故障可能不太直观,例如之前看到的 “foo” 命令不存在
所以有如下命令 shellcheck
1 | ekko@Ling:~/桌面$ shellcheck mcd.sh |
他可以为你提供警告,语法错误和其他可能不正确引用或文件中错放空格的问题
比如对于很简单的 mcd.sh 文件,line 5 提示我们缺少 Shebang,line 8 -
13 提示我们使用短路语句应对 cd
失败的情况(我个人感觉这里这种 cd ... || exit
的写法真不错)
在编写 bash 脚本或函数时,直接运行脚本/函数和载入 Shell
的脚本/函数是有区别的(命令行中处理 bashrc 和
sshrc)但总的来说,如果你对例如你所在的所在路径进行更改,比如你
cd
到一个 bash 脚本并且只执行该 bash 脚本,这个脚本就不会
cd
到 Shell 当前所在路径;但是,如果你直接将代码加载到你的
Shell
中,加载后就是直接找到函数并执行该函数,那么他就可以直接在你当前路径执行,同样的道理也适用于在
Shell 中定义的变量
Shell Tools
接下来将讨论一些处理 Shell 时非常好用的工具
查询如何使用命令
man
命令1
ekko@Ling:~/桌面$ man ls
前篇介绍过,可以获取命令的使用文档或是描述工具,所以在有时候比较不方便,比如
convert
和ffmpeg
它们的man
页内容非常多tldr
工具(据说全称是 Too long;Don't read)1
这个东西很好用,但是我的虚拟机有点小 bug ,解决后补这里
查找文件
一个非常朴素的办法,就是不断地
ls
1
2
3
4
5
6ekko@Ling:~/桌面$ ls
1.txt foo tldr
bar mcd.sh VMwareTools-10.3.23-16594550.tar.gz
example.sh script.py vmware-tools-distrib
ekko@Ling:~/桌面$ ls foo
a b c d e f g h i j x只要你找,应该都能找到(
如果我知道我要找的文件夹的名字,可以使用
find
命令1
2
3ekko@Ling:~/桌面$ find . -name a -type f
./foo/a
./bar/afind
的查找方式有很多种1
2
3
4ekko@Ling:~/桌面$ find . -name src -type d
ekko@Ling:~/桌面$ find . path '*/test/*.py' -type f
ekko@Ling:~/桌面$ find . -mtime -1
ekko@Ling:~/桌面$ find . size +500k -size -10M -name '*.tar.gz'line 1 查找所有名叫 src 的目录
line 2 查找所有路径中,父目录为 test 的 Python 文件
line 3 查找所有修改时间为过去一天内的文件
line 4 查找所有文件大小大于 500k 小于 10M 的文件
find
同样可以在找到这些文件时,执行一些操作1
2ekko@Ling:~/桌面$ find . -name '*.tmp' -exec rm {} \;
ekko@Ling:~/桌面$ find . -name '*.png' -exec convert {} {}.jpg \;将找到的后缀为 .tmp 的文件执行
rm
操作将找到的后缀为 .png 的文件转换为 .jpg 文件
一般来说,关于 Shell 的另一个好处是不仅有这些工具,而且人们会不断发现新的方法,所以会有一些工具的替代品,了解他们非常好。
例如,如果你只是像匹配 .tmp
后缀的文件,可能很难做到这一点,因为他有很长的命令。所以就有了
fd
这样的东西,这是一个更短的命令,它默认使用正则表达式,甚至能忽略搜索你的
git 文件。它会用不同的颜色显示代码,更好的支持 Unicode 编码
locate
命令只会查找你的文件系统重包含你想要的子字符串的路径1
ekko@Ling:~/桌面$ locate foo
grep
命令可以在给定文件里查找指定字符串或正则表达式,并将结果所在行打印1
2ekko@Ling:~/桌面$ grep foobar mcd.sh
foobar-r
可以实现递归查找
假如你知道你已经用某种编程语言编写了一些代码,并且知道它在你的文件系统的某个地方,但你实际上不知道在哪里。你就可以快速搜索他
```shell ekko@Ling:~/桌面$ rg "import requests" -t py ~/scratch
ekko@Ling:~/桌面$ rg -u --files-without-match "^#!" -t sh1
2
3
这样可以在 scratch 文件夹里,搜索所有导入了 “request” 的 Python 文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
`-u` 不忽略隐藏文件
`--files-without-match` 不满足匹配的文件
`"^#\!"` 正则表达式,表示在行的开头有一个 “ # ” 和 “ ! ”,这是一个 Shebang
上面两条命令,结合在一起就可以查找没有 Shebang 的文件了
`-t sh` 表示只查找 .sh 文件
`--stats` 标志,会展示一些附加信息
### 查找使用过的命令
- $\uparrow$ 可以慢慢浏览你的历史记录,寻找匹配项
- `history` 打印使用过的命令的历史记录
```shell
ekko@Ling:~/桌面$ history使用管道也可以只关注那些你希望看到的命令的历史记录
1
ekko@Ling:~/桌面$ history | grep cd
Ctrl-R
反向搜索1
(reverse-i-search)`cd': history | grep cd
输入
Ctrl-R
Shell 提示行就会变成上述样子,输入你需要查找的命令fzf
模糊查找,可以交互式的grep
没试过
history substring search
历史子字符串搜索当键入命令时动态搜索具有相同前缀的命令,当你需要时可以使用 \(\rightarrow\) 自动补全,用来避免重新输入非常长的命令
快速目录列表和目录导航的工具
-R
递归列出某个目录的结构1
2
3ekko@Ling:~/桌面/foo$ ls -R
.:
a b c d e f g h i j xtree
更友好地打印目录结构broot
类似的,更友好地显示
Exercise
根据
man ls
编写一个ls
能够显示包括隐藏文件在内的所有文件的信息(权限,大小,所属,日期时间...)编写两个函数
marco()
和polo()
,当执行marco
时标记当前位置,当执行polo
时回到marco
标记的位置(这函数有点帅呀)marco.sh
1
2
3
4!/bin/bash
marco(){
export MARCO=$(pwd)
}polo.sh
1
2
3
4!/bin/bash
polo(){
cd "$MARCO"
}上述两个文件给一下执行权限后,
source
一下就可以直接用了,你就可以在你的终端上飞雷神了export
提升变量作用域,之前介绍的定义变量的方法为局部变量,export
将其变为全局变量,就可以互相可用了