Shallow Dream

Keep It Simple and Stupid!

0%

Shell Scripting

Scripting language 脚本语言

接下来介绍一些使用 bash 作为脚本语言的基础知识,已经一些 Shell tools,帮助解决在命令行中经常执行的几个常见任务,例如查找一些代码,或者一些藏得很深的文件

接下来将会以 bash 为例

变量

定义变量是学习编程的第一步

在 bash 中,如果你想要创建变量,使用 {变量名}={值} 的语法进行创建,使用该变量时,只需要在变量名前加上 $ 字符

1
2
3
ekko@Ling:~/桌面$ foo=bar
ekko@Ling:~/桌面$ echo $foo
bar

一个需要注意的问题是,在 bash 中,空格非常关键,主要是因为空格是用于分隔参数的字符。

所以,错误使用空格,会导致出错

1
2
ekko@Ling:~/桌面$ foo = bar
Command 'foo' not found

Shell 会告诉你为什么出错,是因为 “foo” 命令没有执行,比如这里提示 “foo” 不存在

我们并没有成功的定义变量(联系 “foo” 和 “bar” )

上面这个错误的语句被 bash 认为是调用了 “foo” 程序,第一个参数是 “=” ,第二个参数是 “bar” 。

所以对于空格,要注意处理,使用引号(也要注意单双引号区别)

流程控制

bash 同样可以进行流程控制,例如 for 循环,while 循环,或者是定义函数

接下来给出一个函数的例子

我们经常会创建一个新的文件夹并进入这个文件夹

1
2
3
4
5
6
ekko@Ling:~/桌面$ vim mcd.sh
ekko@Ling:~/桌面$ cat mcd.sh
mcd () {
mkdir -p "$1"
cd "$1"
}

这里使用 Vim 编写 mcd.sh 文件,文件内容如 cat 所示

我们可以在 Shell 中直接输入这个命令,他会定义该函数并起作用。但是有时候将这些内容写入文件会更好,使用 source 在 Shell 中加载并执行这个脚本,这时候我们的 Shell 中已经定义了 “mcd” 函数

1
2
3
ekko@Ling:~/桌面$ source mcd.sh
ekko@Ling:~/桌面$ mcd test
ekko@Ling:~/桌面/test$

此时我们通过使用该命令,创建并进入一个 “test” 的目录,发现是可以正常运行的

此处的 $1 代表脚本的第一个参数,bash 使用各种特殊变量来引用参数,错误码和其他相关变量。下面是一些变量的列表。

  • $0 脚本名称

  • $1$9 bash 脚本接受到的第一到第九个参数

  • $@ 获取执行脚本传入的所有参数

  • $# 执行脚本传入参数的个数

  • $? 可以获取上一个命令的错误代码

    1
    2
    3
    4
    ekko@Ling:~/桌面$ echo "hello"
    hello
    ekko@Ling:~/桌面$ echo $?
    0

    比如这里获取 echo "hello" 的错误代码,错误代码 “0” 和 C 语言一样,表示一切顺利,没有错误

    1
    2
    3
    ekko@Ling:~/桌面$ grep foobar mcd.sh
    ekko@Ling:~/桌面$ echo $?
    1

    再比如,这里错误代码 “1” ,因为无法在 mcd.sh 中搜索到 “foobar” 并打印,所以 grep 不会有任何输出,但是会返回 “1” 的错误代码,告诉我们发生了错误

    1
    2
    3
    4
    5
    6
    ekko@Ling:~/桌面$ true
    ekko@Ling:~/桌面$ echo $?
    0
    ekko@Ling:~/桌面$ false
    ekko@Ling:~/桌面$ echo $?
    1

    true 的错误代码为 0 ,false 的错误代码为 1

    通过使用这些逻辑运算符,我们可以执行某些条件语句

    1
    2
    3
    4
    5
    6
    7
    ekko@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
    4
    ekko@Ling:~/桌面$ mkdir /mnt/new
    mkdir: 无法创建目录 “/mnt/new”: 权限不够
    ekko@Ling:~/桌面$ sudo mkdir /mnt/new
    ekko@Ling:~/桌面$ sudo !!

    这里的 3 ,4 行命令就是等效的

  • $_ 可以获取上一个命令的最后一个参数

    1
    2
    3
    ekko@Ling:~/桌面$ mkdir foo
    ekko@Ling:~/桌面$ cd $_
    ekko@Ling:~/桌面/foo$

    创建一个目录后,进入该目录,就可以使用 $_ 来代替目录名

在会创建变量后,我们同样希望将一个命令的输出存储到一个变量中

1
2
3
ekko@Ling:~/桌面$ foo=$(pwd)
ekko@Ling:~/桌面$ echo $foo
/home/ekko/桌面

这样就将 pwd 的输出赋值给 foo 变量

同样可以进行命令替换

1
2
ekko@Ling:~/桌面$ echo "we are in $(pwd)"
we are in /home/ekko/桌面

这样扩展为一个字符串

还有一个小众的工具称为进程替换,和命令替换有点类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ekko@Ling:~/桌面$ cat <(ls) <(ls ..)
mcd.sh
VMwareTools-10.3.23-16594550.tar.gz
vmware-tools-distrib
公共的
模板
视频
图片
文档
下载
音乐
桌面
last-modified.txt
snap

它会内部执行,然后将输出放到一个类似临时文件的东西中,并将文件标识符提供给最左边命令

简单脚本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

echo "Starting program at $(date)" # Date will be substituted

echo "Running program $0 with $# arguments with pid $$"

for file in "$@"; do
grep foobar "$file" > /dev/null 2> /dev/null
# When pattern is not found, grep has exit status 1
# we redirect STDOUT and STDERR to a null register since we do not care about them
if [[ "$?" -ne 0 ]]; then
echo "File $file does not have any foobar, adding one"
echo "# foobar" >> "$file"
fi
done
  • 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
2
3
4
5
6
ekko@Ling:~/桌面$ ./example.sh 1.txt example.sh
Starting program at 2023年 06月 24日 星期六 16:43:28 CST
Running program ./example.sh with 2 arguments with pid 3778
File 1.txt does not have any foobar, adding one
ekko@Ling:~/桌面$ cat 1.txt
# foobar

运行该脚本,并给定两个文件作为参数,其中 1.txt 为空白文件,是的他是可以把自己作为参数传进去的

line 4 给出缺失 foobar 并添加的信息

line 5,6 成功添加

通配符

当你在执行脚本的时候,还有一件事需要知道,你不需要一个一个输入文件名。

1
2
3
4
5
ekko@Ling:~/桌面$ ls
1.txt mcd.sh vmware-tools-distrib
example.sh VMwareTools-10.3.23-16594550.tar.gz
ekko@Ling:~/桌面$ ls *.sh
example.sh mcd.sh

这里想要引用所有的 .sh 文件,可以使用 ls *.sh 的命令,其中 * 被称为通配符 globbing

1
2
3
4
5
6
7
8
9
10
ekko@Ling:~/桌面$ ls project*
project1:

project2:

project42:
ekko@Ling:~/桌面$ ls project?
project1:

project2:

有时候,你或许你希望获取 project1 ,project2,可以使用 ? 做为通配符,它只会拓展为一个字符

通常情况下,通配符非常强大,你也可以结合使用

1
2
3
4
ekko@Ling:~/桌面$ convert image.png image.jpg
ekko@Ling:~/桌面$ convert image.{png,jpg}
ekko@Ling:~/桌面$ touch foo{,1,2,10}
ekko@Ling:~/桌面$ touch project{1,2}/src/test/test{1,2,3}.py
  • line 2 和 1 等效

  • line 3 效果等同于 touch foo foo1 foo2 foo10

  • line 4 对于多层操作,他们将做笛卡尔积

结合 *{} ,甚至可以使用类似于区间的东西

1
2
ekko@Ling:~/桌面$ mkdir foo bar
ekko@Ling:~/桌面$ touch {foo,bar}/{a..j}

可以在 foo 和 bar 目录中,创建文件 a 到文件 j

再次演示一下进程替换

1
2
3
4
5
6
ekko@Ling:~/桌面$ touch foo/x bar/y
ekko@Ling:~/桌面$ diff <(ls foo) <(ls bar)
11c11
< x
---
> y

x 文件仅存在于第一个文件夹,y 文件仅存在于第二个文件夹

其他语言和 Shell 的交互

实际上也可以用许多不同语言来实现与 Shell 交互的脚本

1
2
3
4
#!/usr/bin/python3
import sys
for arg in reversed(sys.argv[1:]):
print(arg)

line 1 import sys Python 默认情况下不会与 Shell 交互,所以需要导入库

这个脚本做一件非常简单的事情,遍历 “sys.argv[1:]” ,然后逆向输出他们

这里的 “sys.argv[1:]” 有点类似于在 bash 中我们得到的 “$0” ,“$1”,就像参数向量一样,将其逆序打印

开头这样一个魔法行,叫做解释行 Shebang,是让 Shell 知道如何运行这个脚本

1
2
3
4
ekko@Ling:~/桌面$ python3 script.py a b c
c
b
a

可以这样运行这个脚本

但是,如果我们想让它在 Shell 中直接执行呢?

Shell 知道它必须使用 Python 作为解释器,这样才能通过 Shebang 的方式来运行此文件

Shebang 展示的就是运行这个脚本的程序所在路径(不同的机器会将 Python 放在不同的位置,使用命令 which python3获取)

或者在 Shebang 中给出参数

1
#!/usr/bin/env python3

对于绝大多数系统来说,可以在 /usr/bin/ 中调用 env 命令,以 python3 为参数,然后通过路径环境变量,在该路径中搜索 Python 二进制文件,然后使用它来解释该文件,这将使得它的可移植性变强了,因此这个脚本可以在我的电脑,你的电脑和其他电脑上运行

但是 bash 并不现代化,有时调试起来可能会很棘手,默认情况下,有些故障可能不太直观,例如之前看到的 “foo” 命令不存在

所以有如下命令 shellcheck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ekko@Ling:~/桌面$ shellcheck mcd.sh

In mcd.sh line 1:
mcd () {
^-- SC2148: Tips depend on target shell and yours is unknown. Add a shebang.


In mcd.sh line 3:
cd "$1"
^-----^ SC2164: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

Did you mean:
cd "$1" || exit

For more information:
https://www.shellcheck.net/wiki/SC2148 -- Tips depend on target shell and y...
https://www.shellcheck.net/wiki/SC2164 -- Use 'cd ... || exit' or 'cd ... |...

他可以为你提供警告,语法错误和其他可能不正确引用或文件中错放空格的问题

比如对于很简单的 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

    前篇介绍过,可以获取命令的使用文档或是描述工具,所以在有时候比较不方便,比如 convertffmpeg 它们的 man 页内容非常多

  • tldr 工具(据说全称是 Too long;Don't read)

    1
      

这个东西很好用,但是我的虚拟机有点小 bug ,解决后补这里

查找文件

  • 一个非常朴素的办法,就是不断地 ls

    1
    2
    3
    4
    5
    6
    ekko@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
    3
    ekko@Ling:~/桌面$ find . -name a -type f
    ./foo/a
    ./bar/a

    find 的查找方式有很多种

    1
    2
    3
    4
    ekko@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
    2
    ekko@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
    2
    ekko@Ling:~/桌面$ grep foobar mcd.sh
    # foobar

    -r 可以实现递归查找

假如你知道你已经用某种编程语言编写了一些代码,并且知道它在你的文件系统的某个地方,但你实际上不知道在哪里。你就可以快速搜索他

  • ```shell ekko@Ling:~/桌面$ rg "import requests" -t py ~/scratch

    1
    2
    3

    这样可以在 scratch 文件夹里,搜索所有导入了 “request” 的 Python 文件

    ekko@Ling:~/桌面$ rg -u --files-without-match "^#!" -t sh
    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
    3
    ekko@Ling:~/桌面/foo$ ls -R
    .:
    a b c d e f g h i j x
  • tree 更友好地打印目录结构

  • broot 类似的,更友好地显示

Exercise

  1. 根据 man ls 编写一个 ls 能够显示包括隐藏文件在内的所有文件的信息(权限,大小,所属,日期时间...)

  2. 编写两个函数 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 将其变为全局变量,就可以互相可用了