【Linux】《Linux命令行与shell脚本编程大全 (第4版) 》笔记-汇总 ( Chapter1-Chapter16 )

一、初识 Linux shell
  1. Linux 系统可划分为以下 4 部分。

    • Linux 内核
    • GNU 工具
    • 图形化桌面环境
    • 应用软件
  2. 内核主要负责以下 4 种功能。

    1. 系统内存管理
      • 内核通过硬盘上称为交换空间(swap space)的存储区域来实现虚拟内存。内核在交换空间和实际的物理内存之间反复交换虚拟内存中的内容。这使得系统以为自己拥有比物理内存更多的可用内存。
      • 内存被划分为若干块,这些块称作页面(page)。内核会将每个内存页面置于物理内存或交换空间中。然后,内核会维护一张内存页面表,指明哪些页面位于物理内存,哪些页面被交换到了磁盘。
      • 内核会记录哪些内存页面正在使用中,自动把一段时间未访问的内存页面复制到交换空间区域(称之为换出,swapping out)——即使还有内存可用。当程序要访问一个已被换出的内存页面时,内核必须将物理内存中的另一个页面换出来为其腾出空间,然后从交换空间换入(swapping in)所请求的页面。
    2. 软件程序管理
      • 内核创建了第一个进程(称为 init 进程)来启动系统中所有其他进程。当内核启动时,它会将 init 进程载入虚拟内存。内核在启动其他进程时,会在虚拟内存中给新进程分配一块专有区域来存储该进程用到的数据和代码。
      • 在 Linux 中,有多种 init 进程实现,目前最流行的是以下两种。
        • SysVinit:Linux 最初使用的是 SysVinit(SysV)初始化方法,该方法基于 Unix System V 初始化方法。尽管如今很多 Linux 发行版已经不再使用 SysVinit 了,但在一些比较旧的 Linux 发行版中还能找到其身影。
        • systemd:systemd 初始化方法诞生于 2010 年, 现在已经成为 Linux 发行版中最流行的初 始化和进程管理系统。
      • systemd 初始化方法得以流行起来的原因在于能够根据不同的事件启动进程。
        • 系统启动时
        • 连接到特定的硬件设备时
        • 服务启动时
        • 建立好网络连接时
        • 计时器到期时
      • systemd 方法通过将事件与单元文件(unit file)链接来决定运行哪些进程。每个单元文件定义了特定事件发生时要启动的程序。systemctl 程序允许启动、停止和列出系统中当前运行的单元文件。
      • systemd 方法将单元文件划归为目标(target)。目标定义了 Linux 系统的特定运行状态,这和 SysVinit 运行级的概念类似。在系统启动时,default.target 单元定义了要启动的所有单元文件。可以使用 systemctl 命令查看当前默认目标:
        $ systemctl get-default
        graphical.target
        
      • graphical.target 单元文件定义了多用户图形环境运行时要启动的进程。
    3. 硬件设备管理
      • 任何 Linux 系统需要与之通信的设备都必须在内核代码中加入其驱动程序。向 Linux 内核中插入设备驱动的方法有两种:
        • 将驱动程序编译入内核(低效)
        • 将设备驱动模块加入内核(无需重新编译内核、可插入、可移走)
      • Linux 系统将硬件设备视为一种特殊文件,称为设备文件。设备文件分为 3 种:
        • 字符设备文件
        • 块设备文件
        • 网络设备文件
      • 字符设备文件对应于每次只能处理一个字符的设备。大多数类型的调制解调器和终端是作为字符设备文件创建的。块设备文件对应于每次以块形式处理数据的设备,比如硬盘驱动器。网络设备文件对应于采用数据包发送和接收数据的设备,这包括网卡和一个特殊的环回设备,后者允许 Linux 系统使用常见的网络编程协议同自身通信。
      • Linux 会为系统的每个设备都创建一种称为“节点”的特殊文件。与设备的所有通信都是通过设备节点完成的。每个节点都有一个唯一的数值对,以供 Linux 内核标识。数值对包括一个主 设备号和一个次设备号。类似的设备会被划分到相同的主设备号下。次设备号用于标识主设备组 下的某个特定设备。
    4. 文件系统管理
      • Linux 还能够读写其他操作系统(比如 Microsoft Windows)的文件系统。内核必须在编译时就加入对所有要用到的文件系统的支持。
      • Linux 内核采用虚拟文件系统(virtual file system,VFS)作为和各种文件系统交互的接口。这为 Linux 内核与其他类型文件系统之间的通信提供了一个标准接口。当文件系统被挂载和使用时,VFS 会在内存中缓存相关信息。
  3. GNU 实用工具

    1. 除了由内核来控制硬件设备,操作系统还需要实用工具来实现各种标准功能,比如控制文件和程序。
    2. GNU coreutils 软件包由 3 部分构成。
      • 文件实用工具
      • 文本实用工具
      • 进程实用工具
    3. Linux shell 属于 GNU 核心工具集的一部分。GNU/Linux shell 是一种特殊的交互式工具,为用户提供了启动程序、管理文件系统中的文件以及运行在 Linux 系统中的进程的途径。shell 的核心是命令行提示符,负责 shell 的交互部分,允许用户输入文本命令,然后解释命令并在内核中执行。
    4. 所有 Linux 发行版默认的 shell 都是bash shell。
    5. 其他几种流行的 shell:
      shell描 述
      ash一种简单的轻量级 shell,运行在内存受限环境中, 但与 bash shell 完全兼容
      korn一种与 Bourne shell 兼容的编程 shell,但支持如关联数组和浮点算术等高级编程特性
      tcsh一种将 C语言中的一些元素引入 shell脚本中的 shell
      zsh一种结合了 bash、tcsh 和 korn 的特性,同时提供高级编程特性、共享历史文件和主题化提示符的高级shell
  4. 多数 Linux 发行版也有 LiveCD 版本可用。LiveCD 版本是一个自成一体(self-contained)的 ISO 镜像文件,可以刻录成 DVD(或写入 U 盘),直接引导 Linux 系统,无须安装在硬盘上。LiveDVD 的好处是可以在安装系统之前先测试系统硬件, 解决存在的问题。


二、走进 shell
  1. 控制台终端
    • Linux 系统启动时,会自动创建多个虚拟控制台。虚拟控制台是运行在 Linux 系统内存中的终端会话。多数 Linux 发行版会启动 5~6个(甚至更多)虚拟控制台代替哑终端通过单个计算机键盘和显示器就可以访问这些虚拟控制台。
    • 哑终端(dumb terminal)是由通信电缆(通常是多线束串行电缆,也叫带状电缆)连接到 Unix 系统的显示器和键盘。
    • 虚拟控制台终端的另一种替代方案是使用 Linux 图形化桌面环境中的终端仿真软件包。
    • 尽管在引导时会创建多个虚拟控制台, 但很多 Linux 发行版在完成启动过程之后会切换到图形化环境中。
    • 在大多数 Linux 发行版中, 可以使用简单的按键组合来访问某个 Linux 虚拟控制台。通常必须按下 Ctrl+Alt 组合键, 然后再按一个功能键(F1 ~ F7)来进入你要使用的虚拟控制台,例如功能键 F2 键会生成虚拟控制台 2。Ubuntu 和 CentOS 均使用 F1 键。(单词 tty2,其中的 2 表明这是虚拟控制台 2)
    • 不是所有的 Linux 发行版都会在登录画面显示虚拟控制台的 tty 编号。登入虚拟控制台后,可以输入命令 tty,然后按 Enter 键查看当前使用的是哪个虚拟控制台。
    • 在 Linux 虚拟控制台中是无法运行任何图形化程序的。

三、bash shell 基础命令
  1. GNU bash shell 是一个程序, 提供了对 Linux 系统的交互式访问。它是作为普通程序运行的,通常是在用户登录终端时启动。系统启动的 shell 程序取决于用户账户的配置。

  2. /etc/passwd 文件包含了所有系统用户账户以及每个用户的基本配置信息。

  3. man 命令可以访问 Linux 系统的手册页。在 man 命令之后跟上想要查看的命令名,就可以显示相应的手册页。

  4. 当你使用 man 命令查看命令手册页的时候,其中的信息是由分页程序(pager )来显示的。 分页程序是一种实用工具,能够逐页(或逐行)显示文本。你可以单击空格键进行翻页, 或是使用 Enter 键逐行查看。也可以使用箭头键向前和向后滚动手册页的内容(假设你使用的终端仿真软件包支持箭头键功能)。如果阅读完毕,可以按 q 键退出手册页。

  5. 输入man man 可以查看与手册页相关的信息。

  6. 如果想使用多个命令选项,那么通常可以将其合并在一起。例如,要使用选项-a 和 -b,可以写作-ab。

  7. 如果不记得命令名了,可以使用关键字来搜索手册页。语法为 man -k keyword。例如,要查找与终端相关的命令,可以输入 man -k terminal。

  8. 手册页中还有不同的节。每节都分配了一个数字,从 1 开始,一直到 9。Linux 手册页的节如下表:

节号所涵盖的内容
1可执行程序或 shell 命令
2系统调用
3库调用
4特殊文件
5文件格式与约定
6游戏
7概览、约定及杂项
8超级用户和系统管理员命令
9内核例程(routine)
  1. man 命令通常显示的是指定命令编号最低的节。(一个命令偶尔会在多个节中都有对应的手册页。)

  2. 大多数命令接受-h 或–help 选项。例如,可以输入 hostname --help 来查看简要的帮助信息。

  3. Linux 文件系统

  • Linux 会将文件存储在名为虚拟目录(virtual directory)的单个目录结构中。虚拟目录会将计算机中所有存储设备的文件路径都纳入单个目录结构。
  • Linux 虚拟目录结构只包含一个称为根(root)目录的基础目录。根目录下的目录和文件会 按照其访问路径一一列出,
  • 路径本身并没有提供任何有关文件究竟存放在哪个物理磁盘中的信息。
  • Linux 虚拟目录中比较复杂的部分是它如何来协调管理各个存储设备。我们称在 Linux 系统 中安装的第一块硬盘为根驱动器。根驱动器包含了虚拟目录的核心,其他目录都是从那里开始构建的。
  • Linux 会使用根驱动器上一些特别的目录作为挂载点(mount point)。挂载点是虚拟目录中分配给额外存储设备的目录。 Linux 会让文件和目录出现在这些挂载点目录中,即便它们位于其他物理驱动器中。
  • 系统文件通常存储在根驱动器中,而用户文件则存储在其他驱动器中。
  • 注意,Linux 使用正斜线(/)而不是反斜线(\)来分隔文件路径中的目录。反斜线在 Linux 中用作转义字符,如果误用在文件路径中会造成各种各样的问题。
  1. 常见的 Linux 目录名
目录用途
/虚拟目录的根目录,通常不会在这里放置文件
/bin二进制文件目录,存放了很多用户级的 GNU 实用工具
/boot引导目录,存放引导文件
/dev设备目录, Linux 在其中创建设备节点
/etc系统配置文件目录
/home主目录, Linux 在其中创建用户目录(可选)
/lib库目录, 存放系统和应用程序的库文件
/libname库目录, 存放替代格式的系统和应用程序库文件(可选)
/media媒介目录,可移动存储设备的常用挂载点
/mnt挂载目录,用于临时挂载文件系统的常用挂载点
/opt可选目录,存放第三方软件包
/proc进程目录,存放现有内核、系统以及进程的相关信息
/rootroot 用户的主目录(可选)
/run运行目录,存放系统的运行时数据
/sbin系统二进制文件目录,存放了很多管理级的 GNU 实用工具
/srv服务目录,存放本地服务的相关文件
/sys系统目录,存放设备、驱动程序以及部分内核特性信息
/tmp临时目录,可以在其中创建和删除临时工作文件
/usr用户目录,一个次目录层级结构(secondary directory hierarchy)
/var可变目录,存放经常变化的文件,比如日志文件
  • /usr 目录值得特别关注,因为该目录是一个次目录层级结构,包含可共享的只读文件。你经常会在其中发现用户命令、源代码文件、游戏,等等。
  • 主目录是分配给用户账户的一个特有目录。在创建用户账户时,系统通常会为其分配主目录。
  1. 在 Linux 文件系统中,可以使用目录切换(cd)命令来将 shell 会话切换到另一个目录。
  • 如果没有为 cd 命令指定目标路径,则会切换到你的用户主目录。
  • 绝对路径总是以正斜线(/)作为起始,以指明虚拟文件系统的根目录。
  1. pwd 命令可以显示出 shell 会话的当前目录,该目录被称为当前工作目录。
  • 在切换到新的当前工作目录时使用 pwd 命令,这是一个不错的习惯。因为很多shell 命令是在当前工作目录中进行操作的,所以在发出命令之前,你应该总是确保自己处在正确的目录之中。
  • 可以使用绝对路径切换到 Linux 虚拟目录结构中的任何一级,也可以从 Linux 虚拟目录中的任何一级快速跳回到主目录(cd命令即可(不指定目标路径))。
  1. 可以在任何包含子目录的目录中使用带有相对路径的 cd 命令, 也可以使用特殊字符来表示相对目录位置。
  • 有两个特殊字符可用于相对路径中:
    • 单点号( . ),表示当前目录;
    • 双点号(…),表示当前目录的父目录。
  1. ls 命令最基本的形式会显示当前目录下的文件和目录。
  • ls 命令输出的列表是按字母排序的(按列而不是按行排序)。
  • 可以使用 ls 命令的-F 选项来轻松地区分文件和目录。
  • -F 选项会在目录名之后添加正斜线(/),以方便用户在输出中分辨。它还会在可执行文件之后添加星号(*), 以帮助用户找出可在系统中运行的文件。
  • Linux 经常使用隐藏文件来保存配置信息。在 Linux 中,隐藏文件通常是文件名以点号( . )开始的文件。
  • 要想显示隐藏文件,可以使用-a 选项。
  • -R 是 ls 命令的另一个选项,称作递归选项,可以列出当前目录所包含的子目录中的文件。
  • 选项并不是非得分开输入,像 ls –F –R。 可以将其合并: ls –FR。
  • -l 选项会产生长列表格式的输出,提供目录中各个文件的详细信息。
  • ls -l 输出的第一行显示了为该目录中的文件所分配的总块数。此后的每一行都包含了关于 文件(或目录)的下列信息。
    • 文件类型,比如目录(d)、文件(-)、链接文件(l)、字符设备(c)或块设备(b)
    • 文件的权限
    • 文件的硬链接数
    • 文件属主
    • 文件属组
    • 文件大小(以字节为单位)
    • 文件的上次修改时间
    • 文件名或目录名
  • 如果想查看单个文件的长列表,那么只需在 ls -l 命令之后跟上该文件名即可。但如果 想查看目录的相关信息,而非目录所包含的内容,则除了-l 选项之外,还得添加-d 选项,即 ls -ld Directory-Name。
  1. 过滤输出列表
  • ls 命令还支持在命令行中定义过滤器。ls 会使用过滤器来决定应该在输出中显示哪些文件或目录。
  • 过滤器就是一个字符串,可用作简单的文本匹配。你可以将其作为命令行参数, 放置在选项之后使用
  • ls 命令也能识别标准通配符(wildcard),并在过滤器中用其来进行模式匹配:
    • 问号(?)代表任意单个字符;
    • 星号(*)代表零个或多个字符。
  • 在过滤器中使用星号和问号被称作通配符匹配(globbing),是指使用通配符进行模式匹配 的过程。通配符正式的名称叫作元字符通配符(metacharacter wildcard)。除了星号和问号, 还有 更多的元字符通配符可做文件匹配之用。
    // 例如方括号:方括号代表单个字符位置并给出了该位置上的多个可能的选择。
    $ ls -l my_scr[ay]pt
    // 也可以指定字符范围,比如字母范围 [a–i]:
    $ ls f[a-i]ll
    // 还可以使用惊叹号(!)将不需要的内容排除在外:
    $ ls -l f[!a]ll
    
  • globbing 和 wildcard 的区别: globbing 是对 wildcard 进行扩展的过程。
  1. 可以使用 touch 命令轻松创建空文件。
  • touch 命令会创建好指定的文件并将你的用户名作为该文件的属主。
  • 注意,新文件的大小为 0,因为 touch 命令只是创建了一个空文件。
  • touch 命令还可用来改变文件的修改时间。该操作不会改变文件内容。
  • touch 命令是一个创建空文件和变更已有文件访问时间或修改时间的实用工具。
  1. 复制文件
  • cp 命令最基本的用法需要两个参数,即源对象和目标对象: cp source destination。
    • 当参数 source 和 destination 都是文件名时, cp 命令会将源文件复制成一个新的目标文件,并以 destination 命名。新文件在形式上就像全新的文件一样, 有新的修改时间。
    • 注意:如果目标文件已经存在, 则 cp 命令 可能并不会提醒你这一点。最好加上-i 选项,强制 shell 询问是否需要覆盖已有文件。
  • 也可以将文件复制到现有目录中。
    $ cp -i test_one /home/christine/Documents/
    
    • 在目标目录名尾部加上了一个正斜线(/)。这表明 Documents 是一个目录而非文件。这有助于表明目的,而且在复制单个文件时非常重要。 如果没有使用正斜线, 同时子目录/home/christine/Documents 又不存在, 就会产生麻烦。在这种情况下, 试图将一个文件复制到 Documents 子目录反而会创建名为 Documents 的文件,更是连错误消息 都不会有。因此,记得在目标目录名尾部加上正斜线。
  • 可以用 cp 命令的-R 选项在单个命令中递归地复制整个目录的内容。
    • 在执行 cp –R 命令之前, 目录 NewDocuments 可以并不存在。它可以随着 cp –R 命令被创建,而整个 Documents 目录中的内容都被复制到其中。
      cp -R Documents/ NewDocuments/
      • 注意,新的 NewDocuments 目录中所有的文件都会有对应的新日期。 NewDocuments 目录现在已经成了 Documents 目录的完整副本。
  • 也可以在 cp 命令中使用通配符。
  1. 制表键补全允许你在输入文件名 或目录名的时候,按一下制表键, 让 shell 帮你将内容补充完整。

  2. 链接文件

  • 如果需要在系统中维护同一文件的两个或多个副本,可以使用单个物理副本加多个虚拟副本(链接)的方法代替创建多个物理副本。链接是目录中指向文件真实位置的占位符。在 Linux 中有两种类型的文件链接。
    • 符号链接
    • 硬链接
  • 符号链接(也称为软链接)是一个实实在在的文件, 该文件指向存放在虚拟目录结构中某个 地方的另一个文件。这两个以符号方式链接在一起的文件彼此的内容并不相同。
  • 要为一个文件创建符号链接,原始文件必须事先存在。然后可以使用 ln 命令以及-s 选项来创建符号链接:
    $ ln -s test_file slink_test_file
    $ ls -l *test_file
    lrwxrwxrwx. 1 christine christine  9 Mar  4 09:46 slink_test_file -> test_file
    -rw-rw-r--. 1 christine christine 74 Feb 29 15:50 test_file
    
    • 注意符号链接文件与数据文件的文件大小。符号链接文件 slink_test_file 只有 9 个字节, 而 test_file有 74 个字节。这是因为 slink_test_file 仅仅只是指向 test_file 而已。它们的内容并不相同,是两个完全不同的文件。
    • 另一种证明链接文件是一个独立文件的方法是查看 inode 编号。文件或目录的 inode 编号是内核分配给文件系统中的每一个对象的唯一标识。要查看文件或目录的 inode 编号,可以使用 ls 命令的-i 选项:
      $ ls -i *test_file
      1415020 slink_test_file  1415523 test_file
      
      • 可以看出,test_file 文件的 inode 编号是 1415523,而 slink_test_file 的 inode 编号则是 1415020。 所以说两者是不同的文件。
  • 硬链接创建的是一个独立的虚拟文件,其中包含了原始文件的信息以及位置。但是两者就根本而言是同一个文件。要想创建硬链接,原始文件也必须事先存在, 只不过这次使用 ln 命令时 不需要再加入额外的选项了:
    $ ls -l *test_one
    -rw-rw-r--. 1 christine christine 0 Feb 29 17:26 test_one
    $ ln test_one hlink_test_one
    $ ls -li *test_one
    1415016 -rw-rw-r--. 2 christine christine 0 Feb 29 17:26 hlink_test_one
    1415016 -rw-rw-r--. 2 christine christine 0 Feb 29 17:26 test_one
    
    • 创建好硬链接文件之后,我们使用 ls -li 命令显示了*test_one 的 inode 编号以及长列表。注意,以硬链接相连的文件共享同一个 inode 编号。这是因为两者其实就是同一个文件。另外,彼此的文件大小也一模一样。
    • 注意,只能对处于同一存储设备的文件创建硬链接。要想在位于不同存储设备的文件之间创建链接,只能使用符号链接。(硬链接不能跨文件系统,而符号链接可以跨文件系统。)
  1. 文件重命名
  • 在 Linux 中,重命名文件称为移动(moving)。mv 命令可以将文件和目录移动到另一个位置或是重新命名。
  • mv 只影响文件名,inode 编号和时间戳保持不变。
  • 也可以使用 mv 来移动文件的位置(该操作没有改变文件的 inode 编号或时间戳,唯一变化的就是文件的位置)。
  • 和 cp 命令类似,你也可以在 mv 命令中使用-i 选项。这样在 mv 试图覆盖已有的文件时会发出询问。
  • 可以使用 mv 命令在移动文件的同时进行重命名。
  • 也可以使用 mv 命令移动整个目录及其内容。
    mv OldDocuments NewDocuments
  1. 删除文件
  • bash shell 中用于删除文件的命令是 rm。
    $ rm -i fall
    rm: remove regular empty file 'fall'? y
    
    • -i 选项会询问你是否真的要删除该文件。
    • shell 没有回收站或者垃圾箱这样的东西,文件一旦被删除,就再也找不回来了。所以在使用 rm 命令时, 要养成总是加入-i 选项的好习惯。
  • 也可以使用通配符元字符删除一组文件。
  • rm 命令的另一个特性是, 如果你要删除很多文件,又不想被命令提示干扰,可以用-f 选项来强制删除。
  1. 创建目录
  • 在 Linux 中创建目录很简单,使用 mkdir 命令即可:
    $ mkdir New_Dir
    $ ls -ld New_Dir
    drwxrwxr-x. 2 christine christine 6 Mar 6 14:40 New_Dir
    
    • 在长列表输出中,目录以 d 开头。这表示 New_Dir 并不是文件,而是一个目录。
  • 可以根据需要“批量”地创建目录和子目录。为此,要使用 mkdir 命令的-p 选项:
    $ mkdir -p New_Dir/SubDir/UnderDir
    $ ls -R New_Dir
    New_Dir:
    SubDir
    
    New_Dir/SubDir:
    UnderDir
    
    New_Dir/SubDir/UnderDir:
    
    • mkdir 命令的 -p 选项可以根据需要创建缺失的父目录。父目录是包含目录树中下一级目录的目录。
  1. 删除目录
  • 删除目录的基本命令是 rmdir。
  • 在默认情况下, rmdir 命令只删除空目录(如果目录下有文件,rmdir 命令会拒绝删除该目录)。
  • rmdir 并没有-i 选项可以用来询问是否要删除目录。
  • 也可以在整个非空目录中使用 rm 命令。 -r 选项使得 rm 命令可以向下进入(descend into) 目录,删除其中的文件,然后再删除目录本身。
  • 对于 rm 命令, -r 选项和-R 选项的效果是一样的, 都可以递归地删除目录中的文件。 shell 命令很少会对相同的功能使用大小写不同的选项。
  • 一口气删除目录树的最终解决方案是使用 rm -rf 命令。
  1. 查看文件类型
  • 在显示文件内容之前, 应该先了解文件类型。如果你尝试显示二进制文件, 那么屏幕上会出 现各种乱码, 甚至会把你的终端仿真器挂起。
  • file 命令是一个方便的小工具,能够探测文件的内部并判断文件类型:
    $ file .bashrc
    .bashrc: ASCII text
    
    • file 命令不仅能够确定文件中包含的是文本信息,还能确定该文本文件的字符编码是 ASCII。
  • 可以使用 file 命令作为另一种区分目录的方法:
    $ file Documents
    Documents/: directory
    
  • file 命令甚至能够告诉你它链接到了哪个文件:
    $ file slink_test_file
    slink_test_file: symbolic link to test_file
    
  • 下面的例子展示了 file 命令对于脚本文件的返回结果。尽管这个文件是 ASCII text,但因为是脚本文件,所以可以在系统中执行(运行)
    $ file my_script
    my_script: Bourne-Again shell script, ASCII text executable
    
  • file 命令能够确定该程序编译时所面向的平台以及需要何种类型的库。如果有从未知来源处获得的二进制文件,那么这会是一个非常有用的特性:
    $ file /usr/bin/ls
    /usr/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
    dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
    for GNU/Linux 3.2.0,[...]
    
  1. 查看整个文件
  • cat 命令:cat 命令是显示文本文件中所有数据的得力工具。
    • -n 选项会给所有的行加上行号。
    • 如果只想给有文本的行加上行号, 可以用-b 选项。
    • cat 命令对大文件来说,文件内容会在屏幕上一闪而过。
  • more 命令会显示文本文件的内容,但会在显示每页数据之后暂停下来。
    • more 命令是一个分页工具。
    • 可以使用空格键向前翻页,或是使用 Enter 键逐行向前查看。结束之后,按 q 键退出。
    • more 命令只支持文本文件中基本的移动。如果想要更多的高级特性,可以试试 less 命令。
  • less 命令实为 more 命令的升级版本。
    • less 命令还可以在完成整个文件的读取之前显示文件的内容。 cat 命令和 more 命令则无法做到这一点。
    • 输入 man less 可以查看 less 命令的所有可用选项,也可以使用相同的方法查看 more 命令的各种选项。
    • less 命令能够识别上下箭头键以及上下翻页键(假设你的终端配置正确)。
  1. 查看部分文件
  • tail 命令会显示文件最后几行的内容(文件的“尾部”)。在默认情况下,它会显示文件的末尾 10 行。
    • 可以向 tail 命令中加入-n 选项来修改所显示的行数。
      $ tail -n 2 log_file
      
    • tail 命令有一个非常酷的特性:-f 选项, 该选项允许你在其他进程使用此文件时查看文件的内容。 tail 命令会保持活动状态并持续地显示添加到文件中的内容。这是实时监测系统日志的绝佳方式。
  • head 命令会显示文件开头若干行(文件的“头部”)。在默认情况下,它会显示文件前 10 行的文本。
    • 与 tail 命令类似, head 命令也支持-n 选项, 以便指定想要显示的内容。这两个命令也允 许简单地在连字符后面直接输入想要显示的行数:
      $ head -3 log_file
      
    • 文件的开头部分通常不会改变,因此 head 命令并没有 tail 命令那样的-f 选项。
    • head 命令是一种查看文件起始部分内容的便捷方法。

四、更多的 bash shell 命令
  1. 探查进程
  • 当程序在系统中运行时,它被称为进程(process)。要想监测这些进程,必须熟悉 ps 命令的用法。 ps 命令能够输出系统中运行的所有程序的大量信息。
  • 在默认情况下, ps 命令并没有提供太多的信息。ps 命令默认只显示运行在当前终端中属于当前用户的那些进程。
  • shell 只是运行在系统中的另一个程序而已。
  • ps 命令的基本输出显示了程序的进程 ID(process ID,PID )、进程运行在哪个终端(TTY)及其占用的 CPU 时间。
  • Linux 系统中使用的 GNU ps 命令支持以下 3 种类型的命令行选项:
    • Unix 风格选项,选项前加单连字符;
    • BSD 风格选项,选项前不加连字符;
    • GNU 长选项,选项前加双连字符。
  1. Unix 风格的ps 命令选项
    选项描述
    -A显示所有进程
    -N显示与指定参数不符的所有进程
    -a显示除控制进程(session leader)和无终端进程外的所有进程
    -d显示除控制进程外的所有进程
    -e显示所有进程
    -C cmdlist显示包含在 cmdlist 列表中的进程
    -G grplist显示组 ID 在 grplist 列表中的进程
    -U userlist显示属主的用户 ID 在 userlist 列表中的进程
    -g grplist显示会话或组 ID 在 grplist 列表中的进程
    -p pidlist显示 PID 在 pidlist 列表中的进程
    -s sesslist显示会话 ID 在 sesslist 列表中的进程
    -t ttylist显示终端 ID 在 ttylist 列表中的进程
    -u userlist显示有效用户 ID 在 userlist 列表中的进程
    -F显示更多的额外输出(相对-f 选项而言)
    -O显示默认的输出列以及 format 列表指定的特定列
    -M显示进程的安全信息
    -c显示进程的额外的调度器信息
    -f显示完整格式的输出
    -j显示作业信息
    -l显示长列表
    -o format仅显示由 format 指定的列
    -y不显示进程标志
    -Z显示安全上下文信息
    -H以层级格式显示进程(显示父进程)
    -n namelist定义要在 WCHAN 输出列中显示的值
    -w采用宽输出格式,不限宽度显示
    -L显示进程中的线程
    -V显示 ps 命令的版本号
  • 使用 ps 命令的关键不在于记住所有可用的选项,而在于记住对你来说最有用的那些。
  • 如果需要查看系统中运行的所有进程,可以使用-ef 选项组合(ps 命令允许像这样把选项合并在一起)。
  • -f 选项则扩充输出内容以显示一些有用的信息列。
    • UID :启动该进程的用户。
    • PID:进程 ID。
    • PPID:父进程的 PID(如果该进程是由另一个进程启动的)。
    • C:进程生命期中的 CPU 利用率。
    • STIME :进程启动时的系统时间。
    • TTY :进程是从哪个终端设备启动的。
    • TIME:运行进程的累计 CPU 时间。
    • CMD :启动的程序名称。
  • 使用了-l 选项之后多出的那几列:
    • F :内核分配给进程的系统标志。
    • S:进程的状态( O 代表正在运行; S 代表在休眠; R 代表可运行,正等待运行; Z 代表僵化,已终止但找不到其父进程; T 代表停止)。
    • PRI :进程的优先级(数字越大,优先级越低)。
    • NI:谦让度(nice),用于决定优先级。
    • ADDR :进程的内存地址。
    • SZ :进程被换出时所需交换空间的大致大小。
    • WCHAN :进程休眠的内核函数地址。
  1. BSD 风格的ps 命令选项
    选项描述
    T显示与当前终端关联的所有进程
    a显示与任意终端关联的所有进程
    g显示包括控制进程在内的所有进程
    r仅显示运行中的进程
    x显示所有进程, 包括未分配任何终端的进程
    U userlist显示属于 userlist 列表中某个用户 ID 所有的进程
    p pidlist显示 PID 在 pidlist 列表中的进程
    t ttylist显示与 ttylist 列表中的某个终端关联的进程
    O format除了标准列,还输出由 format 指定的列
    X以寄存器格式显示数据
    Z在输出中包含安全信息
    j显示作业信息
    l采用长格式显示
    o format仅显示由 format 指定的列
    s采用信号格式显示
    u采用基于用户的格式显示
    v采用虚拟内存格式显示
    N namelist定义要在 WCHAN 输出列中显示的值
    O order定义信息列的显示顺序
    S将子进程的数值统计信息(比如 CPU 和内存使用情况)汇总到父进程中
    c显示真实的命令名称(用以启动该进程的程序名称)
    e显示命令使用的环境变量
    f用层级格式来显示进程, 显示哪些进程启动了哪些进程
    h不显示头信息
    k soft指定用于排序输出的列
    n使用数值显示用户 ID 、组 ID 以及 WCHAN 信息
    w为更宽的终端屏幕生成宽输出
    H将线程显示为进程
    m在进程之后显示线程
    L列出所有的格式说明符
    V显示 ps 命令的版本
  • 在使用 BSD 风格的选项时, ps 命令会自动改变输出以模仿 BSD 格式。
  • 注意,尽管上述很多输出列跟使用 Unix 风格选项时是一样的, 但还是有一些不同之处。
    • VSZ:进程占用的虚拟内存大小(以 KB 为单位)。
    • RSS :进程在未被交换出时占用的物理内存大小。
    • STAT :代表当前进程状态的多字符状态码。
  • 多 字符状态码能比 Unix 风格输出的单字符状态码更清楚地表明进程的当前状态。第一个字符采用了与 Unix 风格的 S 输出列相同的值,表明进程是在休眠、运行还是等待。 第二个字符进一步说明了进程的状态。
    • <:该进程以高优先级运行。
    • N:该进程以低优先级运行。
    • L:该进程有锁定在内存中的页面。
    • s:该进程是控制进程。
    • l:该进程拥有多线程。
    • +:该进程在前台运行。
  1. GNU 风格的ps 命令选项
    选项描述
    –deselect显示除命令行中列出的进程之外的其他进程
    –Group grplist显示组 ID 在 grplist 列表中的进程
    –User userlist显示用户 ID 在 userlist 列表中的进程
    –group grplist显示有效组 ID 在 grplist 列表中的进程
    –user userlist显示有效用户 ID 在 userlist 列表中的进程
    –pid pidlist显示 pid 在 pidlist 列表中的进程
    –ppid pidlist显示父 pid 在 pidlist 列表中的进程
    –sid sidlist显示会话 ID 在 sidlist 列表中的进程
    –tty ttylist显示终端设备 ID 在 ttylist 列表中的进程
    –format format仅显示由 format 指定的列
    –context显示额外的安全信息
    –cols n将屏幕宽度设置为 n 列
    –columns n将屏幕宽度设置为 n 列
    –cumulative包含已停止的子进程的信息
    –forest用层级结构显示出进程和父进程之间的关系
    –headers在每页输出中都显示列名
    –no-headers不显示列名
    –lines n将屏幕高度设置为 n 行
    –rows n将屏幕高度设置为 n 行
    –sort order指定用于排序输出的列
    –width n将屏幕宽度设置为 n 列
    –help显示帮助信息
    –info显示调试信息
    –version显示 ps 命令的版本号
  • 可以混用 GNU 长选项和 Unix 或 BSD 风格的选项来定制输出。
  • 作为一个 GNU 长选项, --forest 选项着实讨人喜欢。该选项能够使用 ASCII 字符来绘制可爱的图表以显示进程的层级 信息,这种格式可以轻而易举地跟踪子进程和父进程。
  1. 实时监测进程
  • 与 ps 命令相似, top 命令也可以显示进程信息,但采用的是实时方式。
  • top 命令运行时输出的第一部分显示的是系统概况:第一行显示了当前时间、系统的运行时长、登录的用户数以及系统的平均负载。
  • 平均负载有 3 个值,分别是最近 1 分钟、最近 5 分钟和最近 15 分钟的平均负载。
    • 值越大说 明系统的负载越高。
    • 由于进程短期的突发性活动, 出现最近 1 分钟的高负载值也很常见。
    • 但如果近 15 分钟内的平均负载都很高,就说明系统可能有问题了。
  • top 命令运行时的第二行显示了进程(top 称其为 task)概况:多少进程处于运行、休眠、停止以及僵化状态 (僵化状态指进程已结束, 但其父进程没有响应)。
  • top 命令运行时的第三行显示了 CPU 概况。 top 会根据进程的属主(用户或是系统)和进程的状态(运行、空闲或等待) 将 CPU 利用率分成几类输出。
  • top 命令运行时的第四、五行详细说明了系统内存的状态。前一行显示了系统的物理内存状态: 总共有多少内存、当前用了多少,以及还有多少空闲。后一行显示了系统交换空间(如果分配了的话)的状态。
  • top 命令运行时的最后一部分显示了当前处于运行状态的进程的详细列表,有些列跟 ps 命令的输出类似。
    • PID:进程的 PID。
    • USER :进程属主的用户名。
    • PR :进程的优先级。
    • NI :进程的谦让度。
    • VIRT :进程占用的虚拟内存总量。
    • RES :进程占用的物理内存总量。
    • SHR :进程和其他进程共享的内存总量。
    • S:进程的状态(D 代表可中断的休眠,R 代表运行,S 代表休眠,T 代表被跟踪或停止,Z 代表僵化)。
    • %CPU:进程使用的 CPU 时间比例。
    • %MEM :进程使用的可用物理内存比例。
    • TIME+:自进程启动到目前为止所占用的 CPU 时间总量。
    • COMMAND :进程所对应的命令行名称,也就是启动的程序名。
  • 在默认情况下, top 命令在启动时会按照%CPU 值来对进程进行排序,你可以在 top 命令运行时使用多种交互式命令来重新排序。每个交互式命令都是单字符,在 top 命令运行时键入可改变 top 的行为。键入 f 允许你选择用于对输出进行排序的字段, 键入 d 允许你修改轮询间隔(polling interval),键入 q 可以退出 top。
  1. 结束进程
  • 在 Linux 中,进程之间通过信号来通信。进程的信号是预定义好的一个消息,进程能识别该消息并决定忽略还是做出反应。进程如何处理信号是由开发人员通过编程来决定的。大多数编写完善的应用程序能接收和处理标准 Unix 进程信号。

  • Linux 进程信号

    信号名称描述
    1HUP挂起
    2INT中断
    3QUIT结束运行
    9KILL无条件终止
    11SEGV段错误
    15TERM尽可能终止
    17STOP无条件停止运行,但不终止
    18TSTP停止或暂停,但继续在后台运行
    19CONT在 STOP 或 TSTP 之后恢复执行
  • 在 Linux 中有两个命令可以向运行中的进程发出进程信号: kill 和 pkill。

    • kill 命令可以通过 PID 向进程发送信号。在默认情况下, kill 命令会向命令行中列出的所有 PID 发送 TERM 信号。遗憾的是,你只能使用进程的 PID 而不能使用其对应的程序名,这使得 kill 命令有时并不好用。
      • 要发送进程信号,必须是进程的属主或 root用户。
      • TERM 信号会告诉进程终止运行。但不服管教的进程通常会忽略这个请求。如果要强制终止,则-s 选项支持指定其他信号(用信号名或信号值)。
      • kill 命令不会有任何输出。
      • 要检查 kill 命令是否生效,可以再次执行 ps 命令或 top 命令,看看那些进程是否已经停止运行。
    • pkill 命令可以使用程序名代替 PID 来终止进程。pkill 命令也允许使用通配符。
      • 以 root 身份使用pkill 命令时要格外小心。命令中的通配符很容易意外地将系统的重要进程终止。这可能会导致文件系统损坏。
  1. 挂载存储设备
  • Linux 文件系统会将所有的磁盘都并入单个虚拟目录。在使用新的存储设备之前,需要将其放在虚拟目录中。这项工作称为挂载(mounting)。

  • 用于挂载存储设备的命令叫作 mount。在默认情况下, mount 命令会输出当前系统已挂载的设备列表。但是, 除了标准存储设备, 较新版本的内核还会挂载大量用作管理目的的虚拟文件系统。

  • 如果知道设备分区使用的文件系统类型,可以像下面这样过滤输出。

    $ mount -t ext4
    
  • mount 命令提供了 4 部分信息。

    • 设备文件名
    • 设备在虚拟目录中的挂载点
    • 文件系统类型
    • 已挂载设备的访问状态
  • 要手动在虚拟目录中挂载设备,需要以 root 用户身份登录,或是以 root 用户身份运行 sudo 命令。下面是手动挂载设备的基本命令:

    mount -t type device directory
    
    • 其中, type 参数指定了磁盘格式化所使用的文件系统类型。
    • Linux 可以识别多种文件系统类型。 如果与 Windows PC共用移动存储设备, 那么通常需要使用下列文件系统类型。
      • vfat:Windows FAT32 文件系统,支持长文件名。
      • ntfs:Windows NT 及后续操作系统中广泛使用的高级文件系统。
      • exfat:专门为可移动存储设备优化的 Windows 文件系统。
      • iso9660:标准 CD-ROM 和 DVD文件系统。
    • 大多数 U 盘会使用 vfat 文件系统格式化。如果需要挂载数据 CD 或 DVD,则必须使用 iso9660文件系统类型。
    • 后面两个参数指定了该存储设备的设备文件位置以及挂载点在虚拟目录中的位置。
    • 例如, 手动将 U 盘/dev/sdb1 挂载到/media/disk,可以使用下列命令:
      mount -t vfat /dev/sdb1 /media/disk
      
  • 一旦存储设备被挂载到虚拟目录, root 用户就拥有了对该设备的所有访问权限,而其他用户的访问则会被限制。可以通过目录权限指定用户对设备的访问权限。

  • mount 命令选项

    选项描述
    -a挂载/etc/fstab 文件中指定的所有文件系统
    -f模拟挂载设备, 但并不真正挂载
    -F和-a 选项一起使用时,同时挂载所有文件系统
    -v详细模式,显示挂载设备的每一步操作
    -i不使用/sbin/mount.filesystem 下的任何文件系统协助文件
    -l自动给 ext2 、ext3 、ext4 或 XFS 文件系统添加文件系统标签
    -n挂载设备,但不在/etc/mtab 已挂载设备文件中注册
    -p num进行加密挂载时从文件描述符 num 中获得口令
    -s忽略该文件系统不支持的挂载选项
    -r将设备挂载为只读
    -w将设备挂载为可读写(默认选项)
    -L label将设备按指定的 label 挂载
    -U uuid将设备按指定的 uuid 挂载
    -O和-a 选项一起使用,限制其所作用的文件系统
    -o给文件系统添加特定的选项
  • mount 命令的 -o 选项允许在挂载文件系统时添加一系列以逗号分隔的额外选项。常用选项如下。

    • ro:以只读形式挂载。
    • rw:以读写形式挂载。
    • user:允许普通用户挂载该文件系统。
    • check=none:挂载文件系统时不执行完整性校验。
    • loop:挂载文件
  • 移除可移动设备时,不能直接将设备拔下,应该先卸载。卸载设备的命令是 umount。

  • Linux 不允许直接弹出已挂载的 CD 或 DVD 。如果在从光驱中移除 CD 或 DVD 时遇到麻烦,那么最大的可能是它还在虚拟目录中挂载着。应该先卸载, 然后再尝试弹出。

  • umount 命令的格式:

    umount [directory | device ]
    
  • umount 命令支持通过设备文件或者挂载点来指定要卸载的设备。如果有任何程序正在使用设备上的文件,则系统将不允许卸载该设备。

  • 如果在卸载设备时,系统提示设备繁忙,无法卸载,那么通常是有进程还在访问该设备或使用该设备上的文件。这时可用 lsof 命令获得相关进程的信息,然后将进程终止。 lsof 命令的用法很简单: lsof /path/to/device/node, 或者 lsof /path/to/mount/point。

  1. 使用 df 命令
  • df 命令可以方便地查看所有已挂载磁盘的使用情况。
  • df 命令会逐个显示已挂载的文件系统。与 mount 命令类似, df 命令会输出内核挂载的所有虚拟文件系统,因此可以使用-t 选项来指定文件系统类型, 进而过滤输出结果。该命令的输出如下。
    • 设备文件位置
    • 包含多少以 1024 字节为单位的块
    • 使用了多少以 1024 字节为单位的块
    • 还有多少以 1024 字节为单位的块可用
    • 已用空间所占的百分比
    • 设备挂载点
  • df 命令的常用选项之一是-h,该选项会以人类易读(human-readable)的形式显示磁盘空间,通常用 M 来替代兆字节, 用 G 来替代吉字节。
  • Linux 系统后台一直有进程在处理文件。 df命令的输出值反映的是Linux 系统认为的当前值。正在运行的进程有可能创建或删除了某个文件,但尚未释放该文件。这个值是不会被计算进闲置空间的。
  1. 使用 du 命令
  • du 命令可以显示某个特定目录(默认情况下是当前目录)的磁盘使用情况。这有助于你快速判断系统中是否存在磁盘占用“大户”。
  • 在默认情况下,du 命令会显示当前目录下所有的文件、目录和子目录的磁盘使用情况,并以磁盘块为单位来表明每个文件或目录占用了多大存储空间。对标准大小的目录来说,输出内容可不少。
  • 单纯的 du 命令的执行结果中,每行最左侧的数字是每个文件或目录所占用的磁盘块数。注意,这个列表是从目录层级的最底部开始,然后沿着其中包含的文件和子目录逐级向上的。
  • 单纯的 du 命令作用并不大。下面这些选项能让 du 命令的输出更加清晰易读。
    -c:显示所有已列出文件的总大小。
    -h:按人类易读格式输出大小,分别用 K 表示千字节、 M 表示兆字节、 G 表示吉字节。
    -s:输出每个参数的汇总信息。
  1. 数据排序
  • 处理大量数据时的一个常用命令是 sort。sort 可以轻松地对大数据文件进行排序。

  • 在默认情况下,sort 命令会依据会话所指定的默认语言的排序规则来对文本文件中的数据行进行排序。

  • 在默认情况下,sort 命令会将数字视为字符并执行标准的字符排序,这种结果可能不是你想要的。可以使用-n 选项来解决这个问题,该选项会告诉 sort 命令将数字按值排序。

  • 另一个常用的选项是-M,该选择可以将数字按月排序,sort 命令就能识别三字符的月份名(例如,Jan、Feb等)并正确排序。 Linux 的日志文件经常在每行的起始位置有一个时间戳,以表明事件是什么时候发生的。

  • sort 命令选项

    短选项长选项描述
    -b–ignore-leading-blanks排序时忽略起始的空白字符
    -C–check=quiet不排序, 如果数据无序也不要报告
    -c–check不排序, 但检查输入数据是否有序,无序的话就报告
    -d–dictionary-order仅考虑空白字符和字母数字字符,不考虑特殊字符
    -f–ignore-case大写字母默认先出现,该选项会忽略大小写
    -g–general-numeric-sort使用一般数值进行排序
    -i–ignore-nonprinting在排序时忽略不可打印字符
    -k–key=POS1 [,POS2]排序键从 POS1 位置开始, 到 POS2 位置结束(如果指定了 POS2 的话)
    -M–month-sort用三字符的月份名按月份排序
    -m–merge合并两个已排序数据文件
    -n–numeric-sort将字符串按数值意义排序
    -o–output=file将排序结果写入指定文件
    -R–random-sort根据随机哈希排序
    -R–random-source=FILE指定-R 选项用到的随机字节文件
    -r–reverse逆序排序(升序变成降序)
    -S–buffer-size=SIZE指定使用的内存大小
    -s–stable禁止 last-resort 比较,实现稳定排序
    -T–temporary-directory=DIR指定用于保存临时工作文件的目录
    -t–field-separator=SEP指定字段分隔符
    -u–unique和-c 选项合用时,检查严格排序;不和-c 选项合用时,相同行仅输出一次
    -z–zero-terminated在行尾使用 NULL 字符代替换行符
    • sort -u 等同于 sort | uniq
    • 在对按字段分隔的数据(比如/etc/passwd 文件)进行排序时, -k 选项和-t 选项非常方便。先使用-t 选项指定字段分隔符,然后使用-k 选项指定排序字段。例如,要根据用户 ID 对/etc/ passwd 按数值排序, 可以这么做:
      $ sort -t ':' -k 3 -n /etc/passwd
      root:x:0:0:root:/root:/bin/bash
      bin:x:1:1:bin:/bin:/sbin/nologin
      daemon:x:2:2:daemon:/sbin:/sbin/nologin
      adm:x:3:4:adm:/var/adm:/sbin/nologin
      lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
      sync:x:5:0:sync:/sbin:/bin/sync
      ...
      
      • 现在数据已经按第三个字段(用户 ID 的数值)排序妥当了。
    • -n 选项适合于排序数值型输出,比如 du 命令的输出:
      $ du -sh * | sort -nr
      1008k   mrtg-2.9.29.tar.gz
      972k    bldg1
      888k    fbs2.pdf
      760k    Printtest
      ...
      
      • -r 选项对数值按照降序排列,这样便能轻而易举地看出目录中的哪些文件占用磁盘空间最多。
      • 管道命令(|)用于将 du 命令的输出传入 sort 命令。
  1. 数据搜索
  • grep 命令的格式如下:
    grep [options] pattern [file]
    
  • grep 命令会在输入或指定文件中逐行搜索匹配指定模式的文本。该命令的输出是包含了匹配模式的行。
  • 如果要进行反向搜索(输出不匹配指定模式的行),可以使用-v 选项。
  • 如果要显示匹配指定模式的那些行的行号,可以使用-n 选项。
  • 如果只想知道有多少行含有匹配的模式, 可以使用-c 选项。
  • 如果要指定多个匹配模式,可以使用-e 选项来逐个指定。
  • 在默认情况下, grep 命令使用基本的 Unix 风格正则表达式来匹配模式。 Unix 风格正则表达式使用特殊字符来定义如何查找匹配模式。
    $ grep [tf] file1
    
    • 正则表达式中的方括号表明 grep 应该搜索包含 t 字符或者 f 字符的匹配。如果不用正则表达式, 则 grep 搜索的是匹配字符串 tf 的文本。
  • egrep 命令是 grep 的一个衍生, 支持 POSIX 扩展正则表达式,其中包含更多可用于指定匹配模式的字符。
  • fgrep 则是另外一个版本,支持将匹配模式指定为以换行符分隔的一系列固定长度的字符串。这样就可以将这些字符串放入一个文件中,然后在 fgrep 命令中使用其搜索大文件中的字符串。
  1. 数据压缩
  • Linux 文件压缩工具

    工具文件扩展名描述
    bzip2.bz2采用 Burrows-Wheeler 块排序文本压缩算法和霍夫曼编码
    compress.Z最初的 Unix 文件压缩工具,已经快要无人使用了
    gzip.gzGNU 压缩工具,用 Lempel-Zivwelch 编码
    xz.xz日渐流行的通用压缩工具
    zip.zipWindows 中 PKZIP 工具的 Unix 实现
  • gzip 软件包包括以下文件。

    • gzip :用于压缩文件。
    • gzcat :用于查看压缩过的文本文件的内容。
    • gunzip :用于解压文件。
  • gzip 命令会压缩命令行中指定的文件。也可以指定多个文件名或是用通配符来一次性压缩多个文件,gzip 命令会压缩该目录中匹配通配符的每个文件。

  1. 数据归档
  • tar 命令的格式如下。

    tar function [options] object1 object2 ...
    
  • tar 命令的操作

    操作长选项描述
    -A–concatenate将一个 tar 归档文件追加到另一个 tar 归档文件末尾
    -c–create创建新的 tar 归档文件
    -d–diff检查归档文件和文件系统的不同之处
    –delete从 tar 归档文件中删除文件
    -r–append将文件追加到 tar 归档文件末尾
    -t–list列出 tar 归档文件的内容
    -u–update将比 tar 归档文件中已有的同名文件更新的文件追加到该归档文件
    -x–extract从 tar 归档文件中提取文件
  • 每种操作都使用 option(选项)来定义针对 tar 归档文件的具体行为。

  • tar 命令选项

    选项描述
    -C dir切换到指定目录
    -f file将结果输出到文件(或设备)
    -j将输出传给 bzip2 命令进行压缩
    -J将输出传给 xz 命令进行压缩
    -p保留文件的所有权限
    -v在处理文件时显示文件名
    -z将输出传给 gzip 命令进行压缩
    -Z将输出传给 compress 命令进行压缩
    • 这些选项经常合并使用。可以用下列命令创建归档文件:
      // 该命令创建了一个名为 test.tar 的归档文件,包含目录 test和 test2 的内容
      tar -cvf test.tar test/ test2/
      
      // 该命令列出了(但不提取)tar 文件 test.tar 的内容
      tar -tf test.tar
      
      // 该命令从 tar 文件 test.tar 中提取内容。如果创建的时候 tar 文件含有目录结构,则在当前目 录中重建该目录的整个结构。
      tar -xvf test.tar
      
    • tar 命令可以轻松地为整个目录结构创建归档文件。这是在 Linux 中分发开源程序源代码文件所采用的普遍方法。
  • 在下载开源软件时经常会看到文件名以.tgz 结尾,这是经 gzip压缩过的 tar 文件,可以用命令 tar -zxvf filename.tgz 来提取其中的内容。


五、理解 shell
  1. shell 的类型
  • 当你登录系统时,系统启动什么样的 shell 程序取决于你的个人用户配置。在/etc/passwd 文件中,用户记录的第 7 个字段中列出了该用户的默认 shell 程序。只要用户登录某个虚拟控制台终端或是在 GUI 中启动终端仿真器,默认的 shell 程序就会启动。
  • 在现代 Linux 系统中, bash shell 程序(bash)通常位于/usr/bin 目录(也有可能位于/bin 目录)。which bash 命令可以帮助我们找出bash shell 的位置。
  • 长列表中文件名尾部的星号( * )表明 bash 文件(bash shell)是一个可执行程序。
  • 在大多数 Linux 系统中, /etc/shells 文件中列出了各种已安装的 shell ,这些 shell 可以作为用 户的默认 shell。
  • 在很多 Linux 发行版中,你会发现 shell 文件似乎存在于两个位置: /bin 和/usr/bin。这是因为在现代 Linux 系统中, /bin 是指向/usr/bin 的符号链接。
  • 默认的交互式 shell(default interactive shell) 也称登录shell(login shell), 只要用户登录某个虚拟控制台终端或是在 GUI 中启动终端仿真器,该 shell 就会启动。作为默认的系统 shell(default system shell), sh(/bin/sh)用于那些需要在启动时使用的系统 shell 脚本。
    • CentOS 发行版使用软链接将默认的系统 shell指向 bash shell。
    • Ubuntu 发行版默认的系统 shell(/usr/bin/sh )指向的是 dash shell。
    • 对 bash shell 脚本来说,这两种 shell (默认的交互 shell 和默认的系统 shell)可能会导致问题。
  • 命令 echo $0 会显示当前 shell 的名称。
    • 使用 echo $0 显示当前所用 shell 的做法仅限在 shell 命令行中使用。如果在 shell 脚本中 使用,则显示的是该脚本的名称。
      $ echo $0 
      -bash
      $ dash
      $ echo $0
      dash
      $ exit
      $ echo $0
      -bash
      
      • echo $0 命令的输出: bash 之前有一个连字符(-)。 表明该 shell 是用户的登录 shell。
      • 输入命令 exit 就可以退出 dash shell 程序。
  1. shell 的父子关系
  • 用户登录某个虚拟控制台终端或在 GUI 中运行终端仿真器时所启动的默认的交互式 shell (登录 shell )是一个父 shell。
  • 当你在 CLI 提示符处输入 bash 命令(或是其他 shell 程序名)时, 会创建新的 shell 程序。 这是一个子 shell。
  • 子 shell 的父进程 ID(PPID)就是父 shell 进程 ID(可以通过在生成子 shell 的前后使用 ps -f 命令查看)。
  • 在生成子 shell 进程时, 只有部分父进程的环境被复制到了子 shell环境中。这会对包括变量 在内的一些东西造成影响。
  • 子 shell 既可以从父 shell 中创建, 也可以从另一个子 shell 中创建。
    • 可以通过在连续多次生成子 shell 的后使用 ps --forest 命令查看。
    • ps -f 命令也能够表现子 shell 间的嵌套关系, 因为它会通过 PPID 列显示出谁是谁的父进程。
  • bash shell 程序可以使用命令行选项来修改 shell 的启动方式。
  • bash 的命令行选项:
    选项描述
    -c string从 string 中读取命令并进行处理
    -i启动一个能够接收用户输入的交互式 shell
    -l作为登录 shell 启动
    -r启动一个受限 shell,将用户限制在默认目录中
    -s从标准输入中读取命令
  • 可以输入 man bash 获得关于 bash 命令的更多帮助信息,了解更多的命令行选项。 bash --help 命令也会提供一些额外的协助。
  • 如果想查看 bash shell 的版本号,在命令行中输出 bash --version 即可。该命令不会创建子 shell,只会显示系统中 GNU bash shell 程序的当前版本。
  • 可以使用 exit 命令有条不紊地退出子 shell。exit 命令不仅能够退出子 shell,还可以注销(log out)当前的虚拟控制台终端或终端仿真器软件。只需在父 shell 中输入 exit,就能从容退出 CLI 了。
  • 有时运行 shell 脚本也会创建子 shell。
  1. 查看进程列表
  • 可以在单行中指定要依次运行的一系列命令。这可以通过命令列表来实现,只需将命令之间以分号(;)分隔即可。
    $ pwd ; ls test* ; cd /etc ; pwd ; cd ; pwd ; ls my*
    
    • 在上面的例子中,所有命令依次执行。不过这并不是进程列表。
  • 要想成为进程列表,命令列表必须将命令放入圆括号内:
    $ (pwd ; ls test* ; cd /etc ; pwd ; cd ; pwd ; ls my*)
    
    • 圆括号的加入使命令列表摇身变成了进程列表,生成了一个子 shell 来执行这些命令。
  • 进程列表是命令分组(command grouping)的一种。另一种命令分组是将命令放入花括号内,并在命令列表尾部以分号( ; )作结。语法为: { command; }。使用花括号进行命令分组并不会像进程列表那样创建子 shell。
  • 要想知道是否生成了子 shell,需要使用命令输出一个环境变量(参见第 6 章)的值。这个命令就是 echo $BASH_SUBSHELL。如果该命令返回 0,那么表明没有子 shell。如果该命令返回 1 或者其他更大的数字, 则表明存在子 shell。
  • 进程列表就是使用圆括号包围起来的一组命令,它能够创建子 shell 来执行这些命令。甚至可以在进程列表中嵌套圆括号来创建子 shell 的子 shell。
  • 子 shell 在 shell 脚本中经常用于多进程处理。但是,创建子 shell 的成本不菲(意思是要消耗更多的资源,比如内存和处理能力),会明显拖慢任务进度。在交互式 CLI shell 会话中, 子shell 同样存在问题,它并非真正的多进程处理,原因在于终端与子 shell 的 I/O 绑定在了一起。
  1. 在交互式 shell 中, 一种高效的子 shell 用法是后台模式。

  2. sleep 命令会接受一个参数作为希望进程等待(睡眠)的秒数。该命令在 shell 脚本中常用于引入一段暂停时间。命令 sleep 10 会将会话暂停 10 秒,然后返回 shell CLI 提示符。

    $ sleep 3000&
    [1] 2542
    $ ps -f
    UID        PID  PPID  C STIME TTY   TIME CMD
    christi+  2356  2352  0 13:27 pts/0 00:00:00 -bash
    christi+  2542  2356  0 13:44 pts/0 00:00:00 sleep 3000
    christi+  2543  2356  0 13:44 pts/0 00:00:00 ps -f
    
    • 要想将命令置入后台模式, 可以在命令末尾加上字符&。
    • sleep 命令会在后台(&)睡眠 3000 秒。当其被置入后台时,在 shellCLI 提示符返回之前,屏幕上会出现两条信息。第一条信息是方括号中的后台作业号( 1)。第二条信息是后台作业的进程 ID( 2542)。
    • 除了 ps 命令,也可以使用 jobs 命令来显示后台作业信息。 jobs 命令能够显示当前运行在 后台模式中属于你的所有进程(作业)。
      $ jobs
      [1]+  Running                   sleep 3000 &
      
      $ jobs -l
      [1]+  2542 Running              sleep 3000 &
      
      $
      [1]+  Done                      sleep 3000
      
      • jobs 命令会在方括号中显示作业号(1)。除此之外,还有作业的当前状态(Running)以 及对应的命令(sleep 3000 &)。
      • 利用 jobs 命令的-l (小写字母 l)选项,还可以看到更多的相关信息。除了默认信息, -l 选项还会显示命令的 PID。
      • 如果运行多个后台进程, 则还有一些额外信息可以显示哪个后台作业是最近启动的。在 jobs 命令的显示中, 最近启动的作业在其作业号之后会有一个加号(+),在它之前启 动的进程(the second newest process)则以减号(-)表示。
      • 一旦后台作业完成,就会显示出结束状态。
  3. 通过将进程列表置入后台,可以在子 shell 中进行大量的多进程处理。由此带来的一个好处是终端不再和子 shell 的 I/O 绑定在一起。

  • 将进程列表置入后台会产生一个作业号和进程 ID,然后会返回提示符。
  • 在后台使用进程列表可谓是在 CLI 中运用子 shell 的一种创造性方法。可以用更少的键盘输入换来更高的效率。
  • 使用tar(参见第 4 章) 创建备份文件是有效利用后台进程列表的一个更实用的例子:
    $ (tar -cf Doc.tar Documents ; tar -cf Music.tar Music)&
    [1] 2567
    $
    $ ls *.tar
    Doc.tar Music.tar
    [1]+ Done       ( tar -cf Doc.tar Documents; tar -cf Music.tar Music )
    $
    
  1. 将进程列表置入后台并不是子 shell在 CLI 中仅有的创造性用法,还有一种方法是协程。
  • 协程同时做两件事:一是在后台生成一个子 shell,二是在该子 shell 中执行命令。
  • 要进行协程处理,可以结合使用 coproc 命令以及要在子 shell 中执行的命令:
    $ coproc sleep 10
    [1] 2689
    $ jobs
    [1]+  Running                   coproc COPROC sleep 10 &
    
    $ coproc My_Job { sleep 10; }
    $ jobs
    [1]+ Running                    coproc My_Job { sleep 10; } &
    
    • 除了会创建子 shell,协程基本上就是将命令置入后台。当输入 coproc 命令及其参数之后, 你会发现后台启用了一个作业。屏幕上会显示该后台作业号(1)以及进程 ID(2689)。
    • jobs 命令可以显示协程的状态。
    • 可以看到,在子 shell 中执行的后台命令是 coproc COPROC sleep 10。
    • COPROC 是 coproc 命令给进程起的名字。可以使用命令的扩展语法来自己设置这个名字。使用扩展语法,协程名被设置成了 My_Job。
    • 注意,扩展语法写起来有点儿小麻烦。 你必须确保在左花括号( {)和内部命令名之间有一个空格。还必须保证内部命令以分号(;)结 尾。另外,分号和右花括号( } )之间也得有一个空格。
  • 只有在拥有多个协程时才需要对协程进行命名, 因为你要和它们进行通信。否则的话, 让 coproc 命令 将其设置成默认名称 COPROC 即可。
  • 可以将协程与进程列表结合起来创建嵌套子 shell。只需将命令 coproc 放在进程列表之前即可。
  • 记住,生成子 shell 的成本可不低,而且速度很慢。创建嵌套子 shell更是火上浇油。
  1. 外部命令
  • 外部命令(有时也称为文件系统命令)是存在于 bash shell 之外的程序。也就是说,它并不 属于 shell 程序的一部分。外部命令程序通常位于/bin 、/usr/bin 、/sbin 或/usr/sbin 目录中。
  • ps 命令就是一个外部命令。可以使用 which 命令和 type 命令找到其对应的文件名。
  • 每当执行外部命令时,就会创建一个子进程。这种操作称为衍生(forking)。外部命令 ps 会显示其父进程以及自己所对应的衍生子进程。
  • 只要涉及进程衍生, 就需要耗费时间和资源来设置新子进程的环境。因此, 外部命令系统开销较高。
  • 无论是衍生出子进程还是创建了子 shell,都仍然可以通过信号与其互通,这一点无论是在使用命令行还是在编写脚本时都极其有用。进程间以发送信号的方式彼此通信。
  • 在使用内建命令时,不需要衍生子进程。因此,内建命令的系统开销较低。
  1. 内建命令
  • 与外部命令不同, 内建命令无须使用子进程来执行。内建命令已经和 shell 编译成一体, 作 为 shell 的组成部分存在, 无须借助外部程序文件来执行。
  • cd 命令和 exit 命令都内建于 bash shell。可以使用 type 命令来判断某个命令是否为内建。
  • 因为内建命令既不需要通过衍生出子进程来执行,也不用打开程序文件,所以执行速度更快, 效率也更高。
  • 注意, 有些命令有多种实现。例如, echo 和 pwd 既有内建命令也有外部命令。两种实现略 有差异。要查看命令的不同实现,可以使用 type 命令的-a 选项。
  • 注意, which 命令只显示外部 命令文件。
  • 对于有多种实现的命令,如果想使用其外部命令实现,直接指明对应的文件即可。例如,要使用外部命令 pwd,可以输入/usr/bin/pwd。
  • 使用 history 命令
    • bash shell 会跟踪你最近使用过的命令。history是一个实用的内建命令,能帮助你管理先前执行过的命令。
    • 要查看最近用过的命令列表,可以使用不带任何选项的 history 命令。
    • 可以设置保存在 bash 历史记录中的命令数量。为此,需要修改名为 HISTSIZE 的环境变量。
    • 输入 !!, 然后按 Enter 键,唤回并重用最近那条命令。当输入!!时, bash 会先显示从 shell 的历史记录中唤回的命令, 然后再执行该命令。
    • 命令历史记录被保存在位于用户主目录的隐藏文件.bash_history之中。
    • 在 CLI 会话期间, bash 命令的历史记录被保存在内存中。当shell 退出时才被写入历史文件。
    • 可以在不退出 shell 的情况下强制将命令历史记录写入.bash_history 文件。为此,需要使用 history 命令的-a 选项。
    • 如果打开了多个终端会话,则仍然可以使用 history -a 命令在每个打开的会话中向 .bash_history 文件添加记录。但是历史记录并不会在其他打开的终端会话中自动更新。 这是因为.bash_history 文件只在首次启动终端会话的时候才会被读取。要想强制重新读取.bash_history 文件,更新内存中的终端会话历史记录,可以使用history -n 命令。
    • 可以唤回历史记录中的任意命令。只需输入惊叹号和命令在历史记录中的编号即可。和执行最近的命令一样, bash shell 会先显示从历史记录中唤回的命令, 然后再执行该命令。
    • 如果需要清除命令历史,很简单,输入 history -c 即可。接下来再输入 history-a,清除 .bash_history 文件。
    • 可以通过 man history 来查看 history 命令的 bash手册页。
  • 使用命令别名
    • alias 命令是另一个实用的 shell 内建命令。命令别名允许为常用命令及其参数创建另一个名称,从而将输入量减少到最低。
    • 使用 alias 命令以及选项-p 可以查看当前可用的别名。
    • alias ls=‘ls --color=auto’
      • 该别名中加入了 --color=auto 选项,以便在终端支持彩色显示的情况下, ls 命令可以使用色彩编码(比如, 使用蓝色表示目录)。LS_COLORS 环境变量(环境变量的相关内容参见第 6 章) 控制着所用的色彩编码。
      • 如果经常跳转于不同的发行版,那么在使用色彩编码来分辨某个名称究竟是目录还是文件时,一定要小心。因为色彩编码并未实现标准化, 所以最好使用 ls -F 来判断文件类型。
    • 可以使用 alias 命令创建自己的别名:
      $ alias li='ls -i'
      
      • 要注意,因为命令别名属于内建命令,所以别名仅在其被定义的 shell 进程中才有效。
      • 如果需要,可以在命令行中输入 unalias alias-name 删除指定的别名。记住, 如果被删除的别名不是你设置的, 那么等下次重新登录系统的时候, 该别名就会再次出现。可 以通过修改环境文件永久地删除某个别名。

六、Linux 环境变量
  1. bash shell 使用环境变量来存储 shell 会话和工作环境的相关信息(这也是被称作环境变量的原因)。环境变量允许在内存中存储数据, 以便 shell 中运行的程序或脚本能够轻松访问到这些数据。这也是存储持久数据的一种简便方法。

  2. bash shell 中有两种环境变量。

    • 全局变量
    • 局部变量
  • 尽管 bash shell 使用的专有环境变量是一致的,但不同的 Linux 发行版经常会添加自己的环境变量。
  1. 全局环境变量
  • 全局环境变量对于 shell 会话和所有生成的子 shell 都是可见的。局部环境变量则只对创建它的 shell 可见。如果程序创建的子 shell需要获取父 shell 信息,那么全局环境变量就能派上用场了。
  • Linux 系统在你启动 bash 会话时就设置好了一些全局环境变量。系统环境变量基本上会使用全大写字母, 以区别于用户自定义的环境变量。
  • 可以使用 env 命令或 printenv 命令来查看全局变量。
  • 要显示个别环境变量的值, 可以使用 printenv 命令, 但不要使用env 命令,例如 $ printenv HOME。
  • 也可以使用 echo 命令显示变量的值。在引用某个环境变量时, 必须在该变量名前加上美元 符号( $),例如 $ echo $HOME。
  • 使用 echo 命令时,在变量名前加上$可不仅仅是能够显示变量当前的值,它还能让变量作 为其他命令的参数,例如 $ ls $HOME。
  1. 局部环境变量
  • 局部环境变量只能在定义它的进程中可见。事实上,Linux 系统默认也定义了标准的局部环境变量。不过, 也可以定义自己的局部变量,这些变量被称为用户自定义局部变量。
  • set 命令可以显示特定进程的所有环境变量,既包括局部变量、全局变量,也包括用户自定义变量。
  • 所有通过 env 命令或 printenv 命令能看到的全局环境变量都出现在了set 命令的输出中。除此之外, 还包括局部环境变量、用户自定义变量以及局部 shell 函数(比如_command 函数)。
  • set 命令既会显示全局和局部环境变量、用户自定义变量以及局部 shell 函数,还会按照字母顺序对结果进行排序。 与 set 命令不同, env 命令和 printenv 命令既不会对变量进行排序,也不会输出局部环境变量、局部用户自定义变量以及局部 shell 函数。在这种情况下, env 命令和 printenv命令的输出是重复的。
  1. 设置局部用户自定义变量
  • 启动 bash shell (或者执行 shell 脚本) 之后, 就能创建仅对该 shell 进程可见的局部用户自定 义变量。可以使用等号为变量赋值, 值可以是数值或字符串:
    $ my_variable=Hello
    $ echo $my_variable
    Hello
    
    $ my_variable="Hello World"
    $ echo $my_variable
    Hello World
    
    • 如果要引用 my_variable 变量的值, 使用$my_variable 即可。
    • 如果用于赋值的字符串包含空格,则必须用单引号或双引号来界定该字符串的起止。
    • 如果没有引号,则 bash shell 会将下一个单词(World)视为另一个要执行的命令。
    • 注意,你定义的局部变量用的是小写字母,而系统环境变量用的都是大写字母。
    • 记住, 在变量名、等号和值之间没有空格, 这一点非常重要。如果在赋值表达式中加上了空 格,那么 bash shell 会将值视为单独的命令。
  • bash shell 的惯例是所有的环境变量均使用大写字母命名。如果是你自己创建或在 shell 脚本中使用的局部变量,则使用小写字母命名。变量名区分大小写。坚持使用小写字母命 名用户自定义的局部变量, 能够让你避免不小心与系统环境变量同名可能带来的灾难。
  • 设置好局部变量后,就能在 shell 进程中随意使用了。但如果又生成了另一个 shell,则该变量在子 shell 中不可用。
  • 类似地,如果在子进程中设置了一个局部变量, 那么一旦退出子进程,该局部变量就不能用了。返回父 shell 后,子 shell 中设置的局部变量就不存在了。可以通过将局部用户自定义变量改为全局变量来解决这个问题。
  1. 设置全局环境变量
  • 全局环境变量在设置该变量的父进程所创建的子进程中都是可见的。
  • 创建全局环境变量的方 法是先创建局部变量,然后再将其导出到全局环境中。这可以通过 export 命令以及要导出的变量名(不加$符号)来实现:
    $ my_variable="I am Global now"
    $ export my_variable
    
    或者:可以将设置变量和导出变量放在一个命令里完成。
    $ export my_variable="I am Global Now"
    
  • 修改子 shell 中的全局环境变量并不会影响父 shell 中该变量的值。子 shell 甚至无法使用 export 命令改变父 shell 中全局环境变量的值。
  1. 删除环境变量
  • 可以用 unset 命令删除已有的环境变量。在 unset 命令中引用环境变量时,记住不要使用$。
  • 如果要用到变量,就使用 ;如果要操作变量,则不使用 ;如果要操作变量,则不使用 ;如果要操作变量,则不使用。这条规则的一个例外是使用printenv显示某个变量的值。
  • 和修改变量一样,在子 shell 中删除全局变量后, 无法将效果反映到父 shell 中。
  1. 默认的 shell 环境变量
  • bash shell 与 Unix Bourne shell 兼容的环境变量。bash shell 支持的 Bourne 变量

    变量描述
    CDPATH以冒号分隔的目录列表, 作为 cd 命令的搜索路径
    HOME当前用户的主目录
    IFSshell 用来将文本字符串分割成字段的若干字符
    MAIL当前用户收件箱的文件名(bash shell 会检查这个文件来确认有没有新邮件)
    MAILPATH以冒号分隔的当前用户收件箱的文件名列表(bash shell 会检查列表中的每个文件来确认有没有新邮件)
    OPTARG由 getopt 命令处理的最后一个选项参数
    OPTIND由 getopt 命令处理的最后一个选项参数的索引
    PATHshell 查找命令时使用的目录列表, 以冒号分隔
    PS1shell 命令行的主提示符
    PS2shell 命令行的次提示符
  • 除了默认的 Bourne 环境变量, bash shell 还提供一些自有的变量。bash shell 环境变量

    变量描述
    BASHbash shell 当前实例的完整路径名
    BASH_ALIASES关联数组,包含当前已设置的别名
    BASH_ARGC数组变量,包含传入函数或 shell 脚本的参数个数
    BASH_ARCV数组变量,包含传入函数或 shell 脚本的参数
    BASH_ARCV0包含 shell 的名称或 shell 脚本的名称(如果在脚本中使用的话)
    BASH_CMDS关联数组,包含 shell 已执行过的命令的位置
    BASH_COMMAND正在执行或将要执行的 shell 命令
    BASH_COMPAT指定 shell 兼容级别的值
    BASH_ENV如果设置的话, bash 脚本会在运行前先尝试运行该变量定义的启动文件
    BASH_EXECUTION_STRING使用 bash 命令的-c 选项传递过来的命令
    BASH_LINENO数组变量,包含当前正在执行的 shell 函数在源文件中的行号
    BASH_LOADABLE_PATH以冒号分隔的目录列表, shell 会在其中查找可动态装载的内建命令
    BASH_REMATCH只读数组变量, 在使用正则表达式的比较运算符=~进行肯定匹配(positive match)时, 包含整个模式及子模式所匹配到的内容
    BASH_SOURCE数组变量,包含当前正在执行的 shell 函数所在的源文件名
    BASH_SUBSHELL当前子 shell 环境的嵌套级别(初始值是 0 )
    BASH_VERSINFO数组变量,包含 bash shell 当前实例的主版本号和次版本号
    BASH_VERSIONbash shell 当前实例的版本号
    BASH_XTRACEFD如果设置为有效的文件描述符( 0、1、2),则 'set -x’调试选项生成的跟踪输出可被 重定向。通常用于将跟踪信息输出到文件中
    BASHOPTS当前启用的 bash shell选项
    BASHPID当前 bash进程的 PID
    CHILD_MAX设置 shell 能够记住的已退出子进程状态的数量
    COLUMNSbash shell 当前实例所用的终端显示宽度
    COMP_CWORD变量 COMP_WORDS 的索引,其中包含当前光标的位置
    COMP_LINE当前命令行
    COMP_POINT相对于当前命令起始处的光标位置索引
    COMP_KEY用来调用 shell 函数补全功能的最后一个按键
    COMP_TYPE一个整数值,指明了用以完成 shell 函数补全所尝试的补全类型
    COMP_WORDBREAKSReadline 库中用于单词补全的分隔符
    COMP_WORDS数组变量,包含当前命令行所有单词
    COMPREPLY数组变量,包含由 shell 函数生成的可能的补全代码
    COPROC数组变量,包含用于匿名协程 I/O 的文件描述符
    DIRSTACK数组变量,包含目录栈的当前内容
    EMACS设置为’t’时,表明 emacs shell 缓冲区正在工作,行编辑功能被禁止
    EPOCHREALTIME包含自 Unix 纪元时( 1970 年 1 月 1 日 00:00:00 UTC )以来的秒数,包括微秒
    EPOCHSECONDS包含自 Unix 纪元时( 1970 年 1 月 1 日 00:00:00 UTC )以来的秒数,不包括微秒
    ENV如果设置,则会在 bash shell 脚本运行之前先执行已定义的启动文件(仅当 bash shell 以 POSIX 模式被调用时)
    EUID当前用户的有效用户 ID (数字形式)
    EXECIGNORE以冒号分隔的过滤器列表, 在使用 PATH 搜索命令时, 用于决定要忽略的可执行文件 (比如共享库文件)
    FCEDIT供 fc 命令使用的默认编辑器
    FIGNORE在进行文件名补全时可以忽略后缀名列表,以冒号分隔
    FUNCNAME当前正在执行的 shell 函数的名称
    FUNCNEST当设置成非 0 值时, 表示所允许的函数最大嵌套级数(一旦超出, 当前命令即被终止)
    GLOBIGNORE以冒号分隔的模式列表,定义了在进行文件名扩展时可以忽略的一组文件名
    GROUPS数组变量,包含当前用户的属组
    histchars控制历史记录扩展,最多可有 3 个字符
    HISTCMD当前命令在历史记录中的编号
    HISTCONTROL控制哪些命令留在历史记录列表中
    HISTFILE保存 shell 历史记录的文件名(默认是.bash_history)
    HISTFILESIZE历史记录文件(history file)能保存的最大命令数量
    HISTIGNORE以冒号分隔的模式列表, 用于决定忽略历史文件中的哪些命令
    HISTSIZE能写入历史记录列表( history list)的最大命令数量①
    HISTTIMEFORMAT如果设置且不为空,则作为格式化字符串,用于打印 bash 历史记录中命令的时间戳
    HOSTFILEshell 在补全主机名时读取的文件名
    HOSTNAME当前主机的名称
    HOSTTYPE字符串, 用于描述当前运行 bash shell 的机器
    IGNOREEOFshell 在退出前必须连续接收到的 EOF字符数量(如果该值不存在, 则默认为 1 )
    INPUTRCReadline 的初始化文件名(默认为.inputrc)
    INSIDE_EMACS仅当进程在 Emacs 编辑器的缓冲区中运行时才设置, 并且可以禁用行编辑(行编辑的 禁用也取决于 TERM 变量的值)
    LANGshell 的语言环境种类(locale category)
    LC_ALL定义语言环境种类,能够覆盖 LANG 变量
    LC_COLLATE设置字符串排序时采用排序规则
    LC_CTYPE决定如何解释出现在文件名扩展和模式匹配中的字符
    LC_MESSAGES决定在解释前面带有$的双引号字符串时采用的语言环境设置
    LC_NUMERIC决定格式化数字时采用的语言环境设置
    LC_TIME决定格式化日期和时间时采用的语言环境设置
    LINENO当前正在执行的脚本语句的行号
    LINES定义了终端上可见的行数
    MACHTYPE用“CPU–公司–系统”(CPU-company-system)格式定义的系统类型
    MAILCHECKshell 应该多久检查一次新邮件(以秒为单位,默认为 60 秒)
    MAPFILE数组变量,当未指定数组变量作为参数时,其中保存了 mapfile 所读入的文本
    OLDPWDshell 先前使用的工作目录
    OPTERR如果设置为 1 ,则 bash shell会显示 getopts 命令产生的错误
    OSTYPE定义了 shell 所在的操作系统
    PIPESTATUS数组变量,包含前台进程的退出状态
    POSIXLY_CORRECT如果设置的话, bash 会以 POSIX 模式启动
    PPIDbash shell父进程的 PID
    PROMPT_COMMAND如果设置的话,在显示命令行主提示符之前执行该命令
    PROMPT_DIRTRIM用来定义使用提示符字符串\w 和\W转义时显示的拖尾(trailing)目录名的数量(使用 一组英文句点替换被删除的目录名)
    PS0如果设置的话,指定了在输入命令之后、执行命令之前由交互式 shell 显示的内容
    PS3select 命令的提示符
    PS4在命令行之前显示的提示符(如果使用了 bash的-x 选项的话)
    PWD当前工作目录
    RANDOM返回一个 0 ~ 32 767 的随机数(对该变量的赋值可作为随机数生成器的种子)
    READLINE_LINE当使用 bind –x 命令时, 保存 Readline 缓冲区的内容
    READLINE_POINT当使用 bind –x 命令时, 指明了Readline 缓冲区内容插入点的当前位置
    REPLYread 命令的默认变量
    SECONDS自 shell 启动到现在的秒数(对其赋值会重置计数器)
    SHELLbash shell 的完整路径名
    SHELLOPTS以冒号分隔的已启用的 bash shell选项
    SHLVLshell 的层级,每启动一个新的 bash shell,该值增加 1
    TIMEFORMAT指定了 shell 的时间显示格式
    TMOUTselect 命令和 read 命令在无输入的情况下等待多久(以秒为单位,默认值为 0 ,表 示一直等待)
    TMPDIR目录名, 保存 bash shell 创建的临时文件
    UID当前用户的真实用户 ID (数字形式)
  • HISTFILESIZE 和 HISTSIZE 这两个环境变量的区别。先要区分“历史记录列表”和“历史记 录文件”。前者位于内存中,在 bash 会话进行期间更新。后者位于硬盘上, 在 bash shell 中通常是~/.bash_history。 会话结束后,历史记录列表中的内容会被写入历史记录文件。如果 HISTFILESIZE=200,表示历史记录文件中最多能保存 200 个命令;如果 HISTSIZE=20,表示不管输入多少命令,历史记录列表中只记录 20 个命令,最终也只有这 20个命令会在会话结束后被写入历史记录文件。

  • 不是所有的默认环境变量都会在 set 命令的输出中列出。如果用不到,默认环境变量并不要求必须有值。

  • 系统使用的默认环境变量有时取决于 bash shell 的版本。例如, EPOCHREALTIME 仅在
    bash shell 版本 5 及更高版本中可用。可以在 CLI 中输入 bash --version 来查看 bash shell 的版本号。

  1. 设置 PATH 环境变量
  • 当你在 shell CLI 中输入一个外部命令时,shell 必须搜索系统,从中找到对应的程序。PATH 环境变量定义了用于查找命令和程序的目录。
  • PATH 中的目录之间以冒号分隔。
  • 如果命令或者程序所在的位置没有包括在 PATH 变量中,那么在不使用绝对路径的情况下, shell 是无法找到的。
  • 应用程序的可执行文件目录有时不在 PATH 环境变量所包含的目录中。解决方法是保证 PATH 环境变量包含所有存放应用程序的目录。
  • 有些脚本编写人员使用 env 命令作为 bash shell 脚本的第一行,就像这样: #!/usr/bin/env bash。这种方法的优点在于 env 会在$PATH 中搜索 bash,使脚本具备更好的可移植性。
  • 可以把新的搜索目录添加到现有的 PATH 环境变量中,无须从头定义。 PATH 中各个目录 之间以冒号分隔。只需引用原来的 PATH 值, 添加冒号(:),然后再使用绝对路径输入新目录即可。
    $ PATH=$PATH:/home/christine/Scripts
    
    • 如果希望程序位置也可用于子 shell,则务必确保将修改后的 PATH 环境变量导出。
    • 对于 PATH 变量的修改只能持续到退出或重启系统。这种效果并不能一直奏效。
  • 可以创建自己的全局环境变量和局部环境变量。这些变量在整个 shell 会话期间都是可用的。
  1. 定位系统环境变量
  • 当你登录 Linux 系统启动 bash shell 时, 默认情况下 bash 会在几个文件中查找命令。这些文件 称作启动文件或环境文件。bash 进程的启动文件取决于你启动 bash shell 的方式。启动bash shell 有以下 3 种方式:
    • 登录时作为默认登录 shell;
    • 作为交互式 shell,通过生成子 shell 启动;
    • 作为运行脚本的非交互式 shell。
  1. 登录 shell
  • 当你登录 Linux 系统时, bash shell 会作为登录 shell启动。登录 shell 通常会从 5 个不同的启动文件中读取命令。
    • /etc/profile
    • $HOME/.bash_profile
    • $HOME/.bashrc
    • $HOME/.bash_login
    • $HOME/.profile
  • /etc/profile 文件是系统中默认的 bash shell 的主启动文件。系统中的每个用户登录时都会执行这个启动文件。
  • 另外 4 个启动文件是针对用户的,位于用户主目录中,可根据个人具体需求定制。
  • /etc/profile 文件是bash shell 默认的主启动文件。只要登录Linux 系统,bash 就会执行/etc/profile 启动文件中的命令。不同的 Linux 发行版在这个文件中放置了不同的命令。
  • /etc/profile 文件使用 for 语句来迭代/etc/profile.d 目录下的所有文件。这为 Linux 系统提供了一个放置特定应用程序启动文件和/或管理员自定义启动文件的地方,shell 会在用户登录时执行这些文件。
  • /etc/profile.d 目录下,大部分应用程序会创建两个启动文件: 一个供 bash shell 使用(扩展名为.sh), 另一个供 C shell 使用(扩展名为.csh)。
  1. $HOME 目录下的启动文件
  • 其余的启动文件都用于同一个目的:提供用户专属的启动文件来定义该用户所用到的环境变量。大多数 Linux 发行版只用这 4 个启动文件中的一两个。
    • $HOME/.bash_profile
    • $HOME/.bashrc
    • $HOME/.bash_login
    • $HOME/.profile
  • 注意,这些文件都以点号开头,说明属于隐藏文件(不会出现在一般的 ls 命令输出中)。 因为它们位于用户的$HOME 目录下,所以每个用户可以对其编辑并添加自己的环境变量, 其中的环境变量会在每次启动 bash shell 会话时生效。
  • Linux 发行版在环境文件方面存在的差异非常大。本节所列出的 H O M E 文件下的那些文件并非每个用户都有。例如,有些用户可能只有一个 HOME 文件下的那些 文件并非每个用户都有。例如,有些用户可能只有一个 HOME文件下的那些文件并非每个用户都有。例如,有些用户可能只有一个HOME/.bash_profile 文件。
  • shell 会按照下列顺序执行第一个被找到的文件,余下的则被忽略。(这个列表中并没有$HOME/.bashrc 文件。这是因为该文件通常通过其他文件运行)
    • $HOME/.bash_profile
    • $HOME/.bash_login
    • $HOME/.profile
  • $HOME 代表某个用户的主目录, 和波浪号( ~ )的效果一样。
  1. 交互式 shell 进程
  • 如果不是在登录系统时启动的 bash shell(比如在命令行中输入 bash),那么这时的 shell 称作交互式 shell。与登录 shell 一样,交互式 shell 提供了命令行提示符供用户输入命令。
  • 作为交互式 shell 启动的 bash并不处理/etc/profile 文件,只检查用户$HOME 目录中的.bashrc 文件。
  • CentOS Linux 系统中 .bashrc 文件会做两件事:首先,检查/etc 目录下的通用 bashrc 文件;其次, 为用户提供一个定制自己的命令别名和脚本函数的地方。
  1. 非交互式 shell
  • 系统执行 shell 脚本时用的就是这种 shell。不同之处在于它没有命令行提示符。但是,当你在系统中运行脚本时, 也许希望能够运行一些特定的启动命令。
  • 为了处理这种情况, bash shell 提供了 BASH_ENV 环境变量。当 shell 启动一个非交互式 shell 进程时, 会检查这个环境变量以查看要执行的启动文件名。如果有指定的文件,则 shell 会执行 该文件里的命令,这通常包括 shell脚本变量设置。
  • 脚本能以不同的方式执行。只有部分执行方式会启动子shell。
  • 那如果未设置 BASH_ENV 变量, shell 脚本到哪里去获取其环境变量呢?别忘了有些 shell 脚 本是通过启动一个子 shell 来执行的。子 shell 会继承父 shell 的导出变量。
  • 如果父 shell 是登录 shell,在/etc/profile 文件、 /etc/profile.d/*.sh 文件和$HOME/.bashrc 文件 中设置并导出了变量, 那么用于执行脚本的子 shell就能继承这些变量。
  • 任何由父 shell 设置但未导出的变量都是局部变量,不会被子shell 继承。
  • 对于那些不启动子 shell 的脚本, 变量已经存在于当前 shell 中了。就算没有设置 BASH_ENV, 也可以使用当前 shell 的局部变量和全局变量。
  1. 环境变量持久化
  • 可以利用环境文件文件创建自己的永久性全局变量或局部变量。
  • 对全局环境变量(Linux 系统的所有用户都要用到的变量)来说,可能更倾向于将新的或修 改过的变量设置放在/etc/profile 文件中, 但这可不是什么好主意。如果升级了所用的发行版,则 该文件也会随之更新, 这样一来,所有定制过的变量设置可就都没有了。
  • 最好在/etc/profile.d 目录中创建一个以.sh 结尾的文件。把所有新的或修改过的全局环境变量设置都放在这个文件中。
  • 在大多数发行版中,保存个人用户永久性 bash shell 变量的最佳地点是$HOME/.bashrc 文件。 这适用于所有类型的 shell 进程 。但如果设置了 BASH_ENV 变量,请记住:除非值为 $HOME/.bashrc,否则, 应该将非交互式 shell 的用户变量放在别的地方。
  • 图形化界面组成部分(比如 GUI 客户端)的环境变量可能需要在另外一些配置文件中设置,这和设置 bash shell 环境变量的文件不一样。
  • alias 命令设置无法持久生效。你可以把个人的 alias 设置放在$HOME/.bashrc 启动文件中, 使其效果永久化。
  1. 数组变量
  • 环境变量的一个很酷的特性是可以作为数组使用。数组是能够存储多个值的变量。这些值既 可以单独引用,也可以作为整体引用。
  • 要为某个环境变量设置多个值,可以把值放在圆括号中,值与值之间以空格分隔:
    $ mytest=(zero one two three four)
    $ echo $mytest
    zero
    $ echo ${mytest[2]}
    two
    $ echo ${mytest[*]}
    zero one two three four
    $ mytest[2]=seven
    $ echo ${mytest[2]}
    seven
    
    $ unset mytest[2]
    $ echo ${mytest[*]}
    zero one three four
    $ echo ${mytest[2]}
    
    $ echo ${mytest[3]}
    three
    
    $ unset mytest
    $ echo ${mytest[*]}
    
    
    • 要引用单个数组元素, 必须使用表示其在数组中位置的索引。索引要写在方括号中, $符号之后的所有内容都要放入花括号中。
    • 要显示整个数组变量, 可以用通配符*作为索引。
    • 也可以改变某个索引位置上的值。
    • 甚至能用 unset 命令来删除数组中的某个值, 但是要小心。
    • 上面例子用 unset 命令来删除索引 2 位置上的值。显示整个数组时,看起来好像其他索引 已经填补了这个位置。但如果专门显示索引 2 位置上的值时,你会发现这个位置是空的。
    • 可以在 unset 命令后跟上数组名来删除整个数组。
  • 环境变量数组的索引都是从 0 开始的。
  • 有时候, 数组变量只会把事情搞得更复杂,所以在 shell 脚本编程时并不常用。数组并不太方便移植到其他 shell 环境。

七、理解 Linux 文件权限
  1. Linux 的安全性
  • Linux 安全系统的核心是用户账户。每个能访问 Linux 系统的用户都会被分配一个唯一的用 户账户。用户对系统中各种对象的访问权限取决于他们登录系统时所用的账户。
  • 用户权限是通过创建用户时分配的用户 ID(user ID,UID )来跟踪的。 UID 是个数值, 每个 用户都有一个唯一的 UID。但用户在登录系统时是使用登录名(login name )来代替 UID 登录的。 登录名是用户用来登录系统的最长 8 字符的字符串(字符可以是数字或字母),同时会关联一个对应的密码。
  • Linux 系统使用特定的文件和工具来跟踪及管理系统的用户账户。
  1. /etc/passwd 文件
  • Linux 系统使用一个专门的文件/etc/passwd 来匹配登录名与对应的 UID 值。该文件包含了一 些与用户有关的信息。
  • root 用户账户是 Linux 系统的管理员, 为其固定分配的 UID 是 0。
    $ cat /etc/passwd
    root:x:0:0:root:/root:/bin/bash
    bin:x:1:1:bin:/bin:/sbin/nologin
    ...
    rich:x:500:500:Rich Blum:/home/rich:/bin/bash
    ...
    
  • Linux 系统会为各种各样的功能创建不同的用户账户,我们称其为系统账户,它们是系统中运行的各种服务进程访问资源使用的特殊账 户。所有运行在后台的服务都需要通过一个系统用户账户登录到 Linux 系统中。
  • Linux 为系统账户预留了 500 以下的 UID。有些服务甚至要用特定的 UID 才能正常工作。 为普通用户创建账户时,大多数 Linux 系统会从 500 开始,将第一个可用 UID 分配给这个账户。 (并非所有的 Linux 发行版都是这样,比如 Ubuntu 就是从 1000 开始的。)
  • /etc/passwd 文件中包含的内容远不止用户的登录名和 UID。该文件各个字段的含义如下:
    • 登录用户名
    • 用户密码
    • 用户账户的 UID (数字形式)
    • 用户账户的组 ID (数字形式)
    • 用户账户的文本描述(称为备注字段)
    • 用户$HOME 目录的位置
    • 用户的默认 shell
  • /etc/passwd 文件中的密码字段都被设置为 x,这可不是说所有的用户账户都使用相同的密码。现在,绝大多数 Linux 系统将用户密码保存在单独的文件(称为 shadow 文件,位于/etc/shadow) 中。只有特定的程序(比如登录程序)才能访问该文件。
  1. /etc/shadow 文件
  • /etc/shadow 文件对Linux 系统密码管理提供了更多的控制。只有root 用户才能访问/etc/shadow 文件。
  • /etc/shadow 文件为系统中的每个用户账户都保存了一条记录。记录就像下面这样:
    rich:$1$.FfcK0ns$f1UgiyHQ25wrB/hykCn020:11627:0:99999:7:::
    
  • /etc/shadow 文件中的每条记录共包含 9 个字段。
    • 登录名, 对应于/etc/passwd 文件中的登录名。
    • 加密后的密码。
    • 自上次修改密码后已经过去的天数(从 1970 年 1 月 1 日开始计算)。
    • 多少天后才能更改密码。
    • 多少天后必须更改密码。
    • 密码过期前提前多少天提醒用户更改密码。
    • 密码过期后多少天禁用用户账户。
    • 用户账户被禁用的日期(以从 1970 年 1 月 1 日到当时的天数表示)。
    • 预留给以后使用的字段。
  1. 添加新用户
  • 用来向 Linux 系统添加新用户的主要工具是 useradd。该命令可以一次性轻松创建新用户账 户并设置用户的$HOME 目录结构。 useradd 命令使用系统的默认值以及命令行参数来设置用户 账户。要想查看所使用的 Linux 发行版的系统默认值,可以使用加入了-D 选项的 useradd 命令。

    # useradd -D
    GROUP=100
    HOME=/home
    INACTIVE=-1
    EXPIRE=
    SHELL=/bin/bash
    SKEL=/etc/skel
    CREATE_MAIL_SPOOL=yes
    
  • useradd 命令的默认值使用/etc/default/useradd 文件设置。另外,进一步的安全设置在 /etc/login.defs 文件中定义。可以调整这些文件,改变 Linux 系统默认的安全行为。

  • -D 选项显示了在命令行中创建新用户账户时, 如果不明确指明具体值, useradd 命令所使用的默认值。这些默认值的含义如下。

    • 新用户会被添加到 GID 为 100 的公共组。
    • 新用户的主目录会位于/home/loginname。
    • 新用户账户密码在过期后不会被禁用。
    • 新用户账户不设置过期日期。
    • 新用户账户将 bash shell 作为默认 shell。
    • 系统会将/etc/skel 目录的内容复制到用户的$HOME 目录。
    • 系统会为该用户账户在 mail 目录下创建一个用于接收邮件的文件。
  • useradd 命令允许管理员创建默认的 H O M E 目录配置,然后将其作为创建新用户 HOME 目录配置,然后将其作为创建新用户 HOME目录配置,然后将其作为创建新用户HOME 目录的模板。这样就能自动在每个新用户的$HOME 目录里放置默认的系统文件了。

  • /etc/skel 目录下是bash shell 环境的标准启动文件。系统会自动将这些默认文件复制到你创建的每个用户的$HOME 目录。

  • 对很多 Linux 发行版而言, useradd 命令默认并不创建 H O M E 目录,但是 − m 命令行选项会使其创建 HOME 目录,但是-m 命令行选项会使其创建 HOME目录,但是m命令行选项会使其创建HOME 目录。可以在/etc/login.defs 文件中更改该行为。

  • 用户账户管理命令需要以 root 用户账户登录或者通过 sudo 命令运行。

  • 要想在创建新用户时改变默认值或默认行为,可以使用相应的命令行选项(但如果总是需要改动某个值, 则最好还是修改一下系统默认值),useradd 命令行选项如下:

    选项描述
    -c comment给新用户添加备注
    -d home_dir为主目录指定一个名字(如果不想用登录名作为主目录名的话)
    -e expire_date用 YYYY-MM-DD 格式指定账户过期日期
    -f inactive_days指定账户密码过期多少天后禁用该账户; 0 表示密码一过期就立即禁用, -1 表示不使用这个功能
    -g initial_group指定用户登录组的 GID 或组名
    -G group …指定除登录组之外用户所属的一个或多个附加组
    -k必须和-m 一起使用,将/etc/skel 目录的内容复制到用户的$HOME 目录
    -m创建用户的$HOME 目录
    -M不创建用户的$HOME 目录,即便默认设置里要求创建
    -n创建一个与用户登录名同名的新组
    -r创建系统账户
    -p passwd为用户账户指定默认密码
    -s shell指定默认的登录 shell
    -u uid为账户指定一个唯一的 UID
  • 可以使用-D 选项来修改系统默认的新用户设置。相应的选项如下:

    选项描述
    -b default_home修改用户$HOME 目录默认创建的位置
    -e expiration_date修改新账户的默认过期日期
    -f inactive修改从密码过期到账户被禁用的默认天数
    -g group修改默认的组名称或 GID
    -s shell修改默认的登录 shell
    • 修改默认值非常简单,例如需要 useradd 命令将 tsch shell 作为所有新建用户的默认登录 shell:
      # useradd -D -s /bin/tsch
      # useradd -D
      GROUP=100
      HOME=/home
      INACTIVE=-1
      EXPIRE=
      SHELL=/bin/tsch
      SKEL=/etc/skel
      CREATE_MAIL_SPOOL=yes
      #
      
  1. 删除用户
  • 如果想从系统中删除用户, userdel 可以满足这个需求。在默认情况下, userdel 命令只 删除/etc/passwd 和/etc/shadow 文件中的用户信息,属于该账户的文件会被保留。
  • 如果加入-r 选项,则 userdel 会删除用户的$HOME 目录以及邮件目录。然而,系统中仍 可能存有已删除用户的其他文件。
  1. 修改用户
  • 用户账户修改工具:

    命令描述
    usermod修改用户账户字段,还可以指定主要组(primary group )以及辅助组(secondary group )的所属关系
    passwd修改已有用户的密码
    chpasswd从文件中读取登录名及密码并更新密码
    chage修改密码的过期日期
    chfn修改用户账户的备注信息
    chsh修改用户账户的默认登录 shell
  • usermod 命令是用户账户修改工具中最强大的一个, 提供了修改/etc/passwd 文件中大部分 字段的相关选项,只需指定相应的选项即可。大部分选项与 useradd 命令的选项一样(比如-c 用于修改备注字段, -e 用于修改过期日期, -g 用于修改默认的登录组)。除此之外,还有另外 一些也许能派上用场的选项。

    • -l:修改用户账户的登录名。
    • -L:锁定账户,禁止用户登录。
    • -p:修改账户密码。
    • -U:解除锁定,恢复用户登录。
    • -L 选项尤为实用。该选项可以锁定账户,使用户无法登录,无须删除账户和用户数据。 要恢复账户, 只需使用-U 选项即可。
  • passwd 命令可以方便地修改用户密码。如果只使用 passwd 命令, 则修改的是你自己的密码。系统中的任何用户都能修改自己的密 码,但只有 root 用户才有权限修改别人的密码。

    • -e 选项可以强制用户下次登录时修改密码。你可以先给用户设置一个简单的密码,之后强 制用户在下次登录时改成他们能记住的更复杂的密码。
  • chpasswd 命令可以为系统中的大量用户修改密码。chpasswd 命令能从标准输入自动读取一系列以冒号分隔的登录名和密码对偶(login name and password pair), 自动对密码加密, 然后为用户账户设置密码。你也可以用重定向命令将包含 username:password 对偶的文件重定向给该命令。

    # chpasswd < users.txt
    
  • chsh、chfn 和 chage 用于修改特定的账户信息。 chsh 命令可以快速修改默认的用户登录 shell。使用时必须用 shell 的全路径名作为参数, 不能只用 shell 名。

    #  chsh -s /bin/csh test
    Changing shell for test.
    Shell changed.
    
  • chfn 命令提供了在/etc/passwd 文件的备注字段中保存信息的标准方法。chfn 命令会将用于 Unix 的 finger 命令的信息存入备注字段, 而不是简单地写入一些随机文本(比如名字或昵称之 类), 或是干脆将备注字段留空。

  • finger 命令可以非常方便地查看 Linux 系统的用户信息。出于安全性的考虑,大多数Linux 发行版没有默认安装 finger命令。请注意,安装该命令可能会使你的系统受到攻击漏洞的影响。

  • 如果使用 chfn 命令时不加任何选项,则会询问你要将哪些内容写入备注字段:

  • chage 命令可用于帮助管理用户账户的有效期。相关选项如下:

    选项描述
    -d设置自上次修改密码后的天数
    -E设置密码过期日期
    -I设置密码过期多少天后锁定账户
    -m设置更改密码的最小间隔天数
    -M设置密码的最大有效天数
    -W设置密码过期前多久开始出现提醒信息
  • chage 命令的日期值可以用下面两种方式中的任意一种表示:

    • YYYY-MM-DD 格式的日期
    • 代表从 1970 年 1 月 1 日起的天数
  • chage 命令的一个好用的功能是设置账户的过期日期。有了它,就可以创建在特定日期自动过期的临时用户,再也不用操心删除用户了。过期的账户跟锁定的账户类似:账户仍然存在,但用户无法用其登录。

  1. 使用 Linux 组
  • 组权限允许多个用户对系统对象(比如文件、目录或设备等) 共享一组权限。
  • Linux 发行版在处理默认组的成员关系时略有差异。有些 Linux 发行版会创建一个组,将所 有用户都作为该组的成员。遇到这种情况要特别小心, 因为你的文件有可能对于其他用户也是可 读的。有些发行版会为每个用户创建一个单独的组,这样会更安全一些。
  • 每个组都有唯一的 GID,和 UID 类似,该值在系统中是唯一的。除了 GID ,每个组还有一个唯一的组名。 Linux 系统中有一些组工具可用于创建和管理组。
  • 组可以包含一个或多个用户以支持对系统资源的共享访问。
  1. /etc/group 文件
  • 与用户账户类似,组信息也保存在一个文件中。 /etc/group 文件包含系统中每个组的信息。
    root:x:0:root
    bin:x:1:root,bin,daemon
    ...
    rich:x:500:
    ...
    
  • 对于系统账户组,为其分配的 GID 值低 于 500,而普通用户组的 GID 则从 500开始分配。 /etc/group 文件有 4 个字段。
    • 组名
    • 组密码
    • GID
    • 属于该组的用户列表
  • 组密码允许非组内成员使用密码临时性地成为该组成员。这个功能用得不多, 但确实存在。
  • 使用 usermod 命令向组中添加用户。在将用户添加到不同的组之前,必须先创建组。
  • 用户账户列表多少有些误导人。你会发现列表中的一些组没有任何用户。这并不是说这些组没有成员。当一个用户在/etc/passwd 文件中指定某个组作为主要组时,该用户不会作为该组成员再出现在/etc/group 文件中。
  1. 创建新组
  • groupadd 命令可用于创建新组。
    -在创建新组时, 默认不为其分配任何用户。groupadd 命令没有提供向组中添加用户的选项, 但可以用 usermod 命令来解决。
  • usermod 命令的-G 选项会把这个新组添加到该用户账户的组列表中。
  • 如果更改了已登录系统的用户所属的组,则该用户必须注销后重新登录,这样新的组关系才能生效。
  • 为用户分配组时要格外小心。如果使用了-g 选项,则指定的组名会替换掉在/etc/passwd 文件中为该用户分配的主要组。 -G 选项则会将该组加入该用户的属组列表,不会影响主要组。
  1. 修改组
  • roupmod 命令可以修改已有 组的 GID (使用-g 选项) 或组名(使用-n 选项)。
  • 修改组名时, GID 和组成员保持不变,只有组名会改变。由于所有的安全权限均基于 GID,因此可以随意改变组名,不会影响文件的安全性。
  1. 使用文件权限符号
  • ls 命令输出结果的第一个字段就是描述文件和目录权限的编码。
    $ ls -l
    total 68
    -rw-rw-r-- 1 rich rich 50 2010-09-13 07:49 file1.gz
    ...
    
    • 这个字段的第一个字符表示对象的类型。
        • 代表文件
      • d 代表目录
      • l 代表链接
      • c 代表字符设备
      • b 代表块设备
      • p 代表具名管道
      • s 代表网络套接字
    • 之后是 3组三字符的编码。每一组定义了 3 种访问权限。
      • r 代表对象是可读的
      • w 代表对象是可写的
      • x 代表对象是可执行的
    • 如果没有某种权限, 则在该权限位会出现连字符。这 3 组权限分别对应对象的 3 个安全级别。
      • 对象的属主
      • 对象的属组
      • 系统其他用户
  • 举例子说明:
    -rwxrwxr-x 1 rich rich 4882 2010-09-18 13:58 myprog
    
    • 文件 myprog 具有以下 3 组权限。
      • rwx:属主( rich )权限。
      • rwx:属组( rich )权限。
      • r-x:系统其他用户权限。
    • 这些权限表明用户 rich 可以读取、写入以及执行该文件(有全部权限)。类似地, rich 组的 成员也可以读取、写入以及执行该文件。然而, 不属于 rich 组的其他用户只能读取和执行该文件: w 被连字符取代了,说明这个安全级别没有写入权限。
  1. 默认文件权限
  • 你可能会问这些文件权限从何而来, 答案是 umask。umask 命令用来设置新建文件和目录的默认权限。
  • touch 命令使用分配给当前用户的默认权限创建了新文件。 umask 命令可以显示和设置默认权限。
    $ touch newfile
    $ ls -al newfile
    -rw-r--r--    1 rich     rich             0 Sep 20 19:16 newfile
    $ umask
    0022
    
    • umask 第一个数位(digit)代表了一项特别的安全特性。接下来的 3个数位表示文件或目录对应的 umask 八进制值(如果读权限是唯一置位的权限, 则权限值是 r–,转换成二进制值就是 100,代表的八进制值是 4)。
  • Linux 文件权限编码
    权限二进制值八进制值描述
    0000没有任何权限
    –x0011只有执行权限
    -w-0102只有写入权限
    -wx0113有写入和执行权限
    r–1004只有读取权限
    r-x1015有读取和执行权限
    rw-1106有读取和写入权限
    rwx1117有全部权限(读取、写入和执行)
  • umask 值只是个掩码, 它会屏蔽掉不想授予该安全级别的权限。
  • 要把 umask 值从对象的全权限值(full permission)中减掉。对文件而言, 全权限值是 666 (所有用户都有读取和写入的权限); 对目录而言, 全权限值则是 777 (所有用户都有读取、写入 和执行权限)。
  • 所以,在上面的例子中,文件一开始的权限是 666,减去 umask 值 022 之后,剩下的文件 权限就成了 644。
  • umask 值通常会被设置在/etc/profile 启动文件中。可以使用 umask 命令指定其他的 umask 默认值。
    $ umask 026
    $ touch newfile2
    $ ls -l newfile2
    -rw-r-----    1 rich rich 0 Sep 20 19:46 newfile2
    
    • 将 umask 值设成 026 后,默认的文件权限变成了 640,因此现在对组成员来说新文件是只 读的,而系统其他用户则没有任何权限。
  1. 修改权限
  • chmod 命令可以修改文件和目录的安全设置。只有文件的属主才能修改文件或目录的权限。 但 root 用户可以修改系统中任意文件或目录的安全设置。该命令的格式如下:
    chmod options mode file
    
    • mode 参数允许使用八进制模式或符号模式来进行安全设置。
    • 八进制模式设置非常直观,直接用打算赋予文件的标准 3 位八进制权限编码即可。
  • 符号模式的权限就没这么简单了。与通常用到的 3组权限字符不同, chmod 命令采用的是另一种方法。下面是在符号模式下指定权限的格式:
    [ugoa...][[+-=][rwxXstugo...]
    
    • 第一组字符定义了权限作用的对象:
      • u 代表用户
      • g 代表组
      • o 代表其他用户
      • a 代表上述所有
    • 接下来的符号表示你是想在现有权限基础上增加权限(+)、移除权限(-),还是设置权限(=)。
    • 最后,第三个符号代表要设置的权限。可取的值要比通常的 rwx 多。这些额外值如下:
      • X:仅当对象是目录或者已有执行权限时才赋予执行权限。
      • s:在执行时设置 SUID 或 SGID。
      • t:设置粘滞位(sticky bit)。
      • u:设置属主权限。
      • g:设置属组权限。
      • o:设置其他用户权限。
    • 具体用法如下:
      $ chmod o+r newfile
      $ ls -l newfile
      -rwxrw-r--    1 rich rich 0 Sep 20 19:16 newfile
      
      $ chmod u-x newfile
      $ ls -l newfile
      -rw-rw-r--    1 rich rich 0 Sep 20 19:16 newfile
      
      • 不管其他用户先前在该安全级别具有什么样的权限, o+r 都为其添加了读取权限。
      • u-x 移除了属主已有的执行权限。
      • 如果某个文件具有执行权限, 则 ls 命令的-F 选项 会在该文件的名称后面加上一个星号。
  • options 为 chmod 命令提供了额外的增强特性。 -R 选项能够以递归方式修改文件和目录的权限。你可以使用通配符指定多个文件名,然后用单个命令批量修改权限。
  1. 改变所属关系
  • chown 和 chgrp,前者可以修改文件的属主,后者可以修改文件的默认属组。
  • chown 命令的格式如下:
    chown options owner[.group] file
    
    // 可以使用登录名或 UID 来指定文件的新属主:
    # chown dan newfile
    # ls -l newfile
    -rw-rw-r--    1 dan      rich             0 Sep 20 19:16 newfile
    
    // chown 命令也支持同时修改文件的属主和属组:
    # chown dan.shared newfile
    # ls -l newfile
    -rw-rw-r--    1 dan      shared              0 Sep 20 19:16 newfile
    
    // 可以只修改文件的默认属组:
    # chown .rich newfile
    # ls -l newfile
    -rw-rw-r--    1 dan      rich             0 Sep 20 19:16 newfile
    
    // 如果你的 Linux 系统使用与用户登录名相同的组名,则可以同时修改二者:
    # chown test. newfile
    # ls -l newfile
    -rw-rw-r--    1 test    test              0 Sep 20 19:16 newfile
    
  • -R 选项与通配符相配合可以递归地修改子目录和 文件的所属关系。 -h 选项可以修改文件的所有符号链接文件的所属关系。
  • 只有 root 用户能修改文件的属主。任何用户都可以修改文件的属组, 须是原属组和新属组的成员。
  • chgrp 命令可以方便地修改文件或目录的默认属组:
    // shared 组的任意成员都可以写入该文件:
    $ chgrp shared newfile
    $ ls -l newfile
    -rw-rw-r--    1 rich      shared          0 Sep 20 19:16 newfile
    
  1. 共享文件
  • Linux 系统中共享文件的方法是创建组。

  • 创建新文件时, Linux 会用默认的UID 和 GID来给文件分配权限。要想让其他用户也能访问文件,要么修改所有用户一级的安全权限, 要么给文件分配一个包含其他用户的新默认属组。

  • 如果想在大范围内创建并共享文件, 这会很烦琐。幸好有一种简单的解决方法。Linux 为每个文件和目录存储了 3 个额外的信息位。

    • SUID(set user ID):当用户执行该文件时,程序会以文件属主的权限运行。
    • SGID(set group ID):对文件而言,程序会以文件属组的权限运行;对目录而言,该目录中创建的新文件会以目录的属组作为默认属组。
    • 粘滞位(sticky bit):应用于目录时,只有文件属主可以删除或重命名该目录中的文件。
  • SGID 位对文件共享非常重要。启用 SGID 位后,可以强制在共享目录中创建的新文件都属 于该目录的属组,这个组也就成了每个用户的属组。

  • SGID 位会强制某个目录下新建文件或目录都沿用其父目录的属组,而不是创建这些文件的用户的属组。这便于系统用户之间共享文件。

  • 可以通过 chmod 命令设置 SGID,将其添加到标准 3 位八进制值之前(组成 4 位八进制值),或者在符号模式下用符号 s。

  • SUID 、SGID 和粘滞位的八进制值

    二进制值八进制值描述
    0000清除所有位
    0011设置粘滞位
    0102设置 SGID 位
    0113设置 SGID 位和粘滞位
    1004设置 SUID 位
    1015设置 SUID 位和粘滞位
    1106设置 SUID 位和 SGID 位
    1117设置所有位
  • 要创建一个共享目录,使目录中的所有新文件都沿用目录的属组,只需设置该目录的 SGID 位。

    $ mkdir testdir
    $ ls -l
    drwxrwxr-x    2 rich    rich    4096 Sep 20 23:12   testdir/
    $ chgrp shared testdir
    $ chmod g+s testdir
    $ ls -l
    drwxrwsr-x    2 rich    shared    4096 Sep 20 23:12   testdir/
    $ umask 002
    $ cd testdir
    $ touch testfile
    $ ls -l
    total 0
    -rw-rw-r--    1 rich    shared    0 Sep 20 23:13   testfile
    
    • 首先,使用 mkdir 命令创建希望共享的目录。然后,通过 chgrp 命令修改目录的默认属组, 使其包含所有需要共享文件的用户。最后,设置目录的 SGID 位,保证目录中的新建文件都以 shared 作为默认属组。
    • 为了让这个环境正常工作, 所有组成员都要设置他们的 umask 值,使文件对属组成员可写。 在先前的例子中, umask 被改成了 002,所以文件对属组是可写的。
    • 完成上述步骤后,新文件 testfile 沿用了目录的默认属组,而不是用户账户的默认属组。现在 shared 组的所有用户都能访问这个文件了。
  1. 访问控制列表
  • 对于文件和目录,不同的组需要不同的权限,基本权限方法解决不了这个问题。
  • 访问控制列表(access control list, ACL)允许指定包含多个用户或组的列表以及为其分配的权限。和基本安全方法一样, ACL 权限使用相同的读取、写入和执行权限位, 但现在可以分配给多个用户和组。
  • 可以使用 setfacl 命令和 getfacl 命令在 Linux 中实现 ACL 特性。
  • getfacl 命令能够查看分配给文件或目录的 ACL:
    $ touch test
    $ ls -l
    total 0
    -rw-r----- 1 rich rich 0 Apr 19 17:33 test
    $ getfacl test
    # file: test
    # owner: rich
    # group: rich
    user::rw-
    group::r--
    other::---
    $
    
  • setfacl 命令可以为用户或组分配权限:
    setfacl [options] rule filenames
    
    • setfacl 命令允许使用-m 选项修改分配给文件或目录的权限,或使用-x 选项删除特定权 限。可以使用下列 3 种格式定义规则:
      • u[ser]:uid:perms
      • g[roup]:gid:perms
      • o[ther]::perms
    • 要为用户分配权限, 可以使用 user 格式; 要为组分配权限, 可以使用 group 格式; 要为其他用户分配权限, 可以使用 other格式。对于 uid 或gid,可以使用数字值或名称。
      // 为 test 文件添加了 sales 组的读写权限。
      $ setfacl -m g:sales:rw test
      $ ls -l
      total 0
      -rw-rw----+ 1 rich rich 0 Apr 19 17:33 test
      
      // 可以再次使用 getfacl 命令查看 ACL。
      $ getfacl test
      # file: test
      # owner: rich
      # group: rich
      user::rw-
      group::r--
      group:sales:rw-
      mask::rw-
      other::---
      
      // 要想删除权限,可以使用-x 选项:
      $ setfacl -x g:sales test
      $ getfacl test
      # file: test
      # owner: rich
      # group: rich
      user::rw-
      group::r--
      mask::r--
      other::---
      
      • 注意, setfacl 命令不产生输出。在列出文件时, 只显示标准的属主、属组和其他用户权限, 但在权限列的末尾多了一个加号(+),指明该文件还应用了 ACL。可以再次使用 getfacl 命令查看 ACL。
      • getfacl 的输出显示为两个组分配了权限。默认组(rich)对文件有读权限, sales 组对文件 有读写权限。要想删除权限,可以使用-x 选项。
  • Linux 也允许对目录设置默认 ACL,在该目录中创建的文件会自动继承。这个特性称为 ACL 继承。
  • 要想设置目录的默认 ACL ,可以在正常的规则定义前加上 d:,如下所示:
    $ sudo setfacl -m d:g:sales:rw /sales
    
    • 为/sales 目录添加了 sales 组的读写权限。在该目录中创建的所有文件都会自动为 sales 组分配读写权限。

八、管理文件系统
  1. Linux 支持多种文件系统。每种文件系统都在存储设备上实现了虚拟目录结构,只是特性略有不同。

  2. Linux 操作系统最初引入的文件系统叫作扩展文件系统(extended filesystem,简称 ext),它为 Linux 提供了一个基本的类 Unix 文件系统,使用虚拟目录处理物理存储设备并在其中以固定大小的磁盘块(fixed-length block)形式保存数据。

  3. ext 文件系统使用i 节点(inode)跟踪存储在虚拟目录中文件的相关信息。 i 节点系统在每个 物理存储设备中创建一个单独的表(称作 i 节点表)来保存文件信息。虚拟目录中的每个文件在 i 节点表中有对应的条目。ext 文件系统名称中的 extended 部分得名自其所跟踪的每个文件的额外 数据,包括以下内容:

    • 文件名
    • 文件大小
    • 文件属主
    • 文件属组
    • 文件访问权限
    • 指向存有文件数据的每个块的指针
  • Linux 通过一个唯一的数值(称作 i 节点号) 来引用 i 节点表中的 i 节点,这个值是创建文件 时由文件系统分配的。文件系统是通过 i 节点号而非文件名和路径来标识文件的。
  1. 最早的 ext 文件系统限制颇多,比如文件大小不得超过 2 GB。在 Linux 出现后不久, ext 文 件系统就升级到了第二代扩展文件系统, 称作 ext2。在保持与 ext 相同的文件系统结构的同时, ext2 在功能上做了扩展。
    • 在 i 节点表中加入了文件的创建时间、修改时间以及最后一次访问时间。
    • 允许的最大文件大小增至 2 TB,后期又增加到 32 TB。
    • 保存文件时按组分配磁盘块。
  • ext2 文件系统也有限制。如果系统在存储文件和更新 i 节点表之间发生了什么事情, 则两者 内容可能无法同步, 潜在的结果是丢失文件在磁盘上的数据位置。
  1. 日志文件系统为 Linux 系统增加了一层安全性。它放弃了之前先将数据直接写入存储设备再 更新 i 节点表的做法,而是先将文件变更写入临时文件(称作日志)。在数据被成功写到存储设 备和 i 节点表之后,再删除对应的日志条目。如果系统在数据被写入存储设备之前崩溃或断电, 则日志文件系统会读取日志文件, 处理尚未提交的数据。
  • Linux 中有 3种广泛使用的日志方法,每种的保护等级都不相同。
    方法描述
    数据模式i 节点和文件数据都会被写入日志;数据丢失风险低,但性能差
    有序模式只有 i 节点数据会被写入日志,直到文件数据被成功写入后才会将其删除;在性能和安全性之间取得了良好的折中
    回写模式只有 i 节点数据会被写入日志,但不控制文件数据何时写入;数据丢失风险高,但仍好于不用日志
  • 数据模式日志方法是目前为止最安全的数据保护方法, 但速度也是最慢的。所有写入存储设 备的数据都必须写两次: 一次写入日志, 另一次写入实际的存储设备。
  1. ext3 文件系统是 ext2 的后续版本, 支持最大 2 TB 的文件,能够管理 32 TB 大小的分区。在 默认情况下, ext3 采用有序模式的日志方法, 不过也可以通过命令行选项改用其他模式。 ext3 文 件系统无法恢复误删的文件,也没有提供数据压缩功能。

  2. 作为 ext3 的后续版本, ext4 文件系统最大支持 16 TiB 的文件,能够管理 1 EiB 大小的分区。 在默认情况下, ext4 采用有序模式的日志方法, 不过也可以通过命令行选项改用其他模式。另外 还支持加密、压缩以及单目录下不限数量的子目录。先前的 ext2 和 ext3 也可以作为 ext4 挂载, 以提高性能表现。

  3. JFS 文件系统采用的是有序模式的日志方法,只在日志中保存 i 节点数据,直到文件数据被 写进存储设备后才将其删除。

  4. XFS 文件系统采用回写模式的日志方法, 在提供了高性能的同时也引入了一定的风险, 因为 实际数据并未存进日志文件。

  5. 就文件系统而言, 日志技术的替代选择是一种称作写时复制(copy-on-write,COW)的技术。 COW 通过快照(snapshot)兼顾了安全性和性能。在修改数据时, 使用的是克隆或可写快照。修 改过的数据并不会直接覆盖当前数据,而是被放入文件系统中另一个位置。

  • 真正的 COW 系统仅在数据修改完成之后才会改动旧数据。如果从不覆盖旧数据, 那么这种操作准确来说称作写时重定向(redirect-on-write,ROW )。不过,通常都将 ROW 简称为 COW。
  • 从一个或多个磁盘(或磁盘分区) 创建的存储池提供了生成虚拟磁盘(称作卷)的能力。通过存储池, 可以根据需要增加卷,在提供灵活性的同时大大减少停机时间。
  1. ZFS 是一个稳定的文件系统,与 Resier4 、Btrfs 和 ext4 势均力敌。它拥有数据完整性验证和自动修复功能,支持最大 16 EB 的文件,能够管理 256 万亿 ZB(256 quadrillion zettabyte)的存储空间。

  2. Stratis 文件系统维护的存储池由一个或多个XFS 文件系统组成,同时还提供与传统的卷管理文件系统(比如 ZFS 和 Btrfs)相似的 COW 功能。

  3. 创建分区

  • 需要在存储设备上创建可容纳文件系统的分区。分区范围可以是整个硬盘,也可以是部分硬盘以包含虚拟目录的一部分。
  • 可用于组织和管理分区的工具不止一种。本节重点关注其中 3 种:
    • fdisk
    • gdisk
    • GNU parted
  • Linux 采用了一种标准格式来为硬盘分配设备名称, 在进行分区之前, 必须熟悉这种格式。
    • SATA 驱动器和 SCSI 驱动器:设备命名格式为/dev/sdx ,其中字母 x 具体是什么要根据驱 动器的检测顺序决定(第一个检测到的驱动器是 a ,第二个是 b ,以此类推)。
    • SSD NVMe 驱动器: 设备命名格式为/dev/nvmeNn#,其中数字 N 具体是什么要根据驱动 器的检测顺序决定(从 0 起始)。 #是分配给该驱动器的名称空间编号(从 1 起始)。
    • IDE 驱动器: 设备命名格式为/dev/hdx,其中字母 x 具体是什么要根据驱动器的检测顺序 决定(第一个检测到的驱动器是 a ,第二个是 b ,以此类推)。
  • fdisk
    • fdisk 可以在任何存储设备上创建和管理分区。但是,fdisk 只能处理最大 2 TB 的硬盘。如果大于此容量,则只能使用 gdisk 或 GNUparted 代替。
    • 如果存储设备是首次分区,则 fdisk 会警告你该设备没有分区表。
    • fdisk 是一个交互式程序,允许你输入命令来逐步完成硬盘分区操作。要启动 fdisk,需要 指定待分区的存储设备的名称, 同时还必须有超级用户权限(以 root 用户登录或使用sudo 命令)。
      # whoami
      root
      # fdisk /dev/sda
      Welcome to fdisk (util-linux 2.32.1).
      Changes will remain in memory only, until you decide to write them.
      Be careful before using the write command.
      
      Command (m for help):
      
    • fdisk 使用自己的命令行, 允许你提交命令, 对存储设备进行分区。常用的 fdisk 命令常用命令:
      命令描述
      a设置活动分区标志
      b编辑 BSD Unix 系统使用的标签
      c设置 DOS 兼容标志
      d删除分区
      g创建新的空 GTP 分区表
      G创建 IRIX( SGI )分区表
      l显示可用的分区类型
      m显示命令菜单
      n添加一个新分区
      o创建新的空 DOS 分区表
      p显示当前分区表
      q退出,不保存更改
      s为 Sun Unix 系统创建一个新标签
      t修改分区的系统 ID
      u修改显示单元
      v验证分区表
      w将分区表写入磁盘并退出
      x附加功能(仅供专家使用)
    • fdisk 命令不允许调整现有分区的大小。只能删除现有分区,然后重新创建。
    • 一些发行版和比较旧的发行版在创建好分区后不会自动通知 Linux 系统。在这种情况下,需要使用 partprobe 命令或 hdparm 命令(参见命令相关的手册页),或者重启系统,使其读取更新后的分区表。
    • 如果对分区做了改动, 那么务必使用 w 命令将改动写入硬盘后再退出。如果不想保存修改内 容,则直接使用 q 命令退出。
    • 注意,在使用 fdisk 创建新分区时, 无须输入任何信息。只需按 Enter 键,接受给出的默认值即可。
  • gdisk
    • 如果存储设备要采用 GUID 分区表( GUID partition table ,GPT),就要用到 gdisk。
    • gdisk 会识别存储设备所采用的分区类型。如果当前未使用 GPT 方法, 则 gdisk 会提供相应的选项,将其转换为 GPT。
    • 在转换存储设备分区类型的时候务必小心。所选择的类型必须与系统固件( BIOS 或 UEFI)兼容,否则,将无法引导设备。
    • gdisk 也提供了自己的命令行提示符,允许输入命令进行分区操作。常用的 gdisk 命令:
      命令 | 描述
      b | 将 GTP 数据备份至文件
      c | 修改分区名称
      d | 删除分区
      i | 显示分区的详细信息
      l | 显示可用的分区类型
      n | 添加一个新分区
      o | 创建一个新的空 GUID 分区表( GPT )
      p | 显示当前分区表
      q | 退出,不保存更改
      r | 恢复和转换选项(仅供专家使用)
      s | 排序分区
      t | 修改分区的类型代码
      v | 验证磁盘
      w | 将分区表写入磁盘并退出
      x | 附加功能(仅供专家使用)
      ? | 显示命令菜单
  • GNU parted
    • GNU parted 提供了另一种命令行界面来处理分区。不同于 fdisk 和 gdisk,GNU parted 中的命令更像是单词。
    • parted 允许调整现有的分区大小, 所以可以很容易地收缩或扩大磁盘分区。
  1. 创建文件系统
  • 将数据存储到分区之前, 必须使用某种文件系统对其进行格式化, 以便 Linux 能够使用分区。 每种文件系统类型都有自己专门的格式化工具。创建文件系统的命令行工具:
    工具用途
    mkefs创建 ext 文件系统
    mke2fs创建 ext2 文件系统
    mkfs.ext3创建 ext3 文件系统
    mkfs.ext4创建 ext4 文件系统
    mkreiserfs创建 ReiserFS 文件系统
    jfs_mkfs创建 JFS 文件系统
    mkfs.xfs创建 XFS 文件系统
    mkfs.zfs创建 ZFS 文件系统
    mkfs.btrfs创建 Btrfs 文件系统
    • 并非所有的文件系统工具都已经默认安装过。要想知道某个工具是否可用,可以使用 type 命令:
      $ type mkfs.ext4
      mkfs.ext4 is /usr/sbin/mkfs.ext4
      $ type mkfs.btrfs
      -bash: type: mkfs.btrfs: not found
      
      • 上面的例子显示了 mkfs.ext4 是可用的,而 mkfs.btrfs 则不可用。
      • 另外,还需检查使用的 Linux 发行版是否支持要创建的文件系统。
  • 所有的文件系统命令都允许通过不带选项的简单形式来创建默认的文件系统,但要求你拥有超级用户权限。
  • 每个文件系统命令都有大量命令行选项, 允许你定制如何在分区上创建文件系统。要查看所有可用的命令行选项, 可以使用 man 命令显示相应命令的手册页。
  • 为分区创建好文件系统之后,下一步是将其挂载到虚拟目录中的某个挂载点,以便在新分区中存储数据。挂载点可以是虚拟目录中的任何位置。
    $ sudo mkfs.ext4 /dev/sdb1
    [sudo] password for christine:
    mke2fs 1.44.6 (5-Mar-2019)
    Creating filesystem with 524032 4k blocks and 131072 inodes
    [...]
    Creating journal (8192 blocks): done
    Writing superblocks and filesystem accounting information: done
    $ mkdir /home/christine/part
    $ sudo mount -t ext4 /dev/sdb1 /home/christine/part
    [sudo] password for christine:
    $ lsblk -f /dev/sdb
    NAME   FSTYPE LABEL UUID      MOUNTPOINT
    sdb
    ⌙ sdb1 ext4         a8d1d[...]  /home/christine/part
    
    • 这个新文件系统采用了 ext4 文件系统类型, 这是 Linux 的日志文件系统。注意, 创建过程中 有一步是创建新的日志。
    • mkdir 命令在虚拟目录中创建了挂载点,mount 命令会将新分区的文件系统添加到挂载点。mount 命令的-t 选项指明了要挂载的文件系统类型(ext4)。 lsblk -f 命令可以显示新近格式化过并挂载的分区。
    • 这种挂载文件系统的方法只能实现临时挂载,重启系统后就失效了。要强制 Linux 在启动时自动挂载文件系统,可以将其添加到/etc/fstab 文件中。
  1. 文件系统的检查与修复
  • 每种文件系统各自都有相应的恢复命令。
  • fsck 命令可以检查和修复大部分 Linux 文件系统类型。该命令的格式如下:
    fsck options filesystem
    
    • 可以在命令行中列出多个要检查的文件系统。文件系统可以通过多种方法指定, 比如设备 名或其在虚拟目录中的挂载点。但在对其使用 fsck 之前,必须先卸载设备。
  • fsck 命令会使用/etc/fstab 文件自动决定系统中已挂载的存储设备的文件系统。如果存储设 备尚未挂载(比如刚刚在新的存储设备上创建了文件系统), 则需要用-t 命令行选项指定文件系统类型。
  • fsck 常用的命令行选项:
    选项描述
    -a检测到错误时自动修复文件系统
    -A检查/etc/fstab 文件中列出的所有文件系统
    -N不进行检查,只显示要检查的内容
    -r出现错误时进行提示
    -R使用-A 选项时跳过根文件系统
    -t指定要检查的文件系统类型
    -V在检查时产生详细输出
    -y检测到错误时自动修复文件系统
    • 有些命令行选项是重复的。这是为多个命令实现通用前端带来的部分问题。 有些文件系统修复命令有额外的可用选项。
  • 只能对未挂载的文件系统执行 fsck 命令。对大多数文件系统来说,只需先卸载文件系统,检查完成之后再重新挂载即可。但因为根文件系统含有 Linux 所有的核心命令和日志文件,所以无法在处于运行状态的系统中卸载它。这正是 Linux Live CD、DVD 或 USB 大展身手的时机。只需用 Linux Live 媒介启动系统即可,然后对根文件系统执行 fsck 命令。
  1. 逻辑卷管理
  • Linux 逻辑卷管理器(logical volume manager,LVM)可以通过将另一块硬盘上的分区加入已有的文件系统来动态地添加存储空间。它可以让你在无须重建整个 文件系统的情况下, 轻松地管理磁盘空间。
  1. LVM 布局
  • LVM 允许将多个分区组合在一起, 作为单个分区(逻辑卷)进行格式化、在 Linux 虚拟目 录结构上挂载、存储数据等。随着数据需求的增长,你还可以继续向逻辑卷添加分区。
  • LVM 由 3 个主要部分组成:
    • 物理卷
      • 物理卷(physical volume,PV)通过 LVM 的 pvcreate 命令创建。该命令指定了一个未使 用的磁盘分区(或整个驱动器) 由 LVM 使用。在这个过程中, LVM 结构、卷标和元数据都会被 添加到该分区。
    • 卷组
      • 卷组(volume group,VG)通过 LVM 的 vgcreate 命令创建。该命令会将 PV 加入存储池, 后者随后用于构建各种逻辑卷(也就是说,多个物理卷( PV )集中在一起会形成卷组(VG), 由此形成了一个存储池, 从中为逻辑卷( LV )分 配存储空间)。
      • 可以存在多个卷组。当你使用 vgcreate 将一个或多个 PV 加入 VG时, 也会同时添加卷组 的元数据。
      • 被指定为 PV 的分区只能属于单个 VG。但是,被指定为 PV 的其他分区可以属于其他 VG。
    • 逻辑卷
      • 逻辑卷(logical volume,LV)通过 LVM 的 lvcreate 命令创建。这是逻辑卷创建过程的最 终产物。 LV 由 VG 的存储空间块(这些块叫作 PE(physical extents))组成。你可以使用文件系统格式化 LV ,然后将其挂载,像普 通的磁盘分区那样使用。
      • 尽管可以有多个 VG,但 LV 只能从一个指定的 VG 中创建(也就是说, LV 不能跨 VG创建)。 不过, 多个 LV 可以共享单个 VG 。你可以使用相应的 LVM 命令调整(增加或减少) LV 的容量。该特性赋予了数据存储管理极大的灵活性。
  1. Linux 中的 LVM
  • lvm 是用于创建和管理 LV 的交互式实用工具。如果尚未安装, 可以通过 lvm2 软件包安装。各种 LVM 工具直接在 CLI 中就可以使用,无须进入 lvm。
  • 首次设置逻辑卷的步骤如下。
    (1) 创建物理卷。
    (2) 创建卷组。
    (3) 创建逻辑卷。
    (4) 格式化逻辑卷。
    (5) 挂载逻辑卷。
  • 创建 PV
    • 在指定作为 PV 的存储设备之前, 先确保已经分区且未使用。可以使用 pvcreate 命令指定 要作为 PV 的分区,执行该命令需要有超级用户权限:
    • 最好多设置几个 PV。LVM 的关键在于将额外的存储空间动态添加到 LV。设置过 PV后,就该创建 VG 了。
  • 创建 VG
    • 只要是 PV,就可以加入 VG 。创建 VG 的命令是 vgcreate。
    • 在创建 VG 的过程中可以指定多个 PV 。如果随后需要向 VG 中添加 PV,可以使用 vgextend 命令。
    • 通常的做法是将第一个 VG 命名为 vg00,将下一个 VG 命名为 vg01,以此类推。
    • 由于许多发行版在安装期间为虚拟目录结构的根(/)设置了 LVM, 因此最好使用 vgdisplay 命令检查一下系统当前的 VG。
  • 创建 LV
    • 可以使用 lvcreate 命令创建 LV。LV 的大小由-L 选项设置,使用的空间取自指定的 VG 存储池。
    • 如果出于某种原因, VG 没有足够的存储空间来满足 LV 要求的大小,那么 lvcreate 命令将无法创建 LV 。你会看到错误消息 insufficient free space error。
    • 创建好 LV 之后,可以使用 lvdisplay 命令显示相关信息。注意,命令中的完整路径名用于指定 LV。
    • 除了 lvdisplay 命令,也可以使用 lvs 命令和 lvscan 命令显示系统的 LV 信息。
  1. 使用 Linux LVM
  • 一旦创建好 LV ,就可以将其视作普通分区。可以根据需要扩大或收缩这个分区。但在此之前,必须将 LV 挂载到虚拟目录结构中。
  • 格式化和挂载 LV
    • 对于 LV ,无须执行任何特殊操作就可以在其上创建文件系统,然后再挂载到虚拟目录结构中。
  • 扩大或收缩 VG 和 LV。扩大或收缩 LVM 的命令:
    命令功能
    Vgextend将 PV 加入 VG
    Vgreduce从 VG 中删除 PV
    lvextend扩大 LV 的容量
    lvreduce收缩 LV 的容量
  • 要想了解所有的 LVM 命令,可以在命令行中输入 lvm help。

九、安装软件
  1. 软件包管理系统使用数据库来记录下列内容:

    • Linux 系统中已安装的软件包。
    • 每个软件包安装了哪些文件。
    • 每个已安装的软件包的版本。
  2. 软件包存储在称为仓库(repository)的服务器上, 可以利用本地 Linux 系统中的软件包管理 器通过 Internet访问,在其中搜索新的软件包,或是更新系统中已安装的软件包。

  3. 软件包通常存在依赖关系,为了能够正常运行,被依赖的包必须提前安装。软件包管理器会检测这些依赖关系并提前安装好所有额外的软件包。

  4. Linux 中广泛使用的两种主要的软件包管理系统基础工具是 dpkg 和 rpm。

    • 基于 Debian 的发行版(比如 Ubuntu 和 Linux Mint)使用的是 dpkg 命令,该命令是其软件 包管理系统的底层基础。 dpkg 会直接和 Linux 系统中的软件包管理系统交互,用于安装、管理 和删除软件包。
    • 基于 Red Hat 的发行版(比如 Fedora、CentOS 和 openSUSE)使用的是 rpm 命令, 该命令是 其软件包管理系统的底层基础。与 dpkg 命令类似, rpm 命令也可以安装、管理和删除软件包。
  • 注意, 这两个命令是它们各自软件包管理系统的核心, 并不代表整个软件包管理系统。许多使用 dpkg 命令或 rpm 命令的 Linux 发行版在这些基本命令的基础上构建了另一些专业的软件包管理器。
  1. dpkg 命令是基于 Debian 的软件包管理器的核心,用于在 Linux 系统中安装、更新、删除 DEB 包文件。
  • 从所用的 Linux 发行版仓库中安装软件包,需要使用 APT(advanced package tool)工具集。
    • apt-cache
    • apt-get
    • apt
  • apt 命令本质上是 apt-cache 命令和 apt-get 命令的前端。 APT 的好处是不需要记住什 么时候该使用哪个工具——它涵盖了软件包管理所需的方方面面。
  • apt 命令的基本格式如下:
    apt [options] command
    
    • command 定义了 apt 要执行的操作。如果需要,可以指定一个或多个 options 进行微调。
  1. 使用 apt 管理软件包
  • apt list 命 令会显示仓库中所有可用的软件包, 如果再加入–installed 选项,就可以限制仅输出那些已 安装在系统中的软件包。
    $ apt --installed list
    
  • 如果已经知道系统中的某个软件包, 希望显示其详细信息,可以使用 show 命令来操作:
    apt show package_name
    
    $ apt show zsh
    
  • apt show命令并不会指明软件包是否已经安装。它只根据软件仓库显示该软件包的详细信息。
  • 有一个细节无法通过 apt 获得,即与特定软件包相关的所有文件。为此,需要使用 dpkg 命令:
    dpkg -L package_name
    
    $ dpkg -L acl
    
  • 也可以执行相反的操作,即找出特定的文件属于哪个软件包(注意,文件要使用绝对路径):
    dpkg --search absolute_file_name
    
    $ dpkg --search /bin/getfacl
    acl: /bin/getfacl               // 表明文件 getfacl属于 acl 软件包
    
  1. 使用 apt 安装软件包
  • 同时使用 apt 命令与 search 命令查找特定的软件包:
    apt search package_name
    
    $ apt --names-only search zsh
    
    • search 命令的妙处在于不需要在 package_name 周围添加通配符,直接就有通配符的效果。在默认情况下, search 命令显示的是在名称或描述中包含搜索关键字的那些软件包,这有时候会产生误导。如果只想搜索软件包名称,可以加入–name-only 选项。
  • 一旦找到待安装的软件包, 就可以使用 apt 安装了:
    apt install package_name
    
    $ sudo apt install zsh
    
  • sudo 命令允许以root 用户身份执行命令。 这类管理任务都需要使用 sudo 命令帮助完成。
  • 可以使用 list 命令的–installed 选项检查安装是否正确。如果在输出中看到了软件包, 那么说明已经安装好了。
  • 注意,在安装指定的软件包时, apt 也会要求安装其他软件包。这是因为 apt 会自动解析 必要的依赖关系,根据需要安装额外的库和软件包。
  1. 使用 apt 升级软件
  • upgrade 命令可以使用仓库中的任何新版本安全地升级系统中所有的软件包:
    apt upgrade
    
    • 注意,该命令无须使用任何软件包名称作为参数。原因在于 upgrade 会将所有已安装的软 件包升级为仓库中可用的最新版本。
  • upgrade 命令在升级过程中不会删除任何软件包。如果必须删除某个软件包才能完成升级,可以使用以下命令:
    apt full-upgrade
    
  • 应该定期执行 apt upgrade,保持系统处于最新状态。
  1. 使用 apt 卸载软件包
  • apt 的 remove 命令可以删除软件包, 同时保留数据和配置文件。如果要将软件包以及相关 的数据和配置文件全部删除,那么需要使用 purge 选项:
    $ sudo apt purge zsh
    
    $ sudo apt autoremove
    
    • 注意, 在 purge 的输出中, apt 警告我们 zsh-common 软件包存在依赖, 不能自动删除, 以 免其他软件包还有需要。如果确定有依赖关系的软件包不会再有他用,可以使用 autoremove 命令将其删除。
    • autoremove 命令会检查所有被标记为存在依赖关系且不再被需要的软件包。
  1. apt 仓库
  • apt 默认的软件存储库位置是在安装 Linux 发行版时设置的。仓库位置保存在文件/etc/apt/ sources.list 中。
  • apt 只会从这些仓库中拉取软件。此外,在搜索要安装或更新的软件时, apt 也只检查这些仓库。如果 你想为你的软件包管理系统加入一些额外的软件仓库,那就得修改文件了。
  • Linux 发行版开发人员努力确保加入仓库的软件包版本不会彼此之间发生冲突。通过仓库升级或安装软件包通常是最安全的方法。
  • sources.list 文件使用下列结构来指定仓库源:
    deb (or deb-src) address  distribution_name  package_type_list
    
    • deb 或deb-src 指定了软件包的类型。 deb 表明这是一个已编译程序的仓库源, 而deb-src 表明这是一个源代码的仓库源。
    • address 是软件仓库的网址。 distribution_name 是该软件仓库的发行版的版本名称(并不是说用了这个发行版,只是说用了这个发行版的仓库。例如, 在 Linux Mint 的 sources.list 文件中, 你能看到混用了 Linux Mint 和 Ubuntu 的软件仓库)。
    • package_type_list 可能并不止一个单词,它还表明仓库里面有什么类型的软件包。 你可能会在其中看到如 main 、restricted 、universe 或 partner这样的词。
  1. 和基于 Debian 的发行版类似,基于 Red Hat 的系统有以下几种前端工具:

    • yum:用于 Red Hat、CentOS 和 Fedora。
    • zypper:用于 openSUSE。
    • dnf:yum 的升级版,有一些新增的特性。
  2. dnf 命令的语法为:

    dnf [options]  [...]
    
    // 要找出系统中已安装的软件包,可以使用下列命令:
    dnf list installed
    
    dnf list installed > installed_software
    
    • 输出的信息可能会在屏幕上一闪而过,所以最好是将已安装软件的列表重定向到一个文件 中,然后使用 more 命令或 less 命令(或GUI 编辑器)来仔细查看。
  3. 要想查找特定软件包的详细信息,dnf 除了会给出软件包的详细描述,它还可以告诉你软件包是否已经安装:

    $ dnf list xterm
    $ dnf list installed xterm
    
  4. 如果需要找出文件系统中的某个文件是由哪个软件包安装的,只需输入以下命令:

    dnf provides file_name
    
    $ dnf provides /usr/bin/gzip
    Last metadata expiration check: 0:12:06 ago on Sat 16 May 2020 12:10:24 PM EDT.
    gzip-1.10-1.fc31.x86_64 : The GNU data compression program
    Repo        : @System
    Matched from:
    Filename    : /usr/bin/gzip
    gzip-1.10-1.fc31.x86_64 : The GNU data compression program
    Repo        : fedora
    Matched from:
    Filename    : /usr/bin/gzip
    
    • 上面例子中,dnf 分别检查了两个仓库: 本地系统和默认的 fedora 仓库。
  5. 使用 dnf 安装软件

  • 下面是安装软件包的基础命令,包括其所需的全部库 以及依赖:
    dnf install package_name
    
    $ sudo dnf install zsh
    
    • 该命令允许以root用户身份执行命令。应该仅在执行安装和更新软件这类管理任务的时候才临时切换为 root 用户。
  1. 使用 dnf 升级软件
  • 要查看已安装软件包的所有可用更新,可以输入下列命令:
    dnf list upgrades
    
  • 如果你发现有需要升级的软件包, 可以输入下列命令:
    dnf upgrade package_name
    
  • 如果想升级更新列表中的所有软件包,那么只需输入下列命令即可:
    dnf upgrade
    
  • dnf 还有一个很不错的特性: upgrade-minimal 命令。它会将软件包升级至最新的 bug修复版或安全补丁版, 而不是最新的最高版本。
  1. 使用 dnf 卸载软件
  • dnf 还提供了一种简单的方法来卸载系统中不需要的软件(但尚没有选项或命令可以在卸载软件的同时保留配置文件或数据文件):
    dnf remove package_name
    
  1. 处理损坏的依赖关系
  • 有时在安装多个软件包时, 一个软件包的依赖关系可能会被另一个软件包搞乱。这称为依赖 关系损坏(broken dependency)。
  • 如果你的系统出现了这种情况,可以先试试下列命令:
    dnf clean all
    
  • 然后尝试使用 dnf 命令的upgrade 选项。有时, 只要清理了放错位置的文件就可以了。
  • 如果还解决不了问题,再试试下列命令:
    dnf repoquery --deplist package_name
    
    # dnf repoquery --deplist xterm
    
    • 该命令会显示指定软件包的所有依赖关系以及哪些软件包提供了这种依赖。只要知道软件包 需要哪个库, 就可以自行安装了。
  • yum 工具的 upgrade 命令支持–skip-broken 选项,可以跳过依赖关系损坏的软件包,同时仍尝试继续升级其他包。 dnf 工具则自动执行该操作。
  1. RPM 仓库
  • dnf 在安装时也设置了软件仓库。
  • 要想查看当前拉取软件的仓库,可以使用下列命令:
    dnf repolist
    
  • 如果发现其中没有所需的仓库,那就需要编辑配置文件了。有两个地方可以找到 dnf 仓库的定义。
    • 配置文件/etc/dnf/dnf.conf。
    • /etc/yum.repos.d 目录中的单独文件。
  1. 使用容器管理软件
  • 云计算带来了应用程序打包方式的一种新范式: 应用程序容器(application container)。应用 程序容器创建了一个环境, 其中包含了应用程序运行所需的全部文件, 包括运行时库文件。开发 人员随后可以将应用程序容器作为单个软件包分发,保证能够在任何 Linux 系统中正常运行。
  1. 使用 snap 容器
  • Ubuntu Linux 发行版的创建者 Canonical 开发了一种称为 snap 的应用程序容器格式(于 Ubuntu 16.04 LTS 发布时引入)。
  • snap 打包系统会将应用程序所需的所有文件集中到单个 snap 分发文件中。snapd 应用程序运行在后台,可以使用 snap 命令行工具查询 snap 数据库, 显示已安装的 snap 包,以及安装、升级和删除 snap 包。
  • 使用 snap version 命令检查 snap 是否正在运行:
    $ snap version
    
  • 如果 snap 正在运行, 可以使用 snap list 命令查看当前已安装的 snap 应用程序列表:
    $ snap list
    
  • snap find 命令可以在 snap 仓库中搜索指定程序:
    $ snap find solitaire
    
  • snap info 命令可以查看 snap 应用程序(简称为 snap )的详细信息:
    $ snap info solitaire
    
  • snap install 命令可以安装新的 snap:
    $ sudo snap install solitaire
    
    • 注意,安装 snap 的时候必须有 root 权限,这意味着要用到 sudo 命令。
    • 在安装 snap 的时候, snapd 程序会将其作为驱动器挂载。可以使用 mount 命令查看新的snap 挂载。
  • 如果需要删除某个 snap ,使用 snap remove 命令即可:
    $ sudo snap remove solitaire
    
  • 也可以通过禁用来代替删除。使用 snap disable 命令即可。要想重新恢复 snap,可以使用 snap enable 命令。
  1. 使用 flatpak 容器
  • flatpak 应用程序容器格式是作为一个独立的开源项目创建的, 与任何特定的 Linux 发行版都 没有直接联系。
  • 如果你的 Linux 发行版支持 flatpak,可以使用 flatpak list 命令列出已安装的应用程序容器:
    $ flatpak list
    
  • flatpak search 命令可以在 flatpak 仓库中搜索指定应用程序:
    $ flatpak search solitaire
    Name         Description    Application ID      Version Branch  Remotes
    Aisleriot Solitaire     org.gnome.Aisleriot     stable  fedora
    
    • 使用容器时,必须使用其 Application ID 值,而不是名称。
  • 可以使用 flatpak install 命令安装应用程序:
    $ sudo flatpak install org.gnome.Aisleriot
    
  • 要想检查安装是否正确,可以再次使用 flatpak list 命令:
    $ flatpak list
    Name                    Application ID      Version Branch  Installation 
    Aisleriot Solitaire     org.gnome.Aisleriot         stable  system
    
  • 可以使用 flatpak uninstall 命令删除应用程序容器:
    $ sudo flatpak uninstall org.gnome.Aisleriot
    
  1. 从源代码安装
  • 源代码形式的软件包通常以 tarball 的形式发布(经由 tar 命令创建出的归档文件通常称为 tarball)。
  • 使用软件包 sysstat 作为示例。 sysstat 提供了各种系统监测工具,非常好用。
    // 首先,需要将 sysstat 的 tarball 下载到你的 Linux 系统中。尽管通常能在各种 Linux 网站 上找到 sysstat,但最好直接到程序的官方站点下载。
    $ tar -Jxvf sysstat-12.3.3.tar.xz
    $ cd sysstat-12.3.3
    $ ls
    // 在目录的列表中,应该能看到 README 文件或 INSTALL 文件。务必阅读这些文件,其中写明了软件安装所需的操作步骤。
    // 运行 configure 工具, 检查你的 Linux,确保拥有 合适的能够编译源代码的编译器,以及正确的库依赖关系
    // 如果有问题, 则 configure 会显示错误消息,说明缺失了哪些东西。
    $ ./configure
    // 用 make 命令来构建各种二进制文件。 make 命令会编译源代码,然后由链接器生 成最终的可执行文件。和 configure 命令一样, make 命令会在编译和链接所有源代码文件的 过程中产生大量的输出:
    $ make
    // make 命令结束后, 可运行的sysstat 程序就出现在目录中了。但是从这个目录中运行程序有 点儿不方便。你希望将其安装在 Linux 系统的常用位置。为此, 必须以 root 用户身份登录(或者 使用 sudo 命令), 然后使用 make 命令的 install 选项:
    # make instal
    
  • 大多数 Linux 程序是用 C 或 C++编程语言编写的。要在系统中编译这些源代码,需要安装 gcc 软件包和make 软件包。大多数Linux 桌面发行版默认没有安装。

十、文本编辑器
  1. 检查 vim 软件包
  • readlink -f 命令能够直接找出一系列链接文件的最后一环。
  • 在 Ubuntu 发行版中安装基础版的 vim 软件包:
    $ sudo apt install vim
    
  1. vim 基础
  • vim 编辑器在内存缓冲区中处理数据。
  • 如果在启动 vim 时未指定文件名, 或者指定文件不存在, 则 vim 会开辟一段新的缓冲区进行 编辑。如果指定的是已有文件的名称, 则 vim 会将该文件的整个内容都读入缓冲区以备编辑。
  • vim 编辑器会检测会话的终端类型,使用全屏模式来将整个控制台窗口作为编辑器区域。
  • vim 提供了一些能够提高移动速度的命令:
    • PageDown (或 Ctrl+F): 下翻一屏。
    • PageUp (或 Ctrl+B):上翻一屏。
    • G:移到缓冲区中的最后一行。
    • num G:移到缓冲区中的第 num 行。
    • gg:移到缓冲区中的第一行。
  • 要进入 Ex 模式, 在命令模式中按下冒号键( : )即可。光 标会移动到屏幕底部的消息行处,然后出现冒号, 等待输入命令。
  • Ex 模式中的以下命令可以将缓冲区的数据保存到文件中并退出 vim。
    • q:如果未修改缓冲区数据,则退出。
    • q!:放弃对缓冲区数据的所有修改并退出。
    • w filename:将文件另存为其他名称。
    • wq:将缓冲区数据保存到文件中并退出。
  1. 在命令模式中, vim 编辑器提供了可用于编辑缓冲区数据的命令。常用的vim 编辑命令如下表:
    命令描述
    x删除光标当前所在位置的字符
    dd删除光标当前所在行
    dw删除光标当前所在位置的单词
    d$删除光标当前所在位置至行尾的内容
    J删除光标当前所在行结尾的换行符(合并行)
    u撤销上一个编辑命令
    a在光标当前位置后追加数据
    A在光标当前所在行结尾追加数据
    r char用 char 替换光标当前所在位置的单个字符
    R text用 text 覆盖光标当前所在位置的内容,直到按下 ESC 键
  • 有些编辑命令允许使用数字修饰符来指定重复该命令多少次。比如, 命令 2x 会从光标当前 位置开始删除两个字符,命令 5dd 会删除从光标当前所在行开始的 5 行。
  • 在 vim 编辑器的命令模式中使用Backspace 键(退格键)和 Delete 键(删除键)时要留心。vim 编辑器通常会将 Delete 键识别成 x 命令的功能, 删除光标当前所在位置的字符。通常, vim 编辑器在命令模式中并不将 Backspace 键视为删除操作,而是将光标向后移动一个位置。
  1. 复制和粘贴
  • 当 vim 删除内容时,实际上会将数据保存在一个单独区域内(寄存器),你可以用p 命令从中取回数据。
  • vim 的复制命令是 y(代表 yank)。可以像 d 命令那样, 在y 之后使用另一个字符(yw 表示复制一个单词, y$表示复制到行尾)。复制过文本后, 将光标移动 到想放置文本的位置, 输入 p 命令。已复制的文本就会出现在那里。
  • 可视模式会在光标移动时高亮显示文本。你可以利用该模式选取要复制的文本。要进入可视 模式, 可以移动光标到要开始复制的位置, 按下 v 键。你会注意到光标所在位置的文本已经被高 亮显示了。接下来, 移动光标来覆盖想要复制的文本(甚至可以向下移动几行来复制更多行的文 本)。随着光标的移动, vim 会高亮显示复制区域的文本。当覆盖了要复制的文本后,按下 y 键 来激活复制命令。现在寄存器中已经有了要复制的文本, 剩下的就是移动光标到需要的位置, 使 用 p 命令来粘贴。
  1. 查找和替换
  • 如果要输入一个查找字符串, 可以按下 正斜线(/)键。光标会“跑”到屏幕底部的消息行,然后显示出一个正斜线。在输入要查找的 文本后, 按下 Enter 键。 vim 编辑器会执行下列三种操作之一。
    • 如果要查找的文本出现在光标当前位置之后,则光标会跳到该文本出现的第一个位置。
    • 如果要查找的文本未在光标当前位置之后出现, 则光标会绕过文件末尾,出现在该文本所在的第一个位置(并用一条消息指明)。
    • 输出一条错误消息,说明在文件中没有找到要查找的文本。
  • 如果要继续查找同一个单词,按/键,然后再按 Enter 键,或者按 n 键,表示下一个(next)。
  • Ex 模式的替换命令允许快速将文本中的一个单词替换成另一个单词。要使用替换命令,必须处于命令行模式下。替换命令的格式是:s/old/new/。vim 编辑器会跳到 old 第一次出现的 地方并用 new来替换。可以对替换命令做一些修改来替换多处文本。
    • 😒/old/new/g:替换当前行内出现的所有 old。
    • :n,ms/old/new/g:替换第 n 行和第 m 行之间出现的所有 old。
    • :%s/old/new/g:替换整个文件中出现的所有 old。
    • :%s/old/new/gc:替换整个文件中出现的所有 old,并在每次替换时提示。
  1. nano 编辑器

略。

  1. Emacs 编辑器

略。

  1. KDE 系编辑器

略。

  1. GNOME 编辑器

略。


十一、构建基础脚本
  1. 使用多个命令
  • shell 脚本的关键是能够输入多个命令并处理每个命令的结果,甚至是将一个命令的结果传给另一个命令。 shell 可以让你将多个命令串联起来,一次性执行完。
  • 如果想让两个命令一起运行,可以将其放在同一行中,彼此用分号隔开。通过 这种办法,能将任意多个命令串联在一起使用,只要不超过命令行最大字符数 255 就可以。
  1. 创建 shell 脚本文件
  • 在创建 shell 脚本文件时,必须在文件的第一行指定要使用的 shell,格式如下:
    #!/bin/bash
    
  • 在普通的 shell 脚本中, #用作注释行。 shell 并不会处理 shell 脚本中的注释行。然而, shell 脚本文件的第一行是个例外, #后面的惊叹号会告诉 shell用哪个 shell 来运行脚本。(是的, 可以使用 bash shell,然后使用另一个 shell 来运行你的脚本)
  • 在指明了 shell 之后,可以在文件的各行输入命令,每行末尾加一个换行符。也可以根据需要,使用分号将两个命令放在一行中, 但在 shell 脚本中, 可以将命令放在独立的行中。 shell 会根据命令在文件中出现的顺序进行处理。
  • 要让 shell 找到 test1 脚本,可以采用下列两种方法之一:
    • 将放置 shell 脚本文件的目录添加到 PATH 环境变量中;
    • 在命令行中使用绝对路径或相对路径来引用 shell 脚本文件。
  • 有些 Linux 发行版会将$HOME/bin 目录加入 PATH 环境变量中。这样就在每个用户的 $HOME 目录中提供了一个存放文件的地方,shell 可以在那里查找要执行的命令。
  • 可以使用单点号来引用当前目录下的文件。
    $ ./test1
    bash: ./test1: Permission denied
    $ ls -l test1
    -rw-r--r--    1 user     user                73 Jun 02 15:36 test1
    $ chmod u+x test1
    $ ./test1
    // 执行结果打印
    
    在创建 test1 文件时, umask 的值决定了新文件的默认权限设置。由于 umask 变量被设为 022,因此新建的文件只有文件属主和属组才有读/写权限。因此要通过 chmod 命令赋予文件属主执行文件的权限:
  1. 显示消息
  • 可以通过 echo 命令来实现显示消息。如果在 echo 命令后面加上字符串,那么echo 命令就会显示出这个字符串:
  • 注意, 在默认情况下, 无须使用引号将要显示的字符串划定出来。echo 命令可用单引号或双引号来划定字符串。如果你在字符串中要用到某种引号,可以使 用另一种引号来划定字符串:
    $ echo "This is a test to see if you're paying attention"
    This is a test to see if you're paying attention
     
    $ echo 'Rich says "scripting is easy".'
    Rich says "scripting is easy".
    
  • 可以将 echo 命令添加到shell 脚本中任何需要显示额外信息的地方。
  • 可以使用 echo 命令 的-n 选项把字符串和命令输出显示在同一行中。
    echo The time and date are:
    date
    
    改成:
    echo -n "The time and date are:"
    
    • 需要在字符串的两侧使用引号, 以确保要显示的字符串尾部有一个空格。命令输出会在紧 接着字符串结束的地方出现。
  1. 变量允许在 shell 脚本中临时存储信息,以便同脚本中的其他命令一起使用。

  2. 环境变量

  • shell 维护着一组用于记录特定的系统信息的环境变量, 比如系统名称、已登录系统的用户 名、用户的系统 ID (也称为 UID)、用户的默认主目录以及 shell 查找程序的搜索路径。可以用 set 命令显示一份完整的当前环境变量列表。
  • 在脚本中,可以在环境变量名之前加上$来引用这些环境变量,用法如下:
    #!/bin/bash
    # display user information from the system.
    echo "User info for userid: $USER"
    echo UID: $UID
    echo HOME: $HOME
    
    $ echo "The cost of the item is $15"
    The cost of the item is 5
    
    $ echo "The cost of the item is \$15"
    The cost of the item is $15
    
    • 环境变量 U S E R 、 USER、 USERUID 和$HOME 用来显示已登录用户的相关信息。
    • echo 命令中的环境变量会在脚本运行时被替换成当前值。
    • 在第一个字符串中 可以将 U S E R 系统变量放入双引号中,而 s h e l l 依然能够知道我们的意图。但这种方法存在一个问题。只要脚本在双引号中看到 USER 系统变量放入双引号中, 而 shell 依然能够知道我们的意图。但这种方法存在一个问题。只要脚本在双引号中看到 USER系统变量放入双引号中,而shell依然能够知道我们的意图。但这种方法存在一个问题。只要脚本在双引号中看到,就会以为你在引用变量。在上面的例子中, 脚本会尝试显示变量$1 的值(尚未定义), 再显示数字 5。
    • 要显示$,必须在它前面放置一个反斜线。
  • 反斜线允许 shell 脚本按照字面意义解释$,而不是引用变量。
  • 可能还有通过 v a r i a b l e 形式引用的变量。花括号通常用于帮助界定 {variable}形式引用的变量。花括号通常用于帮助界定 variable形式引用的变量。花括号通常用于帮助界定后的变量名。
  1. 用户自定义变量
  • 定义变量允许在脚本中临时存储并使用数据。
  • 用户自定义变量的名称可以是任何由字母、数字或下划线组成的字符串,长度不能超过 20 个字符。而且变量名区分大小写。
  • 使用等号为变量赋值。在变量、等号和值之间不能出现空格。
    var1=10
    var2=-57
    var3=testing
    var4="still more testing"
    
  • shell 脚本会以字符串形式存储所有的变量值,脚本中的各个命令可以自行决定变量值的数据类型。 shell 脚本中定义的变量在脚本的整个生命周期里会一直保持着它们的值,在脚本结束时会被删除。
  • 与系统变量类似,用户自定义变量可以通过$引用。
  • 引用变量值时要加 ,对变量赋值时则不用加 ,对变量赋值时则不用加 ,对变量赋值时则不用加。少了$,shell 会将变量名解释成普通的字符串。
  1. 命令替换
  • shell 脚本中最有用的特性之一是可以从命令输出中提取信息并将其赋给变量。把输出赋给变量之后, 就可以随意在脚本中使用了。
  • 有两种方法可以将命令输出赋给变量。
    • 反引号(`)
    • $()格式
  • 命令替换允许将 shell 命令的输出赋给变量。要么将整个命令放入反引号内,要么使用$()格式:
    testing=`date`
    testing=$(date)
    
    • shell 会执行命令替换符内的命令, 将其输出赋给变量 testing。
    • 注意, 赋值号和命令替换 符之间没有空格。
  • 下面这个例子在脚本中通过命令替换获得当前日期并用其来生成唯一文件名:
    #!/bin/bash
    # copy the /usr/bin directory listing to a log file
    today=$(date +%y%m%d)
    ls /usr/bin -al > log.$today
    
    • today 变量保存着格式化后的 date 命令的输出。这是提取日期信息,用于生成日志文件名 的一种常用技术。 +%y%m%d 格式会告诉 date 命令将日期显示为两位数的年、月、日的组合。
    • 文件本身包含重定向的目录列表输出。
    • 日志文件采用$today 变量的值作为文件名的一部分。日志文件的内容是 /usr/bin 目录内容的列表输出。
  • 命令替换会创建出子 shell 来运行指定命令,这是由运行脚本的 shell 所生成的一个独立的 shell。因此, 在子 shell 中运行的命令无法使用脚本中的变量。
  • 如果在命令行中使用./路径执行命令,就会创建子 shell,但如果不加路径,则不会创建子 shell。不过,内建的 shell 命令也不会创建子shell 。在命令行中运行脚本时要当心。
  1. 输出重定向
  • 最基本的重定向会将命令的输出发送至文件。 bash shell 使用大于号(>)来实现该操作:
    command > outputfile
    
    • 如果输出文件已存在,则重定向运算符会用新数据覆盖已有的文件。
  • 将命令输出追加到已有文件中,可以用双大于号(>>)来追加数据。
  1. 输入重定向
  • 输入重定向会将文件的内容重定向至命令, 而不是将命令输出重定向至文件。
  • 输入重定向运算符是小于号(<):
    command < inputfile
    
  • wc 命令可以统计数据中的文本。在默认情况下,它会输出 3 个值。
    • 文本的行数
    • 文本的单词数
    • 文本的字节数
  • 通过将文本文件重定向到 wc 命令,立刻就可以得到文件中的行、单词和字节的数量。
  • 还有另外一种输入重定向的方法,称为内联输入重定向(inline input redirection)。这种方法 无须使用文件进行重定向, 只需在命令行中指定用于输入重定向的数据即可。
  • 内联输入重定向运算符是双小于号(<<)。除了这个符号, 必须指定一个文本标记来划分输 入数据的起止。任何字符串都可以作为文本标记, 但在数据开始和结尾的文本标记必须一致:
    command << marker
    data
    marker
    
  • 在命令行中使用内联输入重定向时, shell 会用 PS2 环境变量中定义的次提示符来提示输入数据, 其用法如下所示:
    $ wc << EOF
    > test string 1
    > test string 2
    > test string 3
    > EOF
        3       9      42
    
    • 次提示符会持续显示,以获取更多的输入数据,直到输入了作为文本标记的那个字符串。
    • wc 命令会统计内联输入重定向提供的数据包含的行数、单词数和字节数。
  1. 管道
  • 有时候需要将一个命令的输出作为另一个命令的输入。这可以通过重定向来实现。这种方法的确管用,但仍然是一种比较烦琐的信息生成方式。无须将命令输出重定向至文件,可以将其直接传给另一个命令。这个过程称为管道连接(piping)。
  • 管道被置于命令之间,将一个命令的输出传入另一个命令中:
    command1 | command2
    
  • 可别以为由管道串联起的两个命令会依次执行。实际上, Linux 系统会同时运行这两个命令,在系统内部将二者连接起来。当第一个命令产生输出时,它会被立即传给第二个命令。数据传输不会用到任何中间文件或缓冲区。
  • 管道可以串联的命令数量没有限制。可以持续地将命令输出通过管道传给其他命令来细化操作。
  • 管道最常见的用法之一是将命令产生的大量输出传送给 more 命令。对 ls 命令来说,这种用法尤为常见。
    $ ls -al | more
    
    • ls -l 命令会产生目录中所有文件的长列表。对包含大量文件的目录来说,这个列表会相当长。将输出通过管道传给 more 命令,可以强制分屏显示 ls 的输出列表。
  1. 在 shell 脚本中,执行数学运算有两种方式:

    • expr 命令
    • 使用方括号
  2. expr 命令

  • expr,该命令可在命令行中执行数学运算,但是特别笨拙。
  • expr 命令能够识别少量算术运算符和字符串运算符。expr 命令运算符如下表:
    运算符描述
    ARG1 | ARG2如果 ARG1 既不为 null 也不为 0 ,就返回 ARG1;否则,返回 ARG2
    ARG1 & ARG2如果 ARG1 和 ARG2 都不为 null 或 0 ,就返回 ARG1;否则,返回 0
    ARG1 < ARG2如果 ARG1 小于 ARG2,就返回 1 ;否则,返回 0
    ARG1 <= ARG2如果 ARG1 小于或等于 ARG2,就返回 1 ;否则,返回 0
    ARG1 = ARG2如果 ARG1 等于 ARG2,就返回 1 ;否则,返回 0
    ARG1 != ARG2如果 ARG1 不等于 ARG2,就返回 1 ;否则,返回 0
    ARG1 >= ARG2如果 ARG1 大于或等于 ARG2,就返回 1 ;否则,返回 0
    ARG1 > ARG2如果 ARG1 大于 ARG2,就返回 1 ;否则,返回 0
    ARG1 + ARG2返回 ARG1 和 ARG2 之和
    ARG1 - ARG2返回 ARG1 和 ARG2 之差
    ARG1 * ARG2返回 ARG1 和 ARG2 之积
    ARG1 / ARG2返回 ARG1 和 ARG2 之商
    ARG1 % ARG2返回 ARG1 和 ARG2 之余数
    STRING : REGEXP如果 REGEXP 模式匹配 STRING,就返回该模式匹配的内容
    match STRING REGEXP如果 REGEXP 模式匹配 STRING,就返回该模式匹配的内容
    substr STRING POS LENGTH返回起始位置为 POS (从 1 开始计数)、长度为 LENGTH 的子串
    index STRING CHARS返回 CHARS 在字符串 STRING 中所处的位置;否则,返回 0
    length STRING返回字符串 STRING 的长度
    • TOKEN | 将 TOKEN 解释成字符串, 即使 TOKEN 属于关键字
      (EXPRESSION) | 返回 EXPRESSION 的值
  1. 使用方括号
  • 在 bash 中, 要将数学运算结果赋给变量, 可以使用$和方括号( $[ operation ]):
    $ var1=$[1 + 5]
    $ echo $var1
    6
    $ var2=$[$var1 * 2]
    $ echo $var2
    12
    
  • 用方括号来执行数学运算要比 expr 命令方便得多。这种技术也适用于 shell脚本。
  • 在使用方括号执行数学运算时,无须担心 shell 会误解乘号或其他符号。 shell 清楚方括号内 的星号不是通配符。
  • bash shell 的数学运算符只支持整数运算。
  • z shell(zsh)提供了完整的浮点数操作。如果需要在 shell 脚本中执行浮点数运算,那么不妨考虑一下 z shell。
  1. 浮点数解决方案
  • 能够克服 bash 只支持整数运算的限制的最常见的做法是使用内建的 bash 计算器 bc。
  • bash 计算器实际上是一种编程语言, 允许在命令行中输入浮点数表达式, 然后解释并计算该表达式,最后返回结果。 bash 计算器能够识别以下内容:
    • 数字(整数和浮点数)
    • 变量(简单变量和数组)
    • 注释(以#或 C 语言中的/* */开始的行)
    • 表达式
    • 编程语句(比如 if-then 语句)
    • 函数
  • 可以在 shell 提示符下通过 bc 命令访问 bash 计算器。要退出 bash 计算器,必须输入 quit。
  • 浮点数运算是由内建变量 scale 控制的。你必须将该变量的值设置为希望在计算结果中保 留的小数位数,否则将无法得到期望的结果。
    $ bc -q
    3.44 / 5
    0
    scale=4
    3.44 / 5
    .6880
    quit
    
    • scale 变量的默认值是 0。在 scale 值被设置前, bash 计算器的计算结果不包含小数位。 在将其设置成 4 后, bash 计算器显示的结果包含 4 位小数。
    • -q 选项可以不显示bash 计算器冗长的欢迎信息。
  • 除了普通数字, bash 计算器还支持变量:
    $ bc -q
    var1=10
    var1 * 4
    40
    var2 = var1 / 5
    print var2
    2
    quit
    
    • 变量值一旦被定义,就可以在整个 bash 计算器会话中使用了。 print 语句可以打印变量和数字。
  • 要在脚本中使用 bc,可以用命令替换来运行 bc 命令,将输出赋给变量。基本格式如下:
    variable=$(echo "options; expression" | bc)
    
    • 第一部分的 options 允许你设置变量。如果需要多个变量,可以用分号来分隔它们。 expression 定义了要通过bc 执行的数学表达式。
  • 下面是在脚本中执行此操作的示例:
    #!/bin/bash
    var1=$(echo " scale=4; 3.44 / 5" | bc)
    echo The answer is $var1
    
    • 这个例子将 scale 变量设置为 4位小数,在 expression 部分指定了特定的运算。
  • bc 命令能接受输入重定向,允许将一个文件重定向到 bc 命令来处理。
  • 最好的办法是使用内联输入重定向, 它允许直接在命令行中重定向数据。在 shell 脚本中, 可以将输出赋给一个变量:
    variable=$(bc << EOF
    options
    statements
    expressions
    EOF
    )
    
    • 字符串 EOF 标识了内联重定向数据的起止。
    • 用命令替换符将 bc 命令的输出赋给变量 variable。
  • 可以将 bash 计算器涉及的各个部分放入脚本文件的不同行中。以下示例演示了如何 在脚本中使用这项技术:
    $ cat test12
    #!/bin/bash
    var1=10.46
    var2=43.67
    var3=33.2
    var4=71
    var5=$(bc << EOF
    scale = 4
    a1 = ( $var1 * $var2)
    b1 = ($var3 * $var4)
    a1 + b1
    EOF
    )
    echo The final answer for this mess is $var5
    $
    
    • 将选项和表达式放在脚本的不同行中可以让处理过程变得更清晰并提高易读性。
    • EOF 字符串标识了重定向给 bc 命令的数据的起止。当然,必须用命令替换符标识出用来给变量赋值的命令。
    • 在这个例子中,可以在 bash 计算器中为变量赋值。
  • 在 bash 计算器中创建的变量仅在计算器中有效,不能在 shell 脚本中使用。
  1. 退出脚本
  • shell 中运行的每个命令都使用退出状态码来告诉 shell 自己已经运行完毕。退出状态码是一个 0 ~ 255 的整数值, 在命令结束运行时由其传给 shell。可以获取这个值并在脚本中使用。
  1. 查看退出状态码
  • Linux 提供了专门的变量 ? 来保存最后一个已执行命令的退出状态码。对于需要进行检查的命令,必须在其运行完毕后立刻查看或使用 ?来保存最后一个已执行命令的退出状态码。对于需要进行检查的 命令,必须在其运行完毕后立刻查看或使用 ?来保存最后一个已执行命令的退出状态码。对于需要进行检查的命令,必须在其运行完毕后立刻查看或使用?变量。这是因为该变量的值会随时变成由 shell 所 执行的最后一个命令的退出状态码。

  • 按照惯例,对于成功结束的命令, 其退出状态码是 0。对于因错误而结束的命令,其退出状态码是一个正整数。

  • 无效命令会返回退出状态码 127。

  • Linux 退出状态码

    状态码描述
    0命令成功结束
    1一般性未知错误
    2不适合的 shell 命令
    126命令无法执行
    127没找到命令
    128无效的退出参数
    128+x与 Linux 信号 x 相关的严重错误
    130通过 Ctrl+C 终止的命令
    255正常范围之外的退出状态码
  • 退出状态码 126 表明用户没有执行命令的正确权限

  • 另一个常见错误是给命令提供了无效参数,这会产生一般性的退出状态码 1,表明在命令中发生了未知错误。

  1. exit 命令
  • 在默认情况下, shell 脚本会以脚本中的最后一个命令的退出状态码退出。
  • 可以改变这种默认行为,返回自己的退出状态码。 exit 命令允许在脚本结束时指定一个退出状态码:
    $ cat test13
    #!/bin/bash
    # testing the exit status
    var1=10
    var2=30
    var3=$[ $var1 + $var2 ]
    echo The answer is $var3
    exit 5
    $
    $ chmod u+x test13
    $ ./test13
    The answer is 40
    $ echo $?
    5
    $
    
  • 也可以使用变量作为 exit 命令的参数。但使用这个功能时要小心,因为退出状态码最大只能是 255。
  • 退出状态码被缩减到了 0 ~ 255 的区间。 shell 通过模运算得到这个结果。一个值的模就是被 除后的余数。最终的结果是指定的数值除以 256 后得到的余数。如果指定的值是 300(返回值),余数是 44,这个余数就成了最后的退出状态码。
  • 用 if-then 语句来检查某个命令返回的错误状态码, 以便知道命令是否成功。
  1. date 命令允许使用-d 选项指定特定日期(以任意格式), 然后以我们定义的其他格式输出该日期。
    $date -d "Jan 1, 2020" +%s
    1577854800
    $
    
    • 输出的是纪元时间(纪元时间将时间 指定为 1970 年 1 月 1 日午夜后的整数秒(这是一个古老的 Unix 标准))。

十二、结构化命令
  1. 使用 if-then 语句
  • 最基本的结构化命令是 if-then 语句。if-then 语句的格式如下:
    if command
    then
        commands
    fi
    
  • bash shell 的 if 语句会运行 if 之后的命令。如果该命令的退出状态码为 0(命令成功运行), 那么位于 then 部分的命令就会被执行。如果该命令的退出状态码是其他值,则 then 部分的命令不会被执行,bash shell 会接着处理脚本中的下一条命令。
  • fi 语句用来表示 if-then 语句到此结束。
  • 如果在 if 语句中故意使用了一个不存在的命令 IamNotaCommand,if 语句中的那个错误命令所产生的错误消息依然会显示在脚本的输出中。
  • 通过把分号( ; )放在待求值的命令尾部,可以将 then 语句写在同一行,这样看起来更像其他编程语言中的 if-then 语句。
    if command; then
        commands
    fi
    
  • 能出现在 then 部分的命令可不止一条。可以像脚本中其他地方一样在这里列出多条命令。 bash shell 会将这些命令视为一个代码块,如果 if 语句行命令的退出状态值为 0,那么代码块中 所有的命令都会被执行;否则,会跳过整个代码块。
  1. if-then-else 语句
  • if-then-else 语句在语句中提供了另外一组命令:
    if command
    then
        commands
    else
        commands
    fi
    
  • 当 if 语句中的命令返回退出状态码 0 时, then 部分中的命令会被执行;当 if 语句中的命令返回非 0 退出状态码时, bash shell 会执行 else 部分中的命令。
  • 跟 then 部分一样, else 部分可以包含多条命令。fi 语句说明 else 部分结束。
  1. 嵌套 if 语句
  • ls 命令的另一些使用选项(以及选项组合):
    • -d:只显示目录,但不显示目录内容。
    • -sh:以人类易读的格式显示文件大小。
    • -g:在文件长列表中不显示文件属主。
    • -o:在文件长列表中不显示文件属组。
  • 嵌套部分位于主 if-then-else 语句的 else 代码块中时,代码不易阅读。
    if command1
    then
        commands
    else
        if command2
        then 
            more commands
        fi
    fi
    
  • 可以使用 else 部分的另一种名为 elif 的形式, 这样就不用再写多个 if-then 语句了。 elif 使用另一个 if-then 语句延续 else 部分:
    if command1
    then
        commands
    elif command2
    then
        more commands
    fi
    
    • 如果 elif 之后的命令的退出状态码是 0,则 bash 会执行第二个 then 语句部分的命令。这种嵌套形式使得代码更清晰,逻辑更易懂。
  • 可以通过在嵌套 elif 中加入一个 else 语句:
    if command1
    then
        commands
    elif command2
        then
            more commands1
        else
            more commands2  
    fi
    
    • 在 elif 语句中, 紧跟其后的 else 语句语句的代码块属于 elif 代码块,不属于之前的 if-then 语句的代码块。
  • 可以继续将多个 elif 语句串起来, 形成一个更大的 if-then-elif 嵌套组合:
    if command1
    then
        command set 1
    elif command2
    then
        command set 2
    elif command3
    then
        command set 3
    elif command4
    then
        command set 4
    fi
    
    • 每个代码块会根据命令是否会返回退出状态码 0 来执行。
    • bash shell 会依次执行 if 语句,只有第一个返回退出状态码 0 的语句中的 then 部分会被执行。
  1. test 命令
  • test 命令可以在 if-then 语句中测试不同的条件。如果 test 命令中列出的条件成立, 那么 test 命令就会退出并返回退出状态码 0。这样 if-then 语句的工作方式就和其他编程语言中的 if-then 语句差不多了。如果条件不成立,那么 test 命令就会退出并返回非 0 的退出状态码, 这使得 if-then 语句不会再被执行。
  • test 命令的格式非常简单:
    test condition
    
    if test condition
    then
        commands1
    else
        commands2
    fi
    
    • condition 是 test 命令要测试的一系列参数和值。
    • 如果不写 test 命令的 condition 部分,则它会以非 0 的退出状态码退出并执行 else 代码块语句。
    • 如果加入了条件,则 test 命令会测试该条件。
  • 可以使用 test 命令确定变量中是否为空。这只需要一个简单的条件表达式:
    $ cat test6.sh
    #!/bin/bash
    # testing if a variable has content
    #
    my_variable="Full"      // 情况二:my_variable="" 
    #
    if test $my_variable
    then
        echo "The my_variable variable has content and returns a True."
        echo "The my_variable variable content is: $my_variable"
    else
        echo "The my_variable variable doesn't have content,"
        echo "and returns a False."
    fi
    $
    $ ./test6.sh
    The my_variable variable has content and returns a True.
    The my_variable variable content is: Full
    $
    
    • 由于变量 my_variable 中包含内容( Full),因此当 test 命令测试条件时,返回的退出状态码为 0。这使得 then 语句块中的语句得以执行。
  • bash shell 提供了另一种条件测试方式, 无须在 if-then 语句中写明 test 命令:
    if [ condition ]
    then
        commands
    fi
    
    • 方括号定义了测试条件。注意,第一个方括号之后和第二个方括号之前必须留有空格, 否则 就会报错。
  • test 命令和测试条件可以判断 3 类条件:
    • 数值比较
    • 字符串比较
    • 文件比较
  1. 数值比较
  • test 命令的数值比较功能:

    比较描述
    n1 -eq n2检查 n1 是否等于 n2
    n1 -ge n2检查 n1 是否大于或等于 n2
    n1 -gt n2检查 n1 是否大于 n2
    n1 -le n2检查 n1 是否小于或等于 n2
    n1 -lt n2检查 n1 是否小于 n2
    n1 -ne n2检查 n1 是否不等于 n2
  • 数值条件测试可用于数字和变量。

    if [ $value1 -gt 5 ]
    if [ $value1 -eq $value2 ]
    
  • 对于条件测试, bash shell 只能处理整数。尽管可以将浮点值用于某些命令(比如 echo),但它们在条件测试下无法正常工作。

  1. 字符串比较
  • 条件测试还允许比较字符串值。

  • test 命令的字符串比较功能:

    比较描述
    str1 = str2检查 str1 是否和 str2 相同
    str1 != str2检查 str1 是否和 str2 不同
    str1 < str2检查 str1 是否小于 str2
    str1 > str2检查 str1 是否大于 str2
    -n str1检查 str1 的长度是否不为 0
    -z str1检查 str1 的长度是否为 0
  • 字符串相等性

    if [ $testuser = christine ]
    if [ $testuser != christine ]
    
    • 在比较字符串的相等性时,比较测试会将所有的标点和大小写情况都考虑在内。
  • 字符串顺序

    • 使用测试条件的大于或小于功能时,会出现两个经常困扰 shell 程序员的问题:
      • 大于号和小于号必须转义, 否则 shell 会将其视为重定向符,将字符串值当作文件名。
      • 大于和小于顺序与 sort 命令所采用的不同。
      if [ $string1 > $string2 ]          // 没有报错,但结果与预期结果不符。需要使用反斜线(\)正确地转义大于号。
      if [ $string1 \> $string2 ]
      
    • 字符串 soccer 小于 zorbfootball,因为在比较的时候使用的是每个字符的 Unicode 编码值。
  • sort 命令处理大写字母的方法刚好与 test 命令相反:

    • 在比较测试中,大写字母被认为是小于小写字母的。但 sort 命令正好相反。当你将同样的 字符串放进文件中并用 sort 命令排序时,小写字母会先出现。
    • 比较测试中使用的是标准的 Unicode 顺序, 根据每个字符的 Unicode 编码值来决定排序结果。 sort 命令使用的是系统的语言环境设置中定义的排序顺序。对于英语,语言环境设置指定了在排序顺序中小写字母出现在大写字母之前。
  • test 命令和测试表达式使用标准的数学比较符号来表示字符串比较,而用文本代码来表示数值比较。

  • 字符串大小

    • -n 和-z 可以很方便地用于检查一个变量是否为空:
      if [ -n $string1 ]
      if [ -z $string2 ]
      
      • shell 脚本中并未定义该变量时,长度也视为 0,尽管它未被定义过。
  • 空变量和未初始化的变量会对 shell 脚本测试造成灾难性的影响。如果不确定变量的内容,那么最好在将其用于数值或字符串比较之前先通过-n 或-z 来测试一下变量是否为空。

  1. 文件比较
  • 比较测试允许测试 Linux 文件系统中文件和目录的状态。

  • test 命令的文件比较功能

    比较描述
    -d file检查 file 是否存在且为目录
    -e file检查 file 是否存在
    -f file检查 file 是否存在且为文件
    -r file检查 file 是否存在且可读
    -s file检查 file 是否存在且非空
    -w file检查 file 是否存在且可写
    -x file检查 file 是否存在且可执行
    -O file检查 file 是否存在且属当前用户所有
    -G file检查 file 是否存在且默认组与当前用户相同
    file1 -nt file2检查 file1 是否比 file2 新
    file1 -ot file2检查 file1 是否比 file2 旧
  • 检查目录:-d 测试会检查指定的目录是否存在于系统中。如果打算将文件写入目录或是准备切换到某个目录, 那么先测试一下总是件好事:

    #!/bin/bash
    jump_directory=/home/Torfa
    if [ -d $jump_directory ]
    then
        cd $jump_directory
        ls
    else
        echo "The $jump_directory directory does NOT exist."
    fi
    
  • 检查对象是否存在:-e 测试允许在使用文件或目录前先检查其是否存在:

    location=$HOME
    file_name="sentinel"
    if [ -d $location ]
    if [ -e $location/$file_name ]
    
  • 检查文件:-e 测试可用于文件和目录。如果要确定指定对象为文件,那就必须使用-f 测试:

    object_name=$HOME           // object_name=$HOME/sentinel
    if [ -e $object_name ]
    if [ -f $object_name ]
    
  • 检查是否可读:在尝试从文件中读取数据之前,最好先使用-r 测试检查一下文件是否可读:

    pwfile=/etc/shadow
    if [ -f $pwfile ]
    if [ -r $pwfile ]
    
  • 检查空文件:应该用-s 测试检查文件是否为空, 尤其是当你不想删除非空文件时。要当心,如果-s 测试 成功,则说明文件中有数据:

    file_name=$HOME/sentinel
    if [ -f $file_name ]
    if [ -s $file_name ]
    
  • 检查是否可写:-w 测试可以检查是否对文件拥有可写权限:

    item_name=$HOME/sentinel
    if [ -f $item_name ]
    if [ -w $item_name ]
    
  • 检查文件是否可以执行:-x 测试可以方便地判断文件是否有执行权限。虽然可能大多数命令用不到它,但如果想在 shell 脚本中运行大量程序, 那就得靠它了:

    item_name=$HOME/scripts/can-I-write-to-it.sh
    if [ -x $item_name ]
    
  • 检查所有权:-O 测试可以轻松地检查你是否是文件的属主:

    if [ -O /etc/passwd ]
    
    • 使用-O 测试来检查运行脚本的用户是否是/etc/passwd 文件的属主。
  • 检查默认属组关系:-G 测试可以检查文件的属组, 如果与用户的默认组匹配,则测试成功。 -G 只会检查默认组而非用户所属的所有组:

    if [ -G $HOME/TestGroupFile ]
    
  • 检查文件日期:

    • 比较两个文件的创建日期。这在编写软件安装脚本时非常有用。有时, 你 可不想安装一个比系统中已有文件还要旧的文件。
    • -nt 测试会判定一个文件是否比另一个文件更新。如果文件较新, 那意味着其文件创建日期 更晚。 -ot 测试会判定一个文件是否比另一个文件更旧。如果文件较旧, 则意味着其文件创建日期更早。
    if [ $HOME/Downloads/games.rpm -nt $HOME/software/games.rpm ]
    
    • 如果有其中一个文件不存在, 那么-nt测试返回的信息就不正确。在-nt 或-ot 测试之前,务必确保文件存在。
  1. 复合条件测试
  • if-then 语句允许使用布尔逻辑将测试条件组合起来。可以使用以下两种布尔运算符:
    • [ condition1 ] && [ condition2 ]
    • [ condition1 ] || [ condition2 ]
  • 布尔逻辑是一种将可能的返回值简化(reduce)为真(TRUE)或假(FALSE)的方法。
  1. if-then 的高级特性
  • bash shell 还提供了 3 个可在 if-then 语句中使用的高级特性:
    • 在子 shell 中执行命令的单括号。
    • 用于数学表达式的双括号。
    • 用于高级字符串处理功能的双方括号。
  1. 使用单括号
  • 单括号允许在 if 语句中使用子 shell。单括号形式的 test 命令格式如下:
    (command)
    
  • 在 bash shell 执行 command 之前,会先创建一个子 shell,然后在其中执行命令。如果命令成功结束, 则退出状态码会被设为 0,then 部分的命令就会被执行。如果命令的退出状态码不为 0,则不执行 then 部分的命令。
    echo $BASH_SUBSHELL
    if (echo $BASH_SUBSHELL)
    then
        xxx
    else
        xxx
    fi
    
    • 当脚本第一次(在 if 语句之前)执行 echo $BASH_SUBSHELL 命令时,是在当前 shell 中完成的。该命令会输出 0,表明没有使用子 shell。在 if 语句内,脚本在子 shell 中执行 echo $BASH_SUBSHELL 命令,该命令会输出 1,表明使用了子 shell 。
  • 当你在 if test 语句中使用进程列表时,可能会出现意料之外的结果。哪怕进程列表中除最后一个命令之外的其他命令全都失败,子 shell 仍会将退出状态码设为 0,then 部分的命令将得以执行。
  1. 使用双括号
  • 双括号命令允许在比较过程中使用高级数学表达式。 test 命令在进行比较的时候只能使用 简单的算术操作。双括号命令提供了更多的数学符号。双括号命令的格式如下:

    (( expression ))
    
    • expression 可以是任意的数学赋值或比较表达式。
  • 除了test 命令使用的标准数学运算符,下表还列出了双括号中可用的其他运算符。双括号命令符号:

    符号描述
    val++后增
    val–后减
    ++val先增
    –val先减
    !逻辑求反
    ~位求反
    **幂运算
    <<左位移
    >>右位移
    &位布尔 AND
    |位布尔 OR
    &&逻辑 AND
    ||逻辑 OR
  • 双括号命令既可以在 if 语句中使用,也可以在脚本中的普通命令里用来赋值。

    val1=10
    if (( $val1 ** 2 > 90 ))
    then
        (( val2 = $val1 ** 2 ))
    fi
    
    • 注意,双括号中表达式的大于号不用转义。
  1. 使用双方括号
  • 双方括号命令提供了针对字符串比较的高级特性。双方括号的格式如下:
    [[ expression ]]
    
    • expression 可以使用 test 命令中的标准字符串比较。除此之外, 它还提供了 test 命令 所不具备的另一个特性——模式匹配。
  • 双方括号在 bash shell 中运行良好。不过要小心, 不是所有的 shell 都支持双方括号。
  • 在进行模式匹配时,可以定义通配符或正则表达式来匹配字符串:
    if [[ $BASH_VERSION == 5.* ]]
    
    • 双等号会将右侧的字符串( 5.* )视为一个模式并应用模式匹配规则。双方括号命令会对$BASH_VERSION 环境变量进行匹配,看是否以字符串 5.起始。 如果是,则测试通过,shell 会执行 then 部分的命令。
  • 当在双中括号内使用==运算符或!=运算符时,运算符的右侧被视为通配符。如果使用的是=~运算符,则运算符的右侧被视为 POSIX 扩展正则表达式。
  1. case 命令
  • 有了 case 命令,就无须再写大量的 elif 语句来检查同一个变量的值了。 case 命令会采用列表格式来检查变量的多个值:
    case variable in
    pattern1 | pattern2) commands1;;
    pattern3) commands2;;
    *) default commands;;
    esac
    
    • case 命令会将指定变量与不同模式进行比较。如果变量与模式匹配,那么shell 就会执行为 该模式指定的命令。你可以通过竖线运算符在一行中分隔出多个模式。星号会捕获所有与已知模 式不匹配的值。
  • 下面是一个将 if-then-else 程序转换成使用 case 命令的例子:
    $ cat ShortCase.sh
    #!/bin/bash
    # Using a short case statement
    #
    case $USER in
    rich | christine)
        echo "Welcome $USER"
        echo "Please enjoy your visit.";;
    barbara | tim)
        echo "Hi there, $USER"
        echo "We're glad you could join us.";;
    testing)
        echo "Please log out when done with test.";;
    *)
        echo "Sorry, you are not allowed here."
    esac
    $
    $ ./ShortCase.sh
    Welcome christine
    Please enjoy your visit.
    $
    
  1. 输出重定向出现在单括号内的 which 命令之后。which 命令的常规(标准) 输出和标准错误信息都通过&>符号被重定向至/dev/null,这个地方被幽默地称为 黑洞,因为被送往这里的东西从来都是有去无回。
    if (which yum &> /dev/null)     // 检查是否有 yum 工具
    

十三、更多的结构化命令
  1. for 命令
  • bash shell 提供了 for 命令,以允许创建遍历一系列值的循环。每次迭代都使用其中一个值来执行已定义好的一组命令。for 命令的基本格式如下:
    for var in list
    do
        commands
    done
    
    • 需要提供用于迭代的一系列值作为 list 参数。指定这些值的方法不止一种。
    • 在每次迭代中,变量 var 会包含列表中的当前值。第一次迭代使用列表中的第一个值,第二次迭代使用列表中的第二个值,以此类推,直到用完列表中的所有值。
    • do 语句和done 语句之间的 commands 可以是一个或多个标准的 bash shell 命令。在这些命令中, $var 变量包含着此次迭代对应的列表中的当前值。
    • 也可以将 do 语句和 for 语句放在同一行,但必须用分号将其同列表中的值分开: for var in list; do。
  1. 读取列表中的值
  • for 命令最基本的用法是遍历其自身所定义的一系列值:
    for test in Alabama Alaska Arizona Arkansas California Colorado
    do
        echo The next state is $test
    done
    echo "The last state we visited was $test"
    
    • 每次遍历值列表时,for 命令会将列表中的下一个值赋给$test 变量。
    • 在最后一次迭代结束后, $test 变量的值在 shell 脚本的剩余部分依然有效。它会一直保持最后一次迭代时的值(除非做了修改)。
    • $test 变量保持着它的值, 也允许我们对其做出修改, 在 for 循环之外跟其他变量一样使用。
  1. 读取列表中的复杂值
  • 有两种方法可以解决列表值中的单引号的解析问题:
    • 使用转义字符(反斜线)将单引号转义。
    • 使用双引号来定义含有单引号的值。
    for test in I don't know if this'll work
    for test in I don\'t know if "this'll" work
    
  • for 循环假定各个值之间是以空格分隔的(准确地说,既可以是空格,也可以是制表符或换行符)。
  • 如果某个值含有空格,则必须将其放入双引号内:
    for test in Nevada New Hampshire New Mexico New York North Carolina
    for test in Nevada "New Hampshire" "New Mexico" "New York"
    
    • 当使用双引号引用某个值时, shell 并不会将双引号当成值的一部分。
  1. 从变量中读取值列表
  • 将一系列值集中保存在了一个变量中,然后需要遍历该变量中的整个值列表。可以通过 for 命令完成这个任务:
    list="Alabama Alaska Arizona Arkansas Colorado"
    list=$list" Connecticut"
    for state in $list
    
    • $list 变量包含了用于迭代的值列表。
    • 注意,脚本中还使用了另一个赋值语句向 $list 变量包含的值列表中追加(或者说是拼接)了一项。这是向变量中已有的字符串尾部添加文本的一种常用方法。
  1. 从命令中读取值列表
  • 生成值列表的另一种途径是使用命令的输出。可以用命令替换来执行任何能产生输出的命令,然后在 for 命令中使用该命令的输出:
    file="states.txt"
    for state in $(cat $file)
    
    $ cat states.txt
    Alabama
    Alaska
    Arizona
    
    $ ./test5
    Visit beautiful Alabama
    Visit beautiful Alaska
    Visit beautiful Arizona
    
    • 这个例子在命令替换中使用 cat 命令来输出文件 states.txt 的内容。注意,states.txt 文件中每个值各占一行,而不是以空格分隔。for 命令仍然以每次一行的方式遍历 cat 命令的输出。但这并没有解决数据中含有空格的问题。如果你列出了一个名字中有空格的州,则 for 命令仍然会用空格来分隔值。
    • 这个例子将不包含路径的文件名赋给了变量。这要求文件和脚本位于同一个目录中。如果并非如此, 则需要使用完整路径名(不管是绝对路径还是相对路径)来引用文件位置。
  1. 更改字段分隔符
  • 造成这个问题的原因是特殊的环境变量 IFS(internal field separator,内部字段分隔符)。 IFS 环境变量定义了 bash shell 用作字段分隔符的一系列字符。在默认情况下,bash shell 会将下列字符视为字段分隔符。
    • 空格
    • 制表符
    • 换行符
  • 如果 bash shell 在数据中看到了这些字符中的任意一个,那么它就会认为这是列表中的一个新字段的开始。在处理可能含有空格的数据(比如文件名)时,这就很烦人了。解决这个问题的办法是在 shell脚本中临时更改 IFS 环境变量的值来限制被bash shell视为字段分隔符的字符。如果想修改 IFS 的值,使其只能识别换行符,可以这么做:
    IFS=$'\n'
    
    file="states.txt"
    IFS=$'\n'
    for state in $(cat $file)
    
    • 将该语句加入脚本,告诉 bash shell 忽略数据中的空格和制表符。
  • 在处理代码量较大的脚本时,可能在一个地方需要修改 IFS 的值,然后再将其恢复原状,而脚本的其他地方则继续沿用 IFS 的默认值。一种安全的做法是在修改 IFS 之前保存原来的 IFS 值,之后再恢复它。这种技术可以像下面这样来实现:
    IFS.OLD=$IFS
    IFS=$'\n'
    <在代码中使用新的 IFS 值>
    IFS=$IFS.OLD
    
  • 如果要遍历文件中以冒号分隔的值(比如/etc/passwd 文件),则只需将 IFS 的值设为冒号即可:
    IFS=:
    
  • 如果要指定多个 IFS 字符,则只需在赋值语句中将这些字符写在一起即可:
    IFS=$'\n:;"'
    
    • 该语句会将换行符、冒号、分号和双引号作为字段分隔符。如何使用 IFS 字符解析数据没有任何限制。
  1. 使用通配符读取目录
  • 可以用 for 命令来自动遍历目录中的文件。为此,必须在文件名或路径名中使用通配符,这会强制 shell 使用文件名通配符匹配(file globbing)。文件名通配符匹配是生成与指定通配符匹配的文件名或路径名的过程。
    for file in /home/rich/test/*
    do
        if [ -d "$file" ]
        then
            echo "$file is a directory"
        elif [ -f "$file" ]
        then
            echo "$file is a file"
        fi
    done
    
    • for 命令会遍历/home/rich/test/*匹配的结果。
    • 在 Linux 中,目录名和文件名中包含空格是完全合法的。要应对这种情况,应该将$file 变量放入双引号内。否则,遇到含有空格的目录名或文件名时会产生错误。在 test 命令中, bash shell 会将额外的单词视为参数,引发错误。
  • 也可以在 for 命令中列出多个目录通配符:
    for file in /home/rich/.b* /home/rich/badtest
    
    • for 语句首先遍历了由文件名通配符匹配生成的文件列表,然后遍历了列表中的下一个文件。
    • 可以将任意多的通配符放进列表中。
  • 注意,可以在值列表中放入任何东西。即使文件或目录不存在, for 语句也会尝试把列表处理完。如果是和文件或目录打交道,那就要出问题了。你无法知道正在遍历的目录是否存在:最好在处理之前先测试一下文件或目录。
  1. C 语言中的 for 命令
  • C 语言中的 for 命令包含循环变量初始化、循环条件以及每次迭代时修改变量的方法。当指 定的条件不成立时, for 循环就会停止。迭代条件使用标准的数学符号定义。
  • bash shell 也支持 for 循环,bash 中仿 C 语言的 for 循环的基本格式如下:
    for (( variable assignment ; condition ; iteration process ))
    
  • 注意,有些地方与 bash shell 标准的 for 命令并不一致。
    • 变量赋值可以有空格。
    • 迭代条件中的变量不以美元符号开头。
    • 迭代过程的算式不使用 expr 命令格式。
  • 下面这个例子在 bash shell 程序中使用了仿 C 语言的 for 命令:
    for (( i=1; i <= 10; i++ ))
    do
        echo "The next number is $i"
    done
    
    • for 循环通过定义好的变量(本例中是变量 i)来迭代执行这些命令。在每次迭代中, $i 变量都包含 for 循环中赋予的值。在每次迭代后,循环的迭代过程会作用于变量,在本例中,是将变量值增 1。
  1. 使用多个变量
  • 仿 C 语言的 for 命令也允许为迭代使用多个变量。循环会单独处理每个变量,你可以为每个变量定义不同的迭代过程。尽管可以使用多个变量, 但只能在 for 循环中定义一种迭代条件:
    for (( a=1, b=10; a <= 10; a++, b-- ))
    
  1. while 命令
  • while 命令允许定义一个要 测试的命令, 只要该命令返回的退出状态码为 0,就循环执行一组命令。它会在每次迭代开始时 测试 test 命令,如果 test 命令返回非 0 退出状态码, while 命令就会停止执行循环。
  • while 命令的格式如下:
    while test command
    do
        other commands
    done
    
    • while 命令中定义的 test command 与 if-then 语句中的格式一模一样。 可以使用任何 bash shell 命令,或者用 test command 进行条件测试,比如测试变量值。
    • while 命令的关键在于所指定的 test command 的退出状态码必须随着循环中执行的命令而改变。如果退出状态码不发生变化,那 while 循环就成了死循环。
    • test command 最常见的用法是使用方括号来检查循环命令中用到的 shell 变量值:
      var1=10
      while [ $var1 -gt 0 ]
      do
          echo $var1
          var1=$[ $var1 - 1 ]
      done
      
  1. 使用多个测试命令
  • while 命令允许在 while 语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用于决定是否结束循环。
    var1=10
    while echo $var1
        [ $var1 -ge 0 ]
    do
        echo "This is inside the loop"
        var1=$[ $var1 - 1 ]
    done
    
    • 上述例子在 while 语句中定义了两个测试命令:第一个测试简单地显示了 var1 变量的当前值;第二个测试用方括号来判断 var1 变量的值。
    • while 循环会在 var1 变量等于 0 时执行 echo 语句,然后将 var1 变量的值减 1 。接下来 再次执行测试命令,判断是否进行下一次迭代。首先执行 echo 测试命令,显示 var 变量的值(小于 0)。接着执行 test 命令,因为条件不成立,所以 while 循环停止。
  • 在含有多个命令的 while 语句中,在每次迭代时所有的测试命令都会被执行,包括最后一个测试命令失败的末次迭代。
  • 注意要把每个测试命令都单独放在一行中。
  1. until 命令
  • 与 while 命令工作的方式完全相反, until 命令要求指定一个返回非 0 退出状态码的测试命令。只要测试命令的退出状态码不为 0,bash shell 就会执行循环中列出的命令。一旦测试命令 返回了退出状态码 0,循环就结束了。
  • until 命令的格式如下:
    until test commands
    do
        other commands
    done
    
    • 与 while 命令类似,你可以在 until 命令语句中放入多个 test command。最后一个命令的退出状态码决定了 bash shell 是否执行已定义的 other commands。
  • 一个 until 命令的例子:
    $ cat test12
    #!/bin/bash
    # using the until command
    var1=100
    until [ $var1 -eq 0 ]
    do
        echo $var1
        var1=$[ $var1 - 25 ]
    done
    
  • 同 while 命令一样, 在 until 命令中使用多个测试命令时也要注意:
    var1=100
    until echo $var1
          [ $var1 -eq 0 ]
    do
        echo Inside the loop: $var1
        var1=$[ $var1 - 25 ]
    done
    
    • shell 会执行指定的多个测试命令,仅当最后一个命令成立时才停止。
  1. 嵌套循环
  • 循环语句可以在循环内使用任意类型的命令,包括其他循环命令,这称为嵌套循环。注意, 在使用嵌套循环时是在迭代中再进行迭代, 命令运行的次数是乘积关系。
  • shell 能够区分开内层 for 循环和外层 while 循环各自的 do 命令和 done 命令。
  1. 循环处理文件数据
  • 通过修改 IFS 环境变量,能强制 for 命令将文件中的每一行都作为单独的条目来处理,即便数据中有空格也是如此。从文件中提取出单独的行后,可能还得使用循环来提取行中的数据。
  • 典型的例子是处理/etc/passwd 文件。这要求你逐行遍历该文件, 将 IFS 变量的值改成冒号, 以便分隔开每行中的各个字段:
    #!/bin/bash
    # changing the IFS value
    
    IFS.OLD=$IFS
    IFS=$'\n'
    for entry in $(cat /etc/passwd)
    do
        echo "Values in $entry –"
        IFS=:
        for value in $entry
        do
            echo "   $value"
        done
    done
    $
    
    • 第一个 IFS 值解析出/etc/passwd 文件中的 各行。内层 for 循环接着将 IFS 的值修改为冒号,以便解析出/etc/passwd 文件各行中的字段。
  1. 循环控制
  • 有两个命令可以控制循环的结束时机:
    • break 命令
    • continue 命令
  1. break 命令
  • break 命令是退出循环的一种简单方法。你可以用 break 命令退出任意类型的循环,包括 while 循环和 until 循环。
  • 跳出单个循环:shell 在执行 break 命令时会尝试跳出当前正在执行的循环:
    for var1 in 1 2 3 4 5 6 7 8 9 10
    do
        if [ $var1 -eq 5 ]
        then
            break
        fi
        echo "Iteration number: $var1"
    done
    
  • 跳出内层循环:在处理多个循环时, break 命令会自动结束你所在的最内层循环:
    for (( a = 1; a < 4; a++ ))
    do
        echo "Outer loop: $a"
        for (( b = 1; b < 100; b++ ))
        do
            if [ $b -eq 5 ]
            then
                break
            fi
            echo "   Inner loop: $b"
        done
    done
    
    • 即使 break 命令结束了内层循环, 外层循环依然会继续执行。
  • 跳出外层循环:有时你位于内层循环, 但需要结束外层循环。 break 命令接受单个命令行参数:
    break n
    
    • 其中 n 指定了要跳出的循环层级。在默认情况下,n 为 1,表明跳出的是当前循环。如果将 n 设 为 2,那么 break 命令就会停止下一级的外层循环:
      for (( a = 1; a < 4; a++ ))
      do
          echo "Outer loop: $a"
          for (( b = 1; b < 100; b++ ))
          do
              if [ $b -gt 4 ]
              then
                  break 2
              fi
              echo "   Inner loop: $b"
          done
      done
      
      • 当 shell 执行了 break 命令后,外部循环就结束了。
  1. continue 命令
  • continue 命令可以提前中止某次循环, 但不会结束整个循环。你可以在循环内部设置shell 不执行命令的条件。
  • 来看一个在 for 循环中使用continue 命令的简单例子:
    for (( var1 = 1; var1 < 15; var1++ ))
    do
        if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
        then
            continue
        fi
        echo "Iteration number: $var1"
    done
    
    • 当 if-then 语句的条件成立时(值大于 5 且小于 10),shell 会执行 continue 命令, 跳过 此次循环中剩余的命令, 但整个循环还会继续。当 if-then 的条件不成立时, 一切会恢复如常。
    • 也可以在 while 循环和until 循环中使用 continue 命令, 但要特别小心。记住, 当shell 执行 continue 命令时,它会跳过剩余的命令。如果将测试变量的增值操作放在了其中某个条件里, 那么问题就出现了。
  • 和 break 命令一样, continue 命令也允许通过命令行参数指定要继续执行哪一级循环:
    continue n
    
    • 其中 n 定义了要继续的循环层级。
  1. 处理循环的输出
  • 在 shell 脚本中,可以对循环的输出使用管道或进行重定向。这可以通过在 done 命令之后 添加一个处理命令来实现:
    for file in /home/rich/*
    do
        if [ -d "$file" ]
        then
            echo "$file is a directory"
        elif
            echo "$file is a file"
        fi
    done > output.txt
    
    • shell 会将 for 命令的结果重定向至文件output.txt ,而不再显示在屏幕上。
  • 这种方法同样适用于将循环的结果传输到另一个命令:
    for state in "North Dakota" Connecticut Illinois Alabama Tennessee
    do
        echo "$state is the next place to go"
    done | sort
    
    • for 命令的输出通过管道传给了 sort 命令,由后者对输出结果进行排序。运行该脚本, 可以看出结果已经按 state 的值排好序了。
  1. 实战演练-查找可执行文件

    $ cat test25
    #!/bin/bash
    # finding files in the PATH
    
    IFS=:
    for folder in $PATH
    do
        echo "$folder:"
        for file in $folder/*
        do
            if [ -x $file ]
            then
                echo "   $file"
            fi
        done
    done
    
    • 当运行该脚本时,会得到一个可以在命令行中使用的可执行文件列表(输出显示了环境变量 PATH 所包含的所有目录中的所有可执行文件)。
  2. 实战演练-创建多个用户账户

  • 要想把数据从文件中传入 while 命令, 只需在 while 命令尾部使用一个重定向符即可:
    $ cat test26
    #!/bin/bash
    # process new user accounts
    
    input="users.csv"
    while IFS=',' read -r loginname name
    do
        echo "adding $loginname"
        useradd -c "$name" -m $loginname
    done < "$input"
    
    • users.csv 文本文件的格式如下:loginname, name
    • 第一项是为新用户账户所选用的用户 id。第二项是用户的全名。两个值之间以逗号分隔,这样就形成了一种叫作 CSV(comma-separated value,逗号分隔值)的文件格式。
    • 将 IFS 分隔符设置成逗号, 并将其作为 while 语句的条件测试部分。然后使用 read 命令读取文件中的各行。-r 屏蔽\,如果没有该选项,则\作为一个转义字符,有的话 \就是个正常的字符了。
    • read 命令会自动移往 CSV 文本文件的下一行。
    • 当 read 命令返回假值的时候(也就是读取完整个文件),while 命令就会退出。
    • $input 变量中保存的是数据文件名,该数据文件被作为 while 命令的数据源。
      $ cat users.csv
      rich,Richard Blum
      christine,Christine Bresnahan
      
    • 必须以 root 用户身份运行该脚本,因为 useradd 命令需要 root权限。
    • 最后可查看 /etc/passwd 文件验证执行结果。

十四、处理用户输入
  1. bash shell 提供了一些不同的方法来从
    用户处获取数据,包括命令行参数(添加在命令后的数据)、命令行选项(可改变命令行为的单个字母) 以及直接从键盘读取输入。

  2. 命令行参数允许运行脚本时在命令行中添加数据:

    $ ./addem 10 30
    
    • 向脚本 addem 传递了两个命令行参数(10 和 30)。脚本会通过特殊的变量来处理命令行参数。
  3. 读取参数

  • bash shell 会将所有的命令行参数都指派给称作位置参数(positional parameter)的特殊变量。这也包括 shell 脚本名称。位置变量的名称都是标准数字:$0 对应脚本名,$1 对应第一个命令行参数,$2 对应第二个命令行参数,以此类推,直到$9。
  • 命令行参数是在命令/脚本名之后出现的各个单词,位置参数是用于保存命令行参数(以及函数参数)的变量。
  • 下面是在 shell 脚本中使用单个命令行参数的简单例子:
    $ cat positional1.sh
    #!/bin/bash
    # Using one command-line parameter
    #
    factorial=1
    for (( number = 1; number <= $1; number++ ))
    do
        factorial=$[ $factorial * $number ]
    done
    echo The factorial of $1 is $factorial
    exit
    $
    
    • 在 shell 脚本中, 可以像使用其他变量一样使用$1 变量。 shell 脚本会自动将命令行参数的值 分配给位置变量,无须做任何特殊处理。
  • 如果需要输入更多的命令行参数, 则参数之间必须用空格分开。 shell 会将其分配给对应的位置变量。
  • 也可以在命令行中用文本字符串作为参数。
  • 参数之间是以空格分隔的,所以 shell 会将字符串包含的空格视为两个参数的分隔符。要想在参数值中加入空格,必须使用引号(单引号或双引号均可)。
  • 将文本字符串作为参数传递时,引号并不是数据的一部分,仅用于表明数据的起止位置。
  • 如果脚本需要的命令行参数不止 9 个, 则仍可以继续加入更多的参数, 但是需要稍微修改一下位置变量名。在第 9 个位置变量之后,必须在变量名两侧加上花括号,比如${10}。
    $ cat positional10.sh
    #!/bin/bash
    # Handling lots of command-line parameters
    #
    product=$[ ${10} * ${11} ]
    echo The tenth parameter is ${10}.
    echo The eleventh parameter is ${11}.
    echo The product value is $product.
    exit
    $
    $ ./positional10.sh 1 2 3 4 5 6 7 8 9 10 11 12
    The tenth parameter is 10.
    The eleventh parameter is 11.
    The product value is 110.
    
  1. 读取脚本名
  • 可以使用位置变量$0 获取在命令行中运行的 shell 脚本名。这在编写包含多种功能或生成日志消息的工具时非常方便。
  • 如果使用另一个命令来运行 shell 脚本,则命令名会和脚本名混在一起,出现在位置变量$0 中;如果运行脚本时使用的是绝对路径,那么位置变量$0 就会包含整个路径:
    $ ./positional0.sh
    This script name is ./positional0.sh .
    
    $ $HOME/scripts/positional0.sh
    This script name is /home/christine/scripts/positional0.sh .
    
  • basename 命令可以返回不包含路径的脚本名:
    name=$(basename $0)
    echo This script name is $name.
    
  1. 参数测试
  • 在 shell 脚本中使用命令行参数时要当心。如果运行脚本时没有指定所需的参数,则可能会出问题。当脚本认为位置变量中应该有数据,而实际上根本没有的时候,脚本很可能会产生错误消息。这种编写脚本的方法并不可取。在使用位置变量之前一定要检查是否为空。
  • 例如,可以使用 -n 测试来检查命令行参数$1 中是否为空。
  1. 在 bash shell 中有一些跟踪命令行参数的特殊变量:

    • 参数统计
    • 获取所有的数据
  2. 参数统计

  • 特殊变量$#含有脚本运行时携带的命令行参数的个数。可以在脚本中的任何地方使用这个特殊变量, 就跟普通变量一样。
    if [ $# -eq 1 ]
    if [ $# -ne 2 ]
    
  • 这个变量还提供了一种简便方法来获取命令行中最后一个参数:
    echo The last parameter is ${$#}        // 错误写法
    echo The last parameter is ${!#}        // 正确写法
    
    • 这说明不能在花括号内使用 ,必须将 ,必须将 ,必须将换成!。
  • 当命令行中没有任何参数时, KaTeX parse error: Expected 'EOF', got '#' at position 1: #̲的值即为 0,但{!#}会返回命令行中的脚本名。
  1. 获取所有的数据
  • ∗ 变量和 *变量和 变量和@变量可以轻松访问所有参数,它们各自包含了所有的命令行参数。
  • $*变量会将所有的命令行参数视为一个单词。这个单词含有命令行中出现的每一个参数。 基本上, $*变量会将这些参数视为一个整体,而不是一系列个体。
  • $@变量会将所有的命令行参数视为同一字符串中的多个独立的单词,以便能遍历并处理全部参数。这通常使用 for 命令完成。
  • 相同之处,例子1:
    $ cat grabbingallparams.sh
    #!/bin/bash
    # Testing different methods for grabbing all the parameters
    #
    echo
    echo "Using the \$* method: $*"
    echo
    echo "Using the \$@ method: $@"
    echo
    exit
    $
    $ ./grabbingallparams.sh alpha beta charlie delta
    
    Using the $* method: alpha beta charlie delta
    
    Using the $@ method: alpha beta charlie delta
    
    $
    
    • 从表面上看, 两个变量产生的输出相同, 均显示了所有命令行参数。
  • 不同之处,例子2:
    $ cat grabdisplayallparams.sh
    #!/bin/bash
    # Exploring different methods for grabbing all the parameters
    #
    echo
    echo "Using the \$* method: $*"
    count=1
    for param in "$*"
    do
        echo "\$* Parameter #$count = $param"
        count=$[ $count + 1 ]
    done
    #
    echo
    echo "Using the \$@ method: $@"
    count=1
    for param in "$@"
    do
        echo "\$@ Parameter #$count = $param"
        count=$[ $count + 1 ]
    done
    echo
    exit
    $ ./grabdisplayallparams.sh alpha beta charlie delta
    Using the $* method: alpha beta charlie delta
    $* Parameter #1 = alpha beta charlie delta
    Using the $@ method: alpha beta charlie delta
    $@ Parameter #1 = alpha
    $@ Parameter #2 = beta
    $@ Parameter #3 = charlie
    $@ Parameter #4 = delta
    $
    
    • 通过使用 for 命令遍历这两个特殊变量,可以看出二者如何以不同的方式处理命令行参数。 ∗ 变量会将所有参数视为单个参数,而 *变量会将所有参数视为单个参数, 而 变量会将所有参数视为单个参数,而@变量会单独处理每个参数。这是遍历命令行参数的一种绝妙方法。
  • ∗ 出现在双引号内时,会被扩展成由多个命令行参数组成的单个单词,每个参数之间以 I F S 变量值的第一个字符分隔,也就是说, " *出现在双引号内时,会被扩展成由多个命令行参数组成的单个单词, 每个参数之间以 IFS 变量值的第一个字符分隔,也就是说," 出现在双引号内时,会被扩展成由多个命令行参数组成的单个单词,每个参数之间以IFS变量值的第一个字符分隔,也就是说,"*“会被扩展为”$1c 2 c . . . " (其中 c 是 I F S 变量值的第一个字符)。当 2c..." (其中 c 是 IFS 变量值的第一个字符)。当 2c..."(其中cIFS变量值的第一个字符)。当@出现在双引号内时,其所包含的各个命令行参数会被扩展成独立的单词,也就是说,“$@” 会被扩展为"$1"“$2”…。
  1. 移动参数
  • shift 命令可用于操作命令行参数。shift 命令会根据命令行参数的相对位置进行移动。
  • 在使用 shift 命令时,默认情况下会将每个位置的变量值都向左移动一个位置。因此,变量$3 的值会移入$2,变量$2 的值会移入$1,而变量$1 的值则会被删除(注意,变量$0 的值,也就是脚本名,不会改变)。
  • 这是遍历命令行参数的另一种好方法,尤其是在不知道到底有多少参数的时候。可以只操作第一个位置变量,移动参数,然后继续处理该变量。
    $ cat shiftparams.sh
    #!/bin/bash
    # Shifting through the parameters
    #
    echo
    echo "Using the shift method:"
    count=1
    while [ -n "$1" ]
    do
        echo "Parameter #$count = $1"
        count=$[ $count + 1 ]
        shift
    done
    echo
    exit
    $
    $ ./shiftparams.sh alpha bravo charlie delta
    Using the shift method:
    Parameter #1 = alpha
    Parameter #2 = bravo
    Parameter #3 = charlie
    Parameter #4 = delta
    
    • 该脚本在 while 循环中测试第一个参数值的长度。当第一个参数值的长度为 0 时,循环结 束。测试完第一个参数后, shift 命令会将所有参数移动一个位置。
  • 使用 shift命令时要小心。如果某个参数被移出, 那么它的值就被丢弃了,无法再恢复。
  • 也可以一次性移动多个位置。只需给 shift 命令提供一个参数,指明要移动的位置数即可:
    $ cat bigshiftparams.sh
    #!/bin/bash
    # Shifting multiple positions through the parameters
    #
    echo
    echo "The original parameters: $*"
    echo "Now shifting 2..."
    shift 2
    echo "Here's the new first parameter: $1"
    echo
    exit
    $
    $ ./bigshiftparams.sh alpha bravo charlie delta
    
    The original parameters: alpha bravo charlie delta
    Now shifting 2...
    Here's the new first parameter: charlie
    
    $
    
    • 使用 shift 命令的值可以轻松地跳过不需要的参数。
  1. 处理选项
  • 选项是在连字符之后出现的单个字母,能够改变命令的行为。
  • 还有另一种选项,即以双连字符(–)起始, 紧跟着一个字符串(比如–max-depth),这种形式的选项称作长 选项。
  • 3 种在 shell 脚本中处理选项的方法:
    • 查找选项
    • 使用 getopt 命令
    • 使用 getopts 命令
  1. 查找选项
  • 在命令行中,选项紧跟在脚本名之后,就跟其他命令行参数一样。实际上,如果愿意,可以像处理命令行参数一样处理命令行选项。
  • 命令行参数是在命令/脚本名之后出现的各个单词, 其中,以连字符( - )或双连字符( --)起始的参数, 因其能 够改变命令的行为,称作命令行选项。所以,命令行选项是一种特殊形式的命令行参数。
  • 处理简单选项
    • 可以用 shift 命令来处理命令行选项。
    • 在提取单个参数时,使用 case 语句来判断某个参数是否为选项:
      $ cat extractoptions.sh
      #!/bin/bash
      # Extract command-line options
      #
      echo
      while [ -n "$1" ]
      do
          case "$1" in
              -a) echo "Found the -a option" ;;
              -b) echo "Found the -b option" ;;
              -c) echo "Found the -c option" ;;
              *) echo "$1 is not an option" ;;
          esac
          shift
      done
      echo
      exit
      $
      $ ./extractoptions.sh -a -b -c -d
      Found the -a option
      Found the -b option
      Found the -c option
      -d is not an option
      
      • case 语句会检查每个参数,确认是否为有效的选项。找到一个,处理一个。
  • 分离参数和选项
    • 在 Linux 中,处理这个问题的标准做法是使用特殊字符将两者分开,该字符会告诉脚本选项何时结束,普通参数何时开始。
    • 在 Linux 中,这个特殊字符是双连字符(–)。shell 会用双连字符表明选项部分结束。在双连字符之后, 脚本就可以放心地将剩下的部分作为参数处理了。
    • 要检查双连字符,只需在 case 语句中加一项即可:
      $ cat extractoptionsparams.sh
      #!/bin/bash
      # Extract command-line options and parameters
      #
      echo
      while [ -n "$1" ]
      do
          case "$1" in
              -a) echo "Found the -a option" ;;
              -b) echo "Found the -b option" ;;
              -c) echo "Found the -c option" ;;
              --) shift
                  break;;
              *) echo "$1 is not an option" ;;
          esac
          shift
      done
      #
      echo
      count=1
      for param in $@
      do
          echo "Parameter #$count: $param"
          count=$[ $count + 1 ]
      done
      echo
      exit
      $
      
      • 在遇到双连字符时,脚本会用 break 命令跳出while 循环。由于提前结束了循环,因此需要再加入另一个 shift 命令来将双连字符移出位置变量。
  • 处理含值的选项
    • 当命令行选项要求额外的参数时,脚本必须能够检测到并正确地加以处理。来看下面的处理方法:
      $ cat extractoptionsvalues.sh
      #!/bin/bash
      # Extract command-line options and values
      #
      echo
      while [ -n "$1" ]
      do
          case "$1" in
              -a) echo "Found the -a option" ;;
              -b) param=$2
                  echo "Found the -b option with parameter value $param"
                  shift;;
              -c) echo "Found the -c option" ;;
              --) shift
                  break;;
              *) echo "$1 is not an option" ;;
          esac
          shift
      done
      #
      echo
      count=1
      for param in $@
      do
          echo "Parameter #$count: $param"
          count=$[ $count + 1 ]
      done
      exit
      $
      $ ./extractoptionsvalues.sh -a -b BValue -d
      Found the -a option
      Found the -b option with parameter value BValue
      -d is not an option
      $
      
      • -b 选项还需要一个额外的参数值。由于要处理的选项位于$1,因此额外的参数值就应该位于$2(因为所有的参数在处理完之后都会被移出)。只要将参数值从 $2 变量中提取出来就可以了。
      • 因为这个选项占用了两个位置,所以还需要使用 shift 命令多移动一次。
  1. 使用 getopt 命令
  • 在 Linux 中,合并选项是一种很常见的用法。
  • getopt 命令在处理命令行选项和参数时非常方便。它能够识别命令行参数, 简化解析过程。
  • 命令格式
    • getopt 命令可以接受一系列任意形式的命令行选项和参数,并自动将其转换成适当的格式。
    • getopt 的命令格式如下:
      getopt optstring parameters
      
      • optstring 是这个过程的关键所在。它定义了有效的命令行选项字母,还定义了哪些选项字母需要参数值。
      • 首先,在 optstring 中列出要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后面加一个冒号。 getopt 命令会基于你定义的 optstring 解析提供的参数。
    • getopt 命令有一个更高级的版本叫作 getopts(注意这是复数形式)。注意区分。
    • 例子:
      $ getopt ab:cd -a -b BValue -cd test1 test2
      -a -b BValue -c -d -- test1 test2
      
      • optstring 定义了 4 个有效选项字母: a、b、c 和 d。冒号( :)被放在了字母 b 后面, 因为 b 选项需要一个参数值。当 getopt 命令运行时, 会检查参数列表(-a -b BValue -cd test1 test2),并基于提供的 optstring 进行解析。注意,它会自动将-cd 分成两个单独的选项,并插入双连字符来分隔命令行中额外的参数。
    • 如果 optstring 未包含你指定的选项,则在默认情况下,getopt 命令会产生一条错误消息;如果想忽略这条错误消息, 可以使用 getopt 的-q 选项:
      $ getopt ab:cd -a -b BValue -cde test1 test2
      getopt: invalid option -- 'e'
      -a -b BValue -c -d -- test1 test2
      
      $ getopt -q ab:cd -a -b BValue -cde test1 test2
      -a -b 'BValue' -c -d -- 'test1' 'test2'
      
      • getopt 命令选项必须出现在 optstring 之前。
  • 在脚本中使用 getopt
    • 可以在脚本中使用 getopt 命令来格式化脚本所携带的任何命令行选项或参数。
    • set 命令能够处理 shell 中的各种变量。set 命令有一个选项是双连字符(–),可以将位置变量的值替换成 set 命令所指定的值。
    • 具体做法是将脚本的命令行参数传给 getopt 命令,然后再将 getopt 命令的输出传给 set 命令,用 getopt 格式化后的命令行参数来替换原始的命令行参数,如下所示:
      set -- $(getopt -q ab:cd "$@")
      
    • 处理命令行参数的脚本例子:
      set -- $(getopt -q ab:cd "$@")
      echo
      while [ -n "$1" ]
      do
          case "$1" in
              -a) echo "Found the -a option" ;;
              -b) param=$2
                  echo "Found the -b option with parameter value $param"
                  shift;;
              -c) echo "Found the -c option" ;;
              --) shift
                  break;;
              *) echo "$1 is not an option" ;;
          esac
          shift
      done
      #
      echo
      count=1
      for param in $@
      do
          echo "Parameter #$count: $param"
          count=$[ $count + 1 ]
      done
      exit
      
    • getopt 命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符, 而不是根 据双引号将二者当作一个参数。
  1. 使用 getopts 命令
  • getopts(注意是复数) 是 bash shell 的内建命令。
  • getopt 与 getopts 的不同之处在于,前者在将命令行中选项和参数处理后只生成一个输出,而后者能够和已有的 shell 位置变量配合默契。
  • getopts 每次只处理一个检测到的命令行参数。在处理完所有的参数后, getopts 会退出并返回一个大于 0 的退出状态码。这使其非常适合用在解析命令行参数的循环中。
  • getopts 命令的格式如下:
    getopts optstring variable
    
    • optstring 值与 getopt 命令中使用的值类似。有效的选项字母会在 optstring 中列出, 如果选项字母要求有参数值,就在其后加一个冒号。不想显示错误消息的话, 可以在 optstring 之前加一个冒号。 getopts 命令会将当前参数保存在命令行中定义的 variable 中。
    • getopts 命令要用到两个环境变量。如果选项需要加带参数值,那么 OPTARG 环境变量保存的就是这个值。 OPTIND 环境变量保存着参数列表中 getopts 正在处理的参数位置。这样在处理完当前选项之后就能继续处理其他命令行参数了。
    • 例子:
      $ cat extractwithgetopts.sh
      #!/bin/bash
      # Extract command-line options and values with getopts
      #
      echo
      while getopts :ab:c opt
      do
          case "$opt" in
              a) echo "Found the -a option" ;;
              b) echo "Found the -b option with parameter value $OPTARG";;
              c) echo "Found the -c option" ;;
              *) echo "Unknown option: $opt" ;;
          esac
      done
      exit
      $
      $ ./extractwithgetopts.sh -ab BValue -c
      
      Found the -a option
      Found the -b option with parameter value BValue
      Found the -c option
      $
      
      • while 语句定义了getopts 命令, 指定要查找哪些命令行选项, 以及每次迭代时存储它们 的变量名(opt)。
      • 在解析命令行选项时, getopts 命令会移除起始的连字符,所以在 case 语句中不用连字符。
  • getopts 命令有几个不错的特性:
    • 对新手来说, 可以在参数值中加入空格:
      $ ./extractwithgetopts.sh -b "BValue1 BValue2" -a
      
    • 另一个好用的特性是可以将选项字母和参数值写在一起,两者之间不加空格:
      $ ./extractwithgetopts.sh -abBValue
      
      • getopts 命令能够从-b 选项中正确解析出 BValue 值。
  • 除此之外, getopts 命令还可以将在命令行中找到的所有未定义的选项统一输出成问号:
    $ ./extractwithgetopts.sh -d
    
    Unknown option: ?
    
    • optstring 中未定义的选项字母会以问号形式传给脚本。
  • getopts 命令知道何时停止处理选项,并将参数留给你处理。在处理每个选项时, getopts 会将 OPTIND 环境变量值增 1。处理完选项后,可以使用 shift 命令和 OPTIND 值来移动参数:
    while getopts :ab:cd opt
    do
        case "$opt" in
            a) echo "Found the -a option" ;;
            b) echo "Found the -b option with parameter value $OPTARG";;
            c) echo "Found the -c option" ;;
            d) echo "Found the -d option" ;;
            *) echo "Unknown option: $opt" ;;
        esac
    done
    #
    shift $[ $OPTIND - 1 ]
    #
    echo
    count=1
    for param in "$@"
    do
        echo "Parameter $count: $param"
        count=$[ $count + 1 ]
    done
    exit
    
  1. 选项标准化
  • 在 Linux 中,有些选项字母在某种程度上已经有了标准含义。如果能在 shell 脚本中支持 这些选项,则你的脚本会对用户更友好。
  • 常用的 Linux 命令行选项
    选项描述
    -a显示所有对象
    -c生成计数
    -d指定目录
    -e扩展对象
    -f指定读入数据的文件
    -h显示命令的帮助信息
    -i忽略文本大小写
    -l产生长格式输出
    -n使用非交互模式(批处理)
    -o将所有输出重定向至指定的文件
    -q以静默模式运行
    -r递归处理目录和文件
    -s以静默模式运行
    -v生成详细输出
    -x排除某个对象
    -y对所有问题回答 yes
  1. 获取用户输入
  • bash shell 提供了read 命令在脚本运行时询问用户并等待用户回答。
  1. 基本的读取
  • read 命令从标准输入(键盘)或另一个文件描述符中接受输入。获取输入后, read 命令会 将数据存入变量。下面是该命令最简单的用法:
    echo -n "Enter your name: "
    read name
    echo "Hello $name, welcome to my script."
    exit
    
    • 用于生成提示的 echo 命令使用了-n 选项。该选项不会在字符串末尾输出换行符,允许脚本用户紧跟其后输入数据。
  • read 命令也提供了-p 选项,允许直接指定提示符:
    read -p "Please enter your age: " age
    days=$[ $age * 365 ]
    echo "That means you are over $days days old!"
    
  • read 命令会将提示符后输入的所有数据分配给单个变量。如果指定多个变量,则输入的每个数据值都会分配给变量列表中的下一个变量。如果变量数量不够,那么剩下的数据就全部分配给最后一个变量:
    read -p "Enter your first and last name: " first last
    
  • 也可以在 read 命令中不指定任何变量,这样 read 命令便会将接收到的所有数据都放进特殊环境变量 REPLY 中:
    read -p "Enter your name: "
    echo
    echo "Hello $REPLY, welcome to my script."
    
    • REPLY 环境变量包含输入的所有数据,其可以在 shell 脚本中像其他变量一样使用。
  1. 超时
  • 可以用-t 选项来指定一个计时器。 -t 选项会指定 read 命令等待输入的秒数。如果计时器超时,则 read 命令会返回非 0 退出状态码:
    if read -t 5 -p "Enter your name: " name
    then
        echo "Hello $name, welcome to my script."
    else
        echo
        echo "Sorry, no longer waiting for name."
    fi
    
  • 也可以不对输入过程计时,而是让 read 命令统计输入的字符数。当字符数达到预设值时,就自动退出,将已输入的数据赋给变量:
    read -n 1 -p "Do you want to continue [Y/N]? " answer
    #
    case $answer in
    Y | y) echo
        echo "Okay. Continue on...";;
    N | n) echo
        echo "Okay. Goodbye"
        exit;;
    esac
    echo "This is the end of the script."
    exit
    
    • 本例中使用了-n 选项和数值 1,告诉 read 命令在接收到单个字符后退出。只要按下单个字符进行应答, read 命令就会接受输入并将其传给变量,无须按 Enter 键。
  1. 无显示读取
  • -s 选项可以避免在 read 命令中输入的数据出现在屏幕上(其实数据还是会被显示,只不 过 read 命令将文本颜色设成了跟背景色一样)。
  • 在脚本中使用-s 选项的例子:
    read -s -p "Enter your password: " pass
    echo
    echo "Your password is $pass"
    exit
    
    • 屏幕上不会显示输入的数据,但这些数据会被赋给变量,以便在脚本中使用。
  1. 从文件中读取
  • 也可以使用 read 命令读取文件。每次调用read 命令都会从指定文件中读取一行文本。当文件中没有内容可读时, read 命令会退出并返回非 0 退出状态码。
  • 将文件数据传给 read 命令最常见的方法是对文件使用cat 命令,将结果通过管道直接传给含有 read 命令的 while 命令:
    count=1
    cat $HOME/scripts/test.txt | while read line
    do
        echo "Line $count: $line"
        count=$[ $count + 1 ]
    done
    echo "Finished processing the file."
    exit
    
    • while 循环会持续通过 read 命令处理文件中的各行, 直到 read 命令以非0 退出状态码退出。

十五、呈现数据
  1. 标准文件描述符
  • Linux 用文件描述符来标识每个文件对象。文件描述符是一个非负整数,唯一会标识的是会话中打开的文件。每个进程一次最多可以打开 9 个文件描述符(这个数量并不是固定的)。

  • 出于特殊目的, bash shell 保留了前 3 个文件描述符(0、1 和 2),Linux 的标准文件描述符:

    文件描述符缩写描述
    0STDIN标准输入
    1STDOUT标准输出
    2STDERR标准错误
    • 这 3个特殊的文件描述符会处理脚本的输入和输出。shell 会用它们将其默认的输入和输出送往适合的位置。
  • STDIN

    • STDIN 文件描述符代表 shell 的标准输入。对终端界面来说,标准输入就是键盘。shell 会从 STDIN
      文件描述符对应的键盘获得输入并进行处理。
    • 在使用输入重定向符(<)时, Linux 会用重定向指定的文件替换标准输入文件描述符。 其实就是将标准输入文件描述符的指向由原先的键盘更改为指定的文件。
  • STDOUT

    • STDOUT 文件描述符代表 shell 的标准输出。在终端界面上,标准输出就是终端显示器。shell 的所有输出(包括 shell 中运行的程序和脚本)会被送往标准输出, 也就是显示器。
    • 在默认情况下,大多数 bash 命令会将输出送往 STDOUT 文件描述符。
    • 通过输出重定向符(>),原本应该出现在屏幕上的所有输出会被 shell 重定向到指定的文件。
    • 也可以使用>>将数据追加到某个文件。
    • 当命令产生错误消息时, shell 并未将错误消息重定向到指定文件。shell 创建了输出重定向文件,但错误消息依然显示在屏幕上。
    • shell 对于错误消息的处理是跟普通输出分开的。如果创建了一个在后台运行的 shell 脚本,则通常必须依赖发送到日志文件的输出消息。用这种方法的话,如果出现错误消息,这些消息也不会出现在日志文件中,因此需要换一种方法来处理。
  • STDERR

    • shell 通过特殊的 STDERR 文件描述符处理错误消息。 STDERR 文件描述符代表 shell 的标准错误输出。 shell 或运行在 shell 中的程序和脚本报错时,生成的错误消息都会被送往这个位置。
    • 在默认情况下, STDERR 和 STDOUT 指向同一个地方(尽管二者的文件描述符索引值不同)。也就是说,所有的错误消息也都默认会被送往显示器。
    • STDERR 并不会随着 STDOUT 的重定向发生改变。
  1. 重定向错误
  • 只要在使用重定向符时指定 STDERR 文件描述符就可以重定向 STDERR 数据了。
  • 只重定向错误
    • STDERR 的文件描述符为 2。可以将该文件描述符索引值放在重定向符号之前,只重定向错误消息。注意, 两者必须紧挨着,否则无法正常工作:
      $ ls -al badfile 2> test4
      $ cat test4
      ls: cannot access badfile: No such file or directory
      
      • 命令生成的任何错误消息都会保存在指 定文件中。用这种方法, shell 只重定向错误消息,而非普通数据。
    • 一个混合使用 STDOUT 和 STDERR 错误消息的例子:
      $ ls -al test badtest test2 2> test5
      -rw-rw-r-- 1 rich rich 158 2020-06-20 11:32 test2
      $ cat test5
      ls: cannot access test: No such file or directory
      ls: cannot access badtest: No such file or directory
      $
      
      • ls 命令尝试列出了 3 个文件(test、badtest 和 test2)的信息。正常输出被送往默认的 STDOUT 文件描述符, 也就是显示器。由于该命令将文件描述符 2( STDERR)重定向到了一个输出文件, 因此 shell 会将产生的所有错误消息直接送往指定文件。
  • 重定向错误消息和正常输出
    • 如果想重定向错误消息和正常输出, 则必须使用两个重定向符号。需要在重定向符号之前 放上需要重定向的文件描述符,然后让它们指向用于保存数据的输出文件:
      $ ls -al test test2 test3 badtest 2> test6 1> test7
      $ cat test6
      ls: cannot access test: No such file or directory
      ls: cannot access badtest: No such file or directory
      $ cat test7
      -rw-rw-r-- 1 rich rich 158 2020-06-20 11:32 test2
      -rw-rw-r-- 1 rich rich   0 2020-06-20 11:33 test3
      $
      
      • ls 命令的正常输出本该送往 STDOUT,shell 使用 1>将其重定向到了文件 test7,而本该送往 STDERR 的错误消息则通过 2>被重定向到了文件 test6。
      • 可以用这种方法区分脚本的正常输出和错误消息。
    • 也可以将 STDERR 和 STDOUT 的输出重定向到同一个文件。为此, bash shell 提供了特殊的重定向符&>:
      $ ls -al test test2 test3 badtest &> test7
      $ cat test7
      ls: cannot access test: No such file or directory
      ls: cannot access badtest: No such file or directory
      -rw-rw-r-- 1 rich rich 158 2020-06-20 11:32 test2
      -rw-rw-r-- 1 rich rich   0 2020-06-20 11:33 test3
      $
      
      • 当使用&>时,命令生成的所有输出(正常输出和错误消息)会被送往同一位置。
      • 注意,其 中一条错误消息出现的顺序和预想不同。 badtest 文件(列出的最后一个文件)的这条错误消息出 现在了输出文件的第二行。为了避免错误消息散落在输出文件中,相较于标准输出,bash shell 自动赋予了错误消息更高的优先级。
  1. 在脚本中重定向输出的方法有两种:

    • 临时重定向每一行。
    • 永久重定向脚本中的所有命令。
  2. 临时重定向

  • 如果需要在脚本中生成错误消息,可以将单独的一行输出重定向到 STDERR。这只需要使用输出重定向符号将输出重定向到 STDERR 文件描述符。在重定向到文件描述符时, 必须在文件 描述符索引值之前加一个&:
    echo "This is an error message" >&2
    
    • 这行会在脚本的 STDERR 文件描述符所指向的位置显示文本。
  • 例子:
    $ cat test8
    #!/bin/bash
    # testing STDERR messages
    echo "This is an error" >&2
    echo "This is normal output"
    $
    $ ./test8
    This is an error
    This is normal output
    $ 
    $ ./test8 2> test9
    This is normal output
    $ cat test9
    This is an error
    
    • 在默认情况下, STDERR 和 STDOUT 指向的位置是一样的。但是,如果在运行脚本时重定向了 STDERR,那么脚本中所有送往 STDERR 的文本都会被重定向:
      $ ./test8 2> test9
      This is normal output
      $ cat test9
      This is an error
      $
      
      • 这种方法非常适合在脚本中生成错误消息,直接通过 STDERR 文件描述符重定向错误消息。
  1. 永久重定向
  • 如果脚本中有大量数据需要重定向,那么逐条重定向所有的 echo 语句会很烦琐。这时可以用 exec 命令,它会告诉 shell 在脚本执行期间重定向某个特定文件描述符:
    $ cat test10
    #!/bin/bash
    # redirecting all output to a file
    exec 1>testout
    
    echo "This is a test of redirecting all output"
    echo "from a script to another file."
    echo "without having to redirect every individual line"
    $ ./test10
    $ cat testout
    This is a test of redirecting all output
    from a script to another file.
    without having to redirect every individual line
    $
    
    • exec 命令会启动一个新 shell 并将 STDOUT 文件描述符重定向到指定文件。脚本中送往 STDOUT 的所有输出都会被重定向。
  • 也可以在脚本执行过程中重定向 STDOUT:
    $ cat test11
    #!/bin/bash
    # redirecting output to different locations
    exec 2>testerror
    echo "This is the start of the script"
    echo "now redirecting all output to another location"
    exec 1>testout
    echo "This output should go to the testout file"
    echo "but this should go to the testerror file" >&2
    $
    $ ./test11
    This is the start of the script
    now redirecting all output to another location
    $ cat testout
    This output should go to the testout file
    $ cat testerror
    but this should go to the testerror file
    $
    
    • 该脚本使用 exec 命令将送往 STDERR 的输出重定向到了文件 testerror。接下来,脚本用 echo 语句向 STDOUT 显示了几行文本。随后再次使用 exec 命令将 STDOUT 重定向到 testout 文件。注意,尽管 STDOUT 被重定向了,仍然可以将 echo 语句的输出发送给 STDERR,在本例中仍是重定向到 testerror 文件。
    • 当只想将脚本的部分输出重定向到其他位置(比如错误日志)时,这个特性用起来非常方便。不过这样做的话,会遇到一个问题。一旦重定向了 STDOUT 或 STDERR,就不太容易将其恢复到原先的位置。
  1. 在脚本中重定向输入
  • 可以使用与重定向 STDOUT 和 STDERR 相同的方法, 将 STDIN 从键盘重定向到其他位置。在 Linux 系统中, exec 命令允许将 STDIN 重定向为文件:
    exec 0< testfile
    
    • 该命令会告诉 shell,它应该从文件 testfile 中而不是键盘上获取输入。只要脚本需要输入,这个重定向就会起作用。
  • 例子:
    $ cat test12
    #!/bin/bash
    # redirecting file input
    exec 0< testfile
    count=1
    while read line
    do
        echo "Line #$count: $line"
        count=$[ $count + 1 ]
    done
    $ ./test12
    Line #1: This is the first line.
    Line #2: This is the second line.
    Line #3: This is the third line.
    $
    
    • 将 STDIN 重定向为文件后,当 read 命令试图从 STDIN 读入数据时,就会到文件中而不是键盘上检索数据。
    • 这是一种在脚本中从待处理的文件读取数据的绝妙技术。 Linux 系统管理员的日常任务之一就是从日志文件中读取并处理数据。这是完成该任务最简单的办法。
  1. 创建自己的重定向
  • 在脚本中重定向输入和输出时,并不局限于这 3 个默认的文件描述符。在 shell 中最多可以打开 9个文件描述符。替代性文件描述符从 3 到 8 共 6个,均可用作输入或输出重定向。这些文件描述符中的任意一个都可以分配给文件并用在脚本中。
  • 在重定向时, 如果使用大于 9 的文件描述符, 那么一定要小心,因为有可能会与 shell 内部使用的文件描述符发生冲突。
  1. 创建输出文件描述符
  • 可以用 exec 命令分配用于输出的文件描述符。和标准的文件描述符一样, 一旦将替代性文件描述符指向文件, 此重定向就会一直有效, 直至重新分配。
  • 在脚本中使用替代性文件描述符的简单例子:
    $ cat test13
    #!/bin/bash
    # using an alternative file descriptor
    exec 3>test13out
    echo "This should display on the monitor"
    echo "and this should be stored in the file" >&3
    echo "Then this should be back on the monitor"
    $ ./test13
    This should display on the monitor
    Then this should be back on the monitor
    $ cat test13out
    and this should be stored in the file
    $
    
    • 这个脚本使用 exec 命令将文件描述符 3 重定向到了另一个文件。
  • 也可以不创建新文件,而是使用 exec 命令将数据追加到现有文件:
    exec 3>>test13out
    
    • 现在,输出会被追加到 test13out 文件,而不是创建一个新文件。
  1. 重定向文件描述符
  • 可以将另一个文件描述符分配给标准 文件描述符,反之亦可。这意味着可以将 STDOUT 的原先位置重定向到另一个文件描述符, 然后再利用该文件描述符恢复 STDOUT。
  • 例子:
    $ cat test14
    #!/bin/bash
    # storing STDOUT, then coming back to it
    exec 3>&1
    exec 1>test14out
    echo "This should store in the output file"
    echo "along with this line."
    exec 1>&3
    echo "Now things should be back to normal"
    $
    $ ./test14
    Now things should be back to normal
    $ cat test14out
    This should store in the output file
    along with this line.
    $
    
    • 第一个 exec 命令将文件描述符 3 重定向到了文件描述符 1(STDOUT)的当前位置,也就是显示器。这意味着任何送往文件描述符 3 的输出都会出现在屏幕上。
    • 第二个 exec 命令将 STDOUT 重定向到了文件, shell 现在会将发送给 STDOUT 的输出直接送往该文件。但是,文件描述符 3 仍然指向 STDOUT 原先的位置(显示器)。如果此时将输出数据 发送给文件描述符 3,则它仍然会出现在显示器上,即使 STDOUT 已经被重定向了。
    • 第三个 exec 命令将 STDOUT 重定向到了文件描述符 3 的当前位置(现在仍然是显示器)。这意味着现在 STDOUT 又恢复如初了,即指向其原先的位置——显示器。
  1. 创建输入文件描述符
  • 可以采用和重定向输出文件描述符同样的办法来重定向输入文件描述符。在重定向到文件之前, 先将 STDIN 指向的位置保存到另一个文件描述符, 然后在读取完文件之后将 STDIN 恢复到原先的位置:
    $ cat test15
    #!/bin/bash
    # redirecting input file descriptors
    exec 6<&0
    exec 0< testfile
    count=1
    while read line
    do
        echo "Line #$count: $line"
        count=$[ $count + 1 ]
    done
    exec 0<&6
    read -p "Are you done now? " answer
    case $answer in
    Y|y) echo "Goodbye";;
    N|n) echo "Sorry, this is the end.";;
    esac
    $ ./test15
    Line #1: This is the first line.
    Line #2: This is the second line.
    Line #3: This is the third line.
    Are you done now? y
    Goodbye
    $
    
    • 在这个例子中,文件描述符 6 用于保存 STDIN 指向的位置。然后脚本将 STDIN 重定向到一个文件。 read 命令的所有输入都来自重定向后的 STDIN (也就是输入文件)。
    • 在读完所有行之后,脚本会将 STDIN 重定向到文件描述符 6,恢复 STDIN 原先的位置。该脚本使用另一个 read 命令来测试 STDIN 是否恢复原位,这次 read 会等待键盘的输入。
  1. 创建读/写文件描述符
  • 也可以打开单个文件描述符兼做输入和输出,这样就能用同一个文件描述符对文件进行读和写两种操作了。
  • 但使用这种方法时要特别小心。由于这是对一个文件进行读和写两种操作,因此 shell 会维 护一个内部指针,指明该文件的当前位置。任何读或写都会从文件指针上次的位置开始。
  • 例子:
    $ cat test16
    #!/bin/bash
    # testing input/output file descriptor
    exec 3<> testfile
    read line <&3
    echo "Read: $line"
    echo "This is a test line" >&3
    $ cat testfile
    This is the first line.
    This is the second line.
    This is the third line.
    $ ./test16
    Read: This is the first line.
    $ cat testfile
    This is the first line.
    This is a test line
    ine.
    This is the third line.
    $
    
    • 在这个例子中,exec 命令将文件描述符 3 用于文件 testfile 的读和写。接下来,使用分配好的文件描述符,通过 read 命令读取文件中的第一行,然后将其显示在 STDOUT 中。最后,使用 echo 语句将一行数据写入由同一个文件描述符打开的文件中。
    • 在运行脚本时,一开始还算正常。输出内容表明脚本读取了 testfile 文件的第一行。但如果在脚本运行完毕后查看 testfile 文件内容,则会发现写入文件中的数据覆盖了已有数据。
    • 当脚本向文件中写入数据时, 会从文件指针指向的位置开始。 read 命令读取了第一行数据,这使得文件指针指向了第二行数据的第一个字符。当 echo 语句将数据输出到文件时,会将数据写入文件指针的当前位置,覆盖该位置上的已有数据。
  1. 关闭文件描述符
  • 如果创建了新的输入文件描述符或输出文件描述符,那么 shell 会在脚本退出时自动将其关闭。然而在一些情况下,需要在脚本结束前手动关闭文件描述符。
  • 要关闭文件描述符,只需将其重定向到特殊符号&-即可。在脚本中如下所示:
    exec 3>&-
    
    • 该语句会关闭文件描述符 3,不再在脚本中使用。
  • 一旦关闭了文件描述符,就不能在脚本中向其写入任何数据,否则 shell 会发出错误消息。
  • 在关闭文件描述符时还要注意另一件事。如果随后你在脚本中打开了同一个输出文件, 那么 shell 就会用一个新文件来替换已有文件。这意味着如果你输出数据,它就会覆盖已有文件。例子
  1. 列出打开的文件描述符
  • lsof 命令会列出整个 Linux 系统打开的所有文件描述符,这包括所有后台进程以及登录用户打开的文件。

  • 有大量的命令行选项和参数可用于过滤 lsof 的输出。最常用的选项包括-p 和-d,前者允许指定进程 ID(PID ),后者允许指定要显示的文件描述符编号(多个编号之间以逗号分隔)。

  • 要想知道进程的当前 PID,可以使用特殊环境变量$$(shell 会将其设为当前 PID )。-a 选项 可用于对另外两个选项的结果执行 AND 运算,命令输出如下:

    $ /usr/sbin/lsof -a -p $$ -d 0,1,2
    COMMAND  PID USER   FD   TYPE DEVICE SIZE NODE NAME
    bash    3344 rich    0u   CHR  136,0         2 /dev/pts/0
    bash    3344 rich    1u   CHR  136,0         2 /dev/pts/0
    bash    3344 rich    2u   CHR  136,0         2 /dev/pts/0
    $
    
    • 显示了当前进程(bash shell)的默认文件描述符(0、1 和 2)。
  • lsof 的默认输出

    描述
    COMMAND进程对应的命令名的前 9 个字符
    PID进程的 PID
    USER进程属主的登录名
    FD文件描述符编号以及访问类型(r 代表读, w 代表写, u 代表读/写)
    TYPE文件的类型( CHR 代表字符型, BLK 代表块型, DIR 代表目录, REG 代表常规文件)
    DEVICE设备号(主设备号和从设备号)
    SIZE如果有的话,表示文件的大小
    NODE本地文件的节点号
    NAME文件名
  • 与 STDIN、STDOUT 和 STDERR 关联的文件类型是字符型,因为文件描述符 STDIN、STDOUT 和 STDERR 都指向终端,所以输出文件名就是终端的设备名。这 3 个标准文件都支持读和写。

  • 例子:

    $ cat test18
    #!/bin/bash
    # testing lsof with file descriptors
    exec 3> test18file1
    exec 6> test18file2
    exec 7< testfile
    /usr/sbin/lsof -a -p $$ -d0,1,2,3,6,7
    $ ./test18
    COMMAND  PID USER   FD   TYPE DEVICE SIZE   NODE NAME
    test18  3594 rich    0u   CHR  136,0           2 /dev/pts/0
    test18  3594 rich    1u   CHR  136,0           2 /dev/pts/0
    test18  3594 rich    2u   CHR  136,0           2 /dev/pts/0
    18  3594 rich    3w   REG  253,0    0 360712 /home/rich/test18file1
    18  3594 rich    6w   REG  253,0    0 360715 /home/rich/test18file2
    18  3594 rich    7r   REG  253,0   73 360717 /home/rich/testfile
    $
    
  1. 抑制命令输出
  • 如果不想显示脚本输出,可以将 STDERR 重定向到一个名为 null 文件的特殊文件。shell 输出到 null 文件的任何数据都不会被保存,全部会被丢弃。
  • 在 Linux 系统中,null 文件的标准位置是/dev/null。重定向到该位置的任何数据都会被丢弃,不再显示。这是抑制错误消息出现且无须保存它们的一种常用方法。
  • 也可以在输入重定向中将/dev/null 作为输入文件。由于/dev/null 文件不含任何内容,因此程 序员通常用它来快速清除现有文件中的数据,这样就不用先删除文件再重新创建了:
    $ cat /dev/null > testfile
    
    • 文件 testfile 仍然还在,但现在是一个空文件。这是清除日志文件的常用方法,因为日志文件必须时刻等待应用程序操作。
  1. 使用临时文件
  • Linux 系统有一个专供临时文件使用的特殊目录/tmp,其中存放那些不需要永久保留的文件。大多数 Linux 发行版配置系统在启动时会自动删除/tmp 目录的所有文件。
  • 系统中的任何用户都有权限读写/tmp 目录中的文件。这个特性提供了一种创建临时文件的简单方法,而且还无须担心清理工作。
  • 还有一个专门用于创建临时文件的命令 mktemp,该命令可以直接在/tmp 目录中创建唯一的临时文件。所创建的临时文件不使用默认的 umask 值。作为临时文件属主,你拥有该文件的读写权限,但其他用户无法访问(当然,root 用户除外)。
  1. 创建本地临时文件
  • 在默认情况下, mktemp 会在本地目录中创建一个文件。在使用 mktemp 命令时, 只需指定 一个文件名模板即可。模板可以包含任意文本字符,同时在文件名末尾要加上 6 个 X:
    $ mktemp testing.XXXXXX
    $ ls -al testing*
    -rw-------   1 rich      rich      0 Jun 20 21:30 testing.UfIi13
    
    • mktemp 命令会任意地将 6 个 X 替换为同等数量的字符,以保证文件名在目录中是唯一的。可以创建多个临时文件,mktemp 命令会确保每个文件名都不重复。
  • 在脚本中使用 mktemp 命令时,可以将文件名保存到变量中,这样就能在随后的脚本中引用了:
    tempfile=$(mktemp test19.XXXXXX)
    
  1. 在/tmp 目录中创建临时文件
  • -t 选项会强制 mktemp 命令在系统的临时目录中创建文件。在使用这个特性时, mktemp 命令会返回所创建的临时文件的完整路径名,而不只是文件名:
    $ mktemp -t test.XXXXXX
    /tmp/test.xG3374
    
  • 由于 mktemp 命令会返回临时文件的完整路径名,因此可以在文件系统的任何位置引用该临时文件:
    tempfile=$(mktemp -t tmp.XXXXXX)
    echo "The temp file is located at: $tempfile"
    
    • 在创建临时文件时, mktemp 会将全路径名返回给环境变量。这样就能在任何命令中使用该值来引用临时文件了。
  1. 创建临时目录
  • -d 选项会告诉 mktemp 命令创建一个临时目录。可以根据需要使用该目录,比如在其中创建其他的临时文件:
    tempdir=$(mktemp -d dir.XXXXXX)
    cd $tempdir
    tempfile1=$(mktemp temp.XXXXXX)
    tempfile2=$(mktemp temp.XXXXXX)
    
  1. 记录消息
  • 如果需要将输出同时送往显示器和文件,与其对输出进行两次重定向,不如改用 tee 命令。
  • tee 命令就像是连接管道的 T 型接头,它能将来自 STDIN 的数据同时送往两处。一处是 STDOUT,另一处是 tee 命令行所指定的文件名:
    tee filename
    
  • 由于 tee 会重定向来自 STDIN 的数据,因此可以用它配合管道命令来重定向命令输出:
    $ date | tee testfile
    Sun Jun 21 18:56:21 EDT 2020
    $ cat testfile
    Sun Jun 21 18:56:21 EDT 2020
    
    • 输出出现在了 STDOUT 中, 同时写入了指定文件。
  • 注意,在默认情况下,tee 命令会在每次使用时覆盖指定文件的原先内容。如果想将数据追加到指定文件中,就必须使用-a 选项。
  1. 实战演练-读取 CSV 格式的数据文件,输出 SQL INSERT 语句,并将数据插入数据库
    outfile='members.sql'
    IFS=','
    while read lname fname address city state zip
    do
    cat >> $outfile << EOF
    INSERT INTO members (lname,fname,address,city,state,zip) VALUES
    ('$lname', '$fname', '$address', '$city', '$state', '$zip');
    EOF
    done < ${1}
    
    • $1 代表第一个命令行参数, 指明了待读取数据的文件。
    • read 语句使 用 IFS 字符解析读入的文本,这里将 IFS 指定为逗号。
    • cat 那行语句包含一个输出追加重定向(双大于号)和一个输入追加重定向(双小于号)。输出重定向将 cat 命令的输出追加到由 $outfile 变量指定的文件中。 cat 命令的输入不再取自标准 输入,而是被重定向到脚本内部的数据。EOF 符号标记了文件中的数据起止。
    • 上述文本生成了一个标准的 SQL INSERT 语句。注意,其中的数据由变量来替换,变量中的内容则由 read 语句存入。

十六、脚本控制
  1. Linux 利用信号与系统中的进程进行通信。

  2. 重温 Linux 信号

  • Linux 系统和应用程序可以产生超过 30 个信号。在 shell 脚本编程时会遇到的最常见的 Linux 系统信号如下:

    信号描述
    1SIGHUP挂起(hang up )进程
    2SIGINT中断(interrupt)进程
    3SIGQUIT停止(stop)进程
    9SIGKILL无条件终止(terminate)进程
    15SIGTERM尽可能终止进程
    18SIGCONT继续运行停止的进程
    19SIGSTOP无条件停止,但不终止进程
    20SIGTSTP停止或暂停( pause ),但不终止进程
  • 在默认情况下, bash shell 会忽略收到的任何 SIGQUIT(3)信号和 SIGTERM(15)信号(因此交互式 shell 才不会被意外终止)。但是,bash shell 会处理收到的所有 SIGHUP(1)信号和 SIGINT(2)信号。

  • 如果收到了 SIGHUP 信号(比如在离开交互式 shell 时), bash shell 就会退出。但在退出之前,它会将 SIGHUP 信号传给所有由该 shell 启动的进程, 包括正在运行的shell 脚本。

  • 随着收到 SIGINT 信号, shell 会被中断。 Linux 内核将不再为 shell 分配 CPU 处理时间。当出现这种情况时, shell 会将 SIGINT 信号传给由其启动的所有进程,以此告知出现的状况。

  • shell 会将这些信号传给 shell 脚本来处理。而 shell 脚本的默认行为是忽略这些信号,因为可能不利于脚本运行。

  1. bash shell 允许使用键盘上的组合键来生成两种基本的 Linux 信号。这个特性在需要停止或暂停失控脚本时非常方便。
  • 中断进程
    • Ctrl+C 组合键会生成 SIGINT 信号,并将其发送给当前在 shell 中运行的所有进程。
    • sleep 命令会按照指定的秒数暂停 shell 操作一段时间,然后返回 shell 提示符。 Ctrl+C 组合键会发送 SIGINT 信号,停止 shell 中当前运行的进程。在超时前(60 秒)按下 Ctrl+C 组合键,就可以提前终止 sleep 命令。
  • 暂停进程
    • 也可以暂停进程,而不是将其终止。尽管有时这可能比较危险(比如,脚本打开了一个关键的系统文件的文件锁),但它往往可以在不终止进程的情况下,使你能够深入脚本内部一窥究竟。
    • Ctrl+Z 组合键会生成 SIGTSTP 信号,停止 shell 中运行的任何进程。停止(stopping)进程跟终止(terminating)进程不同,前者让程序继续驻留在内存中,还能从上次停止的位置继续运行。
    • 当使用 Ctrl+Z 组合键时, shell 会通知你进程已经被停止了:
      $ sleep 60
      ^Z
      [1]+  Stopped                  sleep 60
      $
      
      • 方括号中的数字是 shell 分配的作业号。shell 将运行的各个进程称为作业,并为作业在当前 shell 内分配了唯一的作业号。作业号从 1 开始,然后是 2,依次递增。
    • 如果 shell 会话中有一个已停止的作业, 那么在退出shell 时, bash 会发出提醒:
      $ sleep 70
      ^Z
      [2]+  Stopped                  sleep 70
      $
      $ exit
      logout
      There are stopped jobs.
      $
      
    • 可以用 ps 命令查看已停止的作业:
      $ ps -l
      F   S   UID PID PPID    [...]   TTY TIME    CMD
      0   S   1001 1509 1508  [...] pts/0 00:00:00 bash
      0   T   1001 1532 1509  [...] pts/0 00:00:00 sleep
      0   T   1001 1533 1509  [...] pts/0 00:00:00 sleep
      0   R   1001 1534 1509  [...] pts/0 00:00:00 ps
      $
      
      • 在 S 列(进程状态)中, ps 命令将已停止作业的状态显示为 T。这说明命令要么被跟踪,要么被停止。
    • 如果在有已停止作业的情况下仍旧想退出 shell,则只需再输入一遍 exit 命令即可。 shell 会退出,终止已停止作业。
    • 或者,如果知道已停止作业的 PID,那就可以用 kill 命令发送 SIGKILL(9)信号将其终止:
    • 每当 shell 生成命令行提示符时,也会显示 shell 中状态发生改变的作业。“杀死”作业后, shell 会显示一条消息,表示运行中的作业已被“杀死”,然后生成提示符。
    • 在某些 Linux 系统中, “杀死”作业时不会得到任何回应。但当下次执行能让 shell 生成命令行提示符的操作时(比如, 按下 Enter 键),会看到一条消息,表示作业已被“杀死”。
  1. 捕获信号
  • 也可以用其他命令在信号出现时将其捕获,而不是忽略信号。 trap 命令可以指定 shell 脚本需要侦测并拦截的 Linux 信号。如果脚本收到了 trap 命令中列出的信号,则该信号不再由 shell 处理,而是由本地处理。
  • trap 命令的格式如下:
    trap commands signals
    
    • 在 trap 命令中, 需要在 commands 部分列出想要 shell 执行的命令, 在 signals 部分列出 想要捕获的信号(多个信号之间以空格分隔)。指定信号的时候,可以使用信号的值或信号名。
  • 如何使用 trap 命令捕获 SIGINT 信号并控制脚本的行为:
    $ cat trapsignal.sh
    #!/bin/bash
    #Testing signal trapping
    #
    trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT
    #
    echo This is a test script.
    #
    count=1
    while [ $count -le 5 ]
    do
        echo "Loop #$count"
        sleep 1
        count=$[ $count + 1 ]
    done
    #
    echo "This is the end of test script."
    exit
    $
    
    • 每次侦测到 SIGINT 信号时,本例中的 trap 命令都会显示一行简单的文本消息。捕获这些信号可以阻止用户通过组合键 Ctrl+C 停止脚本。
    • 每次使用 Ctrl+C 组合键,脚本都会执行 trap 命令中指定的echo 语句,而不是忽略信号并让 shell 停止该脚本。
  • 如果脚本中的命令被信号中断,使用带有指定命令的 trap 未必能让被中断的命令继续执行。为了保证脚本中的关键操作不被打断,请使用带有空操作命令的 trap 以及要捕获的信号列表,例如:
    trap "" SIGINT
    
    • 这种形式的 trap 命令允许脚本完全忽略 SIGINT 信号,继续执行重要的工作。
  1. 捕获脚本退出
  • 除了在 shell 脚本中捕获信号, 也可以在 shell 脚本退出时捕获信号。这是在 shell 完成任务时执行命令的一种简便方法。
  • 要捕获 shell 脚本的退出, 只需在 trap 命令后加上 EXIT 信号即可:
    $ cat trapexit.sh
    #!/bin/bash
    #Testing exit trapping
    #
    trap "echo Goodbye..." EXIT
    #
    count=1
    while [ $count -le 5 ]
    do
        echo "Loop #$count"
        sleep 1
        count=$[ $count + 1 ]
    done
    #
    exit
    $
    
    • 当脚本运行到正常的退出位置时,触发了 EXIT,shell 执行了在 trap 中指定的命令。如果提前退出脚本,则依然能捕获到 EXIT。
    • 该例子中,因为 SIGINT 信号并未在 trap 命令的信号列表中,所以当按下Ctrl+C 组合键发送 SIGINT 信号时,脚本就退出了。但在退出之前已经触发了 EXIT,于是 shell 会执行 trap 命令。
  1. 修改或移除信号捕获
  • 要想在脚本中的不同位置进行不同的信号捕获处理, 只需重新使用带有新选项的 trap 命令 即可:
    $ cat trapmod.sh
    #!/bin/bash
    #Modifying a set trap
    #
    trap "echo ' Sorry...Ctrl-C is trapped.'" SIGINT
    #
    count=1
    while [ $count -le 3 ]
    do
        echo "Loop #$count"
        sleep 1
        count=$[ $count + 1 ]
    done
    #
    trap "echo ' I have modified the trap!'" SIGINT
    #
    count=1
    while [ $count -le 3 ]
    do
        echo "Second Loop #$count"
        sleep 1
        count=$[ $count + 1 ]
    done
    #
    exit
    $
    
    • 修改了信号捕获之后, 脚本处理信号的方式就会发生变化。但如果信号是在捕获被修改前接收到的,则脚本仍然会根据原先的 trap 命令处理该信号。
  • 如果在交互式 shell 会话中使用trap 命令,可以使用 trap -p 查看被捕获的信号。如果什么都没有显示,则说明 shell 会话按照默认方式处理信号。
  • 也可以移除已设置好的信号捕获。在 trap 命令与希望恢复默认行为的信号列表之间加上两个连字符即可:
    trap "echo ' Sorry...Ctrl-C is trapped.'" SIGINT
    trap -- SIGINT
    
    • 也可以在 trap 命令后使用单连字符来恢复信号的默认行为。单连字符和双连字符的效果一样。
    • 移除信号捕获后, 脚本会按照默认行为处理 SIGINT 信号, 也就是终止脚本运行。但如果信号是在捕获被移除前接收到的,那么脚本就会按照原先 trap 命令中的设置进行处理。
  1. 以后台模式运行脚本
  • 使用 ps -e 命令,可以看到 Linux 系统中运行的多个进程:
    $ ps -e
    PID TTY          TIME CMD
    1 ?        00:00:02 systemd
    2 ?        00:00:00 kthreadd
    3 ?        00:00:00 rcu_gp
    4 ?        00:00:00 rcu_par_gp
    [...]
    2585 pts/0    00:00:00 ps
    $
    
    • 其实有很多进程没有运行在任何终端——它们是在后台运行的。在后台模式中,进程运行时不和终端会话的 STDIN、STDOUT 以及 STDERR 关联。
  1. 后台运行脚本
  • 以后台模式运行 shell 脚本只需在脚本名后面加上&即可:
    $ ./backgroundscript.sh &
    [1] 2595
    $
    [1]+  Done                      ./backgroundscript.sh
    
    • 方括号中的数字(1)是 shell 分配给后台进程的作业号,之后的数字(2595)是 Linux 系统为进程分配的进程 ID(PID)。
    • 当后台进程结束时,终端上会显示一条消息。其中指明了作业号、作业状态(Done),以及用于启动该作业的命令(删除了&)。
  • 在脚本名之后加上&会将脚本与当前 shell 分离开来,并将脚本作为一个独立的后台进程运行。
  • 注意,当后台进程运行时, 它仍然会使用终端显示器来显示 STDOUT 和 STDERR 消息。最好是将后台脚本的 STDOUT 和 STDERR 进行重定向,避免这种杂乱的输出。
  1. 运行多个后台作业
  • 在使用命令行提示符的情况下,可以同时启动多个后台作业。
  • 每次启动新作业时, Linux 系统都会为其分配新的作业号和PID。通过 ps 命令可以看到后台脚本处于运行状态。
  • 在终端会话中使用后台进程一定要小心。注意,在 ps 命令的输出中,每一个后台进程都和终端会话(pts/0)终端关联在一起。如果终端会话退出,那么后台进程也会随之退出。
  • 当要退出终端会话时,如果还有被停止的进程,就会出现警告信息。但如果是后台进程,则只有部分终端仿真器会在退出终端会话前提醒你尚有后台作业在运行。
  1. 在非控制台下运行脚本
  • nohup 命令能阻断发给特定进程的 SIGHUP 信号。当退出终端会话时,这可以避免进程退出。
  • nohup 命令的格式如下:
    nohup command
    
  • 使用一个后台脚本作为 command:
    $ nohup ./testAscript.sh &
    [1] 1828
    $ nohup: ignoring input and appending output to 'nohup.out'
    
    • 和普通后台进程一样,shell 会给 command 分配一个作业号,Linux 系统会为其分配一个 PID 号。区别在于, 当使用 nohup 命令时,如果关闭终端会话,则脚本会忽略其发送的 SIGHUP 信号。
    • 由于 nohup 命令会解除终端与进程之间的关联, 因此进程不再同 STDOUT 和 STDERR 绑定在一起。为了保存该命令产生的输出,nohup 命令会自动将 STDOUT 和 STDERR 产生的消息重定向到一个名为 nohup.out 的文件中。
    • nohup.out 文件一般在当前工作目录中创建,否则会在$HOME 目录中创建。
    • nohup.out 文件包含了原本要发送到终端显示器上的所有输出。进程结束之后,可以查看 nohup.out 文件中的输出结果。nohup.out 文件中的输出结果和脚本在命令行中运行时产生的一样。
  • 如果使用nohop运行了另一个命令,那么该命令的输出会被追加到已有的nohup.out文件中。所以当运行位于同一目录中的多个命令时,所有的命令输出都会发送到同一个 nohup.out 文件中。
  • 有了 nohup,就可以在后台运行脚本。在无须停止脚本进程的情况下,登出终端会话去完成其他任务,随后再检查结果。
  1. 作业控制
  • 在作业停止后,Linux 系统会让你选择是“杀死”该作业还是重启该作业。用 kill 命令可以“杀死”该作业。要重启停止的进程,则需要向其发送 SIGCONT 信号。
  • 作业控制包括启动、停止、“杀死”以及恢复作业。通过作业控制,能完全控制 shell 环境中所有进程的运行方式。
  1. 查看作业
  • jobs 是作业控制中的关键命令,该命令允许用户查看 shell 当前正在处理的作业。

  • 通过 jobs 命令可以查看分配给 shell 的作业,例如:

    $ jobs
    [1]+  Stopped   ./jobcontrol.sh
    [2]-  Running   ./jobcontrol.sh > jobcontrol.out &
    
    • jobs 命令显示了一个已停止的作业和一个运行中的作业,以及两者的作业号和作业使用的命令。
    • 带有加号的作业为默认作业。如果作业控制命令没有指定作业号,则引用的就是该作业。带有减号的作业会在默认作业结束之后成为下一个默认作业。
    • 任何时候,不管 shell 中运行着多少作业,带加号的作业只能有一个,带减号的作业也只能有一个。
  • 可以使用 jobs 命令的-l 选项(小写字母 l)查看作业的 PID。

    $ jobs -l
    [1]+  1580  Stopped   ./jobcontrol.sh
    [2]-  1603  Running   ./jobcontrol.sh > jobcontrol.out &
    
  • jobs 命令提供了一些命令行选项:

    选项描述
    -l列出进程的 PID 以及作业号
    -n只列出上次 shell 发出通知后状态发生改变的作业
    -p只列出作业的 PID
    -r只列出运行中的作业
    -s只列出已停止的作业
  • 如果需要删除已停止的作业, 那么使用 kill 命令向其 PID 发送 SIGKILL(9)信号即可。

    $ jobs -l
    [1]+  1580 Stopped  ./jobcontrol.sh
    $
    $ kill -9 1580
    [1]+  Killed    ./jobcontrol.sh
    $
    
  1. 重启已停止的作业
  • 在 bash 作业控制中,可以将已停止的作业作为后台进程或前台进程重启。前台进程会接管当前使用的终端,因此在使用该特性时要小心。
  • 要以后台模式重启作业,可以使用 bg 命令:
    $ ./restartjob.sh
    ^Z
    [1]+  Stopped                  ./restartjob.sh
    $
    $ bg
    [1]+ ./restartjob.sh &
    $
    $ jobs
    [1]+  Running                  ./restartjob.sh &
    $
    
    • 因为该作业是默认作业(从加号可以看出),所以仅使用 bg 命令就可以将其以后台模式重启。注意,当作业被转入后台模式时,并不会显示其 PID。
  • 如果存在多个作业,则需要在 bg 命令后加上作业号,以便于控制:
    $ jobs
    $
    $ ./restartjob.sh
    ^Z
    [1]+  Stopped                  ./restartjob.sh
    $
    $ ./newrestartjob.sh
    ^Z
    [2]+  Stopped                  ./newrestartjob.sh
    $
    $ bg 2
    [2]+ ./newrestartjob.sh &
    $
    $ jobs
     
    [1]+  Stopped                   ./restartjob.sh
    [2]-  Running                   ./newrestartjob.sh & 
    $
    
    • bg 2 命令用于将第二个作业置于后台模式。
    • 注意,当使用 jobs 命令时,它列出了作业及其状态,即便默认作业当前并未处于后台模式。
  • 要以前台模式重启作业,可以使用带有作业号的 fg 命令:
    $ jobs
    [1]+  Stopped   ./restartjob.sh
    [2]-  Running   ./newrestartjob.sh &
    $
    $ fg 2
    ./newrestartjob.sh
    This is the script's end.
    $
    
    • 由于作业是在前台运行的,因此直到该作业完成后,命令行界面的提示符才会出现。
  1. 调整谦让度
  • 在多任务操作系统(比如 Linux)中,内核负责为每个运行的进程分配 CPU 时间。调度优先级 [也称为谦让度(nice value)]是指内核为进程分配的 CPU时间(相对于其他进程)。在 Linux 系统中,由 shell 启动的所有进程的调度优先级默认都是相同的。
  • 调度优先级是一个整数值, 取值范围从-20(最高优先级)到+19(最低优先级)。在默认情况下,bash shell 以优先级 0 来启动所有进程。-20(最低值)代表最高优先级,+19(最高值)代表最低优先级。
  1. nice 命令
  • nice 命令允许在启动命令时设置其调度优先级。要想让命令以更低的优先级运行,只需用 nice 命令的-n 选项指定新的优先级即可:
    $ nice -n 10 ./jobcontrol.sh > jobcontrol.out &
    [2] 16462
    $
    $ ps -p 16462 -o pid,ppid,ni,cmd
    PID    PPID  NI CMD
    16462    1630  10 /bin/bash ./jobcontrol.sh
    $
    
    • nice 命令和要启动的命令必须出现在同一行中。
    • ps 命令的输出证实,谦让度( NI 列)已经调整到了 10。
    • nice 命令使得脚本以更低的优先级运行。
  • nice 命令会阻止普通用户提高命令的优先级。注意,即便提高其优先级的操作没有成功,指定的命令依然可以运行。只有 root 用户或者特权用户才能提高作业的优先级。
  • nice 命令的-n 选项并不是必需的,直接在连字符后面跟上优先级也可以。当要设置的优先级是负数时,这种写法则很容易造成混淆,因为出现了双连字符。在这种情况下,最好还是使用-n 选项。
  1. renice 命令
  • 如果想要修改系统中已运行命令的优先级,可以用 renice 命令通过指定运行进程的 PID 来改变其优先级:
    $ ./jobcontrol.sh > jobcontrol.out &
    [2] 16642
    $
    $ ps -p 16642 -o pid,ppid,ni,cmd
    PID    PPID  NI CMD
    16642  1630  0 /bin/bash ./jobcontrol.sh
    $
    $ renice -n 10 -p 16642
    16642 (process ID) old priority 0, new priority 10
    $
    $ ps -p 16642 -o pid,ppid,ni,cmd
    PID    PPID  NI CMD
    16642  1630  10 /bin/bash ./jobcontrol.sh
    $
    
    • renice 命令会自动更新运行进程的调度优先级。
    • renice 命令对于非特权用户也有一些限制:只能对属主为自己的进程使用 renice 且只能降低调度优先级。但是,root 用户和特权用户可以使用renice 命令对任意进程的优先级做任意调整。
  1. 定时运行作业
  • Linux 系统提供了多个在预选时间运行脚本的方法:at 命令、 cron 表以及 anacron。每种方法都使用不同的技术 来安排脚本的运行时间和频率。
  1. 使用 at 命令调度作业
  • at 命令允许指定 Linux 系统何时运行脚本。该命令会将作业提交到队列中,指定 shell 何时运行该作业。
  • at 的守护进程 atd 在后台运行,在作业队列中检查待运行的作业。很多 Linux 发行版会在启动时运行此守护进程,但有些发行版甚至都没安装这个软件包。如果你的 Linux 属于后一种情况,则可以自行安装软件包 at。
  • atd 守护进程会检查系统的一个特殊目录(通常位于/var/spool/at 或/var/spool/cron/atjobs),从中获取 at 命令提交的作业。在默认情况下,atd 守护进程每隔 60 秒检查一次这个目录。如果其中有作业, 那么 atd 守护进程就会查看此作业的运行时间。如果时间跟当前时间一致, 就运行此作业。
  • at 命令的格式:
    at [-f filename] time
    
    • 在默认情况下,at 命令会将 STDIN 的输入放入队列。你可以用-f 选项指定用于从中读取命令(脚本文件)的文件名。
    • time 选项指定了你希望何时运行该作业。如果指定的时间已经过去,那么 at 命令会在第二天的同一时刻运行指定的作业。
    • at 命令能识别多种时间格式:
      • 标准的小时和分钟,比如 10:15。
      • AM/PM 指示符,比如 10:15 PM。
      • 特定的时间名称,比如 now、noon、midnight 或者 teatime(4:00 p.m.)
    • 也可以通过不同的日期格式指定特定的日期:
      • 标准日期,比如 MMDDYY 、MM/DD/YY 或 DD.MM.YY。
      • 文本日期,比如 Jul 4或 Dec 25 ,加不加年份均可。
      • 时间增量。
        • Now + 25 minutes
        • 10:15 PM tomorrow
        • 10:15 + 7 days
  • at 命令可用的日期和时间格式有很多种,具体参见/usr/share/doc/at/timespec 文件。
  • 在使用 at 命令时,该作业会被提交至作业队列。作业队列保存着通过 at 命令提交的待处理作业。针对不同优先级,有 52 种作业队列。作业队列通常用小写字母 a~z 和大写字母 A~Z 来指代,A 队列和 a 队列是两个不同的队列。
  • batch 命令可以安排脚本在系统处于低负载时运行。batch 命令是一个脚本(/usr/bin/batch),它会调用 at 命令将作业提交到 b 队列中。
  • 作业队列的字母排序越高,此队列中的作业运行优先级就越低(谦让度更大)。在默认情况下, at 命令提交的作业会被放入 a 队列。如果想以较低的优先级运行作业, 可以用-q 选项指定其他的队列。如果相较于其他进程你希望你的作业尽可能少地占用 CPU,可以将其放入 z 队列。
  • 当在 Linux 系统中运行 at 命令时,显示器并不会关联到该作业。 Linux 系统反而会将提交该作业的用户 email 地址作为 STDOUT 和 STDERR。任何送往 STDOUT 或 STDERR 的输出都会通过邮件系统传给该用户。
  • 在 CentOS发行版中使用 at 命令调度作业的例子:
    $ cat tryat.sh
    #!/bin/bash
    # Trying out the at command
    #
    echo "This script ran at $(date +%B%d,%T)"
    echo
    echo "This script is using the $SHELL shell."
    echo
    sleep 5
    echo "This is the script's end."
    #
    exit
    $
    $ at -f tryat.sh now
    warning: commands will be executed using /bin/sh
    job 3 at Thu Jun 18 16:23:00 2020
    $
    
    • at 命令会显示分配给作业的作业号以及为作业安排的运行时间。
    • -f 选项指明使用哪个脚本文件。now 指示 at 命令立刻执行该脚本。
    • 无须在意 at 命令输出的警告消息,因为脚本的第一行是#!/bin/bash,该命令会由 bash shell 执行。
  • at 命令通过 sendmail 应用程序发送 email。如果系统中没有安装 sendmail,那就无法获得任何输出。因此在使用 at 命令时,最好在脚本中对 STDOUT 和 STDERR 进行重定向,如下例所示:
    $ cat tryatout.sh
    #!/bin/bash
    # Trying out the at command redirecting output
    #
    outfile=$HOME/scripts/tryat.out
    #
    echo "This script ran at $(date +%B%d,%T)" > $outfile
    echo >> $outfile
    echo "This script is using the $SHELL shell." >> $outfile
    echo >> $outfile
    sleep 5
    echo "This is the script's end." >> $outfile
    #
    exit
    $
    $ at -M -f tryatout.sh now
    warning: commands will be executed using /bin/sh
    job 4 at Thu Jun 18 16:48:00 2020
    $
    $ cat $HOME/scripts/tryat.out
    This script ran at June18,16:48:21
    
    This script is using the /bin/bash shell.
    
    This is the script's end.
    $
    
    • 如果不想在 at 命令中使用 email 或者重定向,则最好加上-M 选项,以禁止作业产生的输出信息。
  • atq 命令可以查看系统中有哪些作业在等待:
    $ at -M -f tryatout.sh teatime
    warning: commands will be executed using /bin/sh
    job 5 at Fri Jun 19 16:00:00 2020
    $
    $ at -M -f tryatout.sh tomorrow
    warning: commands will be executed using /bin/sh
    job 6 at Fri Jun 19 16:53:00 2020
    $ atq
    1   Thu Jun 18 16:11:00 2020 a christine
    5   Fri Jun 19 16:00:00 2020 a christine
    6   Fri Jun 19 16:53:00 2020 a christine
    
    • 作业列表中显示了作业号、系统运行该作业的日期和时间,以及该作业所在的作业队列。
  • 一旦知道了哪些作业正在作业队列中等待,就可以用 atrm 命令删除等待中的作业。指定要删除的作业号即可:
    $ atq
    1   Thu Jun 18 16:11:00 2020 a christine
    5   Fri Jun 19 16:00:00 2020 a christine
    6   Fri Jun 19 16:53:00 2020 a christine
    $ atrm 5
    $ atq
    1   Thu Jun 18 16:11:00 2020 a christine
    6   Fri Jun 19 16:53:00 2020 a christine
    
    • 只能删除自己提交的作业,不能删除其他人的。
  1. 调度需要定期运行的脚本
  • Linux 系统使用 cron 程序调度需要定期执行的作业。cron 在后台运行,并会检查一个特殊的表(cron 时间表),从中获知已安排执行的作业。
  • cron 时间表通过一种特别的格式指定作业何时运行,其格式如下:
    minutepasthour hourofday dayofmonth month dayofweek command
    
    • cron 时间表允许使用特定值、取值范围(比如 1~5)或者通配符(星号)来指定各个字段。
    • dayofmonth 字段指定的是月份中的日期值(1~31)。
  • 如果想在每天的 10:15 运行一个命令,可以使用如下 cron 时间表字段:
    15 10 * * * command
    
    • dayofmonth、month 以及 dayofweek 字段中的通配符表明,cron 会在每天 10:15 执行该命令。
  • 要指定一条在每周一的下午 4:15(4:15 p.m. )执行的命令, 可以使用军事时间(1:00 p.m. 是 13:00,2:00 p.m.是 14:00 ,3:00 p.m.是 15:00,以此类推),如下所示:
    15 16 * * 1 command
    
    • 24 小时制在美国和加拿大被称为军事时间(military time),在英国则被称为大陆时间(continental time)。
  • 可以使用三字符的文本值(mon、tue、wed、thu、fri、sat、sun)或数值(0 或 7 代表 周日,6 代表周六)来指定 dayofweek 字段。
  • 要想在每月第一天的中午 12 点执行命令,可以使用下列字段:
    00 12 1 * * command
    
  • 如何设置才能让命令在每月的最后一天执行,因为无法设置一个 dayofmonth 值,涵盖所有月份的最后一天。常用的解决方法是加一个 if-then 语句,在其中使用 date 命令检查明天的日期是不是某个月份的第一天(01):
    00 12 28-31 * * if [ "$(date +%d -d tomorrow)" = 01 ] ; then command ; fi
    

这行脚本会在每天中午 12 点检查当天是不是当月的最后一天(28~31),如果是,就由 cron 执行 command。另一种方法是将 command 替换成一个控制脚本(controlling script),在可能是每月最后一 天的时候运行。控制脚本包含 if-then 语句,用于检查第二天是否为某个月的第一天。如果是,则由控制脚本发出命令,执行必须在当月最后一天执行的内容。

  • 命令列表必须指定要运行的命令或脚本的完整路径。可以像在命令行中那样,添加所需的任何选项和重定向符:
    15 10 * * * /home/christine/backup.sh > backup.out
    
    • cron 程序会以提交作业的用户身份运行该脚本,因此你必须有访问该脚本(或命令)以及输出文件的合理权限。
  • 每个用户(包括 root 用户) 都可以使用自己的 cron 时间表运行已安排好的任务。 Linux 提供了 crontab 命令来处理 cron 时间表。要列出已有的 cron 时间表,可以用-l 选项:
    $ crontab -l
    no crontab for christine
    $
    
    • 在默认情况下,用户的 cron 时间表文件并不存在。
    • 可以使用-e 选项向 cron 时间表添加字段。 在添加字段时, crontab 命令会启动一个文本编辑器,使用已有的 cron 时间表作为文件内容(如果时间表不存在,就是一个空文件)。
  • 如果创建的脚本对于执行时间的精确性要求不高,则用预配置的 cron 脚本目录会更方便。预配置的基础目录共有 4 个: hourly 、daily 、monthly 和 weekly。
    $ ls /etc/cron.*ly
    /etc/cron.daily:
    0anacron  apt-compat    cracklib-runtime  logrotate  [...]
    apport    bsdmainutils  dpkg              man-db     [...]
    
    /etc/cron.hourly:
    
    /etc/cron.monthly:
    0anacron
    
    /etc/cron.weekly:
    0anacron  man-db  update-notifier-common
    $
    
    • 如果你的脚本需要每天运行一次,那么将脚本复制到 daily 目录, cron 就会每天运行它。
  • 如果某个作业在 cron 时间表中设置的运行时间已到,但这时候 Linux 系统处于关闭状态,那么该作业就不会运行。当再次启动系统时, cron 程序不会再去运行那些错过的作业。为了解决这个问题,许多 Linux 发行版提供了 anacron 程序。
  • 如果 anacron 判断出某个作业错过了设置的运行时间,它会尽快运行该作业。这意味着如果 Linux 系统关闭了几天,等到再次启动时,原计划在关机期间运行的作业会自动运行。有了 anacron,就能确保作业一定能运行,这正是通常使用 anacron 代替 cron 调度作业的原因。
  • anacron 程序只处理位于 cron 目录的程序,比如/etc/cron.monthly。它通过时间戳来判断作业是否在正确的计划间隔内运行了。每个 cron 目录都有一个时间戳文件,该文件位于/var/spool/anacron:
    $ ls /var/spool/anacron
    cron.daily  cron.monthly  cron.weekly
    $
    $ sudo cat /var/spool/anacron/cron.daily
    [sudo] password for christine:
    20200619
    $
    
  • anacron 程序使用自己的时间表(通常位于/etc/anacrontab)来检查作业目录。
  • anacron 时间表的基本格式和 cron 时间表略有不同:
    period delay identifier command
    
    • period 字段定义了作业的运行频率(以天为单位)。anacron 程序用该字段检查作业的时间戳文件。delay 字段指定了在系统启动后,anacron 程序需要等待多少分钟再开始运行错过的脚本。
    • identifier 字段是一个独特的非空字符串,比如 cron.weekly。它唯一的作用是标识出现在日志消息和错误 email 中的作业。 command 字段包含了 run-parts 程序和一个 cron 脚本目录名。 run-parts 程序负责运行指定目录中的所有脚本。
  • anacron 不会运行位于/etc/cron.hourly 目录的脚本。 这是因为 anacron 并不处理执行时间需求少于一天的脚本。
  1. 使用新 shell 启动脚本
  • Linux 系统提供了一些脚本文件,可以让脚本在启动新的 bash shell 时运行。与此类似,位于用户主目录中的启动文件(比如.bashrc)提供了一个位置,以存放新 shell 启动时需要运行 的脚本和命令。
  • 基本上,以下所列文件中的第一个文件会被运行, 其余的则会被忽略。(这个列表中并没有$HOME/.bashrc 文件。这是因为该文件通常通过其他文件运行)
    • $HOME/.bash_profile
    • $HOME/.bash_login
    • $HOME/.profile
    • 因此,应该将需要在登录时运行的脚本放在上述第一个文件中。
  • 每次启动新 shell,bash shell 都会运行.bashrc 文件。
  • 一般而言,用户登录时会运行从$ HOME/.bash_profile 、$ HOME/.bash_login 或$ HOME/.profile 中找到的第一个文件,而$HOME/.bashrc 则是由非登录 shell(nonlogin shell)运行的文件。
  • .bashrc 文件通常也借由某个 bash 启动文件来运行,因为.bashrc 文件会运行两次:一次是当用户登录 bash shell 时,另一次是当用户启动 bash shell 时。如果需要某个脚本在两个时刻都运行,可以将其放入该文件中。
  1. 实战演练
  • 例子:
    source $scriptToRun > $scriptOutput &   #Run script in background
    
    • 注意,以上脚本的运行方式并没有使用 bash 或./运行文件, 而是改用了 source 工具。这是另一种运行 bash 脚本的方法,称为源引(sourcing)。这种操作与使用 bash 运行脚本差不多,只是不会创建子 shell。
    • 当使用 source 命令运行脚本时,就像bash 一样,无须在文件中设置执行权限。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值