ENGG1340笔记

自学ENGG1340写的笔记

前言

惊讶的发现这学期ENGG1340是自学课,没有Lecture。讲道理挺舒服的。

ENGG1340期望你拥有Python编程知识作为基础,然后逐渐引入更深层的新内容,包括Linux / Bash Shell,版本控制(Git),C/C++语言等。

叠甲先:笔记可能出错所以轻喷。内容是根据课上发的笔记转写的,所以本文算是个人梳理吧


Module 1: Linux Environment

Linux如同Windows一样是操作系统。Linux是开源的,这导致Linux相较于其他操作系统拥有更多长处,例如:可自定义程度较强,安全性也比较高(因为你可以直接读到源代码)。
因为Linux是开源的,所以使用Linux也是免费的。在一些相对低价的电子产品上,使用Linux作为操作系统就很划算。

Linux存在不同的发行版,例如Ubuntu, CentOS, Debian等。

在ENGG1340中我们将会使用Ubuntu,虽然现阶段不同发行版不会对课程大纲产生太多影响。


Linux Shell

Shell 是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。

Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。

Linux中有不同种类的shell,比如Korn Shell, Bourne Shell, C Shell,以及Bash Shell。
不同的Shell之间命令可能有些许区别,例如Korn Shell使用print命令输出一个字符串,而Bash Shell使用echo命令。

在ENGG1340里我们主要使用Bash Shell。


Shell Commands

date

向命令行中输入date,Shell会返回当前系统的时间:

1
2
$ date
Sat Jan 25 14:51:19 HKT 2025

文件与路径操作

ls命令列举当前目录下的所有文件和路径:

1
2
3
4
5
6
7
8
$ ls 
Documents/ logon.bat* public_html/ t.txt Downloads/
Pictures/ Templates/
Videos/
Music/
Desktop/ engg1340/
Public/
test.txt

命令变体

  • ls -l 以长字符方式输出,包含额外信息如文件大小,文件拥有者,上次编辑日期等
  • ls -a 输出路径下所有内容,包含隐藏的文件和目录。隐藏的内容会以一点开头.
  • ls -la 就是ls -l -a

cd命令用来重定向当前目录。例如我们想要访问当前目录下的engg1340路径,直接敲cd engg1340就行:

命令变体:
cd .. 前往当前目录的父级路径
cd ~ 前往当前用户的主目录
cd ~username 前往指定用户的主目录
cd . 前往当前目录 (虽说没啥效果就是了)

1
2
3
$ cd engg1340 
$ ls
lab1/ lab2/ lab3/

pwd命令将会输出当前的工作目录:

1
2
$ pwd
/home/d003/h978645312

命令使用说明

Shell里面有许多命令,能将所有命令记住基本上是不现实的。因此,Shell提供了man命令(Manual的缩写),能够返回命令的详细解释。

例如我想要查看ls命令的详细解释,就可以使用man ls

1
$ man ls
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
31
32
33
34
35
36
LS(1)                                               User Commands                                               LS(1)

NAME
ls - list directory contents

SYNOPSIS
ls [OPTION]... [FILE]...

DESCRIPTION
List information about the FILEs (the current directory by default). Sort entries alphabetically if none of
-cftuvSUX nor --sort is specified.

Mandatory arguments to long options are mandatory for short options too.

-a, --all
do not ignore entries starting with .

-A, --almost-all
do not list implied . and ..

--author
with -l, print the author of each file

-b, --escape
print C-style escapes for nongraphic characters

--block-size=SIZE
with -l, scale sizes by SIZE when printing them; e.g., '--block-size=M'; see SIZE format below

.
.
.

-l use a long listing format

Manual page ls(1) line 1 (press h for help or q to quit)

从NAME区域我们可以得知ls的解释为”list directory contents”。
从DESCRIPTION区域得知,如果在ls后面加入可选参数-l,代表”use a long listing format”。这会在普通ls的基础上显示更多信息,包括文件大小,文件拥有者,上次编辑日期等等:

1
2
3
4
5
6
$ ls -l
total 26044
drwxr-xr-x 17 1000 1000 4096 Nov 4 01:17 Python-3.11.0
-rw-r--r-- 1 root root 26333656 Oct 24 2022 Python-3.11.0.tgz
drwxr-xr-x 4 root root 4096 Nov 1 13:25 napcat
-rw------- 1 root root 317274 Nov 4 01:38 nohup.out

路径和文件管理

在Linux中根目录用/表示。根目录包含所有的文件和路径。

例如在根目录下有一个”home”路径,”home”下有另一个路径”kit”,那么这个”kit”路径的绝对路径就是:/home/kit

有关相对路径和绝对路径的内容一会介绍。


相对路径和绝对路径

绝对路径总是以/打头。举几个简单的例子:

  • /home/file3是在home路径下的文件file3的绝对路径。
  • /home/kit/Desktop/file4是file4的绝对路径。

而相对路径和当前的工作目录有联系,相对路径不会以/打头。

例如我们使用上面的file3做例子,如果当前的工作目录为/home,则file3的相对路径为file3。注意看刚才这个file3的路径不以/打头,所以能判断出这是一个相对路径。

file4同理。如果当前工作目录是/home/kit,那么file4的相对路径就是Desktop/file4


如果目录A包含另一个目录B,则A称为B的父目录,B称为A的子目录。例如在刚才的例子中,kit是home的子目录,home是kit的父目录。

一旦shell启动,它当前的工作目录就是主目录。我们总是可以通过命令pwd获得当前的工作目录。


主目录

对于任何用户,都有一个属于该用户的主目录。例如,用户 kit 的主目录位于/home/kit

~用来表示用户的主目录。你可以使用cd ~命令回到当前用户的主目录,也可以使用cd ~username来前往其他用户的主目录。

路径管理

创建路径

mkdir命令可以用来创建路径 新建文件夹

1
$ mkdir lab

上面的命令会在当前的工作目录下创建一个lab路径。

如果你想在当前工作目录下创建多个路径,可以直接:

1
$ mkdir lab1 lab2

如果想在路径名中包括空格,可以在创建路径时将路径名用引号引起来:

1
$ mkdir "lab 1"

移除路径

rmdir命令可以用来移除空路径:

1
$ rmdir lab

如果想要移除的路径不是空路径,例如:

1
2
$ ls lab1
file1.txt file2.txt

就需要使用rm -rf或者rm -r -f来移除整个路径:

1
2
3
$ rm -r -f lab1
$ ls lab1
ls: cannot access 'lab1': No such file or directory

可以看到路径被成功移除了。

-r参数代表系统会按照递归方式向下遍历目标路径下的文件和路径,而-f可以确保删除过程不需要你来进行确认。如果当前路径下文件巨多,不带-f就会很麻烦了。


重命名路径

mv命令虽然叫move,但是也可以用来重命名:

1
$ mv lab "lab 1"

这个命令会把lab路径重命名至lab 1

如果mv命令下第一个参数和第二个参数的类型一致,例如参数1和参数2都是文件类型,则该命令会将参数1重命名至参数2。


文件管理

创建文件

touch命令可以用来创建一个空文件。例如:

1
$ touch file1.txt

可以创建一个名叫”file1.txt”的空文件。


展示文件内容

cat,命令可以用来展示文件内容:

1
2
3
$ cat file1.txt
Hello, this is the content of file1.txt
Bye Bye!

移动文件

mv命令也可以用来移动文件或者路径位置:

1
$ mv hello.txt mydir

好比说这串命令会将hello.txt移动到mydir路径下。

如果文件拥有同样的前缀,你可以使用星号()来选中所有符合条件的项。
例如:将所有拥有“ *myfile
”前缀的文件移动到lab路径下:

1
2
$ ls
myfile1.txt myfile2.txt lab/
1
$ mv myfile* lab
1
2
$ ls
lab/
1
2
$ ls lab
myfile1.txt myfile2.txt

复制文件

除此之外还有其他关于文件操作的命令集合:

cp 命令用于复制文件。例如:

1
$ cp file1 file2

会将”file1”复制至”file2”

1
$ cp -r dir1 dir2

会将dir1路径包括路径下的所有内容复制到dir2中。


使用vi编辑器

vi编辑器用来创建并编辑文本。就好比Windows上的记事本,或Linux上的gedit,但是他没有图形用户界面,完全在命令行中使用。

vi编辑器中有两个功能:

  1. 插入模式(Insert mode):在这个模式中你可以编辑文件内容
  2. 命令模式(Command mode):命令模式用来执行文件操作,比如保存文件等

要使用vi编辑器打开一个文件,只需要敲入vi命令即可。例如:

1
$ vi file2.txt

将会打开工作目录下的”file2.txt”文件。如果该文件不存在,编辑器会帮你自动创建一个。

!()[https://note.youdao.com/yws/api/personal/file/WEB851884414ef36fe09b4feda45b512323?method=download&shareKey=cbca8ca352f134b3e06ba8e53e65b13c]

进入vi编辑器后默认为命令模式。因为我们的路径下没有file2.txt,如你所见编辑器目前显示的是空白文件。

按”I”键切换至插入模式。当你在插入模式时,你可以在界面的左下角看到-- INSERT --

在插入模式中你可以按照我们熟悉的样子编辑文件。如果编辑完成想要保存文件,则需要退回至命令模式。

在插入模式时,按”esc”以回到命令模式。当你回到命令模式后,你将不会再左下角看到-- INSERT --字样。这时输入:wq并按下Enter,你会退出vi编辑器,同时你的文件会被保存。


命令模式下的命令

除了“保存并关闭”(:wq)命令外,还有下面这些常见命令:

命令 执行的操作
:wq 保存并退出
:w 保存(但不退出vi编辑器)
:w filename 保存到名叫”filename”的新文件下
:q 退出
:q! 不保存并退出vi编辑器

想要查阅完整的命令列表的话点我

或者你也可以查阅这张Cheatsheet:

Cheatsheet


文件权限

Linux系统中的每个文件或目录都被分配了3种类型的所有者。

  • 用户(User):用户可以作为文件的拥有者
  • 用户组(Group):用户组中可以包含多个用户。若用户组拥有一个文件的权限,则组中的每个用户都有该文件的权限。
  • 其他(Other):有权访问文件的任何其他用户。它既没有创建文件,也不属于拥有该文件的组。

每个用户对于上面的三种所有者定义了三种不同的文件权限:

  • 读取(Read):拥有文件读取权限的个体可以打开并读取文件中的内容。拥有路径读取权限的个体可以列举出该路径下的内容。
  • 写入(Write):拥有文件写入权限的个体可以更改文件中的内容。拥有路径读取权限的个体可以在路径中添加,移除或者重命名内容。
  • 执行(Execute):执行权限允许个体来运行程序

不如举个例子看看:使用之前提到过的ls -l命令来列举出路径下的内容,里面会告诉你文件权限的相关信息:

1
2
3
4
5
6
$ ls -l

total 10
-rw------- 1 cklai ra 50 Jul 30 17:07 file1.txt
-rw------- 1 cklai ra 48 Jul 31 15:14 file2.txt
drwx------ 2 cklai ra 2 Jul 30 17:00 lab/

权限指示

借用上面的例子,我们来仔细讲讲刚才的输出是啥意思:

1
2
total 10
-rw------- 1 cklai ra 50 Jul 30 17:07 file1.txt

total 10代表文件在文件系统中占用了多大空间,单位为千字节(kilobytes)。
由左至右分别是:

条目 释义
-rw------- 代表文件权限。具体的释义马上就解释(
1 代表文件拥有的硬链接数量
cklai 代表文件拥有者
ra 文件拥有者存在的用户组
50 文件大小,单位为字节(byte)
Jul 23 17:07 上次修改时间
file1.txt 文件名

现在我们来讲讲权限指示该怎么解读。
权限指示(Permission indicator)是一个十字符长的字符串,通常分为四部分:

这玩意可以分成四部分解读,Type, User permissions, Group permissionOther permission


Type:

代指权限指示的第一个字符。

  • 如果第一个字符是短横线 “-“,则代表它是一个正常文件
  • 如果第一个字符是字母d “d”,则代表这是一个路径。

还有其他的类型不过这里涵盖不到。可以自己上网查询。


User permissions

第2-4个字符表示用户的权限。三个位置会显示”r”, “w”或者”x”。

比如这里显示了”rw-“,就代表:

  • 可以读取文件 (read, r)
  • 可以写入文件 (write, w)
  • 但是不能执行文件。如果文件可以被执行,第三个位置会显示”x”

Group permission

5-7个字符表示了用户组的权限。同样用rwx表示权限。


Other permission

最后三位表示了文件对其他所有人的权限。

如果显示”—-“,则表示其他人都不可以读取(r),写入(w),并且也不能执行(x)


更改权限

你可以使用chmod命令来更改你拥有的文件或者路径的权限:

chmod命令格式为:

1
$ chmod [who][operator][permissions] filename

  • who

在参数who内,可以键入四个不同的字母来指定要更改的对象群体:

释义
u 指定用户权限
g 指定用户组权限
o 指定其他权限
a 指定所有群体的权限

  • operator

在参数operator内,共有三个运算符,用来代指具体的操作:

释义
+ 增加权限
- 移除权限
= 设定权限

  • permission

在参数permission内,就是我们熟悉的r, w和x了。这些参数用指定要用来增加或者减少的权限:

释义
r 读取权限
w 写入权限
x 运行权限

举例

Example 1:更改权限

在Linux中,可以首先使用touch创建一个新文件。touch命令创建的文件对用户的默认权限为可读可写(rw-)。
想要更改权限,就可以使用chmod来指定文件的权限。例如:

1
2
3
4
5
6
7
8
9
10
11
$ touch file
$ ls -l

total 26
-rw------- 1 cklai ra 0 Aug 1 11:48 file

$ chmod o+w file
$ ls -l

total 26
-rw-----w- 1 cklai ra 0 Aug 1 11:48 file

其中,chmod o+w file意思是对于其他用户(o)授予(+)写入(w)权限。


Example 2: 更改多个权限

可以在permission参数内键入多个值来一次性设置多个权限。就刚才的例子往下继续:

1
2
3
4
5
6
7
8
$ ls -l

-rw-----w- 1 cklai ra 0 Aug 1 11:48 file

$ chmod o+rx file
$ ls -l

-rw----rwx 1 cklai ra 0 Aug 1 11:48 file*

chmod o+rx file的释义为:对其他用户(o)授予(+)读取(r)和运行(x)权限。

可以看到更新完权限后,文件名后面出现了一个星号”*”,这代表文件现在是可执行的。


其他常用命令

这里会介绍很多新的命令:

命令 释义
grep 'abc' file 返回在文件中包含”abc”的行。可以使用更高级的描述来实现更复杂的搜索。
cut -d, -f2 file 返回特定的数据列。
它根据标志-d指定的定界符分隔每行,并返回标志-f指定的列(字段编号从1开始)。
diff file1 file2 显示两个文件的区别。
wc file 返回文件中的行数,字数和字符数。
sort file 按照字母顺序将文件中的每行进行排序
uniq file 用于删除相邻的重复行,最后只会留下一个重复行。
spell file 检查文件中所有的拼写错误。
su 将用户模式更改为超级用户模式。
yum install prog
或者
apt-get install prog
这个命令会链接特定服务器来下载并安装程序。
一般来讲你需要先运行su才能使用这些命令来安装程序。
man cmd 显示命令cmd的手册页。找到使用命令的其他选项对您非常有用。

接下来我们会细致介绍这些命令的用法


文件内搜索 (grep)

grep命令适用于搜索文件内的行。

例如这里有一个”example1.txt“文件包含如下内容:

1
2
3
4
5
6
5 Apple 3.5
4 Chicken 50
1 Coke 5.5
10 Jelly 5
3 Chocolate 15
2 Milk 8

运行下面的命令将会返回文件内所有包含”ke”的行:

1
2
3
$ grep 'ke' example1.txt
4 Chicken 50
1 Coke 5.5

搜索是很大的一块内容。这里仅用来介绍。后面我们会在搜索章节一起将文件内文件外搜索一并讲清楚的。



文件字数 (wc)

同样使用”example1.txt”作为例子:

运行该命令则会返回如下内容:

1
2
$ wc example1.txt
6 18 71 example1.txt

其中:

  • 第一个数字代表行数,也就是文件共有六行
  • 第二个数字代表词数,表明文件中有18个词
  • 第三个数字代表文件大小,代指文件大小为71字节

搭配参数-l使用可以仅仅返回行数:

1
2
$ wc -l example1.txt
6 example1.txt

搭配参数-w使用可以仅仅返回词数:

1
2
$ wc -w example1.txt
18 example1.txt

排序

使用sort命令来对文件内容排序。如果没有指定任何输入参数,则默认排序方式按照字母表顺序排列

1
2
3
4
5
6
7
$ sort example1.txt
1 Coke 5.5
10 Jelly 5
2 Milk 8
3 Chocolate 15
4 Chicken 50
5 Apple 3.5

你可以使用不同的传入参数来指定排序方法。例如:

  • 使用-n则按照数字顺序排序:
1
2
3
4
5
6
7
$ sort -n example1.txt
1 Coke 5.5
2 Milk 8
3 Chocolate 15
4 Chicken 50
5 Apple 3.5
10 Jelly 5
  • 使用-r则按照倒序顺序排序:
1
2
3
4
5
6
7
$ sort -n -r example1.txt
10 Jelly 5
5 Apple 3.5
4 Chicken 50
3 Chocolate 15
2 Milk 8
1 Coke 5.5
  • 使用-k则按照字段排序。需要注意排序字段ID从1开始,而不是0。

例如,将文件按照第三个字段进行排序。在”example1.txt”中,每行最后的那组数字就是第三字段。

1
2
3
4
5
6
7
$ sort -k3 -n example1.txt
5 Apple 3.5
10 Jelly 5
1 Coke 5.5
2 Milk 8
3 Chocolate 15
4 Chicken 50
  • 使用-t用来告诉排序程序,我们使用分隔符并非空格而是逗号。

例如我们有一个”example1.txt”的变体,”example1_comma.txt”:

1
2
3
4
5
6
7
$ cat example1_comma.txt
5,Apple,3.5
4,Chicken,50
1,Coke,5.5
10,Jelly,5
3,Chocolate,15
2,Milk,8

需要在使用sort时加入参数-t来指定分隔符为逗号:

1
2
3
4
5
6
7
$ sort -t, -k3 -n example1_comma.txt
5,Apple,3.5
10,Jelly,5
1,Coke,5.5
2,Milk,8
3,Chocolate,15
4,Chicken,50

剪切文件

这里指的不是我们熟悉的剪切文件到其他目录,而是直接在文件内剪掉一部分内容。

cut命令将会修剪文件并返回特定的内容。要使用这个命令,我们需要向cut命令指定分隔符。

指定分隔符的参数为-d,代表delimiter,注意不要和之前的sort命令搞混了!该参数不是可选的,即便分隔符为空格,你也需要指定参数-d

使用参数-f来告诉程序你想要返回的字段,说白了就是列。和sort一样,字段ID从1开始而不是0。

例如,返回”example1.txt”文件内的第一字段和第三字段:

1
2
3
4
5
6
7
$ cut -d ' ' -f 1,3 example1.txt
5 3.5
4 50
1 5.5
10 5
3 15
2 8

移除重复行

uniq命令删除相邻的重复行,只保留一个重复行。

注意咯,它只删除相邻的重复项。

“example2.txt”:

1
2
3
4
5
6
Apple 
Apple pie
Apple pie
Apple
Apple
Apple pie

想要移除在”example2.txt”中多余的相邻重复行:

1
2
3
4
5
$ uniq example2.txt 
Apple
Apple pie
Apple
Apple pie

拼写检查

spell命令返回文件中所有可能出现拼写错误的词:

“example3.txt”:

1
2
It's a beautiffful day!
I am so happpy todday.
1
2
3
4
$ spell example3.txt
beautiffful
happpy
todday


如果你自己的Linux机子运行这个命令报错,那么你的系统很有可能没有安装需要的软件包。按照下面的步骤修复这个问题吧:

  1. 切换到Superuser账号:
1
$ su

还记得我们之前说过安装软件包之前需要切换到su账号吗

  1. 安装aspell包:
1
$ yum install aspell
  1. 退回到你当前的账户:
1
$ exit

文件差异

diff命令用来显示两个文件的区别。

“fileA.txt”:

1
2
3
aaa
bbb
ccc

“fileB.txt”:

1
2
3
eee
aaa
ddd

下面的命令则会返回将”fileA.txt”转化为”fileB.txt”的步骤:

1
2
3
4
5
6
7
8
9
$ diff fileA.txt fileB.txt
0a1
> eee
2,3c3
< bbb
< ccc

---
> ddd

这里我们需要一点解释了。

0a1

fileA的第0行之后添加(a, add)一行,添加的内容由下一行> eee表示

2, 3c3

fileA中的第2, 3行变成(c, change)fileB的第三行。
完成这一步需要先删除掉两行,由<表示:

< bbb
< ccc
代表删除掉这两行。

分割线隔开后出现了>。这代表我们需要在删除后再次添加一行:

> ddd

代表再添加文本”ddd”

这样就能把”fileA.txt”变成”fileB.txt”了。


再举另一个例子把:

1
2
3
4
5
6
7
8
9
$ diff fileB.txt fileA.txt 
1d0
< eee
3c2,3
< ddd

---
> bbb
> ccc

刚才是由A变B,现在是由B变A了。

1d0

fileB中删除(d, delete)第一行,随后的内容就会在第0行被对齐。

< eee表示我们删除了”eee”

3c2,3

和刚才刚好反过来,将fileB中的第3行变为fileA2,3行。先删去”ddd”,然后加入”bbb”和”ccc”


标准I/O,文件重定向和管道

标准IO和文件重定向

标准输入,输出和标准错误

在Shell中使用命令时,Shell将每个进程与一些打开的文件关联起来,并使用称为文件描述符 (File descriptors)的数字引用这些打开的文件。

文件描述符 文件
0 标准输入(stdin) - 输入文件
1 标准输出(stdout) - 输出文件
2 标准错误(stderr) - 输出错误的文件

重定向运算符

一般来讲当我们执行命令时,输出会在屏幕上通过Shell打印出来。我们可以使用重定向运算符 (Redirection operator) “>“ 来重定向输出到一个文件内。

例如,下面的命令将会保存路径中的文件到”file.txt”中:

1
$ ls -l 1> files.txt

或者也可以用这样的短格式:

1
$ ls -l > files.txt

如果使用了上述命令,则输出将不会显示在屏幕上,而是会被记录到文件”files.txt”中。当用来排查数据,debug或者就是简单保存信息时,这招很有用。

如果多次执行上述命令,系统可能会显示”cannot overwrite existing file“。
为了解决这个问题,可以先手动删除已有的文件,或者使用>|强制写入文件。如下所示:

1
$ ls -l >| files.txt



下面是一些常见的重定向运算符和他们的释义:

命令 解释
command 1> file
或者
command > file
将标准输出发送到 file
command 1>> file
或者
command >> file
将标准输出追加到 file
command 2> file 将标准错误发送到*file*中
command 2>> file 将标准错误追加到*file*中
command < fileA > fileB 2>fileC 首先命令会从fileA中读取,执行后将输出发送到fileB,将标准错误发送到fileC


>>>的区别在于,”>”将会在每次执行时覆盖文件内容,而”>>”则会在文件后面追加内容。


如果我们这里有一个C++程序,我们也可以使用这样的运算符来进行操作。例如下面的程序”add.cpp”会由两个输入,将两个输入加起来之后获得一个输出:

1
2
3
4
5
6
7
8
9
//add.cpp
#include <iostream>
int main() {
int a;
int b;
std::cin >> a;
std::cin >> b;
std::cout << a+b;
}

编译文件为一个叫做”add”的可执行文件:

1
$ g++ add.cpp -o add

这时你可以将自己的输入写到文件内,如”input.txt”:

1
3 4

然后使用”<”传入参数:

1
2
$ ./add < input.txt
7

或者你可以更进一步,将输入和输出都独立出来:

1
$ ./add < input.txt > output.txt

为何不能使用 input.txt > ./add > output.txt这种写法?因为Shell会尝试执行input.txt。



管线

有时候我们需要将一个程序的输出换做另一个程序的输入,我们可以创建并且利用管线来优雅地解决这个问题。

“|”是管线的符号。这个符号用来重定向一个程序的输出到另一个程序的输入,没有中间商赚差价。所谓中间商就是刚才用到的输入输出文本文件。

举个例子,如下是两行命令:

1
2
$ ls -l > files.txt
$ grep "Jan 26" < files.txt

简而言之就是使用ls命令列出所有内容到”files.txt”中,然后从”files.txt”中搜索所有含有”Jan 26”的行。

太复杂了。

整个操作可以换做下面的一行:

1
$ ls -l | grep "Jan 26"

通过使用|,可以将|之前的命令输出直接当作|后面的命令传入。在这个例子中具体是将ls -l的输出当作grep命令的传入来用了。


举几个例子吧。

Example 1:

假设我们现在有一个”data.txt”文件,我们需要按照价格将其排序,然后仅仅将商品的名称和价格保存到”result.txt”文件夹中,要求只能使用一行命令。

来看看标准题解:

1
$ sort -k3 -n data.txt | cut -d' ' -f2,3 > result.txt

首先我们使用sort命令,对”data.txt”的第三个字段(-k3)按照数字顺序(n)进行排序。
然后你看见了|。这代表这一步的输出即将被传入到下一个命令,也就是cut
cut中我们以空格作为分隔符(-d’ ‘),并保留第二第三字段(-f2,3),最后存入到”result.txt”中。

还是很简单的。下面是这个过程的视觉表述方式:


Example 2:

目标是在当前路径下对用户,用户组和其他人有运行权限的文件和路径。

1
2
3
$ ls -l
total 190
-rwx--x--x 1 kit gopher 0 Sep 12 10:30 add.o

对于所有目标都有执行权限的话,则权限指示的第4, 7, 10位应该是x。但是我们应该如何检索呢?看看下面这行命令

1
$ grep -E '^...x..x..x' [传入]

使用-E参数可以告诉Shell我们需要做样式匹配搜索了。对于'^...x..x..x'来讲:

  • “^”表示从行首开始匹配。
  • “.”表示可以匹配任意字符。

加在一起的话,'^...x..x..x'的意思大概就是,从行首开始匹配,返回在第4, 7, 10个位置有字母”x”的行。

那么一行就能解决了:

1
$ ls -l | grep -E '^...x..x..x'

结构上和上一个例子一样所以这里不解释了。



Example 3

加入你想移除文件里的某几列,然后将其输入到程序里。举个例子,”mark.txt”,包含学生信息的单行数据:

1
2
$ cat mark.txt
2011111111 John M 98.5 100 62.5 88 75.5

对应格式是:[UID, 名称,性别,分数1,分数2,分数3,分数4,分数5]

然后我们同样有一个C++程序用来处理信息,用于计算平均分:

1
2
3
4
5
6
7
8
//mark.cpp
#include <iostream>
int main {
double a1, a2, a3, a4, a5;
std::cin >> a1 >> a2 >> a3 >> a4 >> a5;
std::cout << "The overall mark the student get is: ";
std::cout << (a1 + a2 + a3 + a4 + a5) / 5;
}

程序的问题是输入仅有五项成绩,但是我们的”mark.txt”文件内包含UID在内的其他多余信息。

那这就很坏了。

一种方法是我们可以使用管线来先处理”mark.txt”,然后传入程序。我们可以使用cut命令处理一下:

1
2
$ cut -d' ' -f-8 mark.txt
98.5 100 62.5 88 75.5

然后编译程序运行:

1
2
$ g++ mark.cpp -o mark
$ cut -d' ' -f4-8 mark.txt | ./mark > result.txt

这样程序的输出就会被记录到”result.txt”中:

1
2
$ cat result.txt
The overall marks the student get is: 84.9



搜索

搜索文件或路径 (find)

各位应该在Windows或MacOS中使用过搜索功能。你也可以在Linux中实现对文件或路径的搜索操作:

使用find命令可以用来搜索文件或路径。find命令的格式为:

1
find [path] [-name] [-type]
  • [path]告诉系统你想从哪里开始搜索操作。
  • [name]是你想要搜索的文件或路径的名字。
  • [type]是可选参数,参数包含:
    • -type f代表仅搜索文件 (files)
    • -type d命令之搜索路径 (directory)

举个例子,假如在当前位置有下面这几个文件和路径:

1
2
$ ls
hello/ hello.cpp hello.txt

那么在当前目录下搜索”hello.txt”的方法即为:

1
2
$ find . -name "hello.txt" -type f
./hello.txt

上面的命令
在上面的命令中,.代表当前目录。指定当前目录后find会在这个目录为基础向下寻找文件。


或者我们也可以在当前位置下搜索”hello”打头的文件:

1
2
3
$ find . -name "hello.*" -type f
./hello.cpp
./hello.txt

…或者是搜索路径:

1
2
$ find . -name "hello" -type d
./hello

文件内搜索 (grep)

grep (Global Regular Expression Print)命令之前已经介绍过了。这里介绍更多细节:

之前用的是不带-E的使用方式,只是用来返回文件内的匹配行:

“example1.txt”

1
2
Hello how are you?
I am using the bash shell like a pro!
1
2
$ grep 'hell' example1.txt
I am using the bash shell like a pro!

grep命令区分大小写!这就是为什么返回的是包含”shell”的第二行,而不是包含”Hello”的第一行。


grep可以加入可选参数-E,代表按样式匹配。格式为:

1
grep -E [regular experssion] [filename]

regular expression中,你可以按照下面的符号来进行自定义匹配:

符号 释义
. 匹配任何单个字符
^ 仅从行首开始匹配
$ 仅从行尾开始匹配
? 一个字符后面跟一个”?”,则该字符可被匹配0次或1次
+ 一个字符后面跟一个”+”,则该字符可被匹配1次或多次
* 一个字符后面跟一个”“,则该字符可被匹配*0次或多次
[] 被”[]”括起来的字符会被全字匹配
括起来的字符可以是单个字符,也可以是多个字符
你可以用”-“符号来自定义区间
例如,与其使用[12345],可以使用[1-5]代替
\ 转义符
使用转义符来取消特殊字符的特殊效用
例如我们想要寻找”.”而不发挥它“匹配单个字符的属性”,就写成\.
pattern {n} 匹配”n”次出现的pattern
pattern {n,} 匹配至少“n”次出现的pattern
pattern {n,m} 匹配出现”n”和”m”次数之间的pattern
(ab){3} 出现三次”ab”排列
例如(ab){3}会被匹配做”ababab”,不是”abbb”

接下来我们使用刚才”example1.txt”,挑重点解释。


任意单字符匹配

1
2
3
$ grep -E '.ell' example1.txt
Hello how are you?
I am using the bash shell like a pro!

‘.ell’代表”.”处可以匹配任何单个字符,例如”Cell”, “cell”, “bell”会被匹配。
如果如果你想要在”.”处搜索的字母仅仅是大小写H,则可以使用[Hh]ell


行首/行尾匹配

这里启用”example2.txt”:

1
2
3
4
$ cat example2.txt
apple
pineapple
apple pie
1
2
3
4
$ grep 'apple' example2.txt
apple
pineapple
apple pie


使用”^”从行首开始匹配:

1
2
3
$ grep -E '^apple' example2.txt
apple
apple pie


而使用”$”从行尾开始匹配:

1
2
3
$ grep -E 'apple$' example2.txt
apple
pineapple


由此可知,使用”^”和”$”也可以全字匹配某一行:

1
2
$ grep -E '^apple$' example2.txt
apple


或者使用”^”, “$”配合”.”来返回只有五个字符的行:

1
2
$ grep -E '^.....$' example2.txt
apple

?, +, *

“?”

“example3.txt”

1
2
3
4
5
6
apple
coco
cherries
orange
ape
angel
1
2
3
4
$ grep -E '^ap?' example3.txt
apple
ape
angel

没看懂?首先”^”确保行首开始匹配,随后:

  • apple”会被返回,因为在表达式中的p要求出现0次或者1次,这里出现了1次
  • ape”会被返回,因为在表达式中的p要求出现0次或者1次,这里出现了1次
  • angel”会被返回,因为在表达式中的p要求出现0次或者1次,这里出现了0次

那么接下来的东西,”+”和”*”,原理上基本上是一样的了。


+

1
2
3
$ grep -E '^ap+' example3.txt
apple
ape
  • apple”会被返回,因为在表达式中的p要求出现1次或者多次,这里出现了多次
  • ape”会被返回,因为在表达式中的p要求出现1次或者多次,这里出现了1次
  • “angel”没有被返回,因为在表达式中的p要求出现1次或者多次,这里出现了0次

*

1
2
3
4
$ grep -E '^ap*+*' example3.txt
apple
ape
angel
  • apple”会被返回,因为在表达式中的p要求出现0次或者多次,这里出现了多次
  • ape”会被返回,因为在表达式中的p要求出现0次或者多次,这里出现了1次
  • angel”会被被返回,因为在表达式中的p要求出现0次或者多次,这里出现了0次

接下来是一些进阶一点的用法了:

整合”.”和”*“

如果我们想要匹配字符”a”,然后后方跟随任意字符,直到”ge”再次出现:

1
2
3
$ grep -E 'a.*ge' example3.txt
orange
angel

“跟在”.”之后,所以”任意字符匹配”可以被匹配多次,这就是为什么”orange“和”*angel”能被返回的原因。


使用括号”()”

我们可以使用括号来整合子字符串。例如我们想要寻找字符串”co”出现过一次或多次的行:

1
2
$ grep -E '(co)+' example3.txt
coco

为什么要用括号呢,看看如果不用括号会发生什么:

1
2
3
$ grep -E 'co*' example3.txt
coco
cherries

因为”*”只会应用到它的前一个字符。没加括号的话就会被应用到”o”,而不是整个子字符串”co”。


匹配集合

可以使用方括号 “[ ]”来定义一个集合,随后进行搜索。你可以按照下面的格式定义集合:

  • [0123456789]或者[0-9]用来匹配任意单个数字
  • [A-Z]用来匹配任何单个大写字母
  • [a-z]用来匹配任何单个小写字母
  • [A-Za-z]用来匹配任何单个字母,包括大写和小写字母。

例如我们有一个”example4.txt”:

1
2
3
Apple Juice HKD13
apple pie USD 4
Banana phone HKD

查找包含”apple”或者”Apple”的行:

1
2
3
$ grep -E '[Aa]pple' example4.txt
Apple Juice HKD13
apple pie USD 4

查找包含”HKD”,并在其后跟随一个或者多个数字的行:

1
2
3
$ grep -E 'HKD[0-9]*' example4.txt
Apple Juice HKD13
Banana phone HKD

匹配序列

可以使用”{ }”来匹配一个序列 (pattern)。

上”example5.txt”:

1
2
3
2April2013
30-1-2013
13December2013

如果我们想要查找一个日月年的格式,并且日为2个字符,月至少为3个字符,年为4个字符,你可以整成:

1
2
3
$ grep -E '^[0-9]{1,2}[a-zA-Z]{3,}[0-9]{4}' example5.txt
2April2013
13December2013

这很抽象,所以我们需要简单讲讲:

  • [0-9]{1,2}定义一个数字集合,并且需要至少出现一次,最多出现2次
  • [a-zA-Z]{3,}定义一个字母(包含大小写)集合,并且需要至少3个字符长
  • [0-9]{4}定义一个数字集合,并且必须要是正正好好4个字符长

加上行首匹配”^”就ok了。


其他常用匹配写法

表述 释义
[a-z]* 匹配任意数量字符的小写字母
....$ 匹配包含四个字符的行
abc.*abc 匹配出现两次”abc”的行。两个”abc”之间可以有任意数量的字符
[0-9]{2}-[0-9]{2}-[0-9]{4} 匹配dd-mm-yyyy格式。请注意这不会验证日期是否合法!
^.{n,m}$ 匹配行长介于n和m个字符之间的行
(bye)+ 匹配一次或多次出现过”bye”的行,例如”bye”, “byebye”, “byebyebye”,空行不会被匹配。


至此,Module 1全部完结!



Module 2: Shell Script & Version Control

先来深入一下Shell脚本把。

Shell脚本

Shell 脚本 (Shell Script, .sh) 是一种由 Linux shell 运行的计算机程序。
Shell Script是一种解释型语言,而不是编译型语言。因此与 C++ 不同,我们不需要在执行程序之前将 shell 编译为二进制可执行格式。
相反,每次执行用 Shell 脚本编写的程序时,shell 都会对其进行解析和解释。

解释型语言允许我们通过简单地编辑脚本来更快地修改程序。但是,程序通常较慢,因为在执行期间需要进行解析和解释。
假设我们有一系列 Shell 命令,我们不想在每次要执行时重新输入这些命令。我们可以将它们保存在一个文件中,并将其称为 shell 脚本。

注意注意↑!
在 Windows 中编辑脚本文件并导入到 Linux 可能会导致执行失败,因为 Windows 中使用的行尾 (EOL) 不同。
你应该在 Linux 系统内创建和编辑脚本文件(例如,在 SSH 中使用 vi 编辑器,在 X2Go 中使用 gedit)。否则,如果从 Windows 导入脚本文件,则需要确保 Windows 环境中的文本编辑器中的行尾选项设置为 UNIX 格式 (LF) 而不是 Windows 格式 (CRLF)。



我们先来创建一个hello.sh脚本写个典中典Hello world:

1
$ vi hello.sh
1
2
3
#!/bin/bash
echo "Hello world!"
echo "Welcome to ENGG1340"

echo命令用来向控制台输出一行。行中内容可以是变量值,也可以是字符串。

保存并退出,随后我们需要使得文件可以被用户运行:

1
$ chmod u+x hello.sh

然后直接运行:

1
2
3
$ ./hello.sh
Hello world!
Welcome to ENGG1340

所有脚本都应以#!开头,这表示系统应使用哪个程序来处理 shell 脚本。在本例中,它是 bash 程序的路径。
从Module 1 中我们知道有许多不同的 shell(例如,C shell、Korn shell、Bash shell)。而由于我们使用 Bash shell,因此我们需要提供 bash 程序的路径,以便操作系统知道如何解释 bash shell 命令。
在本例中,Bash shell的路径为 /bin/bash。

你也可以使用which -a命令来找到Bash shell的具体位置:

1
2
3
$ which -a bash
/usr/local/bin/bash
/bin/bash

参数-a代指返回bash的所有路径。如你所见返回的路径不止一个,这代表Bash的路径可能有多个。在本例中我们使用/bin/bash路径下的那个。


来看看另一个例子。这里我们引入echo命令的新参数-n看看会发生什么不同的情况:

ex1_1.sh:

1
2
3
4
#!/bin/bash
echo -n "Hello world!"
echo "Welcome to ENGG1340"
echo "bye"
1
2
3
$ ./ex1_1.sh
Hello world!Welcome to ENGG1340
bye

-n可以表示”no trailing”使下一行直接继续打印在上一行的末尾。


这次我们来看个带C++的例子:

add.cpp

1
2
3
4
5
6
7
8
9
//add.cpp
#include <iostream>
int main() {
int a;
int b;
std::cin >> a;
std::cin >> b;
std::cout << a + b;
}

input.txt“:

1
3 4

之前我们使用管线可以封装好整个自动化,不过这里我们试着用Shell脚本:

ex1_2.sh“:

1
2
3
4
5
6
7
#!/bin/bash

# Compile the code
g++ add.cpp -o add
# Run the code and display result
./add < input.txt > output.txt
cat output.txt

运行:

1
2
$ ./ex1_2.sh
7

使用变量

在Shell脚本中只有一种变量类型,那就是字符串。

变量名区分大小写,只支持包含大小写字母,数字和下划线 (_)。


定义并访问变量

我们可以通过这种方式来定义并赋予变量一个值:

1
pet="dog"

等号前后不能有空格。

如果我们需要取用变量中的内容,则需要使用美元符号:

1
2
3
4
#!/bin/bash

pet="dog"
echo $pet

输出为dog


读取用户输入

read命令用来等待用户在控制台向脚本提供输入:

1
2
3
4
5
#!/bin/bash

echo "What is your name?"
read name
echo "Hello $name"

当用户输入完值之后,会被自动存储到变量name中。

比如我们来看这个程序的具体例子:

1
2
3
4
$ ./ex2_2.sh
What is your name?
Kit
Hello, Kit



单引号和双引号

在Shell脚本中区分引用很重要。我们可以用三种方式来确定一个字符串值,分别是不引用单引号引用双引号引用


不引用

我们可以在定义字符串的时候不加任何引用,但是这个方法仅在需要定义的字符串是一个整体,没有空格的情况下才可用:

1
a=cat

下面这种情况就会报错:

1
a=Apple pie

因为 程序会将Apple当作一个指令来看待,而不是一个字符串。


单引号引用

在单引号引用情况下,带空格的字符串可以被成功处理,但是没办法做到变量替换。

变量替换功能是双引号引用的功能:


双引号引用

双引号引用相比单引号引用支持更多功能,其中一个就是变量替换:

符号 解释
$ 变量替换
\ 转义符
\`` 包含bash命令

看不懂?就着下面的例子看看:

1
2
3
4
5
6
#!/bin/bash

name="Apple"
echo 'Hello, $name'
echo "Hello, $name"
echo "\$name = $name"

运行后可以得到:

1
2
3
4
$ ./ex2_3.sh
Hello, $name
Hello, Apple
$name = Apple

第一行会被输出为Hello, $name,因为我们使用了单引号引用,name变量不会被替换。
第二行会被输出为Hello Apple,因为双引号引用下$name会被替换为变量name
第三行会被输出为的替换功能,而第二个$name会被保持原状正常替换。



命令替换

前面提到过一嘴,不过我们决定在这里展开相关说明。

使用反引号(`),一般是esc键下面的那个键,我们可以在脚本中向变量保存Shell命令的输出,来实现进一步的处理:

1
2
3
4
5
6
7
#!/bin/bash

a="`cat file.txt`"
echo $a

b="`wc -l file.txt | cut -d\" \"" -f1`"
echo "There are $b lines in file"

file.txt“:

1
2
3
Apple
Banana
Cherry

运行输出为:

1
2
3
$ ./ex2_4.sh
Apple Banana Cherry
There are 3 lines in file

接下来我们解读一下。在变量b中,首先wc命令返回3 file.txt,告诉我们一共有三行。接着我们使用cut命令截去后面多余的部分,返回一个”3”。

注意在cut命令的参数部分我们使用了转义符,因为原本是cut -d" " -f1,但问题在于外面已经有一对双引号了。解决这个问题只能去转义里面的两个引号。



字符串处理

如果我们想要获得一个字符串的长度:

1
2
3
4
#!/bin/bash

a="Apple"
echo ${#a}


1
2
$ ./ex2_5_1.sh
5

其中${\#a}的意思是返回变量a存储的字符串的长度。


子字符串

使用${a:pos:len}可以用来返回一个字符串的子字符串。例如:

1
2
3
4
#!/bin/bash

a="Apple Pie"
echo ${a:6:3}

会返回变量a从第6个字符位置开始包含并向后数3个字符长度的子字符串,也就是:Pie

和Python一样第一个字符编号为0。



替换字符串内容

使用${a/from/to}来指定替换一个字符串。先看例子:

1
2
3
4
5
6
#!/bin/bash

a="Apple Pie"
from="Pie"
to="Juice"
echo ${a/from/to}

输出为Apple Juice

该替换会在字符串内寻找from的第一个匹配,然后将from替换为to的值。



按数字计算

我们存储的变量都是字符串,但是如果我们存入的是数字类型的字符串,并且我们想要进行一些数学运算该咋整?

虽然听起来很奇怪,但是我们可以使用let命令来进行数学运算。支持加减乘除和整数除法。(+, -, *, /, %)

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

a=10

let "b = $a = 9"
echo $bash

let "c = $a * $a"
echo $C

let "d = $a % 9"
echo $d

运行为:

1
2
3
4
$ ./ex2_5_4.sh
19
100
1



获取控制台参数数量

控制台参数再Shell脚本中会被列为$0, $1, $2一直到$9
作为特殊项,$0会被解析为Shell脚本的名称。
如果想要引用第十个或者更多参数,则需要将序号使用括号括起来:${10}, ${11}这样,否则命令会被解析为$1和数字的组合。
$#代表控制台内参数的数量。

例如我们有一个Shell脚本如下所示:

1
2
3
4
5
6
7
8
#!/bin/bash

echo "There are $# arguments"
echo "$0"
echo "$1"
echo "$2"
echo "$3"
echo "$4"

运行脚本后输出:

1
2
3
4
5
6
7
$ ./ex2_6.sh we are the world
There are 4 arguments
./ex2_6.sh
we
are
the
world



控制语句

在任何脚本语言中,流程控制都是必不可少的一部分。

if else语句

基本的if else语句语法为:

1
2
3
4
if [ condition ]
then
*perform some action
fi

if语句的重点写做fi还有点搞笑。
注意了if语句的条件判断,方括号里那个,需要前后带一个空格。注意condition前后离着括号都差一个空格。如果不遵守这个规定会爆语法错误。

Shell脚本也有else if功能:

1
2
3
4
5
6
7
8
9
if [ condition1 ]
then
echo "condition 1 met"
elif [ condition2 ]
then
echo "condition 2 met"
else
echo "No condition met"
fi



在上面的语句中,condition的语法条件比较特殊。下面的列表展示了可以当作condition来用的语句:

  • 比较字符串

注意$string1$string都被双引号引用了,所以在$string1$string2内即使有空格也可以正确比较内容。

String comparisons Description
[ "$string" ] 如果$string的长度不为空,则返回true
[ -z "$string" ] 如果$string的长度为空,则返回true
[ "$string1" == "$string2" ] 如果$string1$string2完全一致,返回true
[ "$string1" != "$string2" ] 如果$string1$string2不一致,返回true
[ "$string1" \> "$string2" ] 如果$string1按照字母顺序排序排在$string2后面,则返回true
[ "$string1" \< "$string2" ] 如果$string1按照字母顺序排序排在$string2前面,则返回true

注意排序那两个符号前面带转义符。


  • 比较文件或路径

可以用来判断文件或者路径的状态。包括但不限于:

文件检查 解释
[ -e $file ] 如果file存在,返回true
[ -f $file ] 如果file是一个文件,返回true
[ -d $file ] 如果file是一个路径,返回true
[ -s $file ] 如果file的大小>0,返回true
[ -r $file ] 如果file可读,返回true
[ -w $file ] 如果file可写,返回true
[ -x $file ] 如果file可执行,返回true



  • 比较数字

虽然shell里只有字符串一种变量,但我们也可以按下面的方式来比较数字:

数字比较 解释
[ $a -eq $b ] 如果a = b,返回true (equal)
[ $a -ne $b ] 如果a != b,返回true (not equal)
[ $a -lt $b ] 如果a < b,返回true (less than)
[ $a -le $b ] 如果a <= b,返回true (less or equal)
[ $a -gt $b ] 如果a > b,返回true (greater than)
[ $a -ge $b ] 如果a >= b,返回true (greater or equal)

还是比较好记的


例如我们想要写一个小脚本,询问用户想不想要移除所有后缀为.cpp的文件:

1
2
3
4
5
6
7
8
9
#!/bin/bash

echo "Do you want to remove all .cpp files? (Y/N)"
read ans =
if [ "$ans" == "Y" ]
then
rm -rf *.cpp
echo "All .cpp files are removed!"
fi

尤其注意一下if语句带condition的空格问题。留意一下空格都加在哪里了。

运行为:

1
2
3
Do you want to remove all .cpp files? (Y/N)
Y
All .cpp files are removed!



学会了?接下来举几个例子:

Example 1

如果g++在编译c++文件时出现了错误,则编译失败,可执行文件将不会生成。所以我们可以使用[ -e file ]来检查文件是否存在。
同时,我们也可以考虑一下是不是可以查看一下编译失败的log?

比如我们可以使用重定向方法来存储错误log:

1
$ g++ hello.cpp -o hello 2> error.txt

别忘了2>代表重定向标准错误。

那么看看这个shell脚本吧:

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

if [ -e hello.cpp ]
then
echo "hello.cpp exist"
g++ hello.cpp -o hello 2> error.txt
if [ -e hello ]
then
./hello
else
echo "Compilation failed!"
echo "Here are the error message"
cat error.txt
fi
else
echo "hello.cpp not found!"
fi

如果hello.cpp包含语法错误,那么下面就是其中一种可能的输出:

1
2
3
4
5
6
7
hello,cpp exist
Compilation failed!
Here are the error message
hello.cpp: In function 'int main()':
hello.cpp:5:5: error: expected ';' before 'return'
return 0;
^~~~~~



for循环

for循环可以按照指定次数循环。

1
2
3
4
5
6
7
#!/bin/bash

list="1 2 3 4 5"
for i in $list
do
echo "This is iteration $i"
done

for循环以for i in []开始,需要执行的命令以dodone括起来。

例如上面的脚本会被运行为:

1
2
3
4
5
This is iteration 1
This is iteration 2
This is iteration 3
This is iteration 4
This is iteration 5



实际上除了遍历数组,我们也可以遍历一个路径中的文件。
比如下面的脚本将会自动将你路径下所有的.cpp文件记录一个备份:

1
2
3
4
5
6
7
#!/bin/bash

list="`ls *.cpp`"
for filename in $list
do
cp $fileName "$fileName.backup"
done
1
2
3
4
5
$ ls
a.cpp backup.sh* b.cpp c.cpp
$ ./backup.sh
$ ls
a.cpp a.cpp.backup backup.sh* b.cpp b.cpp.backup c.cpp c.cpp.backup



有用的操作

在脚本中隐藏命令

Shell脚本会生成自己的错误和输出信息,有时会引爆你的shell脚本输出。

为了解决这一点你可以使用之前的重定向方法:

1
2
3
4
5
6
7
8
9
#!/bin/bash

cp file123 fileabc 1>/dev/null 2>&1
if [ -e fileabc ]
then
echo "Copy successful"
else
echo "$0: Oops. Copy failed :("
fi

解析一下,1>/dev/null用来将cp命令的标准输出重定向到系统回收站内/dev/null。挺巧妙的。

2>&1则会重定向cp命令的标准错误到同一个位置,只不过我们以&1表示了。

&表示在这里指的是文件描述符,而不是文件名或路径。如果没有&,像2>1这样的命令将无效,因为shell会尝试将1解释为文件名,而不是标准输出文件描述符。

输出为:

1
2
$ ./ex4_1.sh
./ex4_1.sh: Oops. Copy failed :(



输出到标准错误

Shell脚本也可以通过echo命令输出到标准错误中:

1
2
3
4
5
6
7
8
9
#!/bin/bash

cp file123 fileabc 1>/dev/null 2>&1
if [ -e fileabc ]
then
echo "Copy successful"
else
echo "$0: Oops. Copy failed :(" >&2
fi

echo后面跟着>&2时候,则代表我们将该条信息重定向到标准错误层上进行输出了。接下来如果我们执行这个:

1
$ ./ex4_2.sh 2> error.txt

就可以在”error.txt”中看见我们的错误输出了!

1
2
$ cat error.txt
./ex2_6.sh: Oops. Copy failed :(

经过这两步处理后你的shell脚本的输出看起来就像其他shell命令一样了。


版本控制

在版本控制这一部分,我们主要学习如何使用Git。

Git 是一种常见的现代版本控制系统,用于管理和跟踪计算机文件中的更改以及协调多人对这些文件的工作。主要用于软件开发中的源代码管理。
Git 是一种分布式版本控制系统 (DVCS),与大多数替代版本控制系统相比,它具有更高的性能、安全性和灵活性。

那么什么是版本控制系统?

版本控制系统 (VCS) 是一类软件工具,支持软件开发团队管理源代码随时间的变化。
它在一种特殊的数据库中跟踪每个贡献者对代码的单独更改历史记录。如果出现错误或需要修复错误,开发人员可以返回到源代码的早期版本来解决问题,而不会妨碍其他团队成员的工作流程。
如果软件团队不使用 VCS,他们可能会遇到一些问题,例如在项目的两个独立部分之间创建不兼容的代码或对用户可用的更改一无所知。

在这么多版本控制解决方案中,使用Git的原因是它让开发人员可以在一个地方查看任何项目的变更、决策和进展的整个时间线。
借助 Git 这样的 DVCS,可以随时进行协作,同时保持源代码的完整性。使用分支,开发人员可以安全地对生产代码提出更改建议。

如果你用过Github,你应该对上述这些内容和下面将要讲到的内容比较熟悉。如果不,那么这一节将会很有意思。


Git的使用工作流基本分三步。

  1. git init初始化工作目录

首先可以使用Git初始化一个你将要使用的工作目录。Git会尝试在目录内跟踪你对文件的改变。

  1. git add增加一次提交

这一步告诉Git,当前目录下所有被更改的文件有哪些。

  1. git commit创建一次提交(commit)

最后告诉Git让它创建一次提交,类似于创建一个存档点。每一次提交就是某个特定版本。



开始使用Git

安装Git

要使用Git,我们需要先配置好Git的使用环境。首先我们需要在你的电脑上安装Git。

检查Git是否在你的电脑上安装,执行git version即可:

1
2
$ git version
git version 2.45.2.windows.1

如果没有安装Git,则你可以根据下面的步骤来安装Git。


在Linux上安装

在终端执行几行代码就OK了:

1
2
3
$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install git


在MacOS上安装

虽然我不用Mac但是相应的安装方式也是可以提一嘴的。

首先下载Homebrew来更方便快捷的安装软件:

1
2
3
4
$ ruby -e "$(curl -fsSL
https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew doctor
$ brew install git


在Windows上安装

直接去官网下载安装包即可。

https://git-scm.com/downloads



初始化仓库

一个Git仓库包含你工作环境的所有文件,文件夹或者路径等等。配置好Git后Git会自动帮你管理变更。

一共有两种方式来初始化一个本地仓库。你可以从网上clone一个仓库下来,也可以自己创建一个空白仓库使用。


初始化新仓库

  1. 打开终端并转到你想要初始化的路径:
1
$ cd project
  1. 在文件夹内我们使用git init命令创建一个新仓库:
1
2
$ git init
Initialized empty Git repository in /home/research/ra/1801/cklai/project/.git/

执行完这行命令后,一个新的.git/子路径会在当前路径下生成。这个指令用来设置好Git用来跟踪你的文件的所有前置工作。

  1. 现在我们可以使用Git了。比如说下一步我们创建一个”work.txt”:
1
2
Welcome to my Git tutorial.
Today we will learn how to get started with Git.

git init还有其他的一些可选变体可以使用:

命令 解释
git init 创建新的本地仓库
git init --quiet
or
git init -q
静默模式,仅输出重要信息,警告和错误。
git init --bare 创建一个裸仓库。
裸仓库没有工作目录,因此不能在上面进行实际的编辑和修改文件的操作,仅仅是用来存储并共享版本控制的历史记录。
git init --template=<template_directory> 给本地仓库指定一个模板
git init --separate-git-dir=<git dir> 创建一个包含具体工作目录路径的文本文档



clone新仓库

或者你也可以从网上拉取一个新仓库下来。

  1. 如果你想使用其他人的仓库,你可以直接从代码托管平台上clone一个仓库到你的本地。Github就是全球最大的代码托管平台。

注意注意↑!
如果存在.git路径,那么你的仓库就会被拉取到那里。如果没有,则仓库会被clone到你的当前工作路径。

  1. 输入clone命令即可从网上拉取仓库:
1
2
3
4
5
6
git clone https://github.com/[YourUsername]/[YourRepository]
Cloning into `Spoon-Knife`...
remote: Counting objects: 10, done.
remote: Compressing objects: 100% (8/8), done.
remove: Total 10 (delta 1), reused 10 (delta 1)
Unpacking objects: 100% (10/10), done.

同样的这里有一些git clone的附加变体:

命令 解释
git clone --branch <branch_name> 告诉Git你想要拉取的分支是哪一条。有关分支后面再说
git clone --bare 将仓库以裸仓库的形式拉取下来
git clone --mirror clone远程存储库的所有扩展引用,并隐式调用 -bare 参数
git clone --template=<template_directory> <repo_location> clone位于<repo location>的仓库,并应用来自<template_directory>的仓库模板



查看工作路径

当你在当前工作路径内工作时,你也可以使用git status命令来查看当前工作目录版本情况的信息:

1
2
3
4
5
6
7
8
9
10
11
$ git status
On branch master

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)

work.txt

nothing added to commit but untracked files present (use "git add" to track)

注意到在Untracked files下存在”work.txt”,这代表这个文本文档被Git可见,但是Git没有开始追踪这个文档的任何更改。解决这个问题我们需要向Git生命暂存更改。



暂存更改

为了让 Git 开始跟踪你在工作目录中所做的更改,你需要先将这些文件添加到暂存区。
这可以通过使用命令git add <filename>来完成,其中<filename>是您正在处理的文件的名称,例如我们的”work.txt”文件。

1
2
3
4
5
6
7
8
9
10
$ git add work.txt
$ git status
On branch master

No commits yet

Changes to be committed:
(use "git rm --cached <file>..." to unstage)

new file: work.txt

现在”work.txt”已被加入暂存目录。


git add的一些额外变体:

命令 解释
git add . 将当前目录下所有文件添加到暂存区
git add -A
or
git add --all
查找整个项目中存在的所有新文件或更新文件,并将其添加到暂存区



跟踪工作目录中文件变化

现在我们在”work.txt”新增第三行:

1
2
3
Welcome to my Git tutorial.
Today we will learn how to get started with Git.
We start with the Basic Git Workflow.

想要查看暂存区和工作目录中同一个文件的区别,我们完全可以使用git diff <filename>命令来查看区别:

1
2
3
4
5
6
7
8
$ git diff work.txt 
diff --git a/work.txt b/work.txt
index 90ac5e6..f0f679f 100644 --- a/work.txt
+++ b/work.txt
@@ -1,2 +1,3 @@
Welcome to my Git tutorial.
Today we will learn how to get started with Git.
+We start with the Basic Git Workflow.

可以在最后一行看出,Git已经成功登记了文件的更改。如果想要将当前的状态加入暂存,随时可以执行git add work.txt命令。

下面是一些git diff命令的变体:

命令 名称
git diff --base <filename> 查看与基础文件的冲突
git diff <sourcebranch> <targetbranch> 在合并更改之前预览变更

如果git diff的输出过长,则Git会使用分页器显示内容。在这种情况下我们可以按Q来退出分页器。
你也可以在参数中添加--no-pager告诉Git不要使用pager。这个设置对于绝大多数的Git命令都可以使用。



提交更改

从暂存区存到仓库我们需要使用git commit来提交我们所做的所有更改,建立一个存档。

git commit在使用时需要跟一个参数-m,用来写入你这次更改的内容注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git commit -m "Added an introduction."
[master (root-commit) 8327731] Added an introduction.
Committer: Chan Tai Man <tmchan@academy11.cs.hku.hk>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

git config --global --edit

After doing this, you may fix the identity used for this commit with:

git commit --amend --reset-author

1 file changed, 3 insertions(+)
create mode 100644 work.txt

请注意上面有关您的姓名和电子邮件地址配置的消息,实际上你可以按照说明对配置文件进行相应的更改。

始终输入有意义的提交消息,是程序员们的基本操作之一。这对你后期的开发有非常大的帮助,同时对别人也是。


使用git log命令可以查看比较旧版本的项目:

1
2
3
4
5
6
$ git log
commit 8327731fa6a9108fb6b54d0b38b9b59c7fbf316c (HEAD -> master)
Author: Chan Tai Man <tmchan@academy11.cs.hku.hk>
Date: Mon Jan 14 10:38:43 2019 +0800

Added an introduction.

在每个log中会提供如下四个信息:

  • 40字长的哈希码,我们叫SHA。这是每个commit的唯一识别码
  • 提交的作者,也就是你自己
  • 提交的时间和日期
  • 提交消息



与他人合作

使用分支系统

分支可以看作是指向 Git 存储库中最新提交的指针。
当我们初始化存储库时,我们正在处理一个称为master分支的单个分支。我们正在处理的提交称为HEAD,通常是分支的最新提交。

你可以在某个commit节点创建一个分支。

可以看一下上面这张图。这是一个正在开发的项目的Git图表。每一条线代表一个分支,每一个点代表一个commit。

可以看到橙色的那条线从粉红色分支的一个commit拉出,经过一系列的更改,最后在上面的一个commit合并到粉红色分支。这两个粉红色commit之间区别在于并入了整个橙色分支的内容。

要在当前commit拉出一个分支,可以使用git branch BugFix命令:

1
$ git branch BugFix

拉出分支成功后当前工作分支还是在master上。切换到刚拉出来的BugFix分支我们需要:

1
2
$ git checkout BugFix
Switched to branch 'BugFix'



我们也可以使用checkout命令将 HEAD 移至上一个提交。
如果这样做,我们也会恢复此提交中文件的状态。例如,我们可以移动到提交 BugFix^,这是分支 BugFix 中的上一个提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git checkout BugFix^
Note: checking out 'BugFix^'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

git checkout -b <new-branch-name>

HEAD is now at 8327731 Added an introduction.

想要移动一个特定的分支,那就用git log吧。

下面是一些用于分支的可用命令:

命令 释义
git branch 列出所有分支
git branch branchname 创建一个分支
git checkout branchname 切换到分支
git checkout -b branchname 创建并切换到分支
git branch -m branchname new_branchname 重命名分支
git branch --merged 显示所有与当前分支合并过的分支
git branch -d branchname 删除合并完成的分支
git branch -D branch_to_delete 删除未合并的分支



合并分支

用刚才的那张图做例子,橙色分支最后并入到了粉红色分支上,并入操作我们叫做(merge).

1
2
3
4
5
6
7
8
$ git checkout master
Switched to branch 'master'
$ echo "This is some new file in master." > master.txt
$ git add master.txt
$ git commit -m "Added master.txt"
[master 832ceba] Added master.txt
1 file changed, 1 insertions(+)
create mode 100644 master.txt

接下来我们在BugFix分支上也添加一个新文件:

1
2
3
4
5
6
7
8
$ git checkout BugFix
Switched to branch 'BugFix'
$ echo "This is some new file in BugFix" > bugfix.txt
$ git add bugfix.txt
$ git commit -m "Added bugfix.txt"
[BugFix ed600da] Added bugfix.txt
1 file changed, 1 insertions(+)
create mode 100644 bugfix.txt

我们可以选择向BugFix分支并入master,也可以向master并入BugFix。但是我们可以选择前者,因为这样我们就可以在不干扰master分支的前提下验证bug修复是否有用。

1
2
3
4
5
6
7
$ git checkout BugFix
Already on 'BugFix'
$ git merge master -m "Apply changes in master"
Merge made by the 'recursive' strategy.
master.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 master.txt

如果在master分支下出现了会影响到BugFix的更改,则会出现文件冲突。这时Git就会提示你解决冲突。

现在假设我们发现修复没啥问题可以直接并入master,则我们可以直接快进并入,叫做”fast foward”。
这一步不会创建任何commit。

1
2
3
4
5
6
7
$ git checkout master
Switched to branch 'master'
$ git merge BugFix
Fast-forward
bugfix.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 bugfix.txt

下面是一些merge命令的变体:

命令 解释
git merge branchname
git merge --ff-only branchname
git merge --no-ff branchname
git merge --abort
git cherry-pick 073791e7



在远程仓库上工作

目前为止我们只在本地仓库上作法。 如果你想要加入任何在线的Git项目,你需要去了解如何管理远程仓库。

使用git remote命令来查看目前所有的远程仓库,git remote -v命令允许你去查看这些远程仓库的URL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git clone https://github.com/schacon/ticgit
Cloning into 'ticgit'...
remote: Enumerating objects: 1857, done.
remote: Total 1857 (delta 0), reused 0 (delta 0), pack-reused 1857
Receiving objects: 100% (1857/1857), 334.04 KiB | 395.00 KiB/s, done.
Resolving deltas: 100% (837/837), done.

$ cd ticgit

$ git remote
origin

$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)


想要添加远程仓库,你可以使用git remote add <shortname> <URL>命令:

1
2
3
4
$ git remote add pb https://github.com/paulboone/ticgit 
$ git remote
origin
pb


或者如果你想查看一个远程仓库的信息,就使用git remote show <remote>

1
2
3
4
5
6
7
8
9
10
11
12
$ git remote show origin 
* remote origin
Fetch URL: https://github.com/schacon/ticgit
Push URL: https://github.com/schacon/ticgit
HEAD branch: master
Remote branches:
master tracked
ticgit tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (up to date)

下面有一些有关远程仓库的命令和变体:

命令 释义
git remote 查看远程仓库配置
git remote -v 带着URL查看远程仓库配置
git remote add <shortname> <URL> 添加远程仓库
git remote rm <shortname> 删除在本地记录的远程仓库
git remote rename <old-name> <new-name> 重命名在本地记录的远程仓库
git branch --merged 显示所有与当前分支合并过的分支
git branch -d branchname 删除合并完成的分支
git branch -D branch_to_delete 删除未合并的分支



推送到远程仓库

当你在本地对远程仓库做出了更改,并且想要远程仓库同步你本地的版本,就可以使用git push将本地的代码推送到远程仓库。

1
$ git push origin master

此命令仅在您从具有写入权限的服务器克隆且在此期间无人推送时才有效。
如果你和其他人同时克隆,并且他们推送上游,然后您推送上游,您的推送将被拒绝。
这种情况下必须先获取他们的工作并将其合并到当前版本的工作中,然后才允许推送。

命令 释义
git push <remote> <branch> 将某个特定分支推送同步到远程仓库
git push <remote> --force 和上面一样,但是本命令会强制将本地推送到远程仓库
git push <remote> -all 将本地所有分支推送同步到远程仓库
git push <remote> --tags 将所有的本地标签推送到远程仓库



从远程仓库拉取

有点像是下载,也可以算是同步。本命令基本上用来以远程仓库为基准,更新本地仓库。

1
$ git pull
命令 释义
git pull <remote> 将远程仓库的内容同步到本地仓库
git pull --no-commit <remote> 从远程仓库拉取,但是不在本地创建提交
git pull --verbose 从远程仓库拉取时返回verbose内容



作者

ntcs

发布于

2025-01-25

更新于

2025-02-07

许可协议

评论