主题 1 The Shell

课程概览与 shell · the missing semester of your cs education (missing-semester-cn.github.io)

Shell是什么?

一旦你想脱离可视化界面让你做的,然后做点别的事情,那么Shell将是你和计算机交互的最主要的方式之一。

可视化界面受限于,它只能做被设计出来的操作——比如你不能点击一个不存在的按钮或者是用语音输入一个还没有被录入的指令。这就是这门课介绍命令行工具和基于文本的工具的理由,shell则是你去做这些操作的地方。

在Windows和Linux可以找到成堆的终端(Terminal),这些是能显示Shell的文本窗口。其中普遍的是bash,或者叫Bourne Again Shell。由于bash的普遍性,这门课中将使用bash。

使用Shell

终端(Terminal)是你电脑上和shell交互的主要文本界面。

当你打开一个终端,你通常会在终端中看到这样的一行,称为命令行提示符(Shell Prompt)

1
[root@VM-8-17-centos ~]# 

它告诉你,你的主机名是VM-8-17-centos,你的用户名是root,还有你当前所在的路径为~(path)。

可以在终端上执行命令,通常是带着参数(argument)执行程序。参数一般是一些紧随程序名后面的,用空格分开的东西。

  • date

date输入当前日期和时间

1
2
[root@VM-8-17-centos ~]# date
Sat Dec 17 01:04:35 CST 2022
  • echo

echo打印出你传给它的参数

1
2
[root@VM-8-17-centos ~]# echo hello
hello
  • 参数以空格分隔

如上所说,参数是被空格分隔的,如果传递一个多单词的参数,就必须用引号括起来,如:

1
2
[root@VM-8-17-centos ~]# echo "Hello Wrold"
Hello Wrold

这样echo程序会收到一个字符串参数Hello World,中间还有一个空格。此外使用单引号也是可以的。

单双引号的区别将在bash scripting 再说

此外也可以使用转义符将空格转义,如:

1
2
[root@VM-8-17-centos ~]# echo Hello\ World
Hello World

关于如何给参数,变量转义,解析和加括号将在之后涉及

我们在创建目录或文件时,如果某个参数是带空格的,也需要使用引号转义或者用转义符将空格转义,否者shell将会将该参数识别成两个参数。

如下shell将my photo识别成两个参数,创建了两个目录:

1
2
3
[root@VM-8-17-centos ~]# mkdir my photo
[root@VM-8-17-centos ~]# ls
my photo

正确的做法为:

1
2
3
4
[root@VM-8-17-centos ~]# mkdir "my photo"
[root@VM-8-17-centos ~]# ll
total 4
drwxr-xr-x 2 root root 4096 Dec 17 01:23 'my photo'

在Shell中导航

  • 环境变量

你可能会好奇,当输入date或者echo等命令时,Shell怎么知道这些程序要做什么。

你的机器可能内嵌了终端程序,或者某些浏览器。同样的,电脑也内嵌了很多围绕终端工作的程序,这些程序位于你的文件系统(File System),Shell有办法在系统中搜索某个程序,然后执行。

当然,Shell不会在所有文件中进行搜索,那样效率太低了。

Shell借助一个叫做 环境变量(Environment Variable) 的东西来完成搜索。

环境变量就类似编程语言中的变量,Shell或者说bash本身就是一种程序设计语言。你输入的提示符(Prompt)不仅能带参运行程序,你也可以写入while循环,for循环,条件语句等,甚至可以定义函数,甚至变量。关于Shell Scripting的下一讲会有涉及

环境变量是Shell本就设定好的,无论何时打开shell都无需重新设置。一堆东西都会预先设置好,比如哪里是home目录,你的用户名是什么等。

  • PATH变量

如下,当我们执行echo $PATH时,将会输出一些电脑上的目录,这些目录就是Shell寻找程序时所查找的目录。这些目录以冒号分隔。

1
2
[root@VM-8-17-centos ~]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

当你输入一个程序名称时,电脑会在这个列表中的每个目录里,查找名字与你所输入的指令相同的一个程序或者文件。如果在这些目录中可以找到待运行的程序,程序可以正常运行,否则失败。

  • which

如果我们想要知道电脑具体运行了哪一个目录里的程序,可以使用which指令。

1
2
3
4
[root@VM-8-17-centos ~]# which echo
/usr/bin/echo
[root@VM-8-17-centos ~]# which date
/usr/bin/date
  • 路径

路径是用来描述你的计算机里文件位置的东西。

在Linux或者Mac Os上,路径被一连串的斜杠分隔,可以看到上面echo指令的路径起点在根目录/(/ 即整个文件系统的最顶层)

在Windows里,路径以反斜杠\ 而非斜杠/分隔。

在Linux或Mac Os上,所有东西都在一个叫根(root)的空间的下面的某处。因此所有以斜杠开头/的路径都是绝对路径

而在Windows下,每一个分区都有一个根,类似于C:\或者D:\,所以Windows里每一个驱动器(硬盘)下都有独立的一套文件系统的层次结构。

绝对路径:是可以绝对准确地确定一个文件的位置的路径

相对路径:是相对于你当前所在位置的路径

  • pwd

打印工作目录(print working directory)

1
2
[root@VM-8-17-centos ~]# pwd
/root

你可以改变当前工作目录,所有的相对路径都是相对于当前工作目录的

  • cd

change directory 改变当前工作目录

1
2
3
[root@VM-8-17-centos ~]# cd /home
[root@VM-8-17-centos home]# pwd
/home

shell提示只会给路径的最后一段名称,当然也可以通过设置是它总能显示当前的完整路径

  • 特殊的目录 . ..

. 表示当前目录,..表示上一级(父)目录

1
2
3
4
5
[root@VM-8-17-centos lighthouse]# pwd
/home/lighthouse
[root@VM-8-17-centos lighthouse]# cd ../../..
[root@VM-8-17-centos /]# pwd
/

使用相对or绝对路径取决于哪个方便,但是如果有时候你需要运行某个程序或者写一个程序,它调用了类似echo或者date这样的程序,你希望它在哪个地方都能跑起来,要么你就只给出这个要被运行的程序的名字(让shell用path去找出它们在哪里),要么就需要给出绝对路径

一般来说程序默认在当前目录运行

  • ls

输入本级目录下的所有文件信息

1
2
[root@VM-8-17-centos /]# ls
bin boot data dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var

如果给定路径参数,则会输出给定路径目录下的文件信息

1
2
[root@VM-8-17-centos /]# ls home/lighthouse/
dirdemo hello2.txt hello.txt
  • 特殊符号-~

~表示当前用户的home目录

1
2
3
[root@VM-8-17-centos /]# cd ~
[root@VM-8-17-centos ~]# pwd
/root

在cd命令中,-参数表示之前所处的工作目录

1
2
3
4
[root@VM-8-17-centos /]# cd -
/home
[root@VM-8-17-centos home]# cd -
/
  • –help

大多数命令都有一个 –help选项,可以帮助你了解命令的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@VM-8-17-centos /]# ls --help
Usage: ls [OPTION]... [FILE]...
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
-B, --ignore-backups do not list implied entries ending with ~
……
……

比如usage这行信息,[ ]表示这部分内容可填可不填,…表示可以填写多个option或flag

option是有多个参数字符可以选择,flag是只有一个参数字符选择

  • 权限

使用ls -l 命令可以以长列表格式输出当前目录下的文件信息

1
2
3
4
5
[root@VM-8-17-centos lighthouse]# ls -l
total 12
drwxrwxr-x 2 lighthouse lighthouse 4096 Dec 13 00:33 dirdemo
-rw-r--r-- 1 root root 6 Dec 15 19:56 hello2.txt
-rw-rw-r-- 1 lighthouse lighthouse 52 Dec 15 19:59 hello.txt

首先前面带着d的这行条目,代表这是一个目录,例如上面的dirdemo就是一个目录,hello2.txt和hello.txt则是文件。

d后面的字符rwxrwxr-x代表文件被授予的权限。

image-20221222225758192

阅读这一串字符的方法如下:9个字符,每三个一组,分为三组。

第一组:代表权限被授予给了文件的所有者

第二组:代表给拥有这些文件的用户组的权限

第三组:代表给非所有者的其他人的权限

其中 - 表示该用户不具备相应的权限。

同时可以发现所有字符都是有rwx组成的,r(read)表示读取权限,w(write)表示写入权限,x(execute)表示执行权限。这三者的权限使用数字表示:4表示r,2表示w,1表示x。

权限对于文件和目录有不同的解释:

对文件而言,如果你有读取权限,可以读取文件的内容。文件的写权限就是

目录的读取权限可以允许你这个文件夹中有哪些东西(列出这个目录的内容);目录的写入权限是你能否重命名、新建或者删除里面的文件;注意如果你有目录里的文件的写入权限,却没有目录的写入权限,那你就不能删除这个文件(即使你清空了这个文件也不能删除它,因为这要目录的写入权限);最后是目录的执行权限,通常来讲就是搜索权限。这意味着你能不能进入这个目录。

为了进入某个文件夹,用户需要具备该文件夹以及其父文件夹的“搜索”权限(即目录的执行权限)

  • mv

它接受两个路径作为参数,第一个是原有的路径,第二个是新的路径。这意味着mv既可以让你重命名一个文件,又可以让你移动一个文件。

  • cp

copy复制,该命令可以让你复制文件,用法很类似。它也接受两个路径作为参数,复制源路径和目标路径,这些路径要是完整路径(意为着你需要明确指定文件路径,这个命令没有搜索功能)

1
2
3
4
[root@VM-8-17-centos lighthouse]# cp hello.txt ../food.txt
[root@VM-8-17-centos lighthouse]# cd ..
[root@VM-8-17-centos home]# ls
food.txt lighthouse
  • rm

移除(删除一个文件),你可以传递一个路径作为参数。

需要注意默认的移除是非递归的,也就是说你不能rm移除一个目录(因为目录中可能会有文件),你可以传递一个执行递归移除的-r 标识,它就会递归删除目录下的所有内容

  • rmdir

移除目录,同样也是非递归的,这意味着你不能使用该命令删除一个非空目录

  • mkdir

创建一个新目录

  • man

manual pages(手册/说明书),这个程序接受其他程序的名字作为一个参数,然后显示它的说明书。

程序名 --help命令相似。

  • 快捷键Ctrl+L

清空终端,让光标回到顶部(和clear命令相似)

在程序间创建连接

  • 流(Stream)

程序有两个主要的流(stream),默认下程序会有一个输入流(input stream)和一个输出流(output stream)

  1. 默认输入流里的内容来自你的键盘,基本上输入流是终端,无论你向终端输入什么,最后都会传到程序里。

  2. 默认的输出流(即当程序想要输出一些内容时),默认也是终端

    这也是为什么当你在终端中打入echo hello时,hello会直接显示在你的终端里

Shell提供了重定向这些流的方法,把输入和输出都改到程序员指明的地方。最直接的方式就是使用大于小于号(即所谓的尖角括号)。

  1. 小于号表示重定向这个程序的输入流
  2. 大于号表示重定向程序的输出流

例如:

1
2
3
[root@VM-8-17-centos lighthouse]# echo hello > hello.txt
[root@VM-8-17-centos lighthouse]# cat hello.txt
hello

将echo程序输出的内容hello,输入到hello.txt文件中

cat的作用是打印出一个文件的内容,cat同样支持流的重定向。

在这个例子中,Shell就会打开hello.txt,取出它的内容,设置成cat的输入,cat就会把这些内容打印到它的输出流,这里没有重定向,所以cat的输出流还是终端

1
2
[root@VM-8-17-centos lighthouse]# cat < hello.txt
hello

也可以同时使用两种重定向,如

1
2
3
[root@VM-8-17-centos lighthouse]# cat < hello.txt > hello2.txt
[root@VM-8-17-centos lighthouse]# cat hello2.txt
hello

用hello.txt的内容作为cat的输入流,然后把cat输出的所欲内容存到hello2.txt中

  • 双大于号

作用是追加(append)而不是覆写(overwrite)

追加指向文件尾继续添加内容,覆写是清空文件再写入内容

1
2
3
4
5
6
7
[root@VM-8-17-centos lighthouse]# cat < hello.txt > hello2.txt
[root@VM-8-17-centos lighthouse]# cat hello2.txt
hello
[root@VM-8-17-centos lighthouse]# cat < hello.txt >> hello2.txt
[root@VM-8-17-centos lighthouse]# cat hello2.txt
hello
hello
  • 管道符

pipe,管道符就是一个竖线|。管道的意思是,取左侧程序的输出,称为右侧程序的输入。

1
2
3
4
[root@VM-8-17-centos /]# ls -l | tail -n3
drwxrwxrwt. 10 root root 4096 Dec 22 22:03 tmp
drwxr-xr-x. 12 root root 4096 Dec 31 2021 usr
drwxr-xr-x. 20 root root 4096 Dec 31 2021 var

ls的输出作为tail的输入,tail的输出则会输到终端(因为你没有重定向tail的输出)

tail 打印它输入的最后n行

当然也可以重定向tail的输出

1
2
3
4
5
[root@VM-8-17-centos /]# ls -l | tail -n3 > ls.txt
[root@VM-8-17-centos /]# cat ls.txt
drwxrwxrwt. 10 root root 4096 Dec 22 22:03 tmp
drwxr-xr-x. 12 root root 4096 Dec 31 2021 usr
drwxr-xr-x. 20 root root 4096 Dec 31 2021 var

使用管道可以构建一些复杂的命令:

我们可以做一些操作例如

1
2
3
4
5
6
7
8
9
10
11
12
[root@VM-8-17-centos /]# curl --head --silent baidu.com
HTTP/1.1 200 OK
Date: Thu, 22 Dec 2022 16:17:30 GMT
Server: Apache
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
ETag: "51-47cf7e6ee8400"
Accept-Ranges: bytes
Content-Length: 81
Cache-Control: max-age=86400
Expires: Fri, 23 Dec 2022 16:17:30 GMT
Connection: Keep-Alive
Content-Type: text/html

这会给你访问baidu.com的时候所有的HTTP Headers

你可以使用管道将这些输出接到grep -i content-length

1
2
[root@VM-8-17-centos /]# curl --head --silent baidu.com | grep -i content-length
Content-Length: 81

grep 命令支持在输入流里搜索给定的关键字

1
2
[root@VM-8-17-centos /]# curl --head --silent baidu.com | grep -i content-length | cut --delimiter=' ' -f2
81

cut 命令可以接收一个分隔符delimiter,将输入流以分隔符的形式输出,-f设置输出第几个字段

可以发现通过将命令链接起来,你可以做很多文本操作的特技

并且pipe不止用于文本数据,还可以拿来处理比如图片。当你有一个程序可以接收并处理二进制图片,然后输出一个二进制图片的时候,可以像这样把它连进去,你甚至可以这样处理视频。

一个功能全面又强大的工具

  • root用户

在linux和Mac OS中,root用户类似于Windows的管理员(Administrator),有值为0的用户ID。

root用户允许在系统上做任意行为。就算一个文件中任何人都不可读的或者任何人都不可写的,root却可以访问这个文件并且读写。多数情况下,应该使用一个普通用户来操作电脑,因为root具有风险,比如在root下运行了一个错误的程序,可能会毁掉你的整个电脑。

  • sudo

但是如果在普通用户下需要使用root权限操作时,可以使用sudo命令,这可以让你使用超级用户权限运行程序。

sudo的通常用法是,sudo 需要调用的命令

应用场景:

在你的电脑中有很多特别的文件系统,例如sysfs。我们进入到在/sys目录,这个文件系统不是真实存在的文件,相反,这是一堆内核参数。内核(kernel)基本上就是你电脑(操作系统)的核心。

1
2
[lighthouse@VM-8-17-centos sys]$ ls
block bus class dev devices firmware fs hypervisor kernel module power

通过这些像是文件系统的东西,可以访问到内核的参数。

由于这些内核参数是以文件形式展露的,我们可以使用先前的所有工具去操作它们。例如:

你可以在/sys/class/backlight/intel_backlight/下的brightness操作背光亮度

image-20221223021905164

但是如果直接操作,会显示拒绝访问,因为内核的东西基本上都要root权限。

但是如果运行命令sudo echo 500 > brightness,依然显示没有权限

image-20221223022224630

因为输入输出的重定向是程序不知道的,管道和重定向都是Shell设好的,现在的情况是,我告诉Shell去运行sudo,并且包括参数echo 500 ,然后发送输出到brightness这个文件。也就是说,sudo的root权限只给了前面的echo命令。Shell打开brightness的时候,用的不是sudo,因此显示没有权限。

因此,现在的解决方法是:

方法一:切换到root终端,sudo su

su命令能让你以超级用户登录shell

使用超级用户登录后,可以看到提示符从$变成了#。然后运行echo 500 > brightness,屏幕的亮度变暗了,并且没有出现权限不足提示。因为现在Shell以root身份运行,root用户允许打开该内核文件。

image-20221223023516151

方法二:使用管道和重定向

image-20221223024934470

Shell去运行echo 1060,会输出1060,然后告诉Shell去运行sudo tee brightness命令,然后把echo的输出送入tee的输入,然后tee打开brightness文件(tee程序以root权限运行),并将tee的输入流写入到brightness文件和标准输出流(这里是终端)

tee命令取它的输入,然后写入到一个文件,并且写入到标准输出流

tee - read from standard input and write to standard output and files

使用方法二可以毋需登录到root用户。

可以在其他需要root权限的地方法使用这种方法:

例如我现在想让键盘上的滚动锁定灯亮起来,该内核文件在/sys/class/leds/input1::scrolllock/brightness

使用同样的方法,将参数由0变为1

1
2
3
4
5
6
7
8
9
[lighthouse@VM-8-17-centos input1::scrolllock]$ ls
brightness device max_brightness power subsystem trigger uevent
[lighthouse@VM-8-17-centos input1::scrolllock]$ cat brightness
0
[lighthouse@VM-8-17-centos input1::scrolllock]$ echo 1 | tee brightness
tee: brightness: Permission denied
1
[lighthouse@VM-8-17-centos input1::scrolllock]$ echo 1 |sudo tee brightness
1

现在键盘上的滚动锁定灯已经亮起来了

  • 打开文件

xdg-open命令,这个指令可能只在linux上运行,在Mac Os上可能叫做open

你给出一个文件名,然后xdg-open就会使用合适的程序打开它

练习

  1. 本课程需要使用类Unix shell,例如 Bash 或 ZSH。使用echo $SHELL命令可以查看您的 shell 是否满足要求。如果打印结果为/bin/bash/usr/bin/zsh则是可以的。

    1
    2
    [lighthouse@VM-8-17-centos tmp]$ echo $SHELL
    /bin/bash
  2. /tmp 下新建一个名为 missing 的文件夹。

    1
    [lighthouse@VM-8-17-centos tmp]$ mkdir missing
  3. man 查看程序 touch 的使用手册。

    1
    [lighthouse@VM-8-17-centos tmp]$ man touch
  4. touchmissing 文件夹中新建一个叫 semester 的文件。

    1
    [lighthouse@VM-8-17-centos tmp]$ touch ./missing/semester
  5. 将以下内容一行一行地写入

    1
    2
    #!/bin/sh
    curl --head --silent https://missing.csail.mit.edu

    第一行可能有点棘手, # 在Bash中表示注释,而 ! 即使被双引号(")包裹也具有特殊的含义。 单引号(')则不一样,此处利用这一点解决输入问题。更多信息请参考 Bash quoting 手册

    1
    2
    3
    4
    5
    [lighthouse@VM-8-17-centos missing]$ echo '#!/bin/sh' > semester 
    [lighthouse@VM-8-17-centos missing]$ echo "curl --head --silent https://missing.csail.mit.edu" >> semester
    [lighthouse@VM-8-17-centos missing]$ cat semester
    #!/bin/sh
    curl --head --silent https://missing.csail.mit.edu
  6. 尝试执行这个文件。例如,将该脚本的路径(./semester)输入到您的shell中并回车。如果程序无法执行,请使用 ls 命令来获取信息并理解其不能执行的原因。

    1
    2
    3
    4
    5
    [lighthouse@VM-8-17-centos missing]$ ./semester
    -bash: ./semester: Permission denied
    [lighthouse@VM-8-17-centos missing]$ ls -l
    total 4
    -rw-rw-r-- 1 lighthouse lighthouse 60 Dec 30 23:44 semester

    原因是没有执行x权限

  7. 查看 chmod 的手册(例如,使用 man chmod 命令)

  8. 使用 chmod 命令改变权限,使 ./semester 能够成功执行,不要使用 sh semester 来执行该程序。您的 shell 是如何知晓这个文件需要使用 sh 来解析呢?更多信息请参考:shebang

    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
    [lighthouse@VM-8-17-centos missing]$ chmod 764 semester
    [lighthouse@VM-8-17-centos missing]$ ./semester
    HTTP/1.1 200 OK
    Connection: keep-alive
    Content-Length: 7991
    Server: GitHub.com
    Content-Type: text/html; charset=utf-8
    Last-Modified: Mon, 05 Dec 2022 15:59:23 GMT
    Access-Control-Allow-Origin: *
    ETag: "638e155b-1f37"
    expires: Tue, 27 Dec 2022 02:31:08 GMT
    Cache-Control: max-age=600
    x-proxy-cache: MISS
    X-GitHub-Request-Id: 5400:19D5:CB919:12261D:63AA5694
    Accept-Ranges: bytes
    Date: Fri, 30 Dec 2022 15:59:50 GMT
    Via: 1.1 varnish
    Age: 0
    X-Served-By: cache-nrt-rjtf7700066-NRT
    X-Cache: HIT
    X-Cache-Hits: 1
    X-Timer: S1672415990.322601,VS0,VE211
    Vary: Accept-Encoding
    X-Fastly-Request-ID: b5ca5ecd45fb43becb00f6f5b089c1d56b46a765

  9. 使用 |> ,将 semester 文件输出的最后更改日期信息,写入主目录下的 last-modified.txt 的文件中

    1
    2
    3
    [lighthouse@VM-8-17-centos missing]$ ./semester | grep Last > ~/last-modified.txt
    [lighthouse@VM-8-17-centos missing]$ cat ~/last-modified.txt
    Last-Modified: Mon, 05 Dec 2022 15:59:23 GMT
  10. 写一段命令来从 /sys 中获取笔记本的电量信息,或者台式机 CPU 的温度。注意:macOS 并没有 sysfs,所以 Mac 用户可以跳过这一题。