OS假期预习(一)——Linux,Git与Shell脚本

本文最后更新于:2024年4月22日 晚上

OS假期预习(一)——Linux,Git与Shell脚本

Linux

Linux是什么?

Linux 操作系统是基于 Linux 内核的开源操作系统,Linux 内核在 1991 年由 Linus Torvalds 首次发布,在庞大的社区下不断完善和升级。Linux 系统通常制作成 Linux 发行版供用户使用。Linux 发行版包括 Linux 内核,以及相关的软件和库。这些软件和库大多数都是由 GNU 项目提供的。

GNU(GNU is Not Unix 的递归缩写),是一个广泛的自由软件集合,为 Linux 操作系统提供了大量的开源软件。这些开源软件有一些非常出名,可以说是家喻户晓:比如的编译器套件 GCC 和调试器 GDB,编程语言 Pascal,用于科学计算的 R 语言,很多人以之为信仰的 Emacs 编辑器等,都是 GNU 项目的产物。

实验课程使用的 Linux 发行版叫做 Ubuntu,是目前最为流行的 Linux 发行版之一。

命令行界面

使用命令行界面(Command Line Interface,CLI),我们会用一行一行的命令来告诉操作系统我们需要进行什么操作,操作系统也会把执行的结果输出到屏幕上。

Shell 直译为“壳”,也就是操作系统的“外壳”(相对于操作系统“内核”而言),即用于访问操作系统服务的用户界面。Shell 可以分为命令行界面(CLI Shell)和图形用户界面(GUI Shell,GUI 是 Graphical User Interface 的简称)。我们经常使用的 Windows,macOS,都是 GUI Shell,很多 Linux 发行版也会提供 GUI Shell,但服务器上运行的 Linux 一般都是 CLI Shell。

使用CLI Shell的主要原因在于:

  • 节约资源。在很多资源有限的小型服务器或单片机设备上,内存和 CPU 等资源是十分受限的。运行 GUI Shell 本身就是一笔不小的资源开销,这会大大增加系统性能的负担。
  • 高效自动化操作。举个例子:如果你希望把一个目录下的一千多个文件按序号重命名,在 GUI Shell 下可能很难实现,需要一个一个重命名。但在 CLI Shell 下,你就可以写一个脚本文件,利用循环高效完成这些操作。对于很多复杂操作,GUI 可能不会提供相应的操作方式,但命令行可以完成相当复杂的操作逻辑。
  • 精准操作。命令行本质上是对操作和返回结果的文本描述。针对一些复杂的操作,我们只需要将命令的集合打包成一个脚本文件,就可以在以后精准复现这些操作。这种性质在传播经验时也非常奏效:相比于手把手教你在图形界面上点来点去,不如直接发你一个命令脚本,你就立刻能够知道每一步都在做什么,而且可以直接运行复现。

基础操作

在命令行界面中,用户通过一行一行命令向操作系统发出指示,从而达到人机交互的目的。在 Linux 操作系统中,命令的一般格式为:命令名 [选项] [参数] ...,方括号的意思是可选,意为可以没有,也可以有一个或多个。

目录操作

cd 用法:

cd [选项] 目录

作用:切换到某个目录(Change Directory)。

终端底部出现了新的一行:

1
2
git@21xxxxxx:~$ cd /
git@21xxxxxx:/$

这一行大致理解为:我们进入了 / 这个目录。那么 / 这个目录是什么,上一行的 ~ 又是什么,

Linux 文件系统的最顶层是根目录,/ 表示的就是根目录,Linux 一切的文件都存放在根目录下。我们刚刚就进行了“切换到根目录”的操作。根目录下放置所有的二级目录,包括 Linux 内核启动需要的 /boot 目录,系统配置文件的 /etc 目录,存放普通用户主目录的目录 /home,系统管理员的用户(即 root 用户)主目录 /root,用户系统资源存放的目录 /usr 等等……如果对每个目录都存放什么内容感兴趣,可以利用搜索引擎搜索,这里就不赘述了。

~ 则是当前用户主目录的简写,你可以使用 cd ~ 回到当前用户主目录。对于一般用户,主目录是 /home/用户名,对于 root 用户,主目录则是 /root$ 代表当前的用户是普通用户;如果当前用户是系统管理员(root)用户,则此处会显示 #。在下文中,我们统一用 $ xxx 表示你应当在终端中输入命令 xxx,这里 $ 是终端中显示的符号。

假设我们目前处于根目录(即 / 目录),接下来我们试着进入 /etc 这个目录。很容易想到命令是 cd /etc,但输入 cd etc 也能等效地完成任务。可以看出,cd 命令是支持相对路径的。当我们输入 cd etc 时,系统会在当前的目录,也就是 / 中寻找 etc 这个目录并切换到那里。

那我们如何返回上一级目录呢?在 Linux 系统中,. 表示当前目录,.. 表示上一级目录,所以我们输入 cd .. 就可以返回到上一级目录了。如果输入 cd .,就代表切换到当前所在的目录,也就是什么都不做。还有一个特别的小技巧:输入 cd - 可以跳转到上一次访问的目录。

查看目录下的文件,可以使用ls命令。

ls
用法:ls [选项] [目录]
作用:列出目录中的文件。若参数“目录”未给出,则列出当前目录中的文件。
选项(常用):
-a 显示隐藏的文件
-l 每行只列出一个文件

创建一个新的目录,可以使用mkdir命令。

mkdir
用法:mkdir [选项] 目录
作用:创建一个新目录。

使用 cd ~ 命令回到用户的主目录。这时输入 mkdir newdir 就可以在主目录下创建一个新的目录 newdir。输入 cd newdir 就可以进入刚刚创建的目录。可以注意到,终端已经对当前目录做出了提示,变成了:

1
git@21xxxxxx:~/newdir$

我们可以输入 pwd 命令,查看当前目录的绝对路径。

pwd
用法:pwd [选项]
作用:输出当前目录的绝对路径。

结果如下所示,这也和刚才提到的 ~ 的概念相互印证。

1
2
git@21xxxxxx:~/newdir$ pwd
/home/git/newdir

mkdir 对应,我们还有删除目录的命令 rmdir,请注意,这个命令只能删除空目录。

rmdir
用法:rmdir [选项] 目录
作用:删除一个空的目录。请注意,只有空的目录才能被删除。

退回到用户主目录下,我们可以删除 newdir 这个目录。

文件操作

在 Linux 的哲学下一切皆文件,包括目录、设备等等全部是以文件方式存在于操作系统中的。上一节之所以叫做”目录操作“,是因为所介绍的几个命令是“目录”这种文件独有的命令。这一节叫做“文件操作”,介绍的命令是一般文件(包括目录)都可以操作的命令。例如复制和删除操作,其对象可以是几乎所有文件,也包含目录。

我们首先介绍如何利用 touch 命令创建一个新的空文件。

touch
用法:touch [选项] 文件名
作用:当文件存在时更新文件的时间戳,当文件不存在时创建新文件。

我们主要利用的是后者(创建新文件)的作用。进入用户主目录,输入 touch helloworld.c,就可以创建一个叫做 helloworld.c 的文件。

那么如何删除一个文件,或者删除一个非空的目录呢?

rm
用法:rm [选项] 文件
作用:删除文件。
选项(常用):
-r 递归删除目录及其内容,删除非空目录必须有此选项,否则无法删除。
-f 强制删除,不提示用户确认,忽略不存在的目录。
-i 逐一提示用户确认每个将要被删除的文件。

Tips:rm -rf 是十分危险的命令(尤其在 root 用户下),非必要不使用 rm -rf 命令,在执行之前需要再三确认。root 用户具有至高无上的权限,在该用户下执行 rm -rf / 可以删除一切文件,包括 Linux 本身,从而导致系统被毁灭。

常用的文件操作除了新建文件和删除文件,还有复制和移动文件。

cp
用法:cp [选项] 源文件 目标路径
作用:将源文件(也可以是目录)复制为目标路径对应的文件(如果目标路径是文件)或复制到目标路径(如果目标路径是目录)。
选项(常用):
-r 递归复制目录及其子目录内的所有内容。

递归复制指的是当要复制的目录下存在子目录,且子目录中存在子目录或文件的时候,将逐一复制它们。windows的复制默认是递归复制的。

如果不递归复制,结果得到的目录只包含空的子目录,其子目录下的内容将不会复制。

我们在用户主目录中使用命令 touch test.txt 来创建一个名为 test.txt 的文件,然后使用 mkdir dir 创建一个名为 dir 的目录。接下来使用 cp test.txt dir/ 来进行复制操作。我们先用前面提到过的 ls 命令看看当前目录下的 test.txt 是否还存在,然后切换到 dir 目录,使用 ls 命令,可以看到 test.txt 这个文件。

mv
用法:mv [选项] 源文件 目标路径
作用:将源文件(也可以是目录)移动为目标路径对应的文件(如果目标路径是文件)或移动到目标路径(如果目标路径是目录)。
选项(常用):
-r 递归移动目录及其子目录内的所有内容。

mv 命令的另外一个作用就是重命名文件。原理上很好理解,也就是把一个文件以不同的名字移动到当前目录下,这和重命名是等价的。举个例子,我们在 dir 下使用 mv test.txt test2.txt 就可以把 test.txt 重命名为 test2.txt 了。

对于两个纯文本文件来说,我们还可以使用 diff 命令进行比较的操作。

diff
用法:diff [选项] 文件1 文件2
选项(常用):
-b 不检查空白字符的不同。
-B 不检查空行。
-q 仅显示有无差异,不显示详细信息。

查找操作

在 Windows 或 macOS 操作系统中,文件管理器提供了搜索的功能,以便我们查找文件。那么在 Linux 下如何进行查找操作呢?我们引入两个命令:findgrep

find
用法:find [路径] <选项>
作用:在给定路径下递归地查找文件,输出符合要求的文件的路径。如果没有给定路径,则在当前目录下查找。
选项(常用):
-name <文件名> 指定需要查找的文件名。

使用 find 命令并加上 -name 选项可以在当前目录下递归地查找符合参数所示文件名的文件,并将文件的路径输出至屏幕上。

grep 命令则有所不同。find 是根据文件的属性(文件名、修改日期等)查找文件,而 grep 则是匹配文件内的内容查找文件和文件中的匹配位置。

grep
用法:grep [选项] PATTERN FILE
(PATTERN是匹配字符串,FILE是文件或目录的路径)
作用:输出匹配PATTERN的文件和相关的行。
选项(常用):
-a 不忽略二进制数据进行搜索。
-i 忽略大小写差异。
-r 从目录中递归查找。
-n 显示行号。

当你需要在整个项目目录中查找某个函数名、变量名等特定文本的时候,grep 将是你手头一个强有力的工具。

前面讲到 ls 命令的功能是列出文件。在我们的 Linux 系统中有一个比 ls 更“高级”的命令,叫做 tree,它可以直接输出一个目录下的文件树。这个命令一般不是 Linux 发行版预装的,但我们已经为你安装好了,直接使用即可。

tree
用法: tree [选项] [目录名]
选项(常用):
-a 列出全部文件(包含隐藏文件)。
-d 只列出目录。

总结

连接终端后,我们所处的目录是 ~ 目录,也就是用户主目录。我们可以使用 ls 命令列出一个目录下都有什么内容,可以用 cd 进入到一个目录(输入绝对路径和相对路径都可以),可以用 pwd 显示当前目录的绝对路径。我们可以用 mkdir 创建一个目录,用 rmdir 删除一个空目录。我们可以使用 touch 新建文件,rm 删除文件,cp 复制文件,mv 移动或者重命名文件。

如果想要查看一个命令的详尽说明,就需要使用 Linux 下的帮助命令——man 命令,通过 man 命令可以查看 Linux 中的命令帮助、配置文件帮助和编程帮助等信息。

man
用法:man [选项] 命令
作用:查看命令的详细说明手册。

常用的快捷键

  • Ctrl+C 终止当前程序的执行。当程序卡死或者你不想让它继续执行了,可以输入 Ctrl+C 终止程序。请注意,Ctrl+C 在 Windows 中是复制的意思,但在 Linux CLI 的环境下就是结束进程的意思。所以在使用终端模拟器(比如使用浏览器或 XShell 等)连接终端时,复制终端中显示的内容时记得不要按 Ctrl+C,而是老老实实右键复制,以免误杀进程。
  • Ctrl+Z 挂起当前程序。暂停程序,放到后台。Ctrl+Z 挂起程序后会显示该程序挂起编号,若想要恢复该程序可以使用 fg [job_spec]即可,job_spec 即为挂起编号,不输入时默认为最近挂起进程。
  • Ctrl+D 终止输入(若正在使用 Shell,则退出当前 Shell)。在标准输入中输入 Ctrl+D 也意味着输入了一个 EOF。
  • Ctrl+L 清屏。相当于命令 clear

特别提示,Ctrl+S 在终端中的作用是暂停该终端。有的同学在进行编辑的时候会误触此组合键导致终端“卡死”,此时使用 Ctrl+Q 组合键即可让终端继续运行。

Tips:在多数 Shell 中,四个方向键也是有各自特定的功能的:Left 和 Right 可以控制光标的位置,Up 和 Down 可以切换最近使用过的命令。

实用工具

下面介绍的若干 Linux 下的实用工具,它们都是在 Linux 下开发工作中非常重要的工具,可以说是不可或缺。

GCC

GCC(GNU Compiler Collection,GNU 编译器套件)包含了著名的 C 语言编译器 gcc(GNU project C and C++ compiler)。

gcc
用法:gcc [选项] 源代码文件
作用:编译源代码文件。

首先我们在用户主目录下创建一个文件 hello.c,使用 Vim 打开这个文件。进入编辑模式,输入以下内容,保存退出。

1
2
3
4
5
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}

接下来在当前目录下使用 gcc 命令进行编译。

1
$ gcc hello.c

此时我们没有看到相关输出,说明编译过程成功完成。在当前目录下出现了 a.out 这个文件(这个文件名是 gcc 编译器默认的可执行程序的文件名)。

既然 gcc 为我们编译出了一个可执行文件,那么我们就可以执行这个文件。执行 a.out 的命令是:

1
$ ./a.out

我们知道,./ 是“当前目录下”的意思。为什么要加上一个 ./ 呢,难道不是多此一举吗?实际上在 Linux 中,./ 表示被执行的文件在当前目录,Linux 会在当前目录中查找可执行文件。如果不加 ./,那么 Linux 就会在“系统 PATH”中寻找。然而 a.out 并不在系统 PATH 中,所以 Shell 就会报错a.out: command not found

系统 PATHPATH 是一个系统变量,记录了若干目录。如果待运行的程序不在当前目录,操作系统便可以去依次搜索 PATH 变量中记录的目录,如果在这些目录中找到待运行的程序,操作系统便可以运行这个程序。

运行 a.out 之后,我们就可以看到终端输出了 Hello, world!

make & Makefile

GNU 项目中有一个叫做“make”的工具,是最常用的 C 语言项目构建工具。我们都知道,“make”是制作的意思,而 make 构建工具的作用本质上就是“制作出一个文件”。当我们开发了一个较大的项目,make 工具就可以将这些文件编译链接成可执行文件。当然 make 工具进行编译和链接的操作是需要人工指导的,这个“指导文件”就是 Makefile。make 工具可以根据时间戳自动判断项目的哪些部分是需要重新编译的,每次只重新编译必要的部分。make 工具是基于 Shell 命令运行的,所以只要项目的编译器可以通过 Shell 命令调用,make 都可以对项目进行构建。

为了能够清晰地了解 make 的基本概念,我们来使用 Makefile 指导编译一个最简单的项目——也就是刚刚的 Hello World 项目。

不使用 make 工具,我们可以使用下面的命令来对 hello.c 进行编译:

1
$ gcc -o hello hello.c

如果使用 make 工具来构建,我们就需要在当前目录下创建并编辑 Makefile 文件(文件名就叫做 Makefile)。Makefile 的基本格式如下。

1
2
3
4
5
target: dependencies
command 1
command 2
...
command n

其中,target 是我们构建(Build)的目标,可以是真的目标文件、可执行文件,也可以是一个标签(详见参考教程)。而 dependencies 是构建该目标所需的其它文件或其他目标。之后是构建出该目标所需执行的命令。有一点尤为需要注意:每一个命令(command)之前必须有一个制表符(tab)。这里必须使用制表符而不能是空格,否则 make 会报错。

我们通过在 Makefile 中书写这些显式规则来告诉 make 工具文件间的依赖关系:如果想要构建 target,那么首先要准备好 dependencies,接着执行 command 中的命令,以得到 target。在书写完恰当的规则之后,只需要在 shell 中输入 make targettarget 是目标名),即可执行相应的命令、生成相应的目标。

因此,我们的简易的 Makefile 可以写成如下的样子,之后执行 make hello 或是 make,即可产生 hello 这个可执行文件,使用 ./hello 就可以执行它。值得注意的是,当执行 make 指令而不附加其他参数时,默认构建第一个目标。

1
2
3
4
all: hello

hello: hello.c
gcc -o hello hello.c

ctags

ctags 是一个方便代码阅读的工具,我们用到的功能是代码跳转功能。利用 ctags,我们可以在 Vim 下进行更便捷的开发。

在使用之前我们需要修改 Vim 的配置文件,使其支持 ctags 的相关功能。打开 ~/.vimrc 文件(如果没有则新建),添加下面两行并保存。

1
2
set tags=tags
set autochdir

我们修改 hello.c 文件,如下所示。

1
2
3
4
5
6
7
#include "ctags_test.h"
int main() {
struct Pair p;
p.a = 1;
p.b = 2;
return 0;
}

新建一个文件 ctags_test.h,输入如下内容:

1
2
3
4
struct Pair {
int a;
int b;
};

我们保存并退出 Vim,执行命令 ctags -R *,就会发现在该目录下出现了新的文件 tags ——这是 ctags 为我们创建的符号名索引文件。此时我们就能使用 ctags 的功能了。

使用 Vim 打开 hello.c 文件,将光标移动到 ab 上,按下 Ctrl+],就可以跳转到结构体中 ab 的定义处。再按下 Ctrl+O就可以返回跳转前的位置。

tmux

对于我们来说,Linux 终端就是我们眼前的黑框框。Linux 是一个优秀的多用户多进程操作系统,用户经常需要同时运行多个程序并同时观察它们的输出。然而我们的终端看起来只能同时显示一个程序,并不能同时观察多个程序的运行。一个简单的解决方案就是同时打开多个终端,让一个用户同时多次登录 Linux 计算机(Linux 允许这样做)。但这样做有时在窗口切换时会产生麻烦。

另外还有一个问题:当我们的终端和 Linux 计算机断开连接时,终端前台正在运行的进程会被杀死。所以我们在终端中等待程序运行时就需要一直保持着终端的连接,这对于长时间运行程序的服务器来说是不现实的,运维人员不能总是在前台看着程序不断输出。

为了解决这两个问题,我们引入“终端复用神器” tmux(Terminal multiplexer)。它实现了终端窗口和进程分离,同时也可以在窗口中同时显示多个进程的运行。

窗格、窗口、会话是 tmux 的三个基本概念,一个会话可以包含多个窗口,一个窗口可以分割为多个窗格。突然断开连接或主动分离(Detach)后 tmux 仍会保持程序的运行,通过重新连接会话可以直接在之前的环境继续工作。

看到了 tmux 的运行效果,我们立刻会问,如何进入 tmux,实现分屏操作呢?

我们在 Shell 下直接输入命令 tmux,可以看到终端底部出现一行绿色,这时就已经进入了 tmux 的新会话。tmux 的操作由一系列快捷键组成,下面对重要的快捷键进行介绍。

  • Ctrl+B Shift+Num 5(同时按下 Ctrl 和 B,然后松开这两个键,紧接着立刻输入“%”,下面同理),将窗口左右分屏。
  • Ctrl+B Shift+’ ,将窗口上下分屏。
  • 重复以上两个快捷键,可以将目前活动的窗格继续分屏。
  • Ctrl+B Up / Down / Left / Right 根据按键方向切换到某个窗格。
  • Ctrl+B Space,切换窗格布局(上下变成左右,左右变成上下)。
  • Ctrl+B X,关闭当前正在使用的窗格(根据提示按 Y 确认关闭)。
  • Ctrl+B D,分离(Detach)当前会话,回到 Shell 的终端环境。此时程序仍然保持在 tmux 会话中的状态。

当我们使用 Ctrl+B D 分离了会话或者意外断开了连接,我们该如何恢复到之前的会话中呢?

我们首先使用 tmux ls 命令查看当前都有哪些会话。记住会话名(会话名是冒号左边的内容,默认情况下是一个数字),使用 tmux a -t 会话名 恢复到原来的会话。

Git

Git是什么?

Git 是一个先进的版本控制系统。试想一个几百人同时开发一个大项目的场景:有人想在某个版本进行测试,也有人想要在这个版本的基础上进行新功能的开发而不影响测试人员,还有人想进行另外一个功能的开发……在迭代许多版本之后,代码的管理就变得十分复杂了。复杂主要包括两个方面:时间上的复杂(版本不断更新,新功能出现问题需要回退等等)和空间上的复杂(不同人开发不同的部分)。为了解决这些问题,就出现了版本控制系统。

三种储存位置

Git 中的三种储存状态分别是:工作区(Working Directory)、暂存区(Staging Area)和储存库(Repository)。

一般来说,一个项目在 Git 中是以目录的形式存在的,这个目录包含了工作区.git 子目录之外的内容)和储存库.git 子目录)。工作区可以理解为“目前正在编辑的版本”,储存库储存在一个隐藏目录 .git 中(因为它以 . 开头,所以是隐藏的),用来存放提交过的所有版本的内容及其联系。暂存区存放了已经确定修改但尚未提交的文件。暂存区的信息理论上也储存在 .git 目录中,但是在用途上和储存库有区别,因此和储存库的概念区分开来。

四种储存状态

一个文件,在 Git 目录中有以下四种状态:

  • 未跟踪(Untracked):一个文件在储存库的版本信息中没有被记录过。比如在储存库中新建了一个文件,这个文件现在就是未跟踪的状态;在一个非空目录下使用 git init 来初始化一个空储存库,此时这个目录下的所有文件都处于未跟踪的状态。
  • 未修改(Unmodified):一个文件在跟踪之后一直没有改动过,或者改动已经被提交(即工作区的内容和储存库中的内容相同),则处于未修改状态。当我们修改这个文件时,则会使这个文件变成已修改状态。
  • 已修改(Modified):一个文件已经被修改(即工作区的内容和储存库中的内容不同),但还没有加入(git add) 到暂存区中。
  • 已暂存(Staged):一个文件已被加入暂存区。加入暂存区意味着将一个已修改的文件加入下次提交(git commit)需要存入储存库的文件清单中。

常用命令

git init

git init 会自动把当前目录变成一个空的 Git 仓库,这样就可以对当前目录下的内容进行版本管理了。

git clone

git init 创建一个新的储存库,而 git clone <URL> 则是“克隆”一个已有的储存库到当前目录下。通常我们会从一个互联网地址(即 URL,统一资源定位符)进行克隆,所以一般这种操作可以理解为“下载”。

git status

这个命令可以查看当前分支的状态,以及当前工作区的变动和暂存区的内容,便于我们对工作区的概况进行掌握。

git add

使用这个命令,可以把一个新文件或者已经修改过的文件加入暂存区中。在你完成一部分实验内容之后,可以使用 git add . 将你的所有修改加入暂存区,也可以使用 git add <filename> 来将指定的文件加入暂存区。

git restore

我们在修改一个文件之后,可能想要放弃这个修改。当这个文件还没有通过 git add 加入暂存区时,我们可以使用 git restore <filename> 来撤销对这个文件的修改,使其退回到上一个 commit 的状态。如果这个文件已经加入了暂存区,我们可以通过 git restore --staged <filename> 来取消暂存。

git checkout

这个命令涉及到分支的知识,分支的概念在这里不会详细介绍,可以参考 Pro Git。在实验课程中,可能会涉及在各个 Lab 中进行切换。这时使用 git checkout lab<x> 可以切换到相应的分支。

请注意,在切换时,需要保证目前所有文件的状态均为“未修改”(没有修改过,或者已经提交)。

git commit

使用 git commit -m <message> 这个命令将暂存区的修改提交到储存库中。当 message 参数有空格时需要把 message 用引号括起来。在提交时,要求给出一段说明性文字。这段文字可以任意填写,但建议按照提交内容填写,以保证多人协作时的可读性。本实验不会涉及多人协作,所以方便自己开发即可。

git push

这个命令将本地的 commit 推送到一个远程仓库。在课程实验中,这个命令可以将你的 commit 推送到 GitLab。

git pull

这个命令将远程新建的分支下载到本地,并且将远端的更改合并到当前的分支。在利用评测机进行实验分支的初始化之后,可以在开发机中使用这个命令来将新的分支下载到本地。

Shell脚本

所谓 Shell 脚本,其实就是一条一条命令组合起来,放到一个文件中,并且可以直接运行这个文件。

Shell 脚本和 Windows 批处理文件是十分类似的,只不过在 Linux 下,脚本语言编程是用户的必备技能,也是高效操作所必需的技能;而在 Windows 下的批处理脚本则对于普通用户来说可有可无,因为他们只需操作图形界面就可以完成日常生活中的绝大多数操作。

Shell脚本的执行

在 Windows 中,批处理文件的扩展名是 .bat,类似地,在 Linux 中,脚本文件的文件名在最后一般会加上 .sh 后缀。实际上 Linux 中没有扩展名的概念,所以加不加 .sh 对运行是没有影响的。但为了区分于其他文件,一般约定脚本文件的文件名都以 .sh 结尾。

执行 Shell 脚本很容易:./文件名(文件名包含 .sh,因为这是文件名的一部分。再次强调,Linux 中没有扩展名的概念)即可运行。

需要注意的是,被执行的脚本文件必须有“执行”权限。在 Linux 系统中,每个文件对于拥有者、用户组和其他用户都分别有“读”、“写”、“执行”的权限。我们使用 touch 命令创建的文件默认是没有“执行”权限的,需要手动添加。若发现 Shell 脚本执行时出现了“Permission denied”的错误,多半是因为没有添加“执行”权限。我们可以使用下面的命令手动添加“执行”权限。

chmod +x 文件名

后文中提到的 Shell 语法均指的是 Bash Shell 语法。

语法基础

Hello, world!

首先用 touch 命令创建一个新文件 hello.sh,使用 Vim 输入以下内容。

1
2
3
#!/bin/bash
#My first Shell script!
echo "Hello, world!"

保存退出,修改权限,运行。我们可以看到终端输出了 Hello, world!。下面对程序进行逐行解释。

第一行:指定脚本的默认运行程序(即解释器)。在这里指定为 bash,这是我们最常使用的脚本运行程序。

其中,#! 出现在脚本文件的第一行的前两个字符,被称为 Shebang。当文件中存在 Shebang 的情况下,程序加载器会分析 Shebang 后面的内容,并且将这些内容作为脚本文件的解释器。

第二行:注释。注释以 # 开头。

第三行:输出。将 echo 命令后面的字符串输出。像本章开头所说的,Shell 脚本是命令和组合,其实 echo 也是一个命令。不信你把这一行在终端中直接运行一下,看看发生了什么,也可以使用 man 命令查询一下 echo 命令是做什么的。

变量

一般的脚本语言都涉及到变量的操作,那么 Shell 脚本中是如何定义和使用变量的呢?

Shell 是弱类型语言,定义变量时无需指定类型。定义变量(变量名为 var_name,值为 value)的方式是:

1
var_name=value

请注意,等号两边不允许有空格

使用 $var_name 可以获取变量的值。在使用时,建议在变量名的两端加一个花括号(形如 ${var_name}),以帮助解释器识别变量的边界,避免歧义。

下面我们用定义和使用变量的形式,重新编写上面的 Hello, world 程序,将 hello.sh 的内容修改为:

1
2
3
#!/bin/bash
str="Hello, world!"
echo $str

保存退出,运行,达到了同样的效果。

脚本参数——特殊的变量

之前学习命令的时候,我们发现很多命令都需要传入参数和选项。在 Shell 脚本中,传递参数是完全支持的,这也大大提高了 Shell 脚本的通用性和便捷性。下面我们简单讲解如何传递参数。

参数在 Shell 脚本中体现为特殊的变量。在执行语句中,参数以空格分隔,每一个参数在脚本中都是一个字符串变量。第一个参数映射到变量名 1,第二个参数映射到变量名 2,以此类推……

我们用一个实例来说明参数的传递。我们将 hello.sh 的内容修改为:

1
2
3
#!/bin/bash
str="Hello, $1 and $2!"
echo $str

输入如下命令运行,看看控制台输出了什么:

1
$ ./hello.sh world OS

请在双引号中引用变量。如果将上述的双引号改成单引号,则会原文输出引号内的内容,你可以尝试一下。

需要补充的是,对于传递的参数,不仅有 $1$2 这样的特殊变量,还提供了其他的特殊变量:

  • $# 传递的参数个数;
  • $*一个字符串,内容是传递的全部参数。

条件与循环

如果我们想对大批量的文件进行复杂操作,我们一般需要用到条件控制和循环控制。这也是自动化操作的精髓所在。下面我们对 ifwhile 关键字进行简单讲解。

if 语句块的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if condition1
then
command11
command12
......
elif condition2
then
command21
command22
......
else
command31
command32
......
fi

其中,fi 是“if”的倒写,代表 if 语句块的结束;elif 意为“else if”。elifelse 可以按需省略。下面举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
if (( $1 > $2 ))
then
echo "first > second"
elif (( $1 < $2 ))
then
echo "first < second"
elif (( $1 == $2 ))
then
echo "first == second"
else
echo "I don't know..."
fi

其中 (()) 是用于比较整数之间大小的表达方式。传入字符串或浮点数,则是未定义的行为。可以试着传入不同的参数,看看程序输出了什么。

while 语句块的格式如下:

1
2
3
4
5
6
while condition
do
command1
command2
...
done

其中 done 表示“do”语句块的结束。下面举例说明。

1
2
3
4
5
6
7
8
9
#!/bin/bash
mkdir files
cd files
i=1
while (($i <= $1))
do
touch "file$i.txt"
let i=i+1 # or i=$((i+1))
done

这里,let 是为变量赋值的命令,与之等价地,也可以使用 i=$((i+1))

函数

Shell 脚本也支持函数。函数的定义方式如下:

1
2
3
4
function fun_name() {
body...
return int_value;
}

其中 function() 可以省略其中一个。其中返回语句是可选的,函数可以不返回值。int_value 是一个[0,255][0,255]之间的整数,返回其他值是未定义的行为,一般会返回一个错误的结果。

函数的调用方法如下:

1
fun_name param1 param2 ... paramN

其中第 N 个参数在函数体内使用 $N 来获取,且不需要在函数定义开头声明。值得注意的是,当 N>=10 时,需要用 ${N} 来获取参数,否则 $ 只会带第一位数字。如果函数有返回值,则在函数调用的后面需要使用 $? 获取返回值。下面用一个例子说明函数是如何定义和调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
function fun1() {
echo "Hello, world!"
}
function fun2() {
echo "I've got "$1" and "$2"!"
}
function fun3() {
echo "I'm computing first + second!"
return $(($1 + $2))
}
fun1
fun2 2 3
fun3 4 5
echo "The sum is "$?"."

其中,$(($1 + $2)) 的意思是先计算 $1 + $2 的值。

sed——用命令编辑和输出文本

我们已经初步了解到 Shell 脚本的强大功能。但我们目前对于文本的编辑功能仅仅学习了 Vim 等工具的使用。有人可能会想到,可以通过 Shell 脚本(即一条条的命令)对文本文件的内容进行编辑和输出吗?当然可以!sed 是通过命令编辑和输出文本的工具,通过掌握 sed 命令的使用,我们就可以实现文本内容编辑和选择输出的自动化。

sed
sed [选项] ‘命令’ 输入文本
选项(常用):
-n:安静模式,只显示经过sed处理的内容。否则显示输入文本的所有内容。
-i:直接修改读取的档案内容,而不是输出到屏幕。否则,只输出不编辑。
命令(常用):
<行号>a<内容>: 新增,在行号后新增一行相应内容。行号可以是“数字”,在这一行之后新增,也可以是“起始行,终止行”,在其中的每一行后新增。当不写行号时,在每一行之后新增。使用$表示最后一行。后面的命令同理。
<行号>c<内容>:取代。用内容取代相应行的文本。
<行号>i<内容>:插入。在当前行的上面插入一行文本。
<行号>d:删除当前行的内容。
<行号>p:输出选择的内容。通常与选项-n一起使用。
s//:将(正则表达式)匹配的内容替换为

sed 中正则表达式的相关语法可以查阅 sed 文档。sed 等工具中的正则表达式语法和 Java 等语言中不完全相同,请注意区分。

不加 -i 选项时,只在控制台显示编辑后的文件内容,而不修改文件本身。下面举几个使用例子,以便更好地理解 sed 命令的使用方法。

1
2
3
4
5
6
7
8
9
10
11
12
sed -n '3p' my.txt
# 输出 my.txt 的第三行
sed '2d' my.txt
# 删除 my.txt 文件的第二行
sed '2,$d' my.txt
# 删除 my.txt 文件的第二行到最后一行
sed 's/str1/str2/g' my.txt
# 在整行范围内把 str1 替换为 str2。
# 如果没有 g 标记,则只有每行第一个匹配的 str1 被替换成 str2
sed -e '4astr ' -e 's/str/aaa/' my.txt
#-e 选项允许在同一行里执行多条命令。例子的第一条是第四行后添加一个 str,
# 第二个命令是将 str 替换为 aaa。命令的执行顺序对结果有影响。

除了 sed 命令,还有更强大的 awk 命令,但使用也更加复杂。

重定向

很多命令都有控制台输出。如果想要把这些命令的输出写到文件中,应该怎么办呢?答案是使用重定向。Linux 定义了三种流:

  • 标准输入:stdin,由 0 表示。
  • 标准输出:stdout,由 1 表示。
  • 标准错误:stderr,由 2 表示。

默认情况下,这些流使用的设备是控制台,也就是我们可以在控制台上看到命令的输出。在命令后使用>符号可以将输出重定向。举个例子:

1
$ ls / > lsoutput.txt

这个命令就是将根目录下的文件名输出到当前目录的 lsoutput.txt 文件中,覆盖文件的原有内容,如果想在文件后追加命令的输出,使用 >> 符号而不是>。由于输出被重定向到文件,所以我们在控制台中看不到输出了。这就是输出重定向的基本用法。

我们在控制台中单独运行 gcc 命令,什么参数也不加。由于没有输入文件,显然编译器是无法正确运行的。所以我们可以看到 gcc 产生了一个 fatal error:

1
2
3
$ gcc
gcc: fatal error: no input files
compilation terminated.

试试看使用命令 gcc > gccerr,看看发生了什么?我们发现控制台仍然输出了错误,而 gccerr 中没有任何内容。似乎重定向不好用了。这是为什么呢?

实际上,>1> 的简写。在我们刚才提到的三种流中,1 指的是标准输出,而不包括标准错误输出。gcc 输出的编译错误消息属于标准错误输出。如果想要将错误输出到文件,需要使用 2>,也就是将错误输出重定向到文件。试试使用 gcc 2> gccerr,看看是否达到了预期效果。

同理,我们也可以对输入进行重定向。

管道

管道符号|可以对命令进行连接。

1
command1 | command2 | command3 | ...

以上内容是将 command1 的输出传给 command2 的输入,command2 的输出传给 command3 的输入,以此类推。

cat 命令将文件内容输出到标准输出,grep 命令支持从标准输入读取文本。所以这两个命令就可以用管道进行连接。

1
cat hello.txt | grep "Hello"

这个命令的作用就是将 hello.txt 文件输出,输出的内容传入 grep 命令,grep 命令在这些内容中查找 Hello 字符串。


OS假期预习(一)——Linux,Git与Shell脚本
https://galaxy-jewxw.github.io/2024/02/23/OSPre1/
作者
Traumtänzer aka 'Jew1!5!'
发布于
2024年2月23日
许可协议