Linux Shell 学习笔记(一)

1 shell 简介

shell 是操作系统提供的一个用户界面,隐藏了操作系统底层的实现细节。通过 shell,使得用户能够方便地控制作业的执行,与内核进行通信,进而安全地使用硬件资源。

可以通过如下命令查看当前所用的 shell: echo $SHELL

现在最常用的 shell 有 bashzsh,和 fish

除此之外,shell 还是一种编程语言,用它可以编写 shell 脚本文件。

2 交互式 shell 与非交互式 shell

2.1 交互式 shell

就是能够进行人机交互的 shell。

比如说,

  • 点击终端图标打开的就是一个交互式 shell;
  • ssh 远程登录打开的也是一个交互式 shell;
  • 在交互式 shell 中执行 bash 等 shell 可执行文件时,打开的也是一个交互式 shell。

2.2 非交互式 shell

就是不能够进行人机交互的 shell。

比如说,

  • 以图形界面运行 Linux 操作系统,当输入完用户名密码并验证通过后,就会打开一个 shell。这个 shell 是看不到的,是一个非交互式 shell;
  • 当直接执行一个 shell 脚本文件时,打开的也是一个非交互式 shell;
  • 当将一个 shell 脚本文件作为 bash 等 shell 可执行文件的参数执行时,打开的也是一个非交互式 shell。比如: bash 脚本文件
  • 当将一个命令字符串作为 bash 等 shell 可执行文件的参数执行时,打开的也是一个非交互式 shell。比如: bash -c 'ls'

2.3 通过 $- 来判断

执行如下命令: echo $-

  • 如果打印出来的字符串中包含 i,则表示当前打开的 shell 是一个交互式 shell;
  • 否则,是个非交互式 shell。

3 登录 shell 与非登录 shell

3.1 登录 shell 及其修改

/etc/passwd 文件中的每一行代表一个用户的相应登录信息,一行中的最后一个字段是一个 shell 名字,代表的就是该用户的 登录shell

如果某个用户的登录 shell 为设置为 /bin/bash 或者 /bin/zsh,代表该用户登录时使用的 shell 为 bash 或者 zsh。如果使用的是 /sbin/nologin 或者 /usr/bin/false,代表该用户无法登录(比如,有很多系统帐号只会使用系统资源,都不需要登录(比如 apache 帐号,lp 帐号等),此时就可以将它们的登录 shell 设为这个)。

所谓的登录 shell,事实上就是在用户登录随即打开的 shell,或者通过设置 –l 参数打开的 shell。

比如说,

  • 以图形界面运行 Linux 操作系统,当输入完用户名密码并验证通过后,就会打开一个 shell,这个 shell 就是一个登录 shell;
  • ssh 远程登录时,在 login 程序验证完用户名和密码后会打开一个 shell,这个 shell 也是一个登录 shell;
  • 运行 shell 可执行文件时通过加上 -l 参数而打开的 shell 也是一个登录 shell。

可以通过 chsh -s shell路径 来修改当前用户的登录 shell。

3.2 非登录 shell

事实上就是登录 shell 的 shell 子孙进程。

比如说,

  • 点击终端图标打开的就是一个交互式 shell;
  • 在交互式 shell 中执行 bash 等 shell 命令时,打开的也是一个交互式 shell;
  • 当直接执行一个 shell 脚本文件时,打开的是一个非交互式 shell;
  • 当将一个 shell 脚本文件作为 bash 等 shell 可执行文件的参数执行时,打开的也是一个非交互式 shell。比如: bash 脚本文件
  • 当将一个命令字符串作为 bash 等 shell 可执行文件的参数执行时,打开的也是一个非交互式 shell。比如: bash -c 'ls'

3.3 两者的区别

两者的区别在于所读取的配置文件不一样。具体地:

  • 登录 shell (交互式和非交互式) 首先会读取 /etc/profile (会自动读取 /etc/bash.bashrc),然后依次查找 /.bash_profile/.bash_login~/.profile(会自动读取 ~/.bashrc 文件)这三个配置文件,读取这三个中第一个存在且可读的文件(找到一个满足的要求的就不再往后继续查找了)。
  • 交互式非登录 shell 启动时则会依次读取 /etc/bash.bashrc~/.bashrc 文件。
  • 非交互式非登录 shell 启动后则会读取 BASH_ENV 环境变量。

可见,一般情况下如果想添加环境变量,则可以在 ~/.bashrc 中编辑。

另外,这里介绍的只是 bash 的情况。对于 zsh 详见:Zsh Wiki - Startup Files

3.4 判断方式

如果当前打开的 shell 的第一个命令行参数 $0 的值的第一个字符是 -,或者是通过参数 -l 打开的,则当前打开的 shell 是一个登录 shell。

对于后者。比如说当切换至 root 用户时,如果设置参数为 -l,则表示打开的 shell 是一个登录 shell。即: su -l

再者,如果在运行 shell 可执行文件时,若设置参数为 -l,则表示打开的 shell 也是一个登录 shell。即: bash -l

只要是登录 shell,则可以通过执行 logout 命令来退出它们,而非登录 shell 的退出不能用这个命令进行。

当然,退出登录 shell 也可以用 exit 或者 ctr + d 此时会自动执行 logout 命令(会打印出 logout 字符)。

4 shell 内置命令与外部命令以及 shell 脚本程序

所谓的 shell 内置命令,指的是那些 shell 提供的命令,它们内置在 shell 之中。在启动 shell 时,就已经随着 shell 主程序读入到了内存之中。因此在执行内置命令时,就在当前 shell进程 中执行,不会建立子进程运行这些命令。

而外部命令就是其他非 shell 提供的命令,在运行的时候才会调入到内存之中,且作为一个子进程运行。

除了可以用: which command 来查看一个命令是否是内置命令之外,还可以用: type command

可以通过 shell 语法来编辑为一个 shell 脚本程序,其中可以调用内置命令和外部命令。在执行 shell 脚本程序时,一般也是作为一个子进程运行。

5 特殊字符

shell 中的字符串中的特殊字符有:空格,\,",',\$。

一般情况下需要注意以下几点:

  • 一个赋值表达式的左部为变量名,右部为一个字面值或者一个变量的值。若是一个变量的值,则用 $变量 取值。
  • 字符串两边可以不用加双引号或者单引号。此时字符串中的所有特殊字符均需要用 \ 转义。
  • 字符串还可以用双引号或者单引号扩起来,这样的话,字符串中的特殊字符就不需要转义了。 另外,双引号中可以用 $ 符号取变量的值作为字符串的一部分,而单引号中的 $ 则不能用作取值。 当然,如果双引号括起来的字符串中有双引号,需要用 \ 进行转义;但是如果单引号括起来的字符串中有单引号,是不能用 \ 进行转义的,必须使用 '"'"'

    所以,大部分时候最好是用双引号来代表一个字符串。

  • \ 可以用于行尾,表示一个语句还没结束,下行中还有该语句的其余部分。
  • shell 中的表达式如果非必须要求,则不要加多余的空格。比如 = 号两边。
  • 可以用 $( command line ) 或者 `command line` 来获取命令执行的结果(实质上是写入标准输出的结果)。其中 ` 为键盘左上角的那个符号,而非单引号。
  • 通常大写字母为系统定义的变量,用户定义的环境变量与普通变量通常小写,以示区分。

6 echo, print 与 printf

  • echo 用于打印变量内容到屏幕。
  • print 功能与 echo 相同。
  • printf 格式化一个字符串并打印到屏幕。比如:

    1
    2
    3
    myname='nju'
    # myname: nju
    printf 'myname: %s\n' $myname

7 普通变量与环境变量

7.1 普通变量

定义一个普通变量或改变一个普通变量的值: 变量=值

删除普通变量: unset 变量

7.2 环境变量

每个进程都有自己的环境变量,作为进程地址空间的一部分。

常见的环境变量有:

  • HOME。表示用户主目录的路径。
  • PATH。表示搜索命令的目录列表(多个目录以 : 分开)。
  • PS1。一级命令提示符,通常是 用户名 主机名 当前目录名 $
  • PS2。二级命令提示符,通常是 >
  • $0。也即第一个参数,为所正在运行的 shell 或者 shell 脚本的名字。
  • $$。所使用 shell 环境或者执行的 shell 脚本的进程号(并不能得到当前 shell 环境或者 shell 脚本的子进程的进程号)。
  • $!。当前进程的上一个后台子进程的进程号。
  • ?。上一个命令的执行后返回的状态。如果成功执行完,则一般返回 0;否则返回非 0。
  • !!。上一条命令字符串。

可以通过如下形式定义一个环境变量: export 变量=值

将一个普通变量转换为环境变量: export 变量 或者 declare -x 变量

将一个环境变量转换为普通变量: export -n 变量 declare +x 变量

删除环境变量: unset 变量

显示当前 shell 进程的所有环境变量: export -p

7.3 shell 子进程与环境变量

在一个 shell 解释器中,如果执行 bash 或者 zsh 等 shell 可执行文件,或者执行 shell 外部命令或者其他可执行文件,都会先从当前 shell 进程 fork 出一个 shell 子进程(拷贝进程地址空间),然后再在 shell 子进程中执行它们(替换进程地址空间)

在 fork 的过程中,环境变量(以及命令行参数)作为进程地址空间的一部分,也会被拷贝到 shell 子进程中。

然后再在 shell 子进程中执行外部命令或可执行文件时,虽然会替换掉进程地址空间中基本所有内容,但是通常都会保留其中的环境变量。

只针对环境变量,不针对普通变量。

另外,当通过点击终端图标,或者在交互式 shell 中执行 bash 等 shell 可执行文件时,都会打开一个新的 shell。它们首先会将当前 shell 进程的环境变量拷贝到 shell 子进程中,然后再读取 ~/.bashrc 或者 /.zshrc 文件预先设置好的环境变量(可能会发生覆盖)。

7.4 执行命令或可执行文件的几种常用方式

  • 在交互式 shell 中执行内部命令。比如: cd 路径 相当于是直接在当前 shell 进程中调用其内部的一个接口(已经存在于其地址空间中),因此不会产生 shell 子进程。
  • 在交互式 shell 中执行外部命令。比如: ls
  • 在交互式 shell 中运行可执行文件: 文件路径 如果执行的是 bash 等 shell 可执行文件,那么新打开的 shell 仍然是一个交互式 shell。
  • 在交互式 shell 中将外部命令作为 shell 可执行程序的一个字符串参数运行。比如: bash -c 'echo $0, $1' arg_0 arg_1 会先 fork 出一个 shell 子进程,然后在其中执行 bash 可执行文件,从而打开一个新的 shell(非交互式 shell)。紧接着分析传入进来的命令字符串,然后在其中执行这条命令(不会再 fork 出子进程)。
  • 在交互式 shell 中直接运行 shell 脚本文件(脚本文件中第一行必须为 #!/usr/bash 之类的语句来指明所要使用的解释器,且这个文件必须具有相应的可执行权限): 文件路径 如果指定 bash 作为解释器,那么相当于是先 fork 出一个 shell 子进程,然后在其中执行 bash 可执行文件,从而打开一个新的 shell(非交互式 shell)。而 bash 会读取并解释这个 shell 脚本文件。
  • 在交互式 shell 中将 shell 脚本文件作为 shell 可执行程序的参数运行。比如: bash 文件路径 会先 fork 出一个 shell 子进程,然后再在其中执行 bash 可执行文件,从而打开一个新的 shell(非交互式 shell)。而 bash 会读取并解释这个 shell 脚本文件。

    Python 也有类似的两种运行方式。

8 set 命令

set 命令用来设置和显示当前 shell 进程环境。

8.1 -o

可以查看当前 shell 解释器的一些属性(非环境变量)及其状态 (off 或者 on): set -o 然后就可以对某个属性进行相应设置。

比如 errexit 这个属性,如果将它设置为 on(默认情况下它被设置为 off),则在当前 shell 进程中执行命令或者 shell 脚本或者其他可执行程序时,一旦发生错误导致返回给当前 shell 进程的状态码非 0,则会自动终止当前 shell 进程。

可以通过如下命令将一个属性设置为 on 状态: set -o 属性名

通过如下命令将一个属性设置为 off 状态: set +o 属性名

8.2 -m

set -m 可以用来启用作业控制。这在 shell 脚本里很常用,比如用于 trap 子进程的 SIGCHLD 信号。

9 env 命令

可以用来显示当前 shell 进程的所有环境变量: env

还可以用来在一个自定义环境变量中运行命令。

在一个不含任何环境变量的 shell 子进程中运行命令: env -i 命令

可以通过如下命令来展现 -i 参数的作用: env -i env

在去除掉某些环境变量后的 shell 子进程中运行命令: env -u 环境变量 命令

在改变某些环境变量后的 shell 子进程并运行命令: env 环境变量=值 命令

10 alias 与 unalias

可以利用 alias 为一个命令定义别名。它和环境变量一样,在某个 shell 进程中定义的别名都可以在子进程及其子孙进程中所使用。

同样和环境变量一样,如果在某个 shell 子进程中定义了一个命令的别名,那么这个子进程结束掉之后,它的父进程中是不会看到这个别名的。也就是说,即使子进程中有一个和父进程同名的命令别名,当子进程结束时,父进程中的这个同名的变量别名对应的实际命令并不会因为子进程中对这个变量别名的操作而产生任何变化。

查看当前 shell 进程中的所有变量别名: alias

设置变量别名: alias myrm="rm -i"

取消变量别名: unalias myrm

11 路径与命令查找顺序

  • 先根据命令提供的路径进行查找;
  • 然后在 alias 中查找;
  • 然后在 shell 内置命令 中查找;
  • 然后通过 $PATH 中的路径按顺序查找。

12 source 命令和 . 命令

使用方式为: source command 或者 . command

一般情况下,当执行一个脚本程序时会创建一个子进程,且其运行在新的环境中。而 source 命令或者 . 命令(两者用法一样,没什么太大区别)允许脚本程序在当前 shell 进程的环境中执行,而不会 fork 出 shell 子进程。

因此,这两条命令的通常用于通过执行一个脚本程序来改变当前 shell 进程的环境。

这两条命令只适合于 shell 脚本程序,不适合 shell 命令,更不适合其他可执行程序。

另外,当通过这两条命令执行完脚本程序后,原 shell 进程继续执行。

13 exec 命令

使用方式为: exec command

exec 命令与 source 命令或 . 命令类似,当用它执行一个外部命令或 shell 脚本程序或其他可执行程序时,不会 fork 出 shell 子进程。

但是它与后两者不同的是,它将 当前shell 进程地址空间替换为所要执行的外部命令或者 shell 脚本程序或其他可执行程序的地址空间。这意味着,当执行它们后,该 shell 进程会终止运行。

14 trap 命令

类似于 C 中的 signal 函数,用来注册信号处理函数。使用方法如下: trap "echo end" SIGINT

如果是在 shell 脚本中使用,则最好在此之前加上 set -m,使该 shell 脚本启用作业控制。例如:

1
2
set -m
trap "echo end" SIGCHLD

15 通配符与其他特殊符号

前面介绍的特殊字符是在变量赋值时的字符串中要注意的特殊字符,而这里说的通配符和其他特殊字符则是在命令中常用到的。

通配符:

  • *****。代表 0 个或多个任意字符;
  • ?。代表任意一个字符;
  • []。代表中括号内列举出的字符中的任意一个字符,比如 [abc];
  • [-]。代表某一个范围内的所有字符中的其中一个字符,比如 [a-z];
  • [^]。反向选择,代表中括号中,^ 符号之后的的所有字符之外的任意一个字符,比如 [^a-z] 表示非小写字母外的任意一个字符。

注意:通配符并不是正则表达式里边的元字符。

命令中用到的特殊符号:

  • 空格。用于分隔参数。因此若一个文件名中含有空格时,需要用 \ 进行转义,将其作为一个普通字符;
  • |。管道;
  • ;。命令结束符。通过它可以连续地输入多个命令,相邻两个命令间用 ; 间隔,所有命令按先后执行;
  • ~。用户主目录;
  • $。取变量的值;
  • $()``。取命令的输出;
  • &。放在一个命令之后,表示此命令在后台运行。还可以在其后跟上一个无符号整数,表示一个文件描述符,比如 &1 表示标准输出;
  • >>>。输出重定向;
  • <<<。输入重定向;
  • ''。表示一个字符串,如果里面有 $,则它不能取一个变量的值。可以对通配符转义,将其当做一个普通字符;
  • ""。表示一个字符串,如果里面有 $,则它能取一个变量的值。可以对通配符转义,将其当做一个普通字符;
  • -。很多命令中,用 - 来代替一个文件,表示标准输入和标准输出,比如:vim -。
  • ()。里面为一些由 ; 号连接的命令组合。表示建立一个子 shell,然后在子进程中按先后顺序执行命令,执行期间只会改变子 shell 的环境变量,不会改变当前 shell 的环境变量。在该子进程执行完成后再继续执行父进程

    另外,在加括号的情况下会将这些命令组合成一个整体,这时就方便用重定向将它们的打印信息全部写入到文件中了。比如: (ls;pwd)>text.out 如果不加括号,那只会将最后一个命令的信息写入到文件中。

    另外,在用 &&|| 连接多个命令时,它还可以用来指定命令执行的优先级。比如:ls ---f || (pwd && which ps)
  • {}。里面为一些由 ; 号连接的命令组合。在当前 shell 中按先后顺序执行命令,执行期间会改变当前 shell 的环境变量。

    另外,在加花括号的情况下会将这些命令组合成一个整体,这时就方便用重定向将它们的打印信息全部写入到文件中了。比如: {ls;pwd}>text.out 如果不加花括号,那只会将最后一个命令的信息写入到文件中。

    可以通过执行 (cd ..;pwd){cd ..;pwd} 进行测试。

    花括号还可以用作枚举列表,比如: vim doc_{a,b}.txt

  • &&。连接多个命令并在同一个 shell 中执行。从前往后执行。如果某个命令执行错误(自动获取 $? 的值,并进行判断),则不再执行后面的命令;
  • ||。连接多个命令并在同一个 shell 中执行。从前往后执行。如果前一个命令执行成功(自动获取 $? 的值,并进行判断),则不再执行后面的命令。

注意:通配符可以用引号进行转义,引号中的通配符会被 shell 当做普通字符对待。其他特殊字符可以用 \ 进行转义。

16 标准 I/O 重定向

  • <<< 表示标准输入重定向。例如: ./main <file 假设 file 里存的内容是 main 这个可执行程序的命令行参数,那么可以用如下命令将标准输入重定向到文件。

    cat <<eof 可以通过这个命令从标准输入进行输入,以 eof 结尾。
  • >>> 表示标准输出重定向。例如: ls -l >file 将 ls -l 这个命令的输出结果写入 file 这个文件中。如果这个文件不存在,则新建一个;如果存在,则先截断这个文件,然后再写入。

    ls -l >>file 同样是将命令执行结果写入到 file 中,只不过,如果这个文件原来存在,则不会截断,而是在尾部开始写入。
  • 2>2>> 表示标准错误输出重定向。它的用法同标准输出类似。

如果将标准输出和标准错误输出分别重定向到两个文件中,那么可以这样写: ls -l >file1 2>file2 如果想将它们重定向到同一个文件之中,则这样写: ls -l >file 2>file 是错误的,因为这样的话需要打开两次 file 这个文件,而得到的每个文件描述符是不一样的,两个内部指针是独立的,那么输出结果和错误信息可能会无次序地写入到这个文件中,造成内部指针指向错乱。不过可以这样写: ls -l >file 2>&1 其中,&1 表示标准输出。2>&1 就是将标准错误输出重定向到标准输出上。

事实上,重定向功能在实现的时候是调用了 dup2 这个函数,它的声明为 int dup2(int oldfd, int newfd);,表示将 oldfd 这个文件描述符拷贝一份,并重命名为 newfd。这样,写入到 newfd 的信息实际上是写到了 oldfd 打开的这个文件之中。

因此,ls -l >file 2>&1 这个命令就相当于是:

1
2
3
4
5
int file_des = open("file", "w");
dup2(file_des, 1);
// 可以关闭原来的文件描述符,因为用不到了。
close(file_des);
dup2(1, 2);

注意:不能写成 ls -l 2>&1 >file,否则,标准输出会被重定向到文件中,但是标准错误输出会被重定向到标准输出中,也即会打印到屏幕上而非文件中。只要搞清楚了 dup2 的原理就可以知道原因了。

17 tee 命令与双向重定向

可以用 tee 命令,将标准输入的信息同时输出到屏幕(stdout)以及文件: tee haha.txt

还可以利用管道: ls -l | tee haha.txt

如果加上 -a 参数,表示尾部追加(append),否则表示截断。

如果想丢弃标准输出的内容,则执行: ls -l | tee >/dev/null haha.txt

18 管道

管道(这里指的是无名管道)事实上就是同时执行多个命令,其中一个命令的标准输入能够作为另外一个命令的标准输入。见下图: 1.png-30.3kB

因此,要使用管道有两个条件:

  • 某些命令有标准输出信息。对于标准错误输出的信息会自动忽略。
  • 某些命令能够接受标准输入的信息。

比如: ls -l | less 以及 tar -cvf - ~/Downloads/test | tar -xvf - 这个命令中,表示将 ~/Downloads/test 这个目录文件归档,并输出到标准输出(左边命令中的 -)里,然后通过管道将其传给右边命令的标准输入。而右边的命令表示将标准输入(右边命令中的 -)的数据进行释放。


Reference