安装与使用 Linux¶
作为 Day 1,今天的内容还是比较放松的。只需基本的计算机操作水平和一点程序设计基础即可。
本节内容:
- 在虚拟机上安装 Linux
- Shell 的使用
有用的参考资料:
安装 Linux¶
需要说明的是,Linux 严格来说单指 Linux 内核。建立在 Linux 内核之上的完整操作系统,称作 Linux 发行版,或者直接称为 Linux。
关于 Linux 的安装在很多地方都有详细的教程。我们会很快地介绍一下在虚拟机上安装 Linux 的过程。一些你可以选择的虚拟机软件:
- Parallels Desktop(macOS,付费)
- VMware(个人版免费)
- Oracle VirtualBox(开源免费)
当然,如果你有条件且知道你在做什么,也可以尝试在物理机上安装 Linux。
本课程中推荐使用 Debian Linux。当然,如果你已经有了一些了解,完全可以按自己习惯的发行版来,不影响后续课程。
关于 WSL
不推荐使用 WSL(Windows Subsystem for Linux)进行本课程的学习。WSL 与真实的 Linux 系统有很多不同,你也许会在后续的学习中遇到问题。笔者不对任何在 WSL 上的问题负责。
关于 Ubuntu
不推荐使用 Ubuntu。尽管 Ubuntu 是最流行的 Linux 发行版之一,但 Canonical 在 Ubuntu 中加入了一些臭名昭著的私货(例如,为了推广自家的 snap 而劫持 apt 的 chromium),笔者反对这种行为。况且,Debian 作为 Ubuntu 的上游,两者在使用上没有太大区别,而且能带给你更“原汁原味”的 Linux 体验。
社区关于 snap 的优劣已经有无数讨论,这里不再赘述。如果你对这个话题感兴趣,可以搜索“why is ubuntu snap good/bad”。
给某些勇士准备的建议
如果你对自己的 Linux 操作水平非常有信心,或是想锻炼自己的运维能力,不妨挑战一下在 Alpine Linux 上完成本课程!
关于桌面环境
实际上我们不需要桌面环境来完成本课程。所以如果你想要锻炼自己的 CLI 能力,推荐不安装桌面环境并直接进入到 Shell 部分。当然,也可以选择体验一下,这个世界需要更多脱离 Windows 的人。
关于 Docker
可以使用 Docker 来完成本课程,但因为我们会涉及到关于文件系统和网络的内容,使用 Docker 会给你的这部分学习增添不必要的麻烦。
下载 Linux 镜像¶
- 下载安装镜像
- Debian Live DVD @ ZJU Mirror 或 Debian Live DVD @ TUNA
- 不清楚这么多文件该选哪个?推荐下载
-gnome.iso
后缀 - 在用 M 系列芯片的 Mac?见 Apple Silicon Specials
- 使用你的虚拟机软件安装 Linux
- RAM 推荐分配 1~4 GB,硬盘空间 16 GB 即可
Apple Silicon Specials¶
很好,你我都是敢于用 M 系列芯片的敢死队。
目前绝大部分电脑都是 x86_64 指令集架构,这也意味着我们这些 arm64 或称 aarch64 架构的用户注定要走他们不用走的弯路。所幸随着 arm64 架构的普及,越来越多的软件开始支持这一架构。虽然还没有 arm64 版本的 Live CD 环境,但所幸 Debian 足够好,其安装过程中提供了桌面环境的选择。
致决定使用 Ubuntu 的可怜虫
由于 Canonical 没有提供 arm64 版本的 Ubuntu Live CD,你需要从 Ubuntu Server 开始安装,然后再安装桌面环境。而且 APT 源需要使用 ubuntu-ports
。加油,相信爱用 Ubuntu 的你一定能克服这一困难!
- 下载镜像:Debian ISO-DVD @ ZJU Mirror 或 Debian ISO-DVD @ TUNA
- 安装进行到“软件选择”时,选择“... GNOME”
- 注意不要取消选择“SSH server”和“标准系统工具”
Debian 的桌面环境
其实也可以试试列出来的其他桌面环境,不同的桌面环境有不同的风格和特点。
Debian 的默认桌面环境是 GNOME,所以实际上在“软件选择”阶段选择“Debian 桌面环境”和选择“... GNOME”效果等同。不过笔者实测发现,选择“Debian 桌面环境”且语言选择中文时,终端的字体会变得很瘦窄,而选择“... GNOME”就不会有这个问题。右图中上面是选择“Debian 桌面环境”,下面是选择“... GNOME”。感兴趣的同学可以研究一下为什么会有这个问题。
使用 Linux¶
至此,我们的安装过程就算完成了!现在我们会留出一些时间让大家体验体验。开一局 2048 或者俄罗斯方块,体验一把 Linux Gaming(✕
怎么样,打完了?很好,那么我们继续。
Shell¶
现在在你的 Debian 里打开终端。
也许有同学对 Windows 中的 cmd.exe
或 PowerShell 不陌生。这两者都叫做 shell,而我们日常使用的应用程序叫做终端模拟器(terminal emulator),或通常简称为终端,例如 Windows 上的 Windows Terminal 或 macOS 上的 Terminal.app、iTerm2。而 shell 例如 bash、zsh、fish 等则是真正负责干活的程序:解析你的输入并执行命令。终端负责把 shell 呈现给你,在你和 shell 之间传递输入输出,并有时提供一些额外的功能。以后当我们提到 shell 时,你应该明确我们指的是 shell 程序本身,而不是终端模拟器。我们会严格区分 shell 和 terminal 两个术语。
在 Debian GNOME 上预装的终端模拟器是 GNOME Terminal。
首先先来试试 shell 能干什么吧。输入 apt
并按下回车。你会看到:
“超级牛力”?这是什么意思呢?
配置 APT 源¶
现在来溜点传统的 sudo apt update
。sudo
的意思是以 root 用户的权限运行命令,root 用户是 Linux 系统中的最高权限用户。
如果你是在 Apple Silicon 上安装的 Debian,那可能会遇到像这样的报错:
这是因为在安装 Debian 的时候 APT 是从 ISO 镜像文件中获取软件包的,但在安装完成之后这个配置并没有改回来。我们需要参考镜像站使用帮助(ZJU Mirror 或 TUNA)来手动设置 APT 源。
关于 apt
与 apt-get
apt
是 apt-get
的一个更友好的前端。在日常使用中你应当使用 apt
,但在编写 shell script 时更推荐使用 apt-get
,因为它的输出更容易被程序解析。apt
对输出的可解析性没有任何保证,输出格式永远以人类可读为主,可能会在不同版本之间有所不同。
切换主目录为英文(可选)¶
如果安装时选择了中文,那么用户主目录下面的几个目录会是中文。当然,这没什么不好,但一般而言中文路径是应当避免的。但是,不要手动更改。我们需要在终端中设置 LANG
环境变量后运行 xdg-user-dirs-update
。这可以在一行内完成。重启系统生效。
如果重启后弹窗询问是否要将目录切换为中文,勾选“不再提示”并选择否。
切换 shell 环境为英文(可选)¶
同样,如果如果安装时选择了中文,那么 shell 环境也会是中文。我们可以通过设置 LANG
环境变量来切换 shell 环境为英文。在 bash 的配置文件中添加对环境变量 LANG
的设置:
推荐完成这一步,因为并不是所有程序都有完善的本地化支持(例如,你能在上面的 sudo apt update
的输出中找到一处问题吗?),而且英文环境下的错误信息更容易被搜索引擎搜索到。你也可以不进行这一步,但从现在开始我们将默认 LANG
环境变量设置为 en_US.UTF-8
。
其他¶
还有一些别的小技巧,例如换用 zsh、用 SSH 连接到虚拟机、tmux
等等。我们不会在这里详细介绍,但你可以在网络上找到很多教程。这些技巧会让你的 Linux 使用体验更加顺畅。关于更多 shell 的实用技巧,可以参考本页面开头的推荐视频。
Shell 基础语法¶
这部分就是比较索然无味的入门级 shell 语法介绍了。如果你觉得你已经对 shell 的语法足够理解,可以直接开始做 Homework。
首先我们要明确,shell 可以指代 shell 程序,也可以指代 shell script。Shell script 是一系列 shell 命令的集合,类似于 Windows 的批处理文件(.bat
),后缀名通常是 .sh
。不过也可能是 .bash
、.zsh
或完全没有后缀名。在除了 Windows 之外的所有操作系统上,后缀名并不是决定文件类型的唯一标准,而是一个约定。真正决定文件类型的是文件的内容。这一点非常重要。
不同 shell 之间的差异
首先是由 Stephen Bourne 编写的 Bourne shell(sh),最早的 Unix shell。Bourne shell 的语法非常简单,但功能也相对有限。后来由 Brian Fox 编写的 bash(Bourne-again shell)成为了 Linux 系统的默认 shell。bash 在 sh 的基础上增加了很多功能,也让它成为了最流行的 shell 之一。后来就有更多的 shell 出现,例如 Z shell(zsh)和 the friendly interactive shell(fish)。这些 shell 有着更多的功能和更好的用户体验。
当然,更先进的 shell 意味着更多的扩展语法,同义词是与其他 shell(特别是 sh 和 bash)的不兼容性。你可以使用任何你喜欢的 shell,但我们保证课程中只会使用与 bash 兼容的语法 i.e. 只用 bash 也可以顺利完成课程。
Prompt 和工作目录¶
这就是 prompt,提供了一些对当前的命令执行至关重要的信息,比如用户名(user
)、主机名(debian
)、当前的工作目录(~
)和是否为 root($
)。
简单来说,“工作目录”就是你的 shell 当前所处的位置。你大概已经知道可以通过 cd
命令来切换目录,用 ls
来列出当前目录下的内容。这些基础设施将会陪伴我们使用 shell 的一生。~
的意思就是当前用户的“home”,你也可以使用 pwd
(print working directory)输出当前的工作目录。
稍微等等,我们在这里执行了六条命令。通过 sudo -i
,我们将当前 shell 切换到了 root 用户,你可以看到 root 用户的 home 目录是 /root
,而且 prompt 末尾变成了 #
。然后我们退出,切回我们的 user,尝试进入到 root 用户的 home 目录(~root
)但被拦下来了。我们以后会看到,这是 Linux 文件系统权限管理的一部分。对现阶段的我们来说,以 root 用户执行命令还是太危险了,所以不要轻易使用 sudo -i
然后以 root 权限乱搞。
Hello, World!¶
目前对我们最重要的命令是 echo
,作用是输出一段文本。bash 的 Hello, World!
就是:
太简单了。另一种输出的方式是使用 printf
,它的语法更接近 C 语言的 printf
函数:
注意到最后的 \n
:和 C 的 printf
一样,printf
不会自动换行。echo
会自动换行。
关于单双引号
shell 对单引号和双引号有不同的处理方式。对于上面的例子请先使用单引号,其区别我们马上会讲到。
一行可以写多个由分号 ;
分隔的命令,效果和分开写是一样的。前面跟着空格的 #
是注释的开始,直到行尾。之所以要有空格是因为我们马上会看到,#
也有另外的用途。
- 在这里
#
不是注释的开始,而是字符串的一部分。highlight.js 对这里的渲染有问题,将2
渲染为注释。实际运行的结果如我们所示,就是1#2
。
当然,一般来说 echo
后面不一定非要用引号括起来的字符串,也就是你也可以 echo Hello, World!
。但如果有连续的空格,shell 会将其合并为一个空格,这种时候就需要引号了。我们会在 lec3 中究其根本。
变量¶
shell 中的变量定义非常简单,直接使用 =
赋值即可。注意等号两边不能有空格,否则 shell 会认为等号左边是一个命令。
注意到在引用变量时需要在变量名前加上 $
。变量替换是在 shell 层面完成的,也就是说 shell 会首先展开变量,然后再执行命令 echo Alice
。要引用变量,也可以写作 ${name}
,这样可以避免歧义:${name}abc
的结果是 Aliceabc
。引用一个不存在的变量会得到空字符串。所以 echo $nameabc
的结果是空行。
要取消变量的定义,使用 unset
命令:
再体验一下变量的强大之处:你可以在一条命令的(几乎)任何地方插入变量。
因为第二行展开就是 echo echo Hechollo
。你甚至可以嵌套变量:
${!var}
是间接引用变量的方式:先取得 var
的值(得到 my_echo
),再将其作为变量名得到 echo
。
字符串插值¶
现在是时候讲讲单引号和双引号的区别了。在 shell 中,单引号会保留字符串的原始值,而双引号会对字符串进行插值。“插值”的意思是变量替换。当然,前面提到的所有替换规则都是适用的。
当然,不只这样,单引号不需要对内容进行任何转义,而双引号需要转义一些特殊字符。
- 在这里
$$
是 shell 的一个特殊变量,表示当前 shell 进程的 PID(进程 ID)。在你的 shell 中会有不同的值。
是的,如果你足够机灵的话也许会发现,你不能在单引号字符串中尝试用 \'
产生一个单引号。这只会提前终结这个单引号字符串。关于要怎么解决这种问题,我们会在 lec2 中看到。
键盘输入¶
read
命令可以从键盘读取一行输入并赋值给变量。
整数运算¶
shell 支持整数运算:$((...))
语法。这会被替换为运算结果。
另一种整数运算的方式是使用 expr
命令,在这里就不详细介绍了。
说到数学运算,就得提到 let
。let
也是用于整数运算的,但它的语法更接近 C 语言。0
开头的数字会被当作八进制数,0x
开头的数字会被当作十六进制数,引用变量时不需要 $
。也可以用另一种语法 ((...))
。
注意什么时候用 $((...))
什么时候用 ((...))
。
或者,也可以使用 base#number
语法,例如 16#face
就是十六进制数 0xface
。base
的范围是 2 至 64,字母表的范围是 0-9
a-z
A-Z
@
_
共 64 个字符。
在继续之前先确保自己理解了这里的结果是怎么得到的。什么是 64#A@_
?这里的 A@_
是 64 进制数:A
是 36,@
是 62,_
是 63。所以 64#A@_
是 \( 36 \times {64}^2 + 62 \times {64}^1 + 63 \times {64}^0 \)。那 36#1$v
又是?这里首先将 $v
进行变量替换,得到 1zz
,然后 36 进制数的 1zz
结果就是 2591。
最后,这种语法是不支持浮点数的。诸如 bc
和 awk
这样的工具可以用于浮点数运算。
返回值¶
每条命令都有其执行结果。回忆一下 C 语言中 main
函数的 return 0;
;这就是返回值。返回值是一个整数,0 通常表示成功,非 0 表示失败。(在这里“通常”的意思是绝大部分;包括 shell 它自己也是这么判断的。如果你想设计一个返回值为 0 表示失败的程序,你最好先找好足够有说服力的理由。提示:你几乎不可能找到。)可以用 $?
来获取上一条命令的返回值。
true
和 false
命令除了分别返回 0 和 1 之外什么也不做。
嵌套 shell¶
嵌套 shell 本质上和运行别的程序没有区别——不过是在当前的 shell 中再运行一个 shell 罢了。
外层 shell 的 PID 是 8611。然后我们运行了一个新的 shell,它的 PID 是 8651。用 exit
退出内层 shell 后我们重新回到外层这个 PID 为 8611 的 shell。
看着没什么意思。嵌套 shell 真正有意思的地方在于对变量的处理上。我们会在 lec2 中看到。
PATH
环境变量¶
有没有想过我们到现在用到的 sudo
apt
echo
printf
true
false
这些命令都是哪来的?全部是 shell 内置的吗?肯定不是。小提示:你可以使用 which
来查看某个命令的路径。
(顺便一提,tali
是 GNOME 自带的一个游戏。)
从上面的输出中我们能解读到什么?which let
没有输出,这也许说明它确实是一条 shell 内置命令。which what-the-f
也没什么输出,因为确实没有 what-the-f
这条命令。对于其他命令,which
都报告了一个路径——这就是这条命令实际所在的路径。当你单走一个 apt
时,运行的就是 /usr/bin/apt
。
你可以先使用 ls /usr/bin
对我们正在说的话题有一个感性的认识。既然我们已经来到了这一步,是时候意识到“命令其实就是可执行文件”了。
是的,孩子们,shell 之所以知道你打 apt
时要去运行 /usr/bin/apt
,其秘密就在于这个 PATH
变量。它实际上就是一个字符串,其内容是用 :
分隔的路径。这是 PATH
的要求,遵循它便是。
这是什么意思呢?很简单,第一条和第二条就是将 /some/of/my/path
加入到 PATH
中,这样在寻找命令时,你的 shell 就会额外去搜索 /some/of/my/path
。区别在于何时去搜索 /some/of/my/path
:对于第一条,它会在最后被搜索。对于第二条,它会被第一个搜索。这说明 shell 是按 PATH
中路径的顺序搜索命令的。那么第三个是什么?就是将 PATH
设为仅有 /some/of/my/path
,那么后果就是 shell 不会去搜索 /usr/bin
和其他默认路径,而只会去搜这里了。
通过 PATH=
我们将 PATH
设置为了一个空字符串,效果上就是清空了它。于是,只是运行 ls
的话,shell 就不知道要去哪里寻找这个可执行文件了。
那么 echo
呢?
继续试试 echo
,你会发现在 bash 中,即使清空了 PATH
,但 echo
还是能正常使用。这似乎说明 bash 在处理 echo
时,实际上并不是寻找 /usr/bin/echo
并运行之——暗示了 echo
也许也是某种内置命令。
要说的话,这算是 bash 的设计问题了。举个例子,zsh 是这样的:
其他的变量¶
在进行下去之前,我们还需要明确一个概念:shell 中的变量是一个很复杂的话题。变量实际上分为 shell 变量和环境变量,其区别在于作用域。shell 变量只在当前 shell 中有效,而环境变量则会被传递给子进程。PATH
就是一个环境变量。我们会在 lec2 中详细讲解。
你可以用 set
或 declare
命令查看当前所有的 shell 变量,或用 env
查看所有的环境变量。
太多了,我们只列出来了一部分。一些比较有意思的变量是 PATH
PWD
HOME
USER
LANG
,输出它们的值看看吧。
Shell Scripting¶
关于 shell 的语法我们已经聊得足够多了,多到有点无聊了。接下来我们很快地用 shell script 的介绍结束这一节。创建一个 hello.sh
文件,写入以下内容:
然后运行它:
Shell script 就是这样。第一行的 #!/bin/bash
是一种特殊的注释,叫做 shebang。不过由于我们是用 bash hello.sh
来运行的,所以 shebang 的作用并没有体现出来。我们会在 lec2 中看到 shebang 的真正作用。总是在 shell script 的第一行写上 shebang 是一个好习惯。
接下来该试试用 shell script 完成 A+B 了!
很简单吧!
关于 sh 和 bash,还有 shell script 和 bash script
有时候你会看到 sh hello.sh
,也就是用 sh 来运行 shell script。在多数平台上,sh 和 bash 是不同的程序,前者不支持一些 bash 独有的语法。对于我们现在能写出的 shell script 来说,用 sh 运行也是没问题的。不过如果你的 shell script 在用 sh 运行时报了什么奇怪的错误,那还是用 bash 来运行吧。
Shell script 是一个广义的概念,例如可以用 sh / bash / zsh 来运行。Bash script 就更注重强调用 bash 来运行了。
最后,我们来看看一个稍微复杂一点的 shell script。
$1
和 $2
是什么意思呢?
你可以用 $1
$2
$3
... 来引用 shell script 的参数。当然,不存在的参数会被替换为空字符串。还有一些别的特殊变量,例如 $0
会是 shell script 的文件名,$#
是参数个数,$@
是所有参数的列表。同时,注意到那个 "linux distro"
的引号了吗?只有用引号括起来,才会被 shell 认为是一个整体,否则就会在空格处分割。
为什么第一行调用是 5 个参数,应该没有问题吧。
还有一点要注意,如果你的参数超过了 9 个,那么你需要用 ${10}
${11}
... 来引用它们(当然,小于 10 的参数也可以用这种方式引用)。$10
会被解释为 $1
后附 0
。
关于 $@
和 $*
$@
和 $*
表现得几乎一样,都是展开为所有参数。我们会在 lec2 中看到它们的微妙区别。不过在实际应用中,$*
很少使用。你几乎总是应该用 $@
。
最后要说明的是,用这种方式运行的 shell script 是在一个新的 shell 中运行的,而非在当前 shell 中。这意味着你在 shell script 中定义的变量不会传到当前 shell 中。通过使用 source
命令,你可以在当前 shell 中运行 shell script。实际上 ~/.bashrc
就是这样工作的,这是你的 bash 在启动时会自动执行的 script。以及,source
命令也可以用 .
来代替。
Homework¶
简答题¶
-
请解释下面代码中两行
echo
命令的输出分别是如何得到的。
2.
编程题¶
限定语言为 bash。
-
使用
apt
升级你的系统中所有能升级的软件包。 -
写一个参数版本的 A+B。即它应该像这样工作:
-
1 英尺等于 12 英寸等于 0.3048 米。写一个 shell script,将厘米数转换为对应的英尺英寸数,只需保留整数。例如:
我们已经说过 bash 不支持浮点数运算,那要怎么办呢?实际上不需要使用任何外部工具,只用 bash 的整数运算就足够实现。
-
从键盘读入一个三位数,然后输出它的百位、十位和个位数字,以及这个数字的逆序,不带前导零。例如:
-
写一个 shell script,接受一个表达式,计算并输出其结果。表达式只包含加减乘除和括号,整体作为一个参数传入。例如:
你并不需要去做任何复杂的表达式解析。
-
写一个 shell script,将华氏温度 \( F \) 转换为摄氏温度 \( C \),公式是 \( C = 5/9 \times (F - 32) \)。你的答案应该精确到小数点后两位。例如:
与第 3 题一样,以及别忘了我们有
printf
,在此之上你也许还需要一点创意...
如果这些题都没问题,那么你可以自豪地说你已经掌握了 shell 的基础语法。我们会在下一节把 shell 玩出花来。