前言

这是本人观看该课程所做的第一部分的笔记,包含前三课的内容,水平有限,不一定全面。课后习题这边只贴出了部分个人觉得容易有问题的题目以及个人的答案。
课程链接:点击这里
中文讲义:点击这里

第1讲 - 课程概览与 shell

课程目的

如何充分利用你已经了解的工具,同时也介绍一些你之前不知道的工具并怎么将这些工具结合起来。

SHELL

一般我们的计算机都是自带SHELL,通过打开终端就可看到。SHELL是我们与计算机交互的主要文本界面。

使用SHELL:

SHELL提示符通常包含了用户名,机器名,当前所在路径,根据你所配置的一些东西,SHELL提示符也会有所不同,如下所示:
QQ20240113-195227.png
在提示符号后,可以输入命令,命令最终会被 shell 解析。最简单的命令是执行一个程序,比如我们输入date命令,就会执行date这个程序,打印出当前的日期和时间:
QQ20240113-200017.png
echo命令:打印参数,如输入echo hello会输出hello
QQ20240113-200210.png
有多个参数时用’’或者””包裹参数即可,如echo "Hello world"

date这种程序是计算机自带的,存储在文件系统之中。SHELL通过环境变量来搜索这些程序。SHELL就是一种编程语言,可以做到其他语言能实现的基本功能,如for循环、while循环等等。

进行程序搜索的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
echo $PATH # 列出路径列表,当你在键入每个程序的名称时,它都会在计算机上挨个查找这些目录中是否有与命令匹配的程序或文件的名称,然后运行它。
which echo # 输出程序所在的绝对路径(echo是哪一个),结果:/bin/echo
```

### 在SHELL中导航:
- `pwd`命令:打印当前所在的相对路径
- `cd`命令:切换目录。
- `.`表示当前目录,`..`表示父目录,`~`表示主目录,`-`表示之前所在的目录。(这些配合`cd`很好用)
- `ls`命令:列出当前目录下的所有文件。通过配置路径参数也可以输出指定路径下的所有文件,如:`ls ..`。
- 输入`命令 --help`可以查看指定命令的具体参数\选项配置。
- `ls -l`以列表形式列出所有文件,注意开头的第一串参数,表示这个文件的权限设置。第一个字符d表示是一个目录。然后接下来的九个字符,每三个字符构成一组,分别代表文件拥有者(第二个参数),用户组(第三个参数)以及其他所有人具有的权限。
![QQ20240113-204438.png](https://s2.loli.net/2024/01/19/NdakT2oEsOw4Mrq.png)

- 对于文件而言,`r`代表可读,`w`代表可写,`x`代表可执行;对于目录而言,`r`则是代表是否允许查看目录的内容,`w`代表是否可以在该目录下操作文件(重命名/创建/删除),`x`代表是否可以搜索(也就是能不能进入该目录,打开/读取/使用cd都需要这个权限)。`-`代表未拥有权限
- `mv`命令:用来重命名或者移动文件/目录,需要提供当前文件的路径以及目标文件的路径。EX:`mv do.md don.md`,do.md重命名为don.md
- `cp`命令:复制文件,需要提供要复制的文件路径和目标路径,都要是完整路径。EX:`cp d.md ../d.md`
- `rm`命令:删除文件,需要提供目标路径。可以使用`-r`来进行递归删除(也就是删除给定路径下的所有内容)
- `rmdir`命令:删除空目录。
- `mkdir`命令:创建目录。
- `man`命令:提供指定命令的文档。EX:`man ls`,输出结构与ls --help相似,但是可读性更好。

### 组合不同程序:
- 最简单的是 `< file` 和 `> file`。这两个命令可以将程序的输入输出流分别重定向到文件:
```SHELL
XXX:~$ echo hello > hello.txt #将echo的输出输入到hello.txt中
XXX:~$ cat hello.txt #打印hello.txt的内容
hello
XXX:~$ cat < hello.txt > hello2.txt # cat从hello.txt中读取内容,输入到hello2.txt文件中。
XXX:~$ cat hello2.txt
hello
  • >>表示追加。
  • |(管道符)表示将一个程序的输出和另外一个程序的输入连接起来。
    EX:
    1
    2
    XXX:~$ ls -l / | tail -n1 # 打印列表的最后一行
    XXX:~$ curl --head --silent google.com | grep -i content-length # 打印header的内容长度

ROOT用户(根用户)

可以在系统上做任何事情,无视权限限制。但要注意,如果用root用户的身份执行一些错误的操作,有时候会导致不可逆的影响。

  • sudo命令:以root用户的身份执行命令。
  • sudo su命令:切换用户为root用户。
  • SHELL提示符的$表示非root用户,#表示root用户。
  • tee命令:输入内容到文件中同时打印输入。
  • xdg-open命令:用合适的方式(文本编辑器/浏览器等等)打开指定文件。

课后习题(部分)

  1. 将以下内容一行一行地写入 semester 文件:
    1
    2
    #!/bin/sh
    curl --head --silent https://missing.csail.mit.edu
    通过使用echo加上>以及>>即可解决,答案如下:
    1
    2
    echo '#!/bin/sh' > semester
    echo 'curl --head --silent https://missing.csail.mit.edu' >> semester
  2. 使用 chmod 命令改变权限,使 ./semester 能够成功执行,不要使用 sh semester 来执行该程序。
    答案如下:
    1
    2
    chmod +x semester
    ./semester # 执行这个文件
  3. 使用 | 和 > ,将 semester 文件输出的最后更改日期信息,写入主目录下的 last-modified.txt 的文件中
    stat命令来获取semester文件的最后更改日期,个人没想到怎么用上|,答案如下:
    1
    stat -c %y semester > ~/last-modified.txt
  4. 写一段命令来从 /sys 中获取笔记本的电量信息,或者台式机 CPU 的温度。
    这边就只做获取电量的了,答案如下:
    1
    cat /sys/class/power_supply/BAT0/uevent

第2讲 - Shell 工具和 BASH 脚本

脚本相关

定义变量

语法如下:

1
2
foo=bar
echo "$foo" #调用变量,打印 bar

注意,定义时不能带空格,如:foo = bar。因为空格是用来分隔参数的字符。

定义字符串

有两种方式:

1
2
3
4
5
echo "hello" # 双引号,会将变量进行转义
echo 'hello' # 单引号,为原义字符串

echo "$foo" # 打印bar
echo '$foo' # 打印$foo

定义函数

bash 可以接受参数并基于参数进行操作。
示例函数:

1
2
3
4
mcd () {
mkdir -p "$1" # 创建一个文件夹,$1 表示第一个参数
cd "$1" # 进入该文件夹
}

QQ20240116-154107.png

注意,带有$的一般都是shell中的保留关键字,如下所示:

  • $1$9表示bash脚本的第一个到第九个参数

  • $0表示脚本名称

  • $?可以获取前一个命令的返回值,示例如下:
    QQ20240116-155700.png
    注意,退出码常用于进行条件判断。

  • $_用于获取上一条命令的最后一个参数,如下所示:
    QQ20240116-154908.png

  • !!获取完整的上一条命令,包括参数(!!会被直接替换成命令)

  • $(命令)将一个命令的输出存储到一个变量中,如foo=$(pwd),也可以用在字符串中对字符串进行拓展, 如:echo "We are in $(pwd)".

  • 进程替换:cat <(ls) <(ls ..)<(ls)会将结果输出到一个临时文件中,并将文件名进行替换,提供给cat命令,<(ls ..)同理。

  • $#表示参数个数

  • $$表示该脚本的进程的pid

  • $@表示所有参数

脚本实例

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

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

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

for file in "$@"; do
grep foobar "$file" > /dev/null 2> /dev/null
# 使用grep 搜索字符串 foobar,将标准输出流和标准错误流重定向到/dev/null(黑洞文件)进行丢弃。
if [[ $? -ne 0 ]]; then ## -ne 表示 not equal
# 如果文件不存在 foobar , 输出语句并将 foobar 作为注释追加到文件末尾
echo "File $file does not have any foobar, adding one"
echo "# foobar" >> "$file"
fi
done

使用man test或者访问这里可查看一系列的比较运算符.

在输入文件名参数的时候,可以善用通配符?(1或多)和*(0或多)来匹配文件。(正则)
EX: ls *.sh #显示当前目录下所有的sh文件

而涉及文件格式变化的话,可以这样convert image.{png,jpg}.
(PS:注意,想使用 convert 需要安装 imagemagick,安装指令如下:

1
2
sudo apt-get update
sudo apt-get install imagemagick


当你有一系列的命令,其中包含一段公共子串时,可以用{}来自动展开这些命令。这在批量移动或转换文件时非常方便。命令如果包含多个{},会以笛卡尔积的形式进行拓展。
EX:touch {foo,bar}/{a..j} # 在foo和bar下分别创建命名为a到j的路径/文件夹

如果在终端里使用其他语言的脚本的话,需要更改脚本内开头第一行的 shebang去指定具体的解释器。例如使用python脚本,shebang 是这样的 #!/usr/bin/env python
(PS:/usr/bin/下有很多二进制文件。)

shellcheck 这样的工具可以帮助你定位 bash 脚本中的错误。
Ubuntu安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sudo apt install shellcheck
```

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

## SHELL 工具
- `man`命令用于查看命令的使用方式,一些自己安装的工具(比如 Shellcheck )也可以查看。
- 比起`ls`,`find`会是一个更为好用的文件查找工具(`fd`也是),一些例子如下:
```BASH
# 查找所有名称为src的文件夹
find . -name src -type d
# 查找所有test文件夹下的python文件,不在乎test前有多少文件夹
find . -path '*/test/*.py' -type f
# 查找前一天修改的所有文件
find . -mtime -1
# 查找所有大小在500k至10M的tar.gz文件
find . -size +500k -size -10M -name '*.tar.gz'
# 查找全部扩展名为.tmp 的文件然后删除
find . -name '*.tmp' -exec rm {} \;
除此之外,`locate`也是一个好用的文件查找工具,使用`locate`命令查找会直接输出所有包含参数的路径,安装命令如下:  
1
sudo apt install mlocate
  • grep命令是用于对输入文本进行匹配的通用工具,常用于查找代码,它有很多选项,一下是部分选项:

    1
    2
    3
    # -R:遍历整个目录。
    # -C:获取查找结果的上下文。EX:`grep -C 5` 会输出匹配结果前后五行。
    # -v:对结果进行反选,也就是输出不匹配的
  • 查找使用过的命令,第一种方式是”上箭头”,但是并不高效。
    第二种方法就是使用history命令查找历史记录,而查找指定的历史记录,可以与grep相配合,如: history 1 | grep convert会从头查找所有带convert的记录。
    第三种方式就是使用CTRL+R进行回溯查找,输入内容后连续按CTRL+R可以继续向下循环查找。

课后习题(部分)

  1. 阅读 man ls,然后使用ls命令进行如下操作:
  • 所有文件(包括隐藏文件)
  • 文件打印以人类可以理解的格式输出 (例如,使用454M 而不是 454279954)
  • 文件以最近访问顺序排序
  • 以彩色文本显示输出结果

前三个就不说了,第四个的话就是--color=auto,不过大部分Linux系统中,ls命令的输出已经默认启用了彩色显示,所以加不加效果都是一样的。

  1. 编写两个bash函数marcopolo执行下面的操作。每当你执行marco时,当前的工作目录应当以某种形式保存,当执行polo时,无论现在处在什么目录下,都应当cd回到当时执行marco的目录。为了方便debug,你可以把代码写在单独的文件marco.sh中,并通过source marco.sh命令,(重新)加载函数。

首先通过vim marco.sh创建marco.shvim不熟的话就先记这几个,按i进入编辑模式,编辑完后按esc,然后输入:wq保存退出)。
然后具体内容如下:

1
2
3
4
5
6
7
8
9
#!/bin/bash
marco() {
export MARCO_DIR=$(pwd) # 设置环境变量MARCO_DIR用于保存当前的工作目录
}

# 在 polo 函数中返回到之前保存的目录
polo() {
cd "$MARCO_DIR" || return # 返回之前保存的目录,如果目录不存在则失败
}
  1. 假设您有一个命令,它很少出错。因此为了在出错时能够对其进行调试,需要花费大量的时间重现错误并捕获输出。 编写一段bash脚本,运行如下的脚本直到它出错,将它的标准输出和标准错误流记录到文件,并在最后输出所有内容。加分项:报告脚本在失败前共运行了多少次。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #!/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"
    建立两个脚本,一个叫bugger.sh,复制上述脚本;一个叫runner.sh编写以下内容:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #!/usr/bin/env bash

    # 保存输出的文件名
    output_file="output.log"

    # 记录运行次数
    count=0

    # 循环执行脚本,直到它出错
    while true; do
    # 每次执行前增加计数
    ((count++))

    # 执行脚本并将标准输出和标准错误流记录到文件
    ./bugger.sh >> "$output_file" 2>&1

    # 检查脚本的退出状态
    if [ "$?" -ne 0 ]; then
    echo "The script failed after $count runs"
    break
    fi
    done

    # 输出记录的内容
    echo "Contents of $output_file:"
    cat "$output_file"
    这边解释一下2>&1, 2表示标准错误流,1表示标准输出流,>&表示重定向,这里就是将标准错误流重定向到标准输出流然后一起输入到output.log内。
    然后在source runner.sh的时候会有bugger.sh的权限问题,chmod +x bugger.sh更改执行权限就好了。

4、使用xargs 命令,它可以使用标准输入中的内容作为参数。如 ls | xargs rm 会删除当前目录中的所有文件。您的任务是编写一个命令,它可以递归地查找文件夹中所有的HTML文件,并将它们压缩成zip文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行(提示:查看 xargs的参数-d)。

使用touch {1..10}.html生成html文件, 然后执行find . -type f -name "*.html" | xargs -d '\n' zip myhtml.zip即可。
-d '\n'表示以\n(也就是换行符)为输入的定界符。

第3讲 - Vim编辑器

根据Stack Overflow的调查显示,最受欢迎的图形编辑器是VS Code,而基于命令行的最受欢迎的编辑器是VimVim的背后具有很多有趣的思想并且得到了许多其他工具广泛的支持与认可(模拟Vim),不管最后会不会使用Vim去编程,这些思想也应该去了解学习。
本节课目的:学习Vim的核心设计哲学以及一些基础的操作。

Vim的核心设计哲学

  • Vim是一个模态编辑器(model editor),具有多种操作模式,可以用不同的操作模式去处理不同类型的任务。
  • Vim避免使用鼠标,因为那样太慢了。Vim甚至避免用上下左右键因为那样需要太多的手指移动。Vim需要很少的键盘操作,允许你编辑的速度跟上你思维的速度。
  • Vim最重要的设计思想是Vim本身就是一种基于命令的程序语言。

Vim的相关操作

模式切换

打开Vim会先进入普通模式,该模式用于移动光标、阅读内容、从文件到文件的跳转等。
在这种模式下,不同的按键有不同的含义。最经常用的就是i键,用于进入插入模式对文件进行编辑。
切换到其他模式的话,按R进入替换模式,v进入可视模式,可视模式里又有可视行与可视块,V进入可视行模式,<C-v> Ctrl-V, 有时也写作 ^V)进入可视块模式,:进入命令模式。
从其他模式返回普通模式下用esc

打开Vim

在命令行输入vim 指定的文件,执行即可。

编辑文件

  1. i进入插入模式,如图:
    QQ20240119-114304.png
    (注意,光标在哪里进入插入模式就会在哪里开始插入)
  2. :进入命令行模式,命令行常用退出vim
    QQ20240119-115310.png
    一些常用命令如下:
  • :q,退出
  • :w,保存(写)
  • :wq,保存然后退出
  • :e {文件名},打开要编辑的文件
  • :ls,显示打开的缓存
  • :help {标题},打开帮助文档(按:q返回)
    • :help :w,打开:w命令(命令行模式)的帮助文档
    • :help w,打开w命令(普通模式)的帮助文档

缓存区、窗口、标签页

  • :sp(上下切分)或者:vsp(左右切分)来分割窗口
  • 标签页可用于多文件编辑,在普通模式下使用:tabnew 文件名就可以在新的标签页打开指定文件。使用gt切换到下一个标签页,gT切换到上一个标签页
  • 一个标签页可以有多个窗口,每个窗口只对应一个缓存区。一个文件维护一个缓存区,同一个缓存区可以在多个窗口中显示。(因此在一个窗口内改动内容,其他窗口也可以同步看到改动的内容。)
  • :q更具体来说是关闭当前窗口,:qa关闭所有的窗口。

普通模式的工作原理

  1. 移动
  • 移动光标: hjkl(左, 下, 上, 右)
  • 以词为单位移动:w(下一个词初),b(上一个词初),e(移动到词尾或者下一个词的词尾)
  • 行的移动(源于正则): 0(移到行初),$(移到行尾),^(移到行的第一个非空格字符)
  • 滚动:Ctrl-u(上翻),Ctrl-d(下翻)
  • 整个文件的移动:gg(文件头),G(文件尾)
  • 整个屏幕的移动: H(屏幕首行),M(屏幕中间),L(屏幕底部)
  • 查找: f字符(移动到光标以后的第一个目标字符),F字符(移动到光标以前的第一个目标字符),t字符(移动到光标以后的第一个目标字符的前一个字符), T字符(移动到光标以前的第一个目标字符的后一个字符)
  1. 编辑
  • O/o,在光标之上/之下插入行后开始编辑
  • d移动命令,表示根据移动命令进行删除。例如,dw删除词, de删除到词尾,d$删除到行尾, d0删除到行头。dd会删除光标所在的行
  • u,撤销
  • c移动命令,表示改变,在执行删除后进入插入模式进行更改。cc会删除光标所在的行并进入插入模式
  • x,删除字符(等同于dl
  • r,替换字符(先按r,再按想要的字符就会进行替换)
  • y移动命令,复制。p,粘帖。yy复制当前行,yw复制词
  1. 基于可视模式的操作
    常用可视模式来进行文本块复制。可视模式下大部分的移动命令是可用的。
    普通可视模式(正常选中):
    QQ20240119-131806.png
    可视行模式(按行选中):
    QQ20240119-131751.png
    可视块模式(按块选中,是常用编辑器无法做到的):
    QQ20240119-131721.png
    ,反转选中部分的大小写

  2. 计数
    执行指定操作若干次,比如:

  • 5j,向下移动5行
  • 3w,向后移动三个词
  • 7dw,删除7个词
    在可视模式内依旧可用。
  1. 修饰符
    修饰符有i,表示“内部”或者“在内”,和a,表示“周围”。光标通过悬停在不同的括号内对不同的括号内容进行操作。
  • %,移动到匹配的括号
  • ci(,改变当前括号内的内容
  • ci[,改变当前方括号内的内容
  • da',删除一个单引号字符串,包括周围的单引号

所以ia之间的关系就是是否包含符号的关系。

具体演示

建议直接看视频,38:00处开始。

自定义Vim

Vim通过一个名为.vimrc的文件来进行相关自定义,使用vim ~/.vimrc命令进行配置操作。在github与许多博客里都可以找到很多相关的内容。
Vim有很多扩展插件可以提供拓展内容(自行了解)。

课后习题(部分)

  1. 完成 vimtutor。
    这边直接在终端里面输入vimtutor就可以做了,都是一些基础性练习。
    后面这些题目就不贴了,都是比较实践性的题目(安装插件,改配置什么的),大家自己去做就好了,能参考的地方也比较多(比如讲义)。