Shell 学习
在计算机科学中,Shell 俗称壳程序,用来区别于操作系统内核程序。Shell 首先是 UNIX/Linux 下的脚本编程语言,它是解释执行的,无需提前编译。同时它也是一个程序,为使用者(用户或其他应用程序)提供与内核交互的操作界面,它接收用户命令,然后通过命令解析调用相应的应用程序。
广义上的 Shell
广义上讲,只要能够为使用者提供与内核交互的都可以认为是 Shell,因此 Shell 可以分为两大类:图形界面 Shell(GUI Shell)与命令行 Shell(CLI Shell)。
图形界面 Shell 应用最广泛的要属 Windows 系列操作系统了,还有 Linux 的 X window manager,以及功能更加强大的 CDE、GNOME、KDE 等。
命令行 Shell 主要包括 MS-DOS 系统下的命令行,Windows NT 下的 cmd.exe,支持 .NET Framework 的 Windows NT 系统下的 Windows PowerShell 以及 UNIX/Linux 系统下的 sh/bash/csh 等。
我们习惯性地把微软旗下所有的操作系统都叫做 Windows,但其实它们是有很大区别的。最初的 Windows 只是一个运行在 MS-DOS 下的图形界面,从 Windows 1.x/2.x/3.x 一直发展到 Windows 9x/ME,期间从 Windows 95 开始,微软划时代地推出了混合的 16 位 / 32 位操作系统,这个时候的 Windows 虽然图形界面是 32 位的,但是操作系统还是 DOS,仍然能够运行 16 位的 DOS 程序。后来微软放弃了 MS-DOS,推出了另一条产品线 Windows NT。早期的 Windows NT 是一种纯 32 位操作系统,这个时候的操作系统除了在加电启动时是运行在 16 位模式(实模式),其他时候都是运行在 32 位模式(保护模式)。虽然 Windows NT 已经告别了 DOS,但还是可以通过模拟来运行 DOS 程序,比如在 Windows 8 中运行 DOS 程序会提示安装 NTVDM 来模拟 DOS 程序运行时的环境。但是由于 64 位的 Windows 不支持 NTVDM,所以也就没有办法运行 DOS 程序了。
那么我们平常口中说的在 Windows NT 中运行 DOS 命令又是什么意思呢?因为在 MS-DOS 中是通过一系列的命令与内核交互,早期和中期的 Windows 都可以执行这些命令,到了 Windows NT,这些命令的使用方式并没有发生变化,于是就沿用了执行 DOS 命令这一说法了,而实际上此时在执行这些命令时使用的只是 Windows NT 提供的一个叫 cmd.exe 的命令行辅助工具,跟 DOS 已经没有关系了。
Linux 下的 Shell
在 Linux 下有很多不同的 Shell,常见的有 sh、bash、csh、tcsh、ash 等,sh 已经被 bash 代替,/bin/sh
往往是指向 /bin/bash
的符号链接。我们可以通过 cat /etc/shells
命令来查看当前系统下可以使用的 Shell 有哪些。
1 | $ cat /etc/shells |
进入 Shell
一种进入 Shell 的方法是退出图形界面模式,进入控制台模式。现代的 Linux 操作系统在启动时会自动创建几个虚拟控制台(Virtual Console,在 Linux 系统内存中运行的虚拟终端),其中一个供图形桌面程序使用,其他的保留原生控制台的样子。例如:CentOS 在启动时会创建 6 个虚拟控制台,分别对应快捷键 Ctrl + Alt + Fn(n=1,2,3,4,5,6)
,其中图形界面模式对应 Ctrl + Alt + F1
。
另一种方式就是使用 Linux 桌面环境提供的终端模拟包(Terminal emulation package),也就是我们常说的终端(Terminal),这样就可以在图形桌面中使用 Shell。
Bash Shell 的操作环境
在登录主机时,屏幕会显示一些说明文字,比如告知我们 Linux 的版本等信息。我们习惯的环境变量、命令别名等在登录后也会被自动配置出来。这些都是 bash 在启动时通过读取环境配置文件来初始化的。
命令搜寻顺序
一个命令下达,它的搜寻顺序如下:
- 以相对或者绝对路径执行命令,比如
/bin/ls
或者./ls
。 - 以 alias 找到该命令执行。
- 由 bash 内建的命令来执行。
- 通过
$PATH
这个变量值的顺序找到第一个命令来执行。
bash 的环境配置文件
在系统中有一些环境配置文件,在 bash 启动时会去读取这些文件来初始化 bash 的操作环境。而 shell 又有 login shell 和 non-login shell,不同的 shell 读取的配置也是有区别的。
login shell
login shell 指的是在获取 bash 时进行了完整的登录流程。比如,由 tty1 ~ tty6 登录,通过输入用户名和密码成功进入 bash。
一般来说,login shell 只会读取两个文件,一个是 /etc/profile
,它是系统整体的配置文件;另一个是用户的配置文件。
在 /etc/profile
文件中,它会根据用户的 UID 去配置 PATH、MAIL、USER、HISTSIZE 等变量的值,然后设置用户的 umask 的值,最后读取 /etc/profile.d/*.sh
中的一系列文件。
在读取完系统配置文件后,接下来就会去读取用户的配置文件。用户的配置文件按照读取的顺序分别为:~/.bash_profile
、~/.bash_login
和 ~/.profile
,login shell 只会读取其中的某一个。如果在用户目录下存在 .bashrc
文件,则会读取该文件。
source
由于配置文件的内容是在取得 login shell 的时候才会读取,因此如果后续修改配置文件,就需要注销后重新登录才能生效。如果不想重新登录,可以使用 source
命令重新读取配置文件。
1 | # 重新读取配置文件 |
non-login shell
non-login shell 是指在获取 bash 时没有进行登录流程。比如在图形桌面中使用 Ctrl + Alt + T
启动的 shell 就是 non-login shell,在使用 su
命令切换用户时,不加 --login
参数(使用 su - 用户名
或者 su --login 用户名
的方式获取到的是 login shell)获取到的也是 non-login shell。
一般来说,non-login shell 只会读取 ~/.bashrc
,在该文件中除了会应用使用者的个人配置外,还会呼叫外部配置文件 /etc/bashrc
(Red Hat 系统特有的),然后根据用户的 UID 设置 umask 和 PS1 的值,同时还会读取 /etc/profile.d/*.sh
中的一系列文件。
其他配置文件
还有一些配置文件也会影响 bash 环境,比如 ~/.bash_logout
就记录了当注销 bash 后系统将会执行的动作,默认情况下只是清除屏幕信息,当然我们可以自定义其他的动作。
内建命令
bash 有很多的内建命令,这些命令可以在 bash 中直接使用。通常来说,内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘 I/O,还需要 fork 出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前 Shell 进程的一个函数。要判断一个命令是不是内建命令,一种方式是通过 Linux 的联机帮助文件查看,比如使用 man cd
查看 cd
这个命令的说明文档。还有一种方式是通过 type
这个内建命令查询。比如 ls
命令,则可以使用 type ls
来查看该命令是否是内建命令。
命令替换
命令替换是指将命令的输出结果作为值赋给某个变量,在写法上有两种方式,一种是使用反引号,一种是使用 $()
。
1 | DATE_1=`date` |
如果被替换的命令的输出内容包括多行(即有换行符),或者含有多个连续的空白符,那么在输出变量时应该将变量用双引号包围,否则系统会使用默认的空白符来填充,这会导致换行无效,以及连续的空白符被压缩成一个,比如:
1 | # 执行 ls -l |
需要注意的是,使用 $()
相对清晰,且支持嵌套,但是 $()
仅在 bash 环境下有效。而反引号由于看起来与单引号类似,可能会对使用者造成困扰,但是反引号可以在多种 shell 环境中使用。
命令别名
alias
命令用来给命令创建别名,如果直接下达该命令不带任何参数,则会列出 shell 环境中使用的所有别名。
1 | # 设置别名 |
如果想要别名永久生效,需要把别名写入到用户目录下的 .bashrc
文件中。
历史命令
历史命令记录的笔数与环境变量 HISTFILESIZE
的配置有关。
1 | # 列出当前内存中的所有 history 记录 |
资源限制
系统的可用资源是有限的,如果不限制用户和进程对系统资源的使用,则很容易造成资源耗尽。使用 ulimit
命令可以控制进程对可用资源的访问。
默认情况下 Linux 系统的各个资源都做了软硬限制,其中硬限制的作用是控制软限制(即软限制不能高于硬限制)。
1 | # 查看当前系统的软限制 |
1 | # 设置最大可以打开的文件数 |
使用 ulimit
命令可以修改资源的限制,但是当用户注销后会失效。如果想永久生效,则需要修改 /etc/security/limits.conf
文件。
变量
在 bash 中定义变量不需要指明变量的类型,每个变量的值都是字符串,即使在赋值时没有使用引号,它们也会被视为字符串。当然,在实际解释执行的过程中,如果需要进行算术运算,则会自动将值转化为整数(bash 不支持浮点类型的运算,除非借助于第三方工具)。
变量声明及赋值
变量命名时只能使用字母、数字和下划线,首字符不能以数字开头,同时不能使用 bash 里的关键字,比如:
1 | # 定义变量 your_name |
变量的测试与替换
有时候我们需要判断某个变量是否存在,并根据判断的结果进行选择性地赋值。
参数设置表达式 | str 没有设置 | str 为空字符串 | str 为非空字符串 |
---|---|---|---|
var=${str-expr} | var=expr | var= | var=$str |
var=${str:-expr} | var=expr | var=expr | var=$str |
var=${str+expr} | var= | var=expr | var=expr |
var=${str:+expr} | var= | var= | var=expr |
var=${str=expr} | str=expr, var=expr | str 不变, var= | str 不变, var=$str |
var=${str:=expr} | str=expr, var=expr | str=expr, var=expr | str 不变, var=$str |
var=${str?expr} | expr 输出至 stderr | var= | var=$str |
var=${str:?expr} | expr 输出至 stderr | expr 输出至 stderr | var=$str |
使用变量
使用一个定义过的变量,只要在变量名前面加上 $
符号即可。
1 | your_name="Saber" |
变量名外可以不使用花括号,加上花括号是为了帮助解释器识别变量的边界,比如:
1 | for skill in Ada Coffe Java;do |
只读变量
使用 readonly
命令可以将变量定义为只读的,只读变量的值不能被修改。
1 |
|
删除变量
使用 unset
命令可以删除变量,已经删除的变量无法再次使用,该命令无法删除只读变量。
1 | myUrl="https://nekolr.com" |
变量的作用域
bash 中的变量根据作用范围划分共有三种,包括局部变量、全局变量和环境变量。
- 局部变量,只能在函数内部有效。使用
local
命令定义局部变量,该命令只能在函数中使用。如果在函数中没有使用该命令定义变量,那么变量会如同在 JavaScript 里没有使用var
定义变量一样是全局变量。 - 全局变量,在当前 shell 进程中都可以使用,一般在 shell 中定义的变量,默认都是全局变量。
- 环境变量,可以在当前进程或者子进程中使用。
环境变量
全局变量只能在当前 shell 进程中使用,如果使用 export
命令将它导出,那么它就可以在所有的子进程中使用了,这时它也就变成了环境变量。
当登录 Linux 并取得一个 bash 之后,该 bash 就作为一个独立的进程存在。接下来在这个 bash 底下所下达的任何命令都会执行成为这个 bash 的子程序。子程序会继承父程序的环境变量,但是不会继承父程序的自定义变量。
需要注意的是,在父 shell 中创建的环境变量可以在所有的子程序中使用,但是如果使用终端创建一个新的 shell,那么它就不是当前 shell 的子进程,当前 shell 的环境变量对它也就无效了。
我们可以使用 env
命令查看当前 shell 环境下的所有环境变量,比如以下就是常见的环境变量。
变量 | 含义 |
---|---|
HOME | 表示用户的根目录。当我们使用 cd 或者 cd ~ 命令时,取用的就是这个变量。 |
SHELL | 表示当前 shell 环境使用的是什么 shell 程序,比如 Linux 默认使用的是 /bin/bash 。 |
当我们使用 mail 这个命令收信时,系统会去读取的邮件信箱文件。 | |
PATH | 运行文件时搜寻的路径,目录与目录中间以冒号分隔。 |
LANG | 表示系统使用的语言。一般默认是 en_US.UTF-8 |
通过 export 变量名
的形式导出的是临时环境变量,当 shell 会话注销后会失效。如果想永久生效,一种是对当前用户永久生效,这种需要在当前的用户目录下的 .bash_profile
文件中新增变量并导出。另一种是对所有用户永久生效,这种则需要在 /etc/profile
文件中新增变量并导出。最后再使用 source 文件名
的方式通知当前 shell 读取并执行修改后的文件,使配置立即生效。
bash 操作接口有关的变量
bash 中不光有环境变量,还有一些与 bash 操作接口有关的变量,以及用户自定义的变量,这些变量可以使用 set
命令查看,其中有几个比较重要的变量。
**PS1
**,当我们每次按下 [Enter] 按键去运行某个命令后,最后要再次出现提示字符时,就会主动去读取这个变量值。每个 distributions 的 bash 默认的 PS1 变量内容可能有些许的差异。举例来说,CentOS 下 PS1 的值为:'[\u@\h \W]\$ '
,对应的命令提示符可能为:[root@VM_59_13_centos ~]
,具体可以通过 man bash
命令查找这些符号的说明。
$
代表的是当前这个 shell 的进程号,亦即是所谓的 PID (Process ID)。使用 echo $$
命令可以查看当前 shell 的 PID。
?
代表的是上一个运行的命令所回传的值。当我们运行某些命令时,这些命令都会回传一个运行后的代码。一般来说,如果成功的运行该命令,则会回传一个 0 值;如果运行过程发生错误,就会回传错误代码,一般以非为 0 的数值表示,比如:
1 | $ ehco $SHELL |
特殊变量
变量 | 含义 |
---|---|
$0 |
当前脚本的文件名。 |
$n (n≥1) |
传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2。 |
$# |
传递给脚本或函数的参数个数。 |
$* |
传递给脚本或函数的所有参数。 |
$@ |
传递给脚本或函数的所有参数。当变量被双引号包含时,$@ 与 $* 稍有不同。比如传递了 3 个参数,那么对于 "$*" 来说,这 3 个参数会合并到一起形成一份数据,它们之间是无法分割的;而对于 "$@" 来说,这 3 个参数是相互独立的,它们是 3 份数据。 |
$? |
上个命令的退出状态,或函数的返回值。0 表示没有错误,其他任何值表示有错误。 |
$$ |
当前 Shell 进程 ID。对于 Shell 脚本来说就是这些脚本所在的进程 ID。 |
使用一个例子说明这些参数。编写脚本,并保存为 test.sh
:
1 |
|
接下来运行该脚本。
1 | $ chmod +x ./test.sh |
字符串
Shell 中的字符串可以用单引号或者是双引号表示,但是这两种方式是有区别的。
使用单引号字符串,字符串里的内容都会原样输出,在里面使用变量是无效的;而使用双引号,则可以在里面使用变量,同时也支持转义。
1 | your_name='Alice' |
拼接字符串
1 | your_name='Alice' |
获取字符串长度
1 | your_name="Alice" |
字符串切片
具体的格式为:${string: start: length}
。这里需要注意的是,有从左或者从右两种计数方式,如果从左计数,则从 0 开始;如果从右开始计数,则从 1 开始。不管从左还是从右计数,截取始终是从左向右。
1 | your_name="Alice" |
复杂的,还可以根据指定的子串进行截取。
表达式 | 说明 |
---|---|
${变量#关键词} | 若变量内容从头开始的数据符合关键词,则将符合的最短数据删除 |
${变量##关键词} | 若变量内容从头开始的数据符合关键词,则将符合的最长数据删除 |
${变量%关键词} | 若变量内容从尾向前的数据符合关键词,则将符合的最短数据删除 |
${变量%%关键词} | 若变量内容从尾向前的数据符合关键词,则将符合的最长数据删除 |
${变量/旧字符串/新字符串} | 若变量内容中包含旧字符串,则变量中第一个旧字符串会被新字符串取代 |
${变量//旧字符串/新字符串} | 若变量内容中包含旧字符串,则变量中全部的旧字符串会被新字符串取代 |
1 | url="https://nekolr.com" |
大小写转换
1 | your_name='Alice' |
数组
bash 支持一维数组,但不支持多维数组,数组的大小没有限制。
定义数组
1 | arr=("Java" "Python" "Ruby" "Go") |
定义数组无需给每个元素赋值,可以只给特定位置的元素赋值。
1 | # 数组的长度为 3 |
读取数组
1 | arr=("Java" "Python" "Ruby" "Go") |
获取数组长度
获取数组长度的方法与获取字符串长度的方法一样。
1 | arr=([3]="Java" [5]=19 [11]=33) |
删除数组元素
在 shell 中使用 unset
删除数组元素。
1 | arr=([3]="Java" [5]=19 [11]=33) |
数组拼接
1 | arr1=(23 55) |
变量宣告
bash 中的变量默认都是字符串类型,如果需要非字符串类型的变量,就需要通过 declare
或 typeset
命令进行变量宣告,即声明变量的类型。
1 | # 定义数组 |
键盘读取
使用 read
命令能够读取键盘输入的内容。
1 | # 参数 p 后面跟着提示信息 |
数据流重导向
数据流重导向就是将某个命令执行后要出现在屏幕上的数据(standard output 和 standard error output)传输到其他地方,例如:文件、打印机等。
标准输入和输出
名称 | 代码 | 使用 |
---|---|---|
标准输入(stdin) | 0 | 使用 < 或 0< 覆盖,<< 或 0<< 追加。 |
标准输出(stdout) | 1 | 使用 > 或 1> 覆盖,>> 或 1>> 追加。 |
标准错误输出(stderr) | 2 | 使用 2> 覆盖,2>> 追加。 |
1 | # 使用一般用户账号搜寻 /home 底下是否有 .bashrc 文件 |
这里需要注意的是,cmd > file 2>&1
中的 &
表示重定向的目标不是某个文件,而是一个文件描述符,换句话说就是 2>&1
表示将 stderr 重定向到文件描述符为 1 的文件(即 /dev/stdout,这个文件就是 stdout 在文件系统中的映射)中,而 &> file
是另一种写法。
垃圾桶
如果提前知道有错误信息会输出,想要将错误信息忽略掉或者不存储时,可以使用 /dev/null
文件。该文件是一个特殊的文件,写入到它的内容都会被丢弃,如果尝试读取它的内容,那么什么也读不到。
1 | # 将错误的数据丢弃,屏幕只会显示正确的信息 |
命令运行的判断依据
很多时候我们想要一次运行很多条命令,除了使用 shell 脚本,另一种方式就是使用多重命令。
在不用考虑命令的相关性时,连续下达命令。使用 cmd;cmd
的形式,比如:
1 | # 在关机前执行两次同步写入磁盘 |
如果一个命令的运行需要另一个命令先执行,则可以使用 cmd && cmd
或者 cmd || cmd
的形式下达命令。
命令 | 说明 |
---|---|
cmd1 && cmd2 | 如果 cmd1 运行完毕且正确执行(命令回传 $? 的值为 0),则执行 cmd2;否则不执行 cmd2 |
cmd1 || cmd2 | 如果 cmd1 运行完毕且正确执行,则 cmd2 不执行;否则执行 cmd2 |
管线命令
如果命令输出的内容必须经过处理才能得到我们想要的格式,那么此时就需要用到管线命令(pipe)。需要注意的是,管线命令与连续下达命令是不一样的。
管线命令使用的是 |
这个界定符号,它只能处理前面的命令传递过来的 stdout。每个管线后面的第一个数据必定是命令,而且这个命令必须能够接受 stdin 的数据。比如:less
、more
、head
、tail
等命令可以接受 stdin 的数据,而 ls
、cp
、mv
等就不是管线命令了。
cut
cut
命令主要用来切割文件内容。
1 | # 参数 d 代表的是分隔符,参数 f 代表的当使用分隔符切割后的第几段 |
grep
grep
命令主要用来筛选匹配的数据。
1 | # 根据关键字筛选 |
sort
sort
命令主要用来对数据进行排序。
1 | # 按字典顺序排序 |
uniq
uniq
命令主要用来去重。
1 | # 排序完成后,将重复的数据仅显示一份 |
wc
wc
命令主要用来统计文件的行数、字符数等信息。
1 | # 列出文件中有多少行、多少字(英文单字)、多少字符 |
tee
tee
命令会同时将数据流发送到文件和屏幕,而输出到屏幕的,其实就是 stdout,因此后面可以继续使用管线命令。
1 | # 将 last 命令的输出保存一份到文件中 |
tr
tr
命令可以用来删除或替换一段数据中的某些内容。
1 | # 将所有内容小写转大写 |
col
col
命令可以用来将内容中的 Tab 转换为对等的空格。
1 | # 将文件中的 Tab 转换为空格 |
参考
《鸟哥的Linux私房菜》