跳转至

安装与使用 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 镜像

GNOME 用户登录界面

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 的桌面环境

其实也可以试试列出来的其他桌面环境,不同的桌面环境有不同的风格和特点。

终端字体对比

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 并按下回车。你会看到:

user@debian:~$ apt
apt 2.6.1 (arm64)
用法: apt [选项] 命令

命令行软件包管理器 apt 提供软件包搜索,管理和信息查询等功能。
它提供的功能与其他 APT 工具相同(像 apt-get 和 apt-cache),
但是默认情况下被设置得更适合交互。

常用命令:
  list - 根据名称列出软件包
  search - 搜索软件包描述
  show - 显示软件包细节
  install - 安装软件包
  reinstall - 重新安装软件包
  remove - 移除软件包
  autoremove - automatically remove all unused packages
  update - 更新可用软件包列表
  upgrade - 通过 安装/升级 软件来更新系统
  full-upgrade - 通过 卸载/安装/升级 来更新系统
  edit-sources - 编辑软件源信息文件
  satisfy - 使系统满足依赖关系字符串

参见 apt(8) 以获取更多关于可用命令的信息。
程序配置选项及语法都已经在 apt.conf(5) 中阐明。
欲知如何配置软件源,请参阅 sources.list(5)。
软件包及其版本偏好可以通过 apt_preferences(5) 来设置。
关于安全方面的细节可以参考 apt-secure(8).
                                         本 APT 具有超级牛力。

“超级牛力”?这是什么意思呢?

1
2
3
4
5
6
7
8
user@debian:~$ apt moo
                 (__)
                 (oo)
           /------\/
          / |    ||
         *  /\---/\
            ~~   ~~
..."Have you mooed today?"...

配置 APT 源

现在来溜点传统的 sudo apt updatesudo 的意思是以 root 用户的权限运行命令,root 用户是 Linux 系统中的最高权限用户。

如果你是在 Apple Silicon 上安装的 Debian,那可能会遇到像这样的报错:

1
2
3
4
5
6
7
8
9
user@debian:~$ sudo apt update
[sudo] user 的密码:
忽略:1 cdrom://[Debian GNU/Linux 12.6.0 _Bookworm_ - Official arm64 DVD Binary-1 with firmware 20240629-10:19] bookworm InRelease
错误:2 cdrom://[Debian GNU/Linux 12.6.0 _Bookworm_ - Official arm64 DVD Binary-1 with firmware 20240629-10:19] bookworm Release
  请使用 apt-cdrom,通过它可以让 APT 识别该盘片。apt-get upgdate 不能被用来加入新的盘片。
正在读取软件包列表... 完成
E: 仓库 “cdrom://[Debian GNU/Linux 12.6.0 _Bookworm_ - Official arm64 DVD Binary-1 with firmware 20240629-10:19] bookworm Release” 没有 Release 文件。
N: 无法安全地用该源进行更新,所以默认禁用该源。
N: 参见 apt-secure(8) 手册以了解仓库创建和用户配置方面的细节。

这是因为在安装 Debian 的时候 APT 是从 ISO 镜像文件中获取软件包的,但在安装完成之后这个配置并没有改回来。我们需要参考镜像站使用帮助(ZJU MirrorTUNA)来手动设置 APT 源。

关于 aptapt-get

aptapt-get 的一个更友好的前端。在日常使用中你应当使用 apt,但在编写 shell script 时更推荐使用 apt-get,因为它的输出更容易被程序解析。apt 对输出的可解析性没有任何保证,输出格式永远以人类可读为主,可能会在不同版本之间有所不同。

切换主目录为英文(可选)

如果安装时选择了中文,那么用户主目录下面的几个目录会是中文。当然,这没什么不好,但一般而言中文路径是应当避免的。但是,不要手动更改。我们需要在终端中设置 LANG 环境变量后运行 xdg-user-dirs-update。这可以在一行内完成。重启系统生效。

1
2
3
4
5
6
7
8
9
user@debian:~$ LANG=C xdg-user-dirs-update --force
Moving DESKTOP directory from 桌面 to Desktop
Moving DOWNLOAD directory from 下载 to Downloads
Moving TEMPLATES directory from 模板 to Templates
Moving PUBLICSHARE directory from 公共 to Public
Moving DOCUMENTS directory from 文档 to Documents
Moving MUSIC directory from 音乐 to Music
Moving PICTURES directory from 图片 to Pictures
Moving VIDEOS directory from 视频 to Videos

如果重启后弹窗询问是否要将目录切换为中文,勾选“不再提示”并选择否。

英文主目录

切换 shell 环境为英文(可选)

同样,如果如果安装时选择了中文,那么 shell 环境也会是中文。我们可以通过设置 LANG 环境变量来切换 shell 环境为英文。在 bash 的配置文件中添加对环境变量 LANG 的设置:

user@debian:~$ echo 'export LANG=en_US.UTF-8' >> ~/.bashrc  # 注意不要只打一个 >

推荐完成这一步,因为并不是所有程序都有完善的本地化支持(例如,你能在上面的 sudo apt update 的输出中找到一处问题吗?),而且英文环境下的错误信息更容易被搜索引擎搜索到。你也可以不进行这一步,但从现在开始我们将默认 LANG 环境变量设置为 en_US.UTF-8

sudo apt-get 中文本地化笑点解析
  ... apt-get upgdate 不能被用来加入新的盘片。

注意到 upgdate 的拼写错误了吗?

其他

还有一些别的小技巧,例如换用 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 和工作目录

user@debian:~$

这就是 prompt,提供了一些对当前的命令执行至关重要的信息,比如用户名(user)、主机名(debian)、当前的工作目录(~)和是否为 root($)。

简单来说,“工作目录”就是你的 shell 当前所处的位置。你大概已经知道可以通过 cd 命令来切换目录,用 ls 来列出当前目录下的内容。这些基础设施将会陪伴我们使用 shell 的一生。~ 的意思就是当前用户的“home”,你也可以使用 pwdprint working directory)输出当前的工作目录。

user@debian:~$ pwd
/home/user
user@debian:~$ sudo -i
[sudo] password for user:
root@debian:~# pwd
/root
root@debian:~# exit
logout
user@debian:~$ cd /root
-bash: cd: /root: Permission denied
user@debian:~$ cd ~root
-bash: cd: /root: Permission denied

稍微等等,我们在这里执行了六条命令。通过 sudo -i,我们将当前 shell 切换到了 root 用户,你可以看到 root 用户的 home 目录是 /root,而且 prompt 末尾变成了 #。然后我们退出,切回我们的 user,尝试进入到 root 用户的 home 目录(~root)但被拦下来了。我们以后会看到,这是 Linux 文件系统权限管理的一部分。对现阶段的我们来说,以 root 用户执行命令还是太危险了,所以不要轻易使用 sudo -i 然后以 root 权限乱搞。

Hello, World!

目前对我们最重要的命令是 echo,作用是输出一段文本。bash 的 Hello, World! 就是:

echo 'Hello, World!'

太简单了。另一种输出的方式是使用 printf,它的语法更接近 C 语言的 printf 函数:

printf 'Hello, World!\n'

注意到最后的 \n:和 C 的 printf 一样,printf 不会自动换行。echo 会自动换行。

1
2
3
user@debian:~$ printf 'Hello, World!'
Hello, World!user@debian:~$ printf 'Hello, World!\n'
Hello, World!

关于单双引号

shell 对单引号和双引号有不同的处理方式。对于上面的例子请先使用单引号,其区别我们马上会讲到。

一行可以写多个由分号 ; 分隔的命令,效果和分开写是一样的。前面跟着空格的 # 是注释的开始,直到行尾。之所以要有空格是因为我们马上会看到,# 也有另外的用途。

1
2
3
4
user@debian:~$ echo 1#2(1)
1#2
user@debian:~$ echo 3 #4
3
  1. 在这里 # 不是注释的开始,而是字符串的一部分。highlight.js 对这里的渲染有问题,将 2 渲染为注释。实际运行的结果如我们所示,就是 1#2

当然,一般来说 echo 后面不一定非要用引号括起来的字符串,也就是你也可以 echo Hello, World!。但如果有连续的空格,shell 会将其合并为一个空格,这种时候就需要引号了。我们会在 lec3 中究其根本。

1
2
3
4
5
6
user@debian:~$ echo Hello, World!
Hello, World!
user@debian:~$ echo   Hello,     World!
Hello, World!
user@debian:~$ echo ' Hello,     World!'
 Hello,     World!

变量

shell 中的变量定义非常简单,直接使用 = 赋值即可。注意等号两边不能有空格,否则 shell 会认为等号左边是一个命令。

1
2
3
name=Alice
echo $name  # 输出 Alice
echo = wtf  # 输出 = wtf;并不会产生一个叫 echo 的变量

注意到在引用变量时需要在变量名前加上 $。变量替换是在 shell 层面完成的,也就是说 shell 会首先展开变量,然后再执行命令 echo Alice。要引用变量,也可以写作 ${name},这样可以避免歧义:${name}abc 的结果是 Aliceabc。引用一个不存在的变量会得到空字符串。所以 echo $nameabc 的结果是空行。

要取消变量的定义,使用 unset 命令:

unset name
echo $name  # 输出空行;和单打一个 echo 没什么区别

再体验一下变量的强大之处:你可以在一条命令的(几乎)任何地方插入变量。

my_echo=echo
${my_echo} ${my_echo} H${my_echo}llo  # 输出 echo Hechollo

因为第二行展开就是 echo echo Hechollo。你甚至可以嵌套变量:

var=my_echo; ${!var} omg it\'s ${var}  # 输出 omg it's my_echo

${!var} 是间接引用变量的方式:先取得 var 的值(得到 my_echo),再将其作为变量名得到 echo

字符串插值

现在是时候讲讲单引号和双引号的区别了。在 shell 中,单引号会保留字符串的原始值,而双引号会对字符串进行插值。“插值”的意思是变量替换。当然,前面提到的所有替换规则都是适用的。

1
2
3
4
5
name=Alice
echo 'Hello, $name'       # Hello, $name
echo 'Hello, ${name}'     # Hello, ${name}
echo "Hello, $nameabc"    # Hello,
echo "Hello, ${name}abc"  # Hello, Aliceabc

当然,不只这样,单引号不需要对内容进行任何转义,而双引号需要转义一些特殊字符。

1
2
3
4
5
echo "Hello, \"Alice\""  # Hello, "Alice"
echo 'Hello $$$'         # Hello $$$
echo "Hello $$$"         # Hello 1511$ (1)
echo "Hello \$\$\$"      # Hello $$$
echo 'Hello \$\$\$'      # Hello \$\$\$
  1. 在这里 $$ 是 shell 的一个特殊变量,表示当前 shell 进程的 PID(进程 ID)。在你的 shell 中会有不同的值。

是的,如果你足够机灵的话也许会发现,你不能在单引号字符串中尝试用 \' 产生一个单引号。这只会提前终结这个单引号字符串。关于要怎么解决这种问题,我们会在 lec2 中看到。

键盘输入

read 命令可以从键盘读取一行输入并赋值给变量。

1
2
3
4
user@debian:~$ read name
Bob
user@debian:~$ echo "name is $name"
name is Bob

整数运算

shell 支持整数运算:$((...)) 语法。这会被替换为运算结果。

1
2
3
4
echo $((8+5/3))  # 9
i=80
echo $((i / 2))  # 40
echo $((i % 3))  # 2

另一种整数运算的方式是使用 expr 命令,在这里就不详细介绍了。

说到数学运算,就得提到 letlet 也是用于整数运算的,但它的语法更接近 C 语言。0 开头的数字会被当作八进制数,0x 开头的数字会被当作十六进制数,引用变量时不需要 $。也可以用另一种语法 ((...))

1
2
3
4
5
6
let i=80
echo $i      # 80
((j=i--))    # 也可写作 let j=i--
echo $i, $j  # 79, 80
let "k = j - i + 0xface"
echo $k      # 64205

注意什么时候用 $((...)) 什么时候用 ((...))

1
2
3
4
echo ((2**3))   # -bash: syntax error near unexpected token `('
echo $((2**3))  # 8
((2**3))        #
$((2**3))       # -bash: 8: command not found

或者,也可以使用 base#number 语法,例如 16#face 就是十六进制数 0xfacebase 的范围是 2 至 64,字母表的范围是 0-9 a-z A-Z @ _ 共 64 个字符。

v=zz
echo $((2#1010)), $((64#A@_)), $((36#1$v))  # 10, 151487, 2591

在继续之前先确保自己理解了这里的结果是怎么得到的。什么是 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。

最后,这种语法是不支持浮点数的。诸如 bcawk 这样的工具可以用于浮点数运算。

返回值

每条命令都有其执行结果。回忆一下 C 语言中 main 函数的 return 0;;这就是返回值。返回值是一个整数,0 通常表示成功,非 0 表示失败。(在这里“通常”的意思是绝大部分;包括 shell 它自己也是这么判断的。如果你想设计一个返回值为 0 表示失败的程序,你最好先找好足够有说服力的理由。提示:你几乎不可能找到。)可以用 $? 来获取上一条命令的返回值。

true  ; echo $?  # 0
false ; echo $?  # 1

truefalse 命令除了分别返回 0 和 1 之外什么也不做。

嵌套 shell

嵌套 shell 本质上和运行别的程序没有区别——不过是在当前的 shell 中再运行一个 shell 罢了。

1
2
3
4
5
6
7
8
9
user@debian:~$ echo "$$"
8611
user@debian:~$ bash
user@debian:~$ echo "$$"
8651
user@debian:~$ exit
exit
user@debian:~$ echo "$$"
8611

外层 shell 的 PID 是 8611。然后我们运行了一个新的 shell,它的 PID 是 8651。用 exit 退出内层 shell 后我们重新回到外层这个 PID 为 8611 的 shell。

看着没什么意思。嵌套 shell 真正有意思的地方在于对变量的处理上。我们会在 lec2 中看到。

PATH 环境变量

有没有想过我们到现在用到的 sudo apt echo printf true false 这些命令都是哪来的?全部是 shell 内置的吗?肯定不是。小提示:你可以使用 which 来查看某个命令的路径。

user@debian:~$ which apt
/usr/bin/apt
user@debian:~$ which ls
/usr/bin/ls
user@debian:~$ which echo
/usr/bin/echo
user@debian:~$ which let
user@debian:~$ which false
/usr/bin/false
user@debian:~$ which tali
/usr/games/tali
user@debian:~$ which which
/usr/bin/which
user@debian:~$ which what-the-f
user@debian:~$ what-the-f
-bash: what-the-f: command not found

(顺便一提,tali 是 GNOME 自带的一个游戏。)

从上面的输出中我们能解读到什么?which let 没有输出,这也许说明它确实是一条 shell 内置命令。which what-the-f 也没什么输出,因为确实没有 what-the-f 这条命令。对于其他命令,which 都报告了一个路径——这就是这条命令实际所在的路径。当你单走一个 apt 时,运行的就是 /usr/bin/apt

user@debian:~$ apt moo
                 (__)
                 (oo)
           /------\/
          / |    ||
         *  /\---/\
            ~~   ~~
..."Have you mooed today?"...
user@debian:~$ /usr/bin/apt moo
                 (__)
                 (oo)
           /------\/
          / |    ||
         *  /\---/\
            ~~   ~~
..."Have you mooed today?"...

你可以先使用 ls /usr/bin 对我们正在说的话题有一个感性的认识。既然我们已经来到了这一步,是时候意识到“命令其实就是可执行文件”了。

user@debian:~$ echo "$PATH"
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

是的,孩子们,shell 之所以知道你打 apt 时要去运行 /usr/bin/apt,其秘密就在于这个 PATH 变量。它实际上就是一个字符串,其内容是用 : 分隔的路径。这是 PATH 的要求,遵循它便是。

1
2
3
PATH="$PATH:/some/of/my/path"
PATH="/some/of/my/path:$PATH"
PATH="/some/of/my/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 和其他默认路径,而只会去搜这里了。

1
2
3
4
5
user@debian:~$ PATH=
user@debian:~$ ls
-bash: ls: No such file or directory
user@debian:~$ /usr/bin/ls
Desktop  Documents  Downloads  Music  Pictures  Public  Templates  Videos

通过 PATH= 我们将 PATH 设置为了一个空字符串,效果上就是清空了它。于是,只是运行 ls 的话,shell 就不知道要去哪里寻找这个可执行文件了。

那么 echo 呢?

继续试试 echo,你会发现在 bash 中,即使清空了 PATH,但 echo 还是能正常使用。这似乎说明 bash 在处理 echo 时,实际上并不是寻找 /usr/bin/echo 并运行之——暗示了 echo 也许也是某种内置命令。

要说的话,这算是 bash 的设计问题了。举个例子,zsh 是这样的:

debian% which echo
echo: shell built-in command

其他的变量

在进行下去之前,我们还需要明确一个概念:shell 中的变量是一个很复杂的话题。变量实际上分为 shell 变量环境变量,其区别在于作用域。shell 变量只在当前 shell 中有效,而环境变量则会被传递给子进程。PATH 就是一个环境变量。我们会在 lec2 中详细讲解。

你可以用 setdeclare 命令查看当前所有的 shell 变量,或用 env 查看所有的环境变量。

1
2
3
4
5
6
7
user@debian:~$ set
BASH=/usr/bin/bash
BASHOPTS=checkwinsize:cmdhist:complete_fullquote:expand_aliases:extglob:extquote:force_fignore:globasciiranges:globskipdots:histappend:interactive_comments:patsub_replacement:progcomp:promptvars:sourcepath
BASH_ALIASES=()
BASH_ARGC=([0]="0")
BASH_ARGV=()
BASH_CMDS=()

太多了,我们只列出来了一部分。一些比较有意思的变量是 PATH PWD HOME USER LANG,输出它们的值看看吧。

Shell Scripting

关于 shell 的语法我们已经聊得足够多了,多到有点无聊了。接下来我们很快地用 shell script 的介绍结束这一节。创建一个 hello.sh 文件,写入以下内容:

hello.sh
#!/bin/bash
echo 'Hello, World!'

然后运行它:

user@debian:~$ bash hello.sh
Hello, World!

Shell script 就是这样。第一行的 #!/bin/bash 是一种特殊的注释,叫做 shebang。不过由于我们是用 bash hello.sh 来运行的,所以 shebang 的作用并没有体现出来。我们会在 lec2 中看到 shebang 的真正作用。总是在 shell script 的第一行写上 shebang 是一个好习惯。

接下来该试试用 shell script 完成 A+B 了!

a-plus-b.sh
1
2
3
4
#!/bin/bash
read a
read b
echo $((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。

hello-args.sh
#!/bin/bash
echo "$2 is a kind of $1"

$1$2 是什么意思呢?

1
2
3
4
5
6
7
8
user@debian:~$ bash hello-args.sh fruit apple
apple is a kind of fruit
user@debian:~$ bash hello-args.sh linux distro debian
distro is a kind of linux
user@debian:~$ bash hello-args.sh "linux distro" debian
debian is a kind of linux distro
user@debian:~$ bash hello-args.sh character
 is a kind of character

你可以用 $1 $2 $3 ... 来引用 shell script 的参数。当然,不存在的参数会被替换为空字符串。还有一些别的特殊变量,例如 $0 会是 shell script 的文件名,$# 是参数个数,$@ 是所有参数的列表。同时,注意到那个 "linux distro" 的引号了吗?只有用引号括起来,才会被 shell 认为是一个整体,否则就会在空格处分割。

hello-args.sh
#!/bin/bash
echo "$0: There are $# arguments {$@}"
1
2
3
4
user@debian:~$ bash hello-args.sh 9 8L6  ciallo h0h0 "i can haz space"
hello-args.sh: There are 5 arguments {9 8L6 ciallo h0h0 i can haz space}
user@debian:~$ bash ./hello-args.sh
./hello-args.sh: There are 0 arguments {}

为什么第一行调用是 5 个参数,应该没有问题吧。

还有一点要注意,如果你的参数超过了 9 个,那么你需要用 ${10} ${11} ... 来引用它们(当然,小于 10 的参数也可以用这种方式引用)。$10 会被解释为 $1 后附 0

关于 $@$*

$@$* 表现得几乎一样,都是展开为所有参数。我们会在 lec2 中看到它们的微妙区别。不过在实际应用中,$* 很少使用。你几乎总是应该用 $@

最后要说明的是,用这种方式运行的 shell script 是在一个新的 shell 中运行的,而非在当前 shell 中。这意味着你在 shell script 中定义的变量不会传到当前 shell 中。通过使用 source 命令,你可以在当前 shell 中运行 shell script。实际上 ~/.bashrc 就是这样工作的,这是你的 bash 在启动时会自动执行的 script。以及,source 命令也可以用 . 来代替。

1
2
3
source ~/.bashrc
# 或者
. ~/.bashrc

Homework

简答题

  1. 请解释下面代码中两行 echo 命令的输出分别是如何得到的。

    1
    2
    3
    i=80
    echo $((--i))   # 79
    echo $((--$i))  # 79
    

2.

编程题

限定语言为 bash。

  1. 使用 apt 升级你的系统中所有能升级的软件包。

  2. 写一个参数版本的 A+B。即它应该像这样工作:

    user@debian:~$ bash a-plus-b.sh 8 5
    13
    
  3. 1 英尺等于 12 英寸等于 0.3048 米。写一个 shell script,将厘米数转换为对应的英尺英寸数,只需保留整数。例如:

    user@debian:~$ bash cm2ft.sh 170
    5 ft 6 in
    

    我们已经说过 bash 不支持浮点数运算,那要怎么办呢?实际上不需要使用任何外部工具,只用 bash 的整数运算就足够实现。

  4. 从键盘读入一个三位数,然后输出它的百位、十位和个位数字,以及这个数字的逆序,不带前导零。例如:

    1
    2
    3
    4
    5
    6
    user@debian:~$ bash digit.sh
    123            <-- 这是输入
    1 2 3 321
    user@debian:~$ bash digit.sh
    790            <-- 这是输入
    7 9 0 97
    
  5. 写一个 shell script,接受一个表达式,计算并输出其结果。表达式只包含加减乘除和括号,整体作为一个参数传入。例如:

    1
    2
    3
    4
    user@debian:~$ bash calc.sh '1+2*3'
    7
    user@debian:~$ bash calc.sh '(98 + 7) * (6 - 54 * 3) / 21'
    -780
    

    你并不需要去做任何复杂的表达式解析。

  6. 写一个 shell script,将华氏温度 \( F \) 转换为摄氏温度 \( C \),公式是 \( C = 5/9 \times (F - 32) \)。你的答案应该精确到小数点后两位。例如:

    1
    2
    3
    4
    5
    6
    user@debian:~$ bash f2c.sh 32
    0.00
    user@debian:~$ bash f2c.sh 80
    26.67
    user@debian:~$ bash f2c.sh 313
    156.11
    

    与第 3 题一样,以及别忘了我们有 printf,在此之上你也许还需要一点创意...

如果这些题都没问题,那么你可以自豪地说你已经掌握了 shell 的基础语法。我们会在下一节把 shell 玩出花来。

评论