《linux命令行与shell脚本编程大全》 读书笔记

第1章 初识linux shell

1.1 什么是linux

Linux可划分为四部分

  • Linux内核
  • GNU工具
  • 图形化桌面环境
  • 应用软件

在这里插入图片描述

1.1.1深入探究Linux内核

Linux系统的核心是内核。内核控制着计算机系统上的所有硬件和软件,在必要时分配硬件,并根据需要执行软件。
内核主要负责以下四种功能:

  • 系统内存管理
  • 软件程序管理
  • 硬件设备管理
  • 文件系统管理
1.系统内存管理

操作系统内核的主要功能之一就是内存管理。内核不仅管理服务器上的可用物理内存,还可以创建和管理虚拟内存(即实际并不存在的内存)。
内核通过硬盘上的存储空间来实现虚拟内存,这块区域称为交换空间(swap space)。不断地在交换空间和实际的物理内存之间反复交换虚拟内存中的内容。这使得系统以为它拥有比物理内存更多的可用内存。
在这里插入图片描述
内存存储单元按组划分成很多块,这些块称作页面(page)。内核将每个内存页面放在物理内存或交换空间。然后,内核会维护一个内存页面表,指明哪些页面位于物理内存内,哪些页面被换到了磁盘上。
内核会记录哪些内存页面正在使用中,并自动把一段时间未访问的内存页面复制到交换空间区域(称为换出,swapping out)——即使还有可用内存。当程序要访问一个已被换出的内存页面时,内核必须从物理内存换出另外一个内存页面给它让出空间,然后从交换空间换入请求的内存页面。

2.软件程序管理

Linux操作系统将运行中的程序称为进程。
内核创建了第一个进程(称为init进程)来启动系统上所有其他进程。当内核启动时,它会将init进程加载到虚拟内存中。内核在启动任何其他进程时,都会在虚拟内存中给新进程分配一块专有区域来存储该进程用到的数据和代码。
一些Linux发行版使用一个表来管理在系统开机时要自动启动的进程。在Linux系统上,这个表通常位于专门文件/etc/inittab中。另外一些系统(比如现在流行的Ubuntu Linux发行版)则采用/etc/init.d目录,将开机时启动
或停止某个应用的脚本放在这个目录下。这些脚本通过/etc/rcX.d目录下的入口启动,这里的X代表运行级(run level)。
Linux操作系统的init系统采用了运行级。运行级决定了init进程运行/etc/inittab文件或/etc/rcX.d目录中定义好的某些特定类型的进程。Linux操作系统有5个启动运行级。
运行级为1时,只启动基本的系统进程以及一个控制台终端进程。我们称之为单用户模式。单用户模式通常用来在系统有问题时进行紧急的文件系统维护。显然,在这种模式下,仅有一个人(通常是系统管理员)能登录到系统上操作数据。
标准的启动运行级是3。在这个运行级上,大多数应用软件,比如网络支持程序,都会启动。
另一个Linux中常见的运行级是5。在这个运行级上系统会启动图形化的X Window系统,允许用户通过图形化桌面窗口登录系统。
你可以使用ps命令查看当前运行在Linux系统上的进程,如下:
在这里插入图片描述
内核的另一职责是管理硬件设备。任何Linux系统需要与之通信的设备,都需要在内核代码中加入其驱动程序代码。驱动程序代码相当于应用程序和硬件设备的中间人,允许内核与设备之间交换数据。
在Linux内核中有两种方法用于插入设备驱动代码:

  • 编译进内核的设备驱动代码
  • 可插入内核的设备驱动模块

现在多为后一种,因为这种方式能极大地简化和扩展了硬件设备在Linux上的使用。
Linux系统将硬件设备当成特殊的文件,称为设备文件。设备文件有3种分类:

  • 字符型设备文件
  • 块设备文件
  • 网络设备文件

字符型设备文件是指处理数据时每次只能处理一个字符的设备。大多数类型的调制解调器和终端都是作为字符型设备文件创建的。
块设备文件是指处理数据时每次能处理大块数据的设备,比如硬盘。
网络设备文件是指采用数据包发送和接收数据的设备,包括各种网卡和一个特殊的回环设备。这个回环设备允许Linux系统使用常见的网络编程协议同自身通信。
Linux为系统上的每个设备都创建一种称为节点的特殊文件。与设备的所有通信都通过设备节点完成。每个节点都有唯一的数值对供Linux内核标识它。数值对包括一个主设备号和一个次设备号。类似的设备被划分到同样的主设备号下。次设备号用于标识主设备组下的某个特定设备。

4. 文件系统管理

不同于其他一些操作系统,Linux内核支持通过不同类型的文件系统从硬盘中读写数据。除了自有的诸多文件系统外,Linux还支持从其他操作系统(比如Microsoft Windows)采用的文件系统中读写数据。
下表列出了Linux系统用来读写数据的标准文件系统:
在这里插入图片描述
Linux服务器所访问的所有硬盘都必须格式化成表1-1所列文件系统类型中的一种。Linux内核采用虚拟文件系统(Virtual File System,VFS)作为和每个文件系统交互的接口。这为Linux内核同任何类型文件系统通信提供了一个标准接口。

1.1.2 GNU工具

GNU组织(GNU是GNU’s Not Unix的缩写)开发了一套完整的Unix工具,这些工具是在名为开源软件(open source software,OSS)的软件理念下开发的。将Linus的Linux内核和GNU操作系统工具整合起来,就产生了一款完整的、功能丰富的免费操作系统。

1.核心GNU工具

供Linux系统使用的这组核心工具被称为coreutils(core utilities)软件包。GNU coreutils软件包由三部分构成:

  • 用以处理文件的工具
  • 用以操作文本的工具
  • 用以管理进程的工具

这三组主要工具中的每一组都包含一些对Linux系统管理员和程序员至关重要的工具。

2.shell

GNU/Linux shell是一种特殊的交互式工具。shell的核心是命令行提示符。
shell包含了一组内部命令,用这些命令可以完成诸如复制文件、移动文件、重命名文件、显示和终止系统中正运行的程序等操作。
你也可以将多个shell命令放入文件中作为程序执行,这些文件被称作shell脚本。
在Linux系统上,通常有好几种Linux shell可用。不同的shell有不同的特性,有些更利于创建脚本,有些则更利于管理进程。所有Linux发行版默认的shell都是bash shell。bash shell由GNU项目开发,被当作标准Unix shell——Bourne shell(以创建者的名字命名)的替代品。
下表列出了Linux中常见的几种不同shell:
在这里插入图片描述

1.1.3 Linux 桌面环境

在Linux的早期(20世纪90年代初期),能用的只有一个简单的Linux操作系统文本界面。这个文本界面允许系统管理员运行程序,控制程序的执行,以及在系统中移动文件。随着Microsoft Windows的普及,电脑用户已经不再满足于对着老式的文本界面工作了。这推动了OSS社区的更多开发活动,Linux图形化桌面环境应运而生。
Linux有各种图形化桌面可供选择,但在此处就不详细介绍:

  • X Window系统
  • KDE桌面
  • GNOME桌面
  • Unity桌面

1.2 Linux 发行版

我们将完整的Linux系统包称为发行版。有很多不同的Linux发行版来满足可能存在的各种运算需求。不同的Linux发行版通常归类为3种:

  • 完整的核心Linux发行版
  • 特定用途的发行版
  • LiveCD测试发行版

1.2.1 核心 Linux 发行版

核心Linux发行版含有内核、一个或多个图形化桌面环境以及预编译好的几乎所有能见到的Linux应用。它提供了一站式的完整Linux安装。

1.2.2 特定用途的 Linux 发行版

Linux发行版的一个新子群已经出现了。它们通常基于某个主流发行版,但仅包含主流发行版中一小部分用于某种特定用途的应用程序。
在这里插入图片描述

1.2.3 Linux LiveCD

Linux世界中一个相对较新的现象是可引导的Linux CD发行版的出现。它无需安装就可以看到Linux系统是什么样的。多数现代PC都能从CD启动,而不是必须从标准硬盘启动。这是一个不弄乱PC就体验各种Linux发行版的绝妙方法。只需插入CD就能引导了!所有的Linux软件都将直接从CD上运行。
在这里插入图片描述
许多特定用途的Linux发行版都有对应的Linux LiveCD版本。
Linux LiveCD也有一些不足之处。由于要从CD上访问所有东西,应用程序会运行得更慢,而如果再搭配上陈旧缓慢的PC和光驱,那更是慢上加慢。还有,由于无法向CD写入数据,对Linux系统作的任何修改都会在重启后失效。

1.3 小结

Linux内核是系统的核心,控制着内存、程序和硬件之间的交互。GNU工具也是Linux系统中的一个重要部分。本书关注的焦点Linux shell是GNU核心工具集中的一部分。

第2章 走进shell

2.1 进入命令行

在图形化桌面出现之前,与Unix系统进行交互的唯一方式就是借助由shell所提供的文本命令行界面(command line interface,CLI)。CLI只能接受文本输入,也只能显示出文本和基本的图形输出。

2.1.1 控制台终端

进入CLI的一种方法是让Linux系统退出图形化桌面模式,进入文本模式。这样在显示器上就只有一个简单的shell CLI,这种模式称作Linux控制台。

2.1.2 图形化终端

除了虚拟化终端控制台,还可以使用Linux图形化桌面环境中的终端仿真包。终端仿真包会在一个桌面图形化窗口中模拟控制台终端的使用。

2.2 通过 Linux 控制台终端访问 CLI

在大多数Linux发行版中,你可以使用简单的按键组合来访问某个Linux虚拟控制台。通常必须按下Ctrl+Alt组合键,然后按功能键(F1~F7)进入要使用的虚拟控制台。功能键F1生成虚拟控制台1,F2键生成虚拟控制台2,F3键生成虚拟控制台3,F4键生成虚拟控制台4,依次类推。
在login:提示符后输入用户ID,然后再在Password:提示符后输入密码,就可以进入控制台终端了。

2.3 通过图形化终端仿真访问 CLI

相较于虚拟化控制台终端,图形化桌面环境提供了更多访问CLI的方式。在图形化环境下,有大量可用的图形化终端仿真器。每个软件包都有各自独特的特性及选项。
在这里插入图片描述
对于各种图形终端的使用在此处略过。

第3章 基本的bash shell命令

大多数Linux发行版的默认shell都是GNU bash shell。本章将介绍bash shell的一些基本特性。

3.1 启动 shell

GNU bash shell能提供对Linux系统的交互式访问。它是作为普通程序运行的,通常是在用户登录终端时启动。登录时系统启动的shell依赖于用户账户的配置。
/etc/passwd文件包含了所有系统用户账户列表以及每个用户的基本配置信息。以下是从/etc/passwd文件中取出的样例条目:

christine:x:501:501:Christine Bresnahan:/home/christine:/bin/bash

最后一个字段指定了用户使用的shell程序。
尽管bash shell会在登录时自动启动,但是,是否会出现shell命令行界面(CLI)则依赖于所使用的登录方式。如果采用虚拟控制台终端登录,CLI提示符会自动出现,你可以输入shell命令。但如果是通过图形化桌面环境登录Linux系统,你就需要启动一个图形化终端仿真器来访问shell CLI提示符。

3.2 shell 提示符

一旦启动了终端仿真软件包或者登录Linux虚拟控制台,你就会看到shell CLI提示符。默认bash shell提示符是美元符号($),这个符号表明shell在等待用户输入。
在这里插入图片描述
除了作为shell的入口,提示符还能够提供其他的辅助信息,比如用户名、系统名等。

3.3 bash 手册

大多数Linux发行版自带用以查找shell命令及其他GNU工具信息的在线手册。熟悉手册对使用各种Linux工具大有裨益,尤其是在你要弄清各种命令行参数的时候。
man命令用来访问存储在Linux系统上的手册页面。在想要查找的工具的名称前面输入man命令,就可以找到那个工具相应的手册条目。
下图展示了查找ls命令的手册页面的例子。输入命令man ls就可以进入该页面:
在这里插入图片描述
读完了手册页,可以点击q键退出。
bash手册甚至包含了一份有关其自身的参考信息。输入man man来查看与手册页相关的手册页:
在这里插入图片描述
如果不记得命令名怎么办?可以使用关键字搜索手册页。语法是:man -k 关键字。例如,要查找与终端相关的命令,可以输入man -k terminal:
在这里插入图片描述
手册页还有对应的内容区域。每个内容区域都分配了一个数字,从1开始,一直到9,如下表所示:
在这里插入图片描述
在这里插入图片描述
man工具通常提供的是命令所对应的最低编号的内容。在手册的左上角和右上角,单词ls后的括号中有一个数字:(1)。这表示所显示的手册页来自内容区域1(可执行程序或shell命令):
在这里插入图片描述
一个命令偶尔会在多个内容区域都有对应的手册页,要想查看所需要的页面,可以输入man section# topic。比如可以输入man 7 hostname查看hostname 第7分区的手册内容:
在这里插入图片描述
手册页不是唯一的参考资料。还有另一种叫作info页面的信息。可以输入info hostname来了解hostname页面的相关内容。
另外,大多数命令都可以接受-help或–help选项。例如你可以输入hostname -help来查看帮助。关于帮助的更多信息,可以输入help help

3.4 浏览文件系统

当登录系统并获得shell命令提示符后,你通常位于自己的主目录中。

3.4.1 Linux 文件系统

如果你刚接触Linux系统,可能就很难弄清楚Linux如何引用文件和目录,对已经习惯Microsoft Windows操作系统方式的人来说更是如此。
你将注意到的第一个不同点是,Linux在路径名中不使用驱动器盘符。举个例子,在Windows中经常看到这样的文件路径:

c:\Users\Rich\Documents\test.doc	

c:表示文件在c盘中。
Linux则采用了一种不同的方式。Linux将文件存储在单个目录结构中,这个目录被称为虚拟目录(virtual directory)。虚拟目录将安装在PC上的所有存储设备的文件路径纳入单个目录结构中。
Linux虚拟目录结构只包含一个称为根(root)目录的基础目录。根目录下的目录和文件会按照访问它们的目录路径一一列出,这点跟Windows类似。在Linux中,你会看到下面这种路径:

/home/Rich/Documents/test.doc

你将会发现Linux使用正斜线/而不是反斜线\在文件路径中划分目录。在Linux中,反斜线用来标识转义字符的。
Linux虚拟目录中比较复杂的部分是它如何协调管理各个存储设备。在Linux PC上安装的第一块硬盘称为根驱动器。Linux会在根驱动器上创建一些特别的目录,我们称之为挂载点(mount point)。挂载点是虚拟目录中用于分配额外存储设备的目录。虚拟目录会让文件和目录出现在这些挂载点目录中,然而实际上它们却存储在另外一个驱动器中。
通常系统文件会存储在根驱动器中,而用户文件则存储在另一驱动器中,如下图:
在这里插入图片描述
Linux文件系统结构是从Unix文件结构演进过来的。在Linux文件系统中,通用的目录名用于表示一些常见的功能:
在这里插入图片描述
常见的目录名均基于文件系统层级标准(filesystem hierarchy standard,FHS)。很多Linux发行版都遵循了FHS。这样一来,你就能够在任何兼容FHS的Linux系统中轻而易举地查找文件。
在登录系统并获得一个shell CLI提示符后,会话将从主目录开始。主目录是分配给用户账户的一个特有目录。用户账户在创建之后,系统通常会为其分配一个特有的目录。

3.4.2 遍历目录

在Linux文件系统上,可以使用切换目录命令cd将shell会话切换到另一个目录。cd命令的格式非常简单:

cd destination

如果没有为cd命令指定目标路径,它将切换到用户主目录。
destination参数可以用两种方式表示:一种是使用绝对文件路径,另一种是使用相对文件路径。

1. 绝对目录

绝对文件路径定义了在虚拟目录结构中该目录的确切位置,以虚拟目录的根目录开始,相当于目录的全名。绝对文件路径总是以正斜线/作为起始,指明虚拟文件系统的根目录。比如如果要指向usr目录所包含的bin目录下的用户二进制文件,可以使用如下绝对文件路径:

/usr/bin

在这里插入图片描述
你可以使用pwd命令来显示出shell会话的当前目录,这个目录被称为当前工作目录:
在这里插入图片描述
可以使用绝对文件路径切换到Linux虚拟目录结构中的任何一级,还可以从Linux虚拟目录中的任何一级跳回主目录。

2. 相对文件路径

相对文件路径允许用户指定一个基于当前位置的目标文件路径。相对文件路径不以代表根目录的正斜线/开头,而是以目录名(如果用户准备切换到当前工作目录下的一个目录)或是一个特殊字符开始。
假如你位于home目录中,并希望切换到Documents子目录,那你可以使用cd命令加上一个相对文件路径:
在这里插入图片描述
也可以使用一个特殊字符来表示相对目录位置,下面是一些通用字符的含义:

  • 单点符.,表示当前目录
  • 双点符..,表示当前目录的父目录。
  • 波浪号~,表示当前用户根目录
    下图是它们的用法示例:
    在这里插入图片描述

3.5 文件和目录列表

要想知道系统中有哪些文件,可以使用列表命令ls。本节将描述ls命令和可用来格式化其输出信息的选项。

3.5.1 基本列表功能

ls命令最基本的形式会显示当前目录下的文件和目录:
在这里插入图片描述
ls命令输出的列表是按字母排序的(按列排序而不是按行排序)。
可用带-F参数的ls命令轻松区分文件和目录:
在这里插入图片描述
-F参数在目录名后加了正斜线/,以方便用户在输出中分辨它们。类似地,它会在可执行文件(比如上面的my_script文件)的后面加个星号*,以便用户找出可在系统上运行的文件。
需要注意的是,基本的ls命令不显示隐藏文件。Linux经常采用隐藏文件来保存配置信息。在Linux上,隐藏文件通常是文件名以点号.开始的文件。要把隐藏文件和普通文件及目录一起显示出来,就得用到-a参数:
在这里插入图片描述
-R参数是ls命令可用的另一个参数,叫作递归选项。它列出了当前目录下包含的子目录中的文件。如果目录很多,这个输出就会很长:
在这里插入图片描述
-d选项用于只列出目录本身的信息,不列出其中的内容:
在这里插入图片描述
另外多个选项参数并不一定要分开写,而是可以进行合并,比如ls -a -F等同于ls -aF

3.5.2 显示长列表

在基本的输出列表中,ls命令并未输出太多每个文件的相关信息。要显示附加信息,另一个常用的参数是-l-l参数会产生长列表格式的输出,包含了目录中每个文件的更多相关信息:
在这里插入图片描述
这种长列表格式的输出在每一行中列出了单个文件或目录。除了文件名,输出中还有其他有用信息:

  • 文件类型,比如目录d、文件-、字符型文件c或块设备b
  • 文件的权限(参见第6章)
  • 文件的硬链接总数
  • 文件属主的用户名
  • 文件属组的组名
  • 文件的大小(以字节为单位)
  • 文件的上次修改时间
  • 文件名或目录名

-l参数是一个强大的工具。有了它,你几乎可以看到系统上任何文件或目录的大部分信息。

3.5.3 过滤输出列表

默认情况下,ls命令会输出目录下的所有非隐藏文件。有时这个输出会显得过多,当你只需要查看单个少数文件信息时更是如此。
幸而ls命令还支持在命令行中定义过滤器。它会用过滤器来决定应该在输出中显示哪些文件或目录。这个过滤器就是一个进行简单文本匹配的字符串。可以在要用的命令行参数之后添加这个过滤器,并且过滤器支持下面的通配符:

  • 问号?代表一个字符
  • 星号*代表零个或多个字符

在这里插入图片描述
在过滤器中使用星号和问号被称为文件扩展匹配(file globbing),指的是使用通配符进行模式匹配的过程。通配符正式的名称叫作元字符通配符(metacharacter wildcards)。除了星号和问号之外,还有更多的元字符通配符可用于文件扩展匹配,如下:

  • 使用中括号指定可能出现的字符:ls -l my_scr[ai]pt
  • 使用中括号指定字符范围,例如字母范围[a – i]: ls -l f[a-i]ll
  • 可以使用感叹号将不需要的内容排除在外:ls -l f[!a]ll

文件扩展匹配是一个功能强大的特性。它也可以用于ls以外的其他shell命令。

3.6 处理文件

3.6.1 创建文件

touch命令可以用来创建空文件:

touch filename

在这里插入图片描述
touch命令创建了你指定的新文件,并将你的用户名作为文件的属主。如果此文件已经存在,touch命令还可用来改变文件的修改时间,这个操作并不需要改变文件的内容:
在这里插入图片描述

3.6.2 复制文件

cp命令被用于在文件系统中将文件和目录从一个位置复制到另一个位置。在最基本的用法里,cp命令需要两个参数——源对象和目标对象:

cp source destination

当source和destination参数都是文件名时,cp命令将源文件复制成一个新文件,并且以destination命名:
在这里插入图片描述
如果目标文件已经存在,cp命令可能并不会提醒这一点。最好是加上-i选项,强制shell询问是否需要覆盖已有文件:
在这里插入图片描述
也可以将文件复制到指定目录中,目录路径可以是绝对路径或相对路径:
在这里插入图片描述
上图的例子在目标目录名尾部加上了一个正斜线/,这表明test是目录而非文件。这有助于明确目的,而且在复制单个文件时非常重要。如果没有使用正斜线,子目录./test又不存在时,就会有麻烦。在这种情况下,试图将一个文件复制到test子目录反而会创建一个名为test的文件,连错误消息都不会显示!
cp命令的-R参数威力强大,可以用它在一条命令中递归地复制整个目录的内容:
在这里插入图片描述
在执行cp –R命令之前,目录test_two并不存在。它是随着cp –R命令被创建的,整个test_one目录中的内容都被复制到其中。
也可以在cp命令中使用通配符:
在这里插入图片描述
该命令将所有以test开头并以.lua结尾的文件复制到lua_test目录中。

3.6.3 制表键自动补全

在使用命令行时,很容易输错命令、目录名或文件名。实际上,对长目录名或文件名来说,
输错的几率还是蛮高的。这正是制表键自动补全挺身而出的时候。制表键自动补全允许你在输入文件名或目录名时按一下制表键Tab,让shell帮忙将内容补充完整。

3.6.4 链接文件

链接文件是Linux文件系统的一个优势。如需要在系统上维护同一文件的两份或多份副本,除了保存多份单独的物理文件副本之外,还可以采用保存一份物理文件副本和多个虚拟副本的方法。这种虚拟的副本就称为链接。链接是目录中指向文件真实位置的占位符。在Linux中有两种不同类型的文件链接:

  • 符号链接
  • 硬链接

符号链接就是一个实实在在的文件,它指向存放在虚拟目录结构中某个地方的另一个文件。这两个通过符号链接在一起的文件,彼此的内容并不相同。要为一个文件创建符号链接,原始文件必须事先存在。然后可以使用ln命令以及-s选项来创建符号链接:ln -s file link
在这里插入图片描述
在上面的例子中,显示在长列表中符号文件名后的->符号表明该文件是链接到文件data_file上的一个符号链接。
另外还要注意的是,符号链接的文件大小与数据文件的文件大小。符号链接sl_data_file只有9个字节,而data_file有194个字节。这是因为sl_data_file仅仅只是指向data_file而已。它们的内容并不相同,是两个完全不同的文件。
另一种证明链接文件是独立文件的方法是查看inode编号。文件或目录的inode编号是一个用于标识的唯一数字,这个数字由内核分配给文件系统中的每一个对象。要查看文件或目录的inode编号,可以给ls命令加入-i参数:
ls -i
在这里插入图片描述
从上图中我们可以看到两个文件有不同的inode编号,所以说它们是不同的文件。
硬链接会创建独立的虚拟文件,其中包含了原始文件的信息及位置。但是它们从根本上而言是同一个文件。引用硬链接文件等同于引用了源文件。要创建硬链接,原始文件也必须事先存在,只不过这次使用ln命令时不再需要加入额外的参数了:
在这里插入图片描述
从上图我们可以看到硬链接和原文件有相同的inode编号,因为它们终归是同一个文件。
注意,只能对处于同一存储媒体的文件创建硬链接。要想在不同存储媒体的文件之间创建链接,只能使用符号链接。
复制链接文件的时候一定要小心。如果使用cp命令复制一个文件,而该文件又已经被链接到了另一个源文件上,那么你得到的其实是源文件的一个副本。这很容易让人犯晕。用不着复制链接文件,可以创建原始文件的另一个链接。同一个文件拥有多个链接,这完全没有问题。但是,千万别创建软链接文件的软链接。这会形成混乱的链接链,不仅容易断裂,还会造成各种麻烦。

3.6.5 重命名文件

在Linux中,重命名文件称为移动(moving)。mv命令可以将文件和目录移动到另一个位置或重新命名。
在这里插入图片描述
注意,移动文件会将文件名从fall更改为fzll,但inode编号和时间戳保持不变。这是因为mv只影响文件名。
也可以使用mv来移动文件的位置:
在这里插入图片描述
和cp命令类似,也可以在mv命令中使用-i参数。这样在命令试图覆盖已有的文件时,你就会得到提示。
也可以使用mv命令移动文件位置并修改文件名称,这些操作只需一步就能完成:
在这里插入图片描述
移动后的文件的时间戳和inode编号都没有改变,改变的只有位置和名称。
也可以使用mv命令移动整个目录及其内容:
在这里插入图片描述
目录内容没有变化,只有目录名发生了改变。

3.6.6 删除文件

在Linux中,删除(deleting)叫作移除(removing)①。bash shell中删除文件的命令是rm。rm
命令的基本格式非常简单:rm filename
在这里插入图片描述
-i命令参数提示你是不是要真的删除该文件。bash shell中没有回收站或垃圾箱,文件一旦删除,就无法再找回。因此,在使用rm命令时,要养成总是加入-i参数的好习惯。
rm命令的另外一个特性是,如果要删除很多文件且不受提示符的打扰,可以用-f参数强制删除。

3.7 处理目录

在Linux中,有些命令(比如cp命令)对文件和目录都有效,而有些只对目录有效。

3.7.1 创建目录

在Linux中创建目录很简单,用mkdir命令即可:mkdir dir_name
在这里插入图片描述
可以根据需要批量地创建目录和子目录。但是,如果你想单单靠mkdir命令来实现,就会得到下面的错误消息:
在这里插入图片描述
要想同时创建多个目录和子目录,需要加入-p参数:
在这里插入图片描述

3.7.2 删除目录

删除目录的基本命令是rmdir。默认情况下,rmdir命令只删除空目录,如果一个目录下有别的文件,则必须先把这个文件删除才能使用rmdir删除此目录,rmdir并没有-i选项来询问是否要删除目录:
在这里插入图片描述
也可以在整个非空目录上使用rm命令。使用-r选项使得命令可以向下进入目录,删除其中的文件,然后再删除目录本身:
在这里插入图片描述
这种方法同样可以向下进入多个子目录,当需要删除大量目录和文件时,这一点尤为有效。但是你依然要确认每个文件是否要被删除。如果该目录有很多个文件和子目录,这将非常琐碎。一口气删除目录及其所有内容的终极大法就是使用带有-r参数和-f参数的rm命令:rm -rf dir_name
在这里插入图片描述
rm -rf命令既没有警告信息,也没有声音提示,务必谨慎使用,请再三检查你所要进行的操作是否符合预期。

3.8 查看文件内容

Linux中有几个命令可以查看文件的内容,而不需要调用其他文本编辑器。

3.8.1 查看文件类型

在显示文件内容之前,应该先了解一下文件的类型,file命令可以完成这项任务。其可以查看目录、文本文件、二进制文件等各种文件的信息:
在这里插入图片描述

3.8.2 查看整个文件

在Linux上有3个不同的命令可以用来查看整个文件。

1. cat命令

cat命令是显示文本文件中所有数据的得力工具,命令很简单:cat file_name
在这里插入图片描述
-n参数会给所有的行加上行号:
在这里插入图片描述

2. more命令

cat命令的主要缺陷是:一旦运行,你就无法控制后面的操作。为了解决这个问题,开发人员编写了more命令。more命令会显示文本文件的内容,但会在显示每页数据之后停下来。more命令是分页工具。在本章前面的内容里,当使用man命令时,分页工具会显示所选的bash手册页面。和在手册页中前后移动一样,你可以通过按空格键或回车键以逐行向前的方式浏览文本文件, 浏览完之后,按q键退出。
在这里插入图片描述

3. less命令

从名字上看,它并不像more命令那样高级。但是,less命令的命名实际上是个文字游戏(从俗语“less is more”得来),它实为more命令的升级版。less命令的操作和more命令基本一样,一次显示一屏的文件文本。除了支持和more命令相同的命令集,它还包括更多的选项。要想查看less命令所有的可用选项,可以输入man less浏览对应的手册页。

3.8.3 查看部分文件

1. tail命令

tail命令会显示文件最后几行的内容(文件的“尾部”)。默认情况下,它会显示文件的末尾10行。
在这里插入图片描述
可以向tail命令中加入-n参数来修改所显示的行数:tail -n num filename
在这里插入图片描述

2. head命令

head命令,顾名思义,会显示文件开头那些行的内容。默认情况下,它会显示文件前10行的文本。类似于tail命令,它也支持-n参数,这样就可以指定想要显示的内容了,在此不再一一演示。

第4章 更多的bash shell 命令

4.1 监测程序

Linux系统管理员面临的最复杂的任务之一就是跟踪运行在系统中的程序。

4.1.1 探查进程

当程序运行在系统上时,我们称之为进程(process)。想监测这些进程,需要熟悉ps命令的用法。默认情况下,ps命令只会显示运行在当前控制台下的属于当前用户的进程:
在这里插入图片描述
Linux系统中使用的GNU ps命令支持3种不同类型的命令行参数:

  • Unix风格的参数,前面加单破折线
  • BSD风格的参数,前面不加破折线
  • GNU风格的长参数,前面加双破折线
1. Unix风格的参数

在这里插入图片描述
在这里插入图片描述
上面给出的参数已经很多了,不过还有很多。使用ps命令的关键不在于记住所有可用的参数,而在于记住最有用的那些参数。大多数Linux系统管理员都有自己的一组参数,他们会牢牢记住这些用来提取有用的进程信息的参数。举个例子,如果你想查看系统上运行的所有进程,可用-ef参数组合:
在这里插入图片描述
-e参数指定显示所有运行在系统上的进程;-f参数则扩展了输出,这些扩展的列含义如下:

  • UID:启动这些进程的用户
  • PID:进程的进程ID
  • PPID:父进程的进程号(如果该进程是由另一个进程启动的)
  • C:进程生命周期中的CPU利用率
  • STIME:进程启动时的系统时间。
  • TTY:进程启动时的终端设备
  • TIME:运行进程需要的累计CPU时间
  • CMD:启动的程序名称

如果想要获得更多的信息,可采用-l参数,它会产生一个长格式输出:
在这里插入图片描述
多出的那些列意义如下:

  • F:内核分配给进程的系统标记。
  • S:进程的状态(O代表正在运行;S代表在休眠;R代表可运行,正等待运行;Z代表僵化,进程已结束但父进程已不存在;T代表停止)。
  • PRI:进程的优先级(越大的数字代表越低的优先级)。
  • NI:谦让度值用来参与决定优先级。
  • ADDR:进程的内存地址。
  • SZ:假如进程被换出,所需交换空间的大致大小。
  • WCHAN:进程休眠的内核函数的地址。
2. BSD风格的参数

BSD版的ps命令参数如下表所示:
在这里插入图片描述
在这里插入图片描述
Unix和BSD类型的参数有很多重叠的地方,大多数情况下,你只要选择自己所喜欢格式的参数类型就行了:
在这里插入图片描述

3. GNU长参数

在这里插入图片描述
在这里插入图片描述
mac似乎不支持此套参数系统,故不在此演示。

4.1.2 实时监测进程

top命令跟ps命令相似,能够显示进程信息,但top是实时显示的, 而ps只能显示某个特定时间点的信息。下面是top命令展示的界面:
在这里插入图片描述
字段含义在此不再一一列举。默认情况下,top命令在启动时会按照%CPU值对进程排序。可以在top运行时使多种交互命令重新排序。每个交互式命令都是单字符,在top命令运行时键入可改变top的行为。键入f
许你选择对输出进行排序的字段,键入d允许你修改轮询间隔,键入q可以退出top。

4.1.3 结束进程

在Linux中,进程之间通过信号来通信。进程的信号就是预定义好的一个消息,进程能识别它并决定忽略还是作出反应。进程如何处理信号是由开发人员通过编程来决定的。大多数编写完善的程序都能接收和处理标准Unix进程信号。这些信号如下表:
在这里插入图片描述
在Linux上有两个命令可以向运行中的进程发出进程信号:killkillall

kill命令

kill命令可通过进程ID(PID)给进程发信号:kill -s sign_name PID
默认情况下,kill命令会向命令行中列出的全部PID发送一个TERM信号,即kill PID 等同于kill -s TERM PID
要发送进程信号,你必须是进程的属主或登录为root用户。
kill命令不会有任何输出,要检查kill命令是否有效,可再运行ps或top命令,看看问题进程是否已停止。

killall命令

killall命令非常强大,它支持通过进程名而不是PID来结束进程。killall命令也支持通配符,这在系统因负载过大而变得很慢时很有用。比如killall http*会结束所有以http开头的进程。

4.2 监测磁盘空间

4.2.1 挂载存储媒体

Linux文件系统将所有的磁盘都并入一个虚拟目录下。在使用新的存储媒体之前,需要把它放到虚拟目录下。这项工作称为挂载(mounting)。本节将介绍一些可以帮你管理可移动存储设备的Linux命令行命令。

1. mount命令

Linux上用来挂载媒体的命令叫作mount。默认情况下,mount命令会输出当前系统上挂载的设备列表。
在这里插入图片描述
要手动在虚拟目录中挂载设备,需要以root用户身份登录,或是以root用户身份运行sudo命令。下面是手动挂载媒体设备的基本命令:

mount -t type device directory

4.2.2 使用 df 命令

有时你需要知道在某个设备上还有多少磁盘空间。df命令可以让你很方便地查看所有已挂载磁盘的使用情况:
在这里插入图片描述
一个常用的参数是-h。它会把输出中的磁盘空间按照用户易读的形式显示,通常用M来替代兆字节,用G替代吉字节:在这里插入图片描述

4.2.3 使用 du 命令

另一个有用的命令是du命令。du命令可以显示某个特定目录(默认情况下是当前目录)的磁盘使用情况。这一方法可用来快速判断系统上某个目录下是不是有超大文件。
默认情况下,du命令会显示当前目录下所有的文件、目录和子目录的磁盘使用情况,它会以磁盘块为单位来表明每个文件或目录占用了多大存储空间。对标准大小的目录来说,这个输出会是一个比较长的列表:
在这里插入图片描述
当然我们可以使用命令行参数使du命令更易用:

  • -c:显示所有已列出文件总的大小。
  • -h:按用户易读的格式输出大小,即用K替代千字节,用M替代兆字节,用G替代吉字节。
  • -s:显示每个输出参数的总计。

4.3 处理数据文件

当你有大量数据时,通常很难处理这些信息及提取有用信息。Linux系统提供了一些命令行工具来处理大量数据。

4.3.1 排序数据

处理大量数据时的一个常用命令是sort命令。顾名思义,sort命令是对数据进行排序的。默认情况下,sort命令按照会话指定的默认语言的排序规则对文本文件中的数据行排序:
在这里插入图片描述
另外需要注意的是,默认情况下,sort命令会把数字当做字符来执行标准的字符排序,产生的输出可能根本就不是你要的:在这里插入图片描述
解决这个问题可用-n参数,它会处理数据文件 告诉sort命令把数字识别成数字而不是字符,并且按值排序:
在这里插入图片描述
还有其他一些方便的sort参数可用:在这里插入图片描述
现在我们可以通过dusort命令来找到占用空间最大的文件了,命令如下:

du -sh * | sort -nr

命令中用到的符号|为管道符号,它将du命令的输出重定向到sort命令。我们将在第11章中进一步讨论:
在这里插入图片描述

4.3.2 搜索数据

你会经常需要在大文件中找一行数据,而这行数据又埋藏在文件的中间。这时并不需要手动翻看整个文件,用grep命令来帮助查找就行了,grep命令的命令行格式如下:

grep [options] pattern [file]

grep命令会在输入或指定的文件中查找包含匹配指定模式的字符的行。grep的输出就是包含了匹配模式的行,看下面的例子:
在这里插入图片描述
由于grep命令非常流行,它经历了大量的更新。有很多功能被加进了grep命令。如果查看一下它的手册页面,你会发现它是多么的无所不能。下面列出一些常用的参数选项:

  • -v:反向搜索(输出不匹配该模式的行)
  • -n:要显示匹配模式的行所在的行号
  • -c:只要知道有多少行含有匹配的模式
  • -e:指定多个匹配模式
    在这里插入图片描述
    默认情况下,grep命令用基本的Unix风格正则表达式来匹配模式。Unix风格正则表达式采用特殊字符来定义怎样查找匹配的模式。
    egrep命令是grep的一个衍生,支持POSIX扩展正则表达式。POSIX扩展正则表达式含有更多的可以用来指定匹配模式的字符(参见第20章)。fgrep则是另外一个版本,支持将匹配模式指定为用换行符分隔的一列固定长度的字符串。这样就可以把这列字符串放到一个文件中,然后在fgrep命令中用其在一个大型文件中搜索字符串了。

4.3.3 压缩数据

gzip是Linux上最流行的压缩工具。gzip软件包是GNU项目的产物,意在编写一个能够替代原先Unix中compress工具的免费版本。这个软件包含有下面的工具:

  • gzip:用来压缩文件。
  • gzcat:用来查看压缩过的文本文件的内容。
  • gunzip:用来解压文件。

gzip命令会压缩你在命令行指定的文件,也可以在命令行指定多个文件名甚至用通配符来一次性批量压缩文件:
在这里插入图片描述

4.3.4 归档数据

虽然zip命令能够很好地将数据压缩和归档进单个文件,但它不是Unix和Linux中的标准归档工具。目前,Unix和Linux上最广泛使用的归档工具是tar命令。下面是tar命令的格式:

tar function [options] object1 object2 ...

function参数定义了tar命令应该做什么, 如下表所示:
在这里插入图片描述
这些选项经常合并到一起使用。首先,你可以用下列命令来创建一个归档文件:

tar -cvf test.tar test/ test2/

上面的命令创建了名为test.tar的归档文件,含有test和test2目录内容。接着,用下列命令:

tar -tf test.tar

列出tar文件test.tar的内容(但并不提取文件)。最后,用命令:

tar -xvf test.tar

通过这一命令从tar文件test.tar中提取内容。如下:
在这里插入图片描述
下载了开源软件之后,你会经常看到文件名以.tgz结尾。这些是gzip压缩过的tar文件可以用命令tar -zxvf filename.tgz来解压。

第5章 理解shell

5.1 shell 的类型

系统启动什么样的shell程序取决于你个人的用户ID配置。在/etc/passwd文件中,在用户ID记录的第7个字段中列出了默认的shell程序:
在这里插入图片描述
有些系统中可能还有其他的shell程序:
在这里插入图片描述
它们各自的区别在此不做表述,使用最多的一般为bash shell。

5.2 shell 的父子关系

用于登录某个虚拟控制器终端或在GUI中运行终端仿真器时所启动的默认的交互shell,是一个父shell。在CLI提示符后输入/bin/bash命令或其他等效的bash命令时,会创建一个新的shell程序,这个shell程序被称为子shell(child shell)。当输入bash、生成子shell的时候,你是看不到任何相关的信息的,因此需要另一条命令帮助我们理清这一切。第4章中讲过的ps命令能够派上用场,在生成子shell的前后配合选项-f来使用:
在这里插入图片描述
可以看到启动bash后多了一个进程2348,且其父进程是2339即第一个bash shell程序。
子shell(child shell,也叫subshell)可以从父shell中创建,也可以从另一个子shell中创建。
bash shell程序可使用命令行参数修改shell启动方式,如下表:
在这里插入图片描述
可以输入man bash获得关于bash命令的更多帮助信息,了解更多的命令行参数。可以利用exit命令有条不紊地退出子shell。exit命令不仅能退出子shell,还能用来登出当前的虚拟控制台终端或终端仿真器软件。只需要在父shell中输入exit,就能够从容退出CLI了。

5.2.1 进程列表

就算是不使用bash shell命令或是运行shell脚本,你也可以生成子shell。一种方法就是使用进程列表。你可以在一行中指定要依次运行的一系列命令。这可以通过命令列表来实现,只需要在命令之间加入分号;即可。
在这里插入图片描述
在上面的例子中,所有的命令依次执行,不存在任何问题。不过这并不是进程列表。命令列表要想成为进程列表,这些命令必须包含在括号里:
在这里插入图片描述
尽管多出来的括号看起来没有什么太大的不同,但起到的效果确是非同寻常。括号的加入使命令列表变成了进程列表,生成了一个子shell来执行对应的命令。
要想知道是否生成了子shell,得借助一个使用了环境变量的命令。(环境变量会在第6章中详
述。)这个命令就是echo $BASH_SUBSHELL。如果该命令返回0,就表明没有子shell。如果返回1或者其他更大的数字,就表明存在子shell:
在这里插入图片描述
在shell脚本中,经常使用子shell进行多进程处理。但是采用子shell的成本不菲,会明显拖慢处理速度。在交互式的CLI shell会话中,子shell同样存在问题。它并非真正的多进程处理,因为终端控制着子shell的I/O。

5.2.2 别出心裁的子 shell 用法

在交互式shell中,一个高效的子shell用法就是使用后台模式。在讨论如果将后台模式与子shell搭配使用之前,你得先搞明白什么是后台模式。

1. 探索后台模式

在后台模式中运行命令可以在处理命令的同时让出CLI,以供他用。演示后台模式的一个经典命令就是sleep。sleep命令接受一个参数,该参数是你希望进程等待(睡眠)的秒数。这个命令在脚本中常用于引入一段时间的暂停。要想将命令置入后台模式,可以在命令末尾加上字符&。把sleep命令置入后台模式可以让我们利用ps命令来小窥一番:
在这里插入图片描述
除了ps命令,你也可以使用jobs命令来显示后台作业信息。jobs命令可以显示出当前运行在后台模式中的所有用户的进程(作业)。利用jobs命令的-l(字母L的小写形式)选项,你还能够看到更多的相关信息。除了默认信息之外,-l选项还能够显示出命令的PID:
在这里插入图片描述

2. 将进程列表置入后台

在CLI中运用子shell的创造性方法之一就是将进程列表置入后台模式。你既可以在子shell中进行繁重的处理工作,同时也不会让子shell的I/O受制于终端。方法就是在进程列表后加一个引用符号&
在这里插入图片描述
当然了,sleep和echo命令的进程列表只是作为一个示例而已。使用tar(参见第4章)创建备份文件是有效利用后台进程列表的一个更实用的例子:

$ (tar -cf Rich.tar /home/rich ; tar -cf My.tar /home/christine)&
3. 协程

将进程列表置入后台模式并不是子shell在CLI中仅有的创造性用法,协程就是另一种方法。
协程可以同时做两件事。它在后台生成一个子shell,并在这个子shell中执行命令。要进行协程处理,得使用coproc命令,还有要在子shell中执行的命令:
在这里插入图片描述
你可以使用命令的扩展语法自己设置协程的名字:在这里插入图片描述
记住,生成子shell的成本不低,而且速度还慢。在命令行中使用子shell能够获得灵活性和便利。要想获得这些优势,重要的是理解子shell的行为方式。对于命令也是如此。

5.3 理解 shell 的内建命令

5.3.1 外部命令

外部命令,有时候也被称为文件系统命令,是存在于bash shell之外的程序。它们并不是shell程序的一部分。外部命令程序通常位于/bin、/usr/bin、/sbin或/usr/sbin中。ps就是一个外部命令。你可以使用whichtype命令找到它:
在这里插入图片描述
当外部命令执行时,会创建出一个子进程。这种操作被称为衍生(forking)。外部命令ps很方便显示出它的父进程以及自己所对应的衍生子进程。当进程必须执行衍生操作时,它需要花费时间和精力来设置新子进程的环境。所以说,外部命令多少还是有代价的。

5.3.2 内建命令

内建命令和外部命令的区别在于前者不需要使用子进程来执行。它们已经和shell编译成了一体,作为shell工具的组成部分存在,不需要借助外部程序文件来运行。cd和exit命令都内建于bash shell。可以利用type命令来了解某个命令是否是内建的:
在这里插入图片描述
要注意,有些命令有多种实现。例如echo和pwd既有内建命令也有外部命令。两种实现略有不同。要查看命令的不同实现,使用type命令的-a选项:
在这里插入图片描述
对于有多种实现的命令,如果想要使用其外部命令实现,直接指明对应的文件就可以了。例如,要使用外部命令pwd,可以输入/bin/pwd。

1. 使用history命令

一个有用的内建命令是history命令。bash shell会跟踪你用过的命令。你可以唤回这些命令并重新使用。要查看最近用过的命令列表,可以输入不带选项的history命令:
在这里插入图片描述
通常历史记录中会保存最近的1000条命令。你可以设置保存在bash历史记录中的命令数。要想实现这一点,你需要修改名为HISTSIZE的环境变量(参见第6章),当然你可以通过输出此环境变量来查看自己电脑上保存的历史命令条数:
在这里插入图片描述
输入!!,然后按回车键就能够唤出刚刚用过的那条命令来使用:
在这里插入图片描述
当输入!!时,bash首先会显示出从shell的历史记录中唤回的命令。然后执行该命令。命令历史记录被保存在隐藏文件.bash_history中,它位于用户的主目录中。这里要注意的是,bash命令的历史记录是先存放在内存中,当shell退出时才被写入到历史文件中。你可以使用命令vi ~/.bash_history打开此文件查看。

2. 命令别名

alias命令是另一个shell的内建命令。命令别名允许你为常用的命令(及其参数)创建另一个名称,从而将输入量减少到最低。你所使用的Linux发行版很有可能已经为你设置好了一些常用命令的别名。要查看当前可用
的别名,使用alias命令以及选项-p
可以使用alias命令alias easy_name=‘cmd’创建属于自己的别名:
在这里插入图片描述
在定义好别名之后,你随时都可以在shell中使用它,就算在shell脚本中也没问题。要注意,因为命令别名属于内部命令,一个别名仅在它所被定义的shell进程中才有效:
在这里插入图片描述

第6章 使用Linux环境变量

6.1 什么是环境变量

bash shell用一个叫作环境变量(environment variable)的特性来存储有关shell会话和工作环境的信息(这也是它们被称作环境变量的原因)。
在bash shell中,环境变量分为两类:

  • 全局变量
  • 局部变量

6.1.1 全局环境变量

全局环境变量对于shell会话和所有生成的子shell都是可见的。局部变量则只对创建它们的shell可见。
要查看全局变量,可以使用envprintenv命令:
在这里插入图片描述
要显示个别环境变量的值,可以使用printenv命令,但是不要用env命令。也可以使用echo显示变量的值。在这种情况下引用某个环境变量的时候,必须在变量前面加上一个美元符$:
在这里插入图片描述
在变量名前加上$可以将它变为命令行参数:
在这里插入图片描述

6.1.2 局部环境变量

顾名思义,局部环境变量只能在定义它们的进程中可见。查看局部环境变量的列表有点复杂。遗憾的是,在Linux系统并没有一个只显示局部环境变量的命令。set命令会显示为某个特定进程设置的所有环境变量,包括局部变量、全局变量以及用户定义变量:
在这里插入图片描述

6.2 设置用户定义变量

可以在bash shell中直接设置自己的变量。

6.2.1 设置局部用户定义变量

一旦启动了bash shell(或者执行一个shell脚本),就能创建在这个shell进程内可见的局部变量了。可以通过等号给环境变量赋值,值可以是数值或字符串:
在这里插入图片描述
所有的环境变量名均使用大写字母,这是bash shell的标准惯例。如果是你自己创建的局部变量或是shell脚本,请使用小写字母。变量名区分大小写。在涉及用户定义的局部变量时坚持使用小写字母,这能够避免重新定义系统环境变量可能带来的灾难。另外记住,变量名、等号和值之间没有空格,这一点非常重要。如果在赋值表达式中加上了空格,bash shell就会把值当成一个单独的命令。

6.2.2 设置全局环境变量

创建全局环境变量的方法是先创建一个局部环境变量,然后再把它导出到全局环境中。这个过程通过export命令来完成,变量名前面不需要加$:
在这里插入图片描述
另外需要注意的点是,修改子shell中全局环境变量并不会影响到父shell中该变量的值:
在这里插入图片描述

6.3 删除环境变量

可以用unset命令来删除环境变量。在unset命令中引用环境变量时,记住不要使用$。在涉及环境变量名时,什么时候该使用$,什么时候不该使用$,实在让人摸不着头脑。记住一点就行了:如果要用到变量,使用$;如果要操作变量,不使用$。这条规则的一个例外就是使用printenv显示某个变量的值。
在这里插入图片描述
另外,如果你是在子进程中删除了一个全局环境变量,这只对子进程有效。该全局环境变量在父进程中依然可用。

6.4 默认的 shell 环境变量

默认情况下,bash shell会用一些特定的环境变量来定义系统环境。这些变量在你的Linux系统上都已经设置好了,只管放心使用。比如最常用的HOME,其余变量不再此一一列举。

6.5 设置 PATH 环境变量

当你在shell命令行界面中输入一个外部命令时,shell必须搜索系统来找到对应的程序。PATH环境变量定义了用于进行命令和程序查找的目录。
在这里插入图片描述
PATH中的目录使用冒号分隔。
可以把新的搜索目录添加到现有的PATH环境变量中,无需从头定义。PATH中各个目录之间是用冒号分隔的。你只需引用原来的PATH值,然后再给这个字符串添加新目录就行了:

PATH=$PATH:/home/christine/Scripts

当前更常见的方法是将单点符也加入PATH环境变量,该单点符代表当前目录:

PATH=$PATH:.

对PATH变量的修改只能持续到退出或重启系统。这种效果并不能一直持续。在下一节中,你会学到如何永久保持环境变量的修改效果。

6.6 定位系统环境变量

在你登入Linux系统启动一个bash shell时,默认情况下bash会在几个文件中查找命令。这些文件叫作启动文件或环境文件。bash检查的启动文件取决于你启动bash shell的方式。启动bash shell有3种方式:

  • 登录时作为默认登录shell
  • 作为非登录shell的交互式shell
  • 作为运行脚本的非交互shell

6.6.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个启动文件是针对用户的,可根据个人需求定制。

1. /etc/profile文件

/etc/profile文件是bash shell默认的的主启动文件。只要你登录了Linux系统,bash就会执行/etc/profile启动文件中的命令。不同的Linux发行版在这个文件里放了不同的命令。你可以直接打开脚本来查看它的登陆逻辑。

2. $HOME目录下的启动文件

大多数Linux发行版只用这四个启动文件中的一到两个,shell会按照按照下列顺序,运行第一个被找到的文件,余下的则被忽略:

  • $HOME/.bash_profile
  • $HOME/.bash_login
  • $HOME/.profile

注意,这个列表中并没有$HOME/.bashrc文件。这是因为该文件通常通过其他文件运行的。CentOS Linux系统中,.bash_profile启动文件会先去检查HOME目录中是不是还有一个叫.bashrc的启动文件。如果有的话,会先执行启动文件里面的命令。mac电脑则没有这个文件,所有需要配置在此文件的配置可以直接配置.bash_profile中。

6.6.2 交互式 shell 进程

如果你的bash shell不是登录系统时启动的(比如是在命令行提示符下敲入bash时启动),那么你启动的shell叫作交互式shell。如果bash是作为交互式shell启动的,它就不会访问/etc/profile文件,只会检查用户HOME目录中的.bashrc文件。bashrc文件有两个作用:一是查看/etc目录下通用的bashrc文件,二是为用户提供一个定制自己的命令别名(参见第5章)和私有脚本函数(将在第17章中讲到)的地方。

6.6.3 非交互式 shell

最后一种shell是非交互式shell。系统执行shell脚本时用的就是这种shell。
当你在系统上运行脚本时,也许希望能够运行一些特定启动的命令。为了处理这种情况,bash shell提供了BASH_ENV环境变量。当shell启动一个非交互式shell进程时,它会检查这个环境变量来查看要执行的启动文件。如果有指定的文件,shell会执行该文件里的命令,这通常包括shell脚本变量设置,这个环境变量在默认情况下并未设置。那如果BASH_ENV变量没有设置,shell脚本到哪里去获得它们的环境变量呢?别忘了有些shell脚本是通过启动一个子shell来执行的(参见第5章)。子shell可以继承父shell导出过的变量。

6.6.4 环境变量持久化

对全局环境变量来说,可能更倾向于将新的或修改过的变量设置放在/etc/profile文件中,但这可不是什么好主意。如果你升级了所用的发行版,这个文件也会跟着更新,那你所有定制过的变量设置可就都没有了。最好是在/etc/profile.d目录中创建一个以.sh结尾的文件。把所有新的或修改过的全局环境变量设置放在这个文件中。
在大多数发行版中,存储个人用户永久性bash shell变量的地方是$HOME/.bashrc文件。
想想第5章中讲过的alias命令设置就是不能持久的。你可以把自己的alias设置放在$HOME/.bashrc启动文件中,使其效果永久化。

6.7 数组变量

环境变量有一个很酷的特性就是,它们可作为数组使用。要给某个环境变量设置多个值,可以把值放在括号里,值与值之间用空格分隔。直接引用这个环境变量,则会直接引用数组中的第一个值,要引用一个其他单独的数组元素,就必须用代表它在数组中位置的数值索引值,另外环境变量数组的索引值都是从零开始。要显示整个数组变量,可用星号作为通配符放在索引值的位置:
在这里插入图片描述
可以在unset命令后跟上数组名来删除整个数组:
在这里插入图片描述
有时数组变量会让事情很麻烦,所以在shell脚本编程时并不常用。

第7章 理解Linux文件权限

7.1 Linux 的安全性

缺乏安全性的系统不是完整的系统。Linux安全系统的核心是用户账户。每个能进入Linux系统的用户都会被分配唯一的用户账户。用户对系统中各种对象的访问权限取决于他们登录系统时用的账户。

7.1.1 /etc/passwd 文件

Linux系统使用一个专门的文件来将用户的登录名匹配到对应的UID值。这个文件就是/etc/passwd文件,它包含了一些与用户有关的信息:在这里插入图片描述
root用户账户是Linux系统的管理员,固定分配给它的UID是0。就像上例中显示的,Linux系统会为各种各样的功能创建不同的用户账户,而这些账户并不是真的用户。这些账户叫作系统账户,是系统上运行的各种服务进程访问资源用的特殊账户。
/etc/passwd文件的字段包含了如下信息:

  • 登录用户名
  • 用户密码
  • 用户账户的UID(数字形式)
  • 用户账户的组ID(GID)(数字形式)
  • 用户账户的文本描述(称为备注字段)
  • 用户HOME目录的位置
  • 用户的默认shell

/etc/passwd文件中的密码字段都被设置成了x,是出于安全的考虑,现在,绝大多数Linux系统都将用户密码保存在另一个单独的文件中(叫作shadow文件,位置在/etc/shadow)。只有特定的程序(比如登录程序)才能访问这个文件。
/etc/passwd是一个标准的文本文件。你可以用任何文本编辑器在/etc/password文件里直接手动进行用户管理(比如添加、修改或删除用户账户)。但这样做极其危险。如果/etc/passwd文件出现损坏,系统就无法读取它的内容了,这样会导致用户无法正常登录(即便是root用户)。用标准的Linux用户管理工具去执行这些用户管理功能就会安全许多。

7.1.2 /etc/shadow 文件

/etc/shadow文件对Linux系统密码管理提供了更多的控制。只有root用户才能访问/etc/shadow文件,这让它比起/etc/passwd安全许多。

7.1.3 添加新用户

useradd命令用于添加新用户。useradd命令使用系统的默认值以及命令行参数来设置用户账户。系统默认值被设置在/etc/default/useradd文件中。可以使用加入了-D选项的useradd命令查看所用Linux系统中的这些默认值:
在这里插入图片描述

7.1.4 删除用户

userdel用于删除用户。默认情况下,userdel命令会只删除/etc/passwd文件中的用户信息,而不会删除系统中属于该账户的任何文件。如果加上-r参数,userdel会删除用户的HOME目录以及邮件目录。然而,系统上仍可能存有已删除用户的其他文件。在有大量用户的环境中使用-r参数时要特别小心。你永远不知道用户是否在其HOME目录下存放了其他用户或其他程序要使用的重要文件。记住,在删除用户的HOME目录之前一定要检查清楚!

7.1.5 修改用户

Linux提供了一些不同的工具来修改已有用户账户的信息, 如下表所示:
在这里插入图片描述
用法在此不一一列举。
注:mac上不支持useradd userdel usermod这三个命令,其使用命令dscl做用户管理。

7.2 使用 Linux 组

组权限允许多个用户对系统中的对象(比如文件、目录或设备等)共享一组共用的权限。每个组都有唯一的GID——跟UID类似,在系统上这是个唯一的数值。

7.2.1 /etc/group 文件

与用户账户类似,组信息也保存在系统的一个文件中。/etc/group文件包含系统上用到的每个组的信息。

7.2.2 创建新组

groupadd命令可在系统上创建新组。在创建新组时,默认没有用户被分配到该组,但可以用usermod命令来弥补这一点。usermod命令的-G选项会把这个新组添加到该用户账户的组列表里:

usermod -G groupname username

7.2.3 修改组

groupmod命令可以修改已有组的GID(加-g选项)或组名(加-n选项)。修改组名时,GID和组成员不会变,只有组名改变。

7.3 理解文件权限

如果你还记得第3章,那应该知道ls命令可以用来查看Linux系统上的文件、目录和设备的权限:
在这里插入图片描述
输出结果的第一个字段就是描述文件和目录权限的编码。这个字段的第一个字符代表了对象的类型:

  • 代表文件
  • d代表目录
  • l代表链接
  • c代表字符型设备
  • b代表块设备
  • n代表网络设备

之后有3组三字符的编码。每一组定义了3种访问权限:

  • r代表对象是可读的
  • w代表对象是可写的
  • x代表对象是可执行的

若没有某种权限,在该权限位会出现单破折线。这3组权限分别对应对象的3个安全级别:

  • 对象的属主
  • 对象的属组
  • 系统其他用户

在这里插入图片描述

7.3.2 默认文件权限

touch命令用分配给我的用户账户的默认权限创建了这个文件。umask命令用来设置所创建文件和目录的默认权限:
在这里插入图片描述
要理解umask是怎么工作的,得先理解八进制模式的安全性设置。
八进制模式的安全性设置先获取这3个rwx权限的值,然后将其转换成3位二进制值,用一个八进制值来表示。在这个二进制表示中,每个位置代表一个二进制位。因此,如果读权限是唯一置位的权限,权限值就是r–,转换成二进制值就是100,代表的八进制值是4。下表列出了可能会遇到的组合:
在这里插入图片描述
八进制模式先取得权限的八进制值,然后再把这三组安全级别(属主、属组和其他用户)的八进制值顺序列出。因此,八进制模式的值664代表属主和属组成员都有读取和写入的权限,而其他用户都只有读取权限。对文件来说,全权限的值是666(所有用户都有读和写的权限);而对目录来说,则是777(所有用户都有读、写、执行权限)。
了解八进制模式权限是怎么工作的之后,umask值反而更叫人困惑了。我的Linux系统上默认的八进制的umask值是0022,而我所创建的文件的八进制权限却是644,这是如何得来的呢?
在这里插入图片描述
这是因为umask是掩码,它会屏蔽掉不想授予该安全级别的权限。要把umask值从对象的全权限值中减掉,才是默认的权限值。文件一开始的权限是666,减去umask值022之后,剩下的文件权限就成了644。
可以用umask命令为默认umask设置指定一个新值:
在这里插入图片描述

7.4 改变安全性设置

7.4.1 改变权限

chmod命令用来改变文件和目录的安全性设置。该命令的格式如下:

chmod options mode file

mode参数可以使用八进制模式或符号模式进行安全性设置。options为chmod命令提供了另外一些功能,-R选项可以让权限的改变递归地作用到文件和子目录。你可以使用通配符指定多个文件,然后利用一条命令将权限更改应用到这些文件上。比如命令chmod -R 777 dir可以把一个文件夹和所有子目录和文件都设置为所有权限。

7.4.2 改变所属关系

有时你需要改变文件的属主,Linux提供了两个命令来实现这个功能:chown命令用来改变文件的属主,chgrp命令用来改变文件的默认属组。
chown命令的格式如下:

chown options owner[.group] file

可用登录名或UID来指定文件的新属主,chown命令也支持同时改变文件的属主和属组。

# 修改文件属主为dan,属组为shared
chown dan.shared newfile

chown命令采用一些不同的选项参数。-R选项配合通配符可以递归地改变子目录和文件的所属关系。-h选项可以改变该文件的所有符号链接文件的所属关系。
chgrp命令可以更改文件或目录的默认属组:

chgrp shared newfile

7.5 共享文件

Linux还为每个文件和目录存储了3个额外的信息位:

  • 设置用户ID(SUID):当文件被用户使用时,程序会以文件属主的权限运行
  • 设置组ID(SGID):对文件来说,程序会以文件属组的权限运行;对目录来说,目录中创建的新文件会以目录的默认属组作为默认属组。
  • 粘着位:进程结束后文件还驻留(粘着)在内存中。

SGID位对文件共享非常重要。启用SGID位后,你可以强制在一个共享目录下创建的新文件都属于该目录的属组,这个组也就成为了每个用户的属组。它会加到标准3位八进制值之前(组成4位八进制值),或者在符号模式下用符号s。
在这里插入图片描述
因此,要创建一个共享目录,使目录里的新文件都能沿用目录的属组,只需将该目录的SGID位置位。

第8章 管理文件系统

第9章 安装软件程序

常见的安装软件有apt、yum、brew等,也可以采用源码安装,相应的软件可以查看软件的文档,在此不再列举,略过。

第10章 使用编辑器

主流编辑器有vim、emacs、nano、vscode等,本人较多使用vscode,有时会用到vim,具体使用方法可参考相关文档,此处略过。

第11章 构建基本脚本

11.1 使用多个命令

shell可以让你将多个命令串起来,一次执行完成。如果要两个命令一起运行,可以把它们放在同一行中,彼此间用分号隔开,比如
在这里插入图片描述
date命令先运行,显示了当前日期和时间,后面紧跟着who命令的输出,显示当前是谁登录到了系统上。可以将这些命令组合成一个简单的文本文件,这样就不需要在命令行中手动输入了。在需要运行这些命令时,只用运行这个文本文件就行了。

11.2 创建 shell 脚本文件

在创建shell脚本文件时,必须在文件的第一行指定要使用的shell。其格式为:

#!/bin/bash

在通常的shell脚本中,井号(#)用作注释行。shell并不会处理shell脚本中的注释行。然而,shell脚本文件的第一行是个例外,#后面的惊叹号会告诉shell用哪个shell来运行脚本(是的,你可以使用bash shell,同时还可以使用另一个shell来运行你的脚本)。编写完成后的脚本如下所示:
在这里插入图片描述
接下来我们要让shell找到test脚本,我们可以在提示符中用绝对或相对文件路径来引用shell脚本文件:
在这里插入图片描述
现在还剩一个问题:shell指明了你还没有执行文件的权限(x),我们可以使用命令chmod提升文件权限后执行:
在这里插入图片描述

11.3 显示消息

大多数shell命令都会产生自己的输出,这些输出会显示在脚本所运行的控制台显示器上。可以通过echo命令
来实现这一点。-n参数可以使输出不换行而紧接下一行,代码如下:

#!/bin/bash
echo -n "The time and date are: "
date
who

执行结果:
在这里插入图片描述

11.4 使用变量

11.4.1 环境变量

在脚本中,你可以在环境变量名称之前加上美元符$来使用环境变量,看下面的代码:

#!/bin/bash
echo "User info for userid: $USER" 
echo UID: $UID 
echo HOME: $HOME

U S E R 、 USER、 USERUID和$HOME环境变量用来显示已登录用户的有关信息。执行结果如下:
在这里插入图片描述

注意,将系统变量放置到双引号中,而shell依然能够知道我们的意图。但采用这种方法也有一个问题,即我们将在字符串中使用$符号, 如下面的例子,脚本会尝试显示变量$1(但并未定义),再显示数字5。
在这里插入图片描述
要显示美元符,你必须在它前面放置一个反斜线:
在这里插入图片描述

11.4.2 用户变量

除了环境变量,shell脚本还允许在脚本中定义和使用自己的变量。用户变量可以是任何由字母、数字或下划线组成的文本字符串,长度不超过20个,并且区分大小写。使用等号将值赋给用户变量,在变量、等号和值之间不能出现空格
shell脚本会自动决定变量值的数据类型。在脚本的整个生命周期里,shell脚本中定义的变量会一直保持着它们的值,但在shell脚本结束时会被删除掉。与系统变量类似,用户变量可通过美元符$引用。重要的是要记住,引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不要使用美元符。看下面的例子:

#!/bin/bash
v1=10
v2=v1
v3=$v1
echo "v2 is: $v2"
echo "v3 is: $v3"

输出:
在这里插入图片描述

11.4.3 命令替换

shell脚本中最有用的特性之一就是可以从命令输出中提取信息,并将其赋给变量。有两种方法可以将命令输出赋给变量:

  • 反引号字符(`)
  • $()格式

看下面的例子:

#!/bin/bash
t1=$(date)
t2=`date`
echo "t1: $t1"
echo "t2: $t2"

输出:
在这里插入图片描述
shell会运行命令替换符号中的命令,并将其输出赋给变量t1,t2。
命令替换会创建一个子shell来运行对应的命令。子shell(subshell)是由运行该脚本的shell所创建出来的一个独立的子shell(child shell)。正因如此,由该子shell所执行命令是无法使用脚本中所创建的变量的。

11.5 重定向输入和输出

11.5.1 输出重定向

最基本的重定向将命令的输出发送到一个文件中。bash shell用大于号>来完成这项功能:

command > outputfile

在这里插入图片描述
重定向操作符创建了一个文件test_log,并将date命令的输出重定向到该文件中。如果输出文件已经存在了,重定向操作符会用新的文件数据覆盖已有文件。有时,你可能并不想覆盖文件原有内容,而是想要将命令的输出追加到已有文件中,可以用双大于号>>来追加数据。
在这里插入图片描述

11.5.2 输入重定向

输入重定向和输出重定向正好相反,输入重定向将文件的内容重定向到命令:

command < inputfile

这里有个和wc命令一起使用输入重定向的例子,wc命令可以对对数据中的文本进行计数,默认情况下,它会输出3个值:文本的行数, 文本的词数,文本的字节数。
在这里插入图片描述
还有另外一种输入重定向的方法,称为内联输入重定向(inline input redirection)。这种方法无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据就可以了。内联输入重定向符号是远小于号<<。除了这个符号,你必须指定一个文本标记来划分输入数据的开始和结尾。任何字符串都可作为文本标记,但在数据的开始和结尾文本标记必须一致:

command << marker 
data 
marker

在这里插入图片描述

11.6 管道

有时需要将一个命令的输出作为另一个命令的输入,这时我们可以使用管道连接(piping)。
管道符号为单个竖线|,管道被放在命令之间,将一个命令的输出重定向到另一个命令中:

command1 | command2

可以在一条命令中使用任意多条管道。可以持续地将命令的输出通过管道传给其他命令来细化操作。
到目前为止,管道最流行的用法之一是将命令产生的大量输出通过管道传送给more命令。这对ls命令来说尤为常见。ls -l命令产生了目录中所有文件的长列表。对包含大量文件的目录来说,这个列表会相当长。通过将输出管道连接到more命令,可以强制输出在一屏数据显示后停下来:

ls -l | more

11.7 执行数学运算

另一个对任何编程语言都很重要的特性是操作数字的能力。

11.7.1 expr 命令

Bourne shell提供了一个特别的命令expr用来处理数学表达式,expr命令能够识别少数的数学和字符串操作符:
在这里插入图片描述
尽管标准操作符在expr命令中工作得很好,但在脚本或命令行上使用它们时仍有问题出现。许多expr命令操作符在shell中另有含义(比如星号)。当它们出现在在expr命令中时,会得到一些诡异的结果:
在这里插入图片描述
这时我们需要用shell的转义字符(反斜线)将其标出来才能正确使用:
在这里插入图片描述
这样使用起来十分繁琐。

11.7.2 使用方括号

bash shell提供了一种更简单的方法来执行数学表达式。在bash中,在将一个数学运算结果赋给某个变量时,可以用美元符和方括号$[ operation ]将数学表达式围起来。看下面的代码:

#!/bin/bash
var1=$[1 + 5] 
echo $var1 
var2=$[$var1 * 2] 
echo $var2

输出
在这里插入图片描述
用方括号执行shell数学运算比用expr命令方便很多,并且不用担心shell会误解乘号或其他符号。
bash shell数学运算符只支持整数运算。若要进行任何实际的数学计算,这是一个巨大的限制。z shell(zsh)提供了完整的浮点数算术操作。如果需要在shell脚本中进行浮点数运算,可以考虑看看z shell。

11.7.3 浮点解决方案

有几种解决方案能够克服bash中数学运算的整数限制。最常见的方案是用内建的bash计算器,叫作bc

1. bc的基本用法

bash计算器实际上是一种编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该表达式,最后返回结果。可以在shell提示符下通过bc命令访问bash计算器,-q命令行选项可以不显示bash计算器冗长的欢迎信息。要退出bash计算器,你必须输入quit。除了普通数字,bash计算器还能支持变量:
在这里插入图片描述

2. 在脚本中使用bc

现在你可能想问bash计算器是如何在shell脚本中帮助处理浮点运算的。还记得命令替换吗?是的,可以用命令替换运行bc命令,并将输出赋给一个变量。基本格式如下:

variable=$(echo "options; expression" | bc)

这个方法适用于较短的运算,但有时你会涉及更多的数字。如果需要进行大量运算,在一个命令行中列出多个表达式就会有点麻烦。最好的办法是使用内联输入重定向,它允许你直接在命令行中重定向数据。在shell脚本中,
你可以将输出赋给一个变量:

variable=$(bc << EOF 
options 
statements 
expressions 
EOF 
)

下面是在脚本中使用这种技术的例子:

#!/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 $var5

输出:
在这里插入图片描述
其中scale=4将运算结果设置为4位小数,为bc自身的语法。

11.8 退出脚本

shell中运行的每个命令都使用退出状态码(exit status)告诉shell它已经运行完毕。退出状态码是一个0~255的整数值,在命令结束运行时由命令传给shell。可以捕获这个值并在脚本中使用。

11.8.1 查看退出状态码

Linux提供了一个专门的变量$?来保存上个已执行命令的退出状态码。对于需要进行检查的命令,必须在其运行完毕后立刻查看或使用$?变量。它的值会变成由shell所执行的最后一条命令的退出状态码:
在这里插入图片描述
退出码含义如下表所示:
在这里插入图片描述

11.8.2 exit 命令

exit命令允许你在脚本结束时指定一个退出状态码:exit mask。比如下面的例子,代码如下:

#!/bin/bash
var1=10 
var2=30 
var3=$[$var1 + $var2] 
echo The answer is $var3 
exit 5

输出:
在这里插入图片描述
你要注意这个功能,因为退出状态码最大只能是255。如果退出状态码超过255,则会被缩减到了0~255的区间。shell通过模运算得到这个结果。
如下面的代码:

#!/bin/bash
exit 300

输出:
在这里插入图片描述

第12章 使用结构化命令

12.1 使用 if-then 语句

最基本的结构化命令就是if-then语句,if-then语句有如下格式:

if command
then 
commands
fi

bash shell的if语句会运行if后面的那个命令。如果该命令的退出状态码(参见第11章)是0(该命令成功运行),位于then部分的命令就会被执行。如果该命令的退出状态码是其他值,then部分的命令就不会被执行,bash shell会继续执行脚本中的下一个命令。fi语句用来表示if-then语句到此结束。下面是个简单的例子:

#!/bin/bash
if pwd
then
    echo "pwd work"
fi
if notcmd
then
    echo "notcmd work"
fi

输出:
在这里插入图片描述
你可能在有些脚本中看到过if-then语句的另一种形式:

if command; then 
commands
fi 

通过把分号放在待求值的命令尾部,就可以将then语句放在同一行上了,这样看起来更像其他编程语言中的if-then语句。

12.2 if-then-else 语句

if-then-else语句在语句中提供了另外一组命令,其格式如下:

if command
then 
	commands
else 
	commands
fi

12.3 嵌套 if

格式如下:

if command1
then 
	commands
elif command2
then 
	more commands
fi

elif相当于else if的缩写。

12.4 test 命令

test命令提供了在if-then语句中测试不同条件的途径。如果test命令中列出的条件成立,test命令就会退出并返回退出状态码0。这样if-then语句就与其他编程语言中的if-then语句以类似的方式工作了。如果条件不成立,test命令就会退出并返回非零的退出状态码,这使得if-then语句不会再被执行。test命令的格式非常简单:

test condition

condition是test命令要测试的一系列参数和值。当用在if-then语句中时,test命令看起来是这样的:

if test condition
then 
	commands
fi

如果不写test命令的condition部分,它会以非零的退出状态码退出,看下面的例子:

#!/bin/bash
if test; then
    echo "return true"
else
    echo "return false"
fi

输出:
在这里插入图片描述
bash shell提供了另一种条件测试方法,无需在if-then语句中声明test命令,即使用方括号,其形式如下:

if [ condition ] 
then 
	commands
fi

注意,第一个方括号之后和第二个方括号之前必须加上一个空格,否则就会报错。
test命令可以判断三类条件:

  • 数值比较
  • 字符串比较
  • 文件比较

12.4.1 数值比较

使用test命令最常见的情形是对两个数值进行比较。表12-1列出了测试两个值时可用的条件参数:
在这里插入图片描述下面是一个例子:

#!/bin/bash
value1=10
value2=11
if [ $value1 -gt 5 ]; then
    echo "value1 is greater than 5"
fi
if [ $value1 -eq $value2 ]; then
    echo "value1 and value2 is equal"
else
    echo "value1 and value2 are diffrent"
fi

输出:
在这里插入图片描述
需要注意的是,bash shell只能处理整数,如果处理浮点数则会报错:

#!/bin/bash
value1=5.555
if [ $value1 -gt 5 ]; then
    echo "value1 is greater than 5"
fi

在这里插入图片描述

12.4.2 字符串比较

在这里插入图片描述
其中需要注意的是大小比较。
要测试一个字符串是否比另一个字符串大就是麻烦的开始。当要开始使用测试条件的大于或小于功能时,就会出现两个经常困扰shell程序员的问题:

  • 大于号和小于号必须转义,否则shell会把它们当作重定向符号,把字符串值当作文件名;
  • 大于和小于顺序和sort命令所采用的不同。

脚本会把大于小于号解释成了重定向符号,因此,在使用时必须加上转义符号,如下

#!/bin/bash
val1="baseball"
val2="hockey"
if [ $val1 > $val2 ]; then
    echo "$val1 is greater than $val2";
else 
    echo "$val1 is less than $val2" 
fi
if [ $val1 \> $val2 ]; then
    echo "$val1 is greater than $val2";
else 
 echo "$val1 is less than $val2" 
fi

输出:
在这里插入图片描述
我们可以看到两种比较的结果不一样,是因为第一次比较脚本把大于号解释成了输出重定向。
第二个问题更细微,除非你经常处理大小写字母,否则几乎遇不到。sort命令处理大写字母的方法刚好跟test命令相反。让我们在脚本中测试一下这个特性:

#!/bin/bash
val1="Test"
val2="test"
if [ $val1 \> $val2 ]; then
    echo "$val1 is greater than $val2";
else 
    echo "$val1 is less than $val2" 
fi
rm -f test_file
touch test_file
echo $val1 >> test_file
echo $val2 >> test_file
sort test_file

输出:
在这里插入图片描述
比较测试中使用的是标准的ASCII顺序,根据每个字符的ASCII数值来决定排序结果。sort命令使用的是系统的本地化语言设置中定义的排序顺序(此顺序和系统有关,比如mac上就是大写小于小写)。对于英语,本地化设置指定了在排序顺序中小写字母出现在大写字母前。
另外需要注意的是,如果你对数值使用了数学运算符号,shell会将它们当成字符串值,可能无法得到正确的结果。

12.4.3 文件比较

最后一类比较测试很有可能是shell编程中最为强大、也是用得最多的比较形式。它允许你测试Linux文件系统上文件和目录的状态:
在这里插入图片描述
下面是一个例子,用于把当前目录下的文件夹名全部打出:

#!/bin/bash
for dir in $(ls); do
    if [ -d $dir ]; then
        echo $dir
    fi
done

在这里插入图片描述

12.5 复合条件测试

if-then语句允许你使用布尔逻辑来组合测试。有两种布尔运算符可用:

  • [ condition1 ] && [ condition2 ]
  • [ condition1 ] || [ condition2 ]

12.6 if-then 的高级特性

bash shell提供了两项可在if-then语句中使用的高级特性:

  • 用于数学表达式的双括号
  • 用于高级字符串处理功能的双方括号

12.6.1 使用双括号

双括号命令允许你在比较过程中使用高级数学表达式。双括号命令的格式如下:

(( expression ))

expression可以是任意的数学赋值或比较表达式。除了test命令使用的标准数学运算符,表12-4列出了双括号命令中会用到的其他运算符:
在这里插入图片描述
注意,不需要将双括号中表达式里的大于号转义,这是双括号命令提供的另一个高级特性。下面是示例:

#!/bin/bash
v1=10
if (( $v1 ** 2 > 90 )); then
    (( v2 = $v1 ** 2 ))
    echo $v2
fi

在这里插入图片描述

12.6.2 使用双方括号

双方括号命令提供了针对字符串比较的高级特性。双方括号命令的格式如下:

[[ expression ]]

双方括号里的expression使用了test命令中采用的标准字符串比较。但它提供了test命令未提供的另一个特性——模式匹配(pattern matching)。在模式匹配中,可以定义一个正则表达式来匹配字符串值:

#!/bin/bash
if [[ $USER == z* ]]; then
    echo $USER
fi

在这里插入图片描述
在上面的脚本中,我们使用了双等号==。双等号将右边的字符串z*视为一个模式,并应用模式匹配规则。注意,右边的模式不要加双引号。

12.7 case 命令

case命令会采用列表格式来检查单个变量的多个值,形式如下:

case variable in 
pattern1 | pattern2) commands1;; 
pattern3) commands2;; 
*) default commands;; 
esac

case命令会将指定的变量与不同模式进行比较。如果变量和模式是匹配的,那么shell会执行为该模式指定的命令。可以通过竖线操作符在一行中分隔出多个模式模式。星号会捕获所有与已知模式不匹配的值。看下面的示例:

#!/bin/bash
v1=$1
case $v1 in 
z* | r*)
    echo "hellp $v1";;
test)
    echo "special test";;
*)
    echo "error";;
esac

在这里插入图片描述
注意不要忘记每个匹配项后面的两个分号;;

第13章 更多的结构化命令

13.1 for 命令

bash shell提供了for命令,允许你创建一个遍历一系列值的循环。每次迭代都使用其中一个值来执行已定义好的一组命令。下面是bash shell中for命令的基本格式:

for var in list 
do 
	commands 
done

可以通过几种不同的方法指定列表list中的值,下面一一作介绍。

13.1.1 读取列表中的值

for命令最基本的用法就是遍历for命令自身所定义的一系列值,如下例:

#!/bin/bash
for num in 0 1 2 3 4 5 6 7 8 9; do
    echo "next num is $num"
done

在这里插入图片描述
for循环假定列表中每个值都是用空格分割的。

13.1.3 从变量读取列表

你可以将一系列值都集中存储在了一个变量中,然后通过for循环遍历变量中的整个列表:

#!/bin/bash
nums="0 1 2 3 4 5 6 7 8 9 10"
for num in $nums; do
    echo "next num is $num"
done

13.1.4 从命令读取值

生成列表中所需值的另外一个途径就是使用命令的输出。可以用命令替换来执行任何能产生输出的命令,然后在for命令中使用该命令的输出:

#!/bin/bash
nums_file="nums_file"
for num in  $(cat $nums_file); do
    echo "next num is $num"
done

这个例子在命令替换中使用了cat命令来输出文件的内容。

13.1.5 更改字段分隔符

默认情况下,bash shell会将下列字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

如果bash shell在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。在处理可能含有空格的数据(比如文件名)时,这会非常麻烦。要解决这个问题,可以在shell脚本中临时更改IFS环境变量的值来限制被bash shell当作字段分隔符的字符。例如,如果你想修改IFS的值,使其只能识别换行符,那就必须这么做:

IFS=$'\n'

将这个语句加入到脚本中,告诉bash shell在数据值中忽略空格和制表符,如下例:

#!/bin/bash
IFS=$'\n'
for name in $(cat names_file); do
    echo "next name is $name"
done

names_file中存储名字如下:

zhang san
li si

输出:
在这里插入图片描述

在处理代码量较大的脚本时,可能在一个地方需要修改IFS的值,然后忽略这次修改,在脚本的其他地方继续沿用IFS的默认值,你可以如下操作:

#!/bin/bash
IFS.OLD = $IFS
IFS=$'\n'
...
...
IFS=IFS.OLD

还有其他一些IFS环境变量的绝妙用法。假定你要遍历一个文件中用冒号分隔的值(比如在/etc/passwd文件中)。你要做的就是将IFS的值设为冒号:IFS=:,如果要指定多个IFS字符,只要将它们在赋值行串起来就行:IFS=$'\n':;",这个赋值会将换行符、冒号、分号和双引号作为字段分隔符。

13.1.6 用通配符读取目录

最后,可以用for命令来自动遍历目录中的文件。进行此操作时,必须在文件名或路径名中使用通配符。它会强制shell使用文件扩展匹配。文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程:

#!/bin/bash
for file in ./github/test/*; do
    if [ -d $file ]; then
        echo "$file is a directory!"
    elif [ -f $file ]; then
        echo "$file is a file!"
    fi
done

在这里插入图片描述

13.2 C 语言风格的 for 命令

13.2.1 C 语言的 for 命令

bash shell也支持一种C 语言风格的for循环,形式如下:

for (( variable assignment ; condition ; iteration process ))

注意,有些部分并没有遵循bash shell标准的for命令:

  • 变量赋值可以有空格
  • 条件中的变量不以美元符开头
  • 迭代过程的算式未用expr命令格式

所以在脚本中使用C语言风格的for循环时要小心。下面看示例:

#!/bin/bash
for (( i = 1; i < 10; ++i )); do
    echo "next num is $i"
done

在这里插入图片描述

13.2.2 使用多个变量

C语言风格的for命令也允许为迭代使用多个变量:

#!/bin/bash
for (( i = 1, j = 10; i < 10; ++i, --j )); do
    echo "next i is $i, next j is $j"
done

在这里插入图片描述

13.3 while 命令

while命令某种意义上是if-then语句和for循环的混杂体。

13.3.1 while 的基本格式

while命令的格式是:

while test command 
do 
	other commands 
done

13.3.2 使用多个测试命令

while命令允许你在while语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。在含有多个命令的while语句中,在每次迭代中所有的测试命令都会被执行,包括测试命令失败的最后一次迭代。要留心这种用法。另一处要留意的是该如何指定多个测试命令。注意,每个测试命令都出现在单独的一行上:

#!/bin/bash
var1=5
while echo $var1
    [ $var1 -ge 0 ]
do
    echo "this is inside the loop"
    var1=$[ $var1 - 1 ]
done

在这里插入图片描述

13.4 until 命令

until命令和while命令工作的方式完全相反。until命令要求你指定一个通常返回非零退出状态码的测试命令。只有测试命令的退出状态码不为0,bash shell才会执行循环中列出的命令。一旦测试命令返回了退出状态码0,循环就结束了。其形式如下:

until test commands 
do
	other commands 
done

在until命令中也可以使用多个测试命令。

13.5 嵌套循环

循环语句可以在循环内使用任意类型的命令,包括其他循环命令。这种循环叫作嵌套循环(nested loop)。

13.6 循环处理文件数据

通常必须遍历存储在文件中的数据。这要求结合已经讲过的两种技术:

  • 使用嵌套循环
  • 修改IFS环境变量

典型的例子是处理/etc/passwd文件中的数据。这要求你逐行遍历/etc/passwd文件,并将IFS变量的值改成冒号,这样就能分隔开每行中的各个数据段了,如下:

#!/bin/bash
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

在这里插入图片描述

13.7 控制循环

13.7.1 break 命令

可以用break命令来退出任意类型的循环,包括while和until循环。在处理多个循环时,break命令会自动终止你所在的最内层的循环。有时你在内部循环,但需要停止外部循环。break命令接受单个命令行参数值:break n其中n指定了要跳出的循环层级。默认情况下,n为1,表明跳出的是当前的循环。如果你将n设为2,break命令就会停止下一级的外部循环。

13.7.2 continue 命令

continue命令可以提前中止某次循环中的命令,但并不会完全终止整个循环。记住,当shell执行continue命令时,它会跳过剩余的命令。和break命令一样,continue命令也允许通过命令行参数指定要继续执行哪一级循环:continue n

13.8 处理循环的输出

最后,在shell脚本中,你可以对循环的输出使用管道或进行重定向。这可以通过在done命令之后添加一个处理命令来实现:

#!/bin/bash
for num in 0 1 2 3 4 5; do
    echo "next num is $num"
done > log.txt

在这里插入图片描述
这种方法同样适用于将循环的结果管接给另一个命令:

#!/bin/bash
for num in 5 4 3 2 1; do
    echo $num
done | sort

在这里插入图片描述

第14章 处理用户输入

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

14.1 命令行参数

向shell脚本传递数据的最基本方法是使用命令行参数。命令行参数允许在运行脚本时向命令行添加数据。如下:

//向脚本传递了两个命令行参数10和30
./test.sh 10 30

14.1.1 读取参数

bash shell会将一些称为位置参数(positional parameter)的特殊变量分配给输入到命令行中的所有参数。这也包括shell所执行的脚本名称。位置参数变量是标准的数字:$0是程序名,$1是第一个参数, 依次类推。
记住,每个参数都是用空格分隔的,所以shell会将空格当成两个值的分隔符。要在参数值中包含空格,必须要用引号(单引号或双引号均可)。
第9个变量之后,你必须在变量数字周围加上花括号,比如${10}

14.1.2 读取脚本名

可以用$0参数获取shell在命令行启动的脚本名。但是这里存在一个潜在的问题。如果使用另一个命令来运行shell脚本,命令会和脚本名混在一起,出现在$0参数中,比如:

./test.sh

$0是“./test.sh"而不是“test.sh"。幸好有个方便的小命令可以帮到我们。basename命令会返回不包含路径的脚本名:

#!/bin/bash
echo $(basename $0)

在这里插入图片描述

14.1.3 测试参数

当脚本认为参数变量中会有数据而实际上并没有时,脚本很有可能会产生错误消息。这种写脚本的方法并不可取。在使用参数前一定要检查其中是否存在数据:

#!/bin/bash
if [ -n "$1" ]; then
    echo Hello $1
else
    echo "bad argument"
fi

在这里插入图片描述
在本例中,使用了-n测试来检查命令行参数$1中是否有数据。

14.2 特殊参数变量

在bash shell中有些特殊变量,它们会记录命令行参数。

14.2.1 参数统计

特殊变量$#含有脚本运行时携带的命令行参数的个数:

#!/bin/bash
echo "there is $# param supplied"

在这里插入图片描述
你可能会觉得既然$#变量含有参数的总数,那么变量${$#}就代表了最后一个命令行参数变量。但实际上,不能在花括号内使用美元符。必须将美元符换成感叹号,即使用${!#}来获取最后一个参数:

#!/bin/bash
echo "the last param is ${!#}"

在这里插入图片描述

14.2.2 抓取所有的数据

$*$@变量可以用来轻松访问所有的参数。这两个变量都能够在单个变量中存储所有的命令行参数。但它们在用法上有所不同,$*变量会将命令行上提供的所有参数当作一个单词保存,$@变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词, 这样你就能够通过for循环遍历所有的参数值。

#!/bin/bash
for param in "$*"; do 
 echo "\$* Parameter = $param" 
done 
echo "-------------"
for param in "$@"; do
 echo "\$@ Parameter  = $param" 
done

在这里插入图片描述

14.3 移动变量

bash shell工具箱中另一件工具是shift命令。shift命令会根据它们的相对位置来移动命令行参数。在使用shift命令时,默认情况下它会将每个参数变量向左移动一个位置。所以,变量$2的值会移到$1中,而变量$1的值则会被删除(注意,变量$0的值,也就是程序名,不会改变)。这是遍历命令行参数的另一个好方法,尤其是在你不知道到底有多少参数时:

#!/bin/bash
count=1
while [ -n "$1" ]; do
    echo "param $count = $1"
    count=$[ $count + 1]
    shift
done

在这里插入图片描述

14.4 处理选项

选项是跟在单破折线后面的单个字母,它能改变命令的行为。

14.4.1 查找选项

你可以像处理命令行参数一样处理命令行选项。

1. 处理简单选项

你可以像处理参数一样处理简单的选项:

#!/bin/bash
while [ -n "$1" ]; do
    case "$1" in
    -a) echo "Found -a option";;
    -b) echo "Found -b option";;
    *) echo "$1 is not a option";;
    esac
    shift
done

在这里插入图片描述

2. 分离参数和选项

你会经常遇到想在shell脚本中同时使用选项和参数的情况。Linux中处理这个问题的标准方式是用特殊字符来将二者分开,该字符会告诉脚本何时选项结束以及普通参数何时开始。对Linux来说,这个特殊字符是双破折线--,如下:

#!/bin/bash
while [ -n "$1" ]; do
    case "$1" in
    -a) echo "Found -a option";;
    -b) echo "Found -b option";;
    --) shift; break;;
    *) echo "$1 is not a option";;
    esac
    shift
done

count=1 
for param in $@ 
do 
 echo "Parameter $count: $param" 
 count=$[ $count + 1 ] 
done

在这里插入图片描述

3. 处理带值的选项

当命令行选项要求额外的参数时,脚本必须能检测到并正确处理。下面是如何处理的例子:

#!/bin/bash
while [ -n "$1" ]; do
    case "$1" in
    -a) if [ -n "$2" ]; then
            echo "Found option -a, with param value $2"
        else
            echo "option -a has not param"
        fi
        shift
        ;;
    *) echo "$1 is not a option";;
    esac
    shift
done

在这里插入图片描述

14.4.2 使用 getopt 命令

getopt命令是一个在处理命令行选项和参数时非常方便的工具。它能够识别命令行参数,从而在脚本中解析它们时更方便。

1. 命令的格式

getopt命令可以接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。它的命令格式如下:

getopt optstring parameters

optstring是这个过程的关键所在。它定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值。在optstring中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。getopt命令会基于你定义的optstring解析提供的参数。下面是示例:
在这里插入图片描述
如果指定了一个不在optstring中的选项,默认情况下,getopt命令会产生一条错误消息。如果想忽略这条错误消息,可以在optstring之前加一个冒号:

2. 在脚本中使用getopt

可以在脚本中使用getopt来格式化脚本所携带的任何命令行选项或参数,方法是用getopt命令生成的格式化后的版本来替换已有的命令行选项和参数,用set命令能够做到。set命令的选项之一是双破折线--,它会将命令行参数替换成set命令的命令行值。看起来如下所示:

set -- $(getopt -q ab:cd "$@")

现在原始的命令行参数变量的值会被getopt命令的输出替换,而getopt已经为我们格式化好了命令行参数。现在就可以写出能帮我们处理命令行参数的脚本:

#!/bin/bash
set -- $(getopt :ab:cd "$@")
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

count=1 
for param in "$@" 
do 
 echo "Parameter #$count: $param" 
 count=$[ $count + 1 ] 
done

在这里插入图片描述

14.4.3 使用更高级的 getopts

getopts命令(注意是复数)内建于bash shell。它跟近亲getopt看起来很像,但多了一些扩展功能。与getopt不同,前者将命令行上选项和参数处理后只生成一个输出,而getopts命令能够和已有的shell参数变量配合默契。每次调用它时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于0的退出状态码。这让它非常适合用解析命令行所有参数的循环中。getopts命令的格式如下:

getopts optstring variable

optstring值类似于getopt命令中的那个。有效的选项字母都会列在optstring中,如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,可以在optstring之前加一个冒号。getopts命令将当前参数保存在命令行中定义的variable中。
getopts命令会用到两个环境变量。如果选项需要跟一个参数值,OPTARG环境变量就会保存这个值。OPTIND环境变量保存了参数列表中getopts正在处理的参数位置:

#!/bin/bash
while getopts :ab:c opt 
do 
 case "$opt" in 
 a) echo "Found the -a option" ;; 
 b) echo "Found the -b option, with value $OPTARG";; 
 c) echo "Found the -c option" ;; 
 *) echo "Unknown option: $opt";; 
 esac 
done

在这里插入图片描述
getopts命令有几个好用的功能:

  • 可以在参数值中包含空格./test.sh -ab "test1 test2"
  • 能将选项字母和参数值放在一起使用,而不用加空格./test.sh -abtest1
  • getopts还能够将命令行上找到的所有未定义的选项统一输出成问号

getopts处理每个选项时,它会将OPTIND环境变量值增一。在getopts完成处理时,你可以使用shift命令和OPTIND值来移动参数开始解析参数:shift $[ $OPTIND - 1 ]

14.5 将选项标准化

有些字母选项在Linux世界里已经拥有了某种程度的标准含义。如果你能在shell脚本中支持这些选项,脚本看起来能更友好一些:
在这里插入图片描述

14.6 获得用户输入

14.6.1 基本的读取

read命令从标准输入(键盘)或另一个文件描述符中接受输入。在收到输入后,read命令会将数据放进一个变量。下面是read命令的最简单用法:

#!/bin/bash
echo -n "enter your name:"
read name
echo "hello $name"

在这里插入图片描述
read命令包含了-p选项,允许你直接在read命令行指定提示符:

#!/bin/bash
read -p "enter your name: " name
echo "hello $name"

输出与上面的例子一样。
read命令会将提示符后输入的所有数据分配给单个变量,要么你就指定多个变量。输入的每个数据值都会分配给变量列表中的下一个变量。如果变量数量不够,剩下的数据就全部分配给最后一个变量:

read -p "enter your name: " first last

也可以在read命令行中不指定变量。如果是这样,read命令会将它收到的任何数据都放进特殊环境变量REPLY中。

14.6.2 超时

你可以用-t选项来指定一个计时器。-t选项指定了read命令等待输入的秒数。当计时器过期后,read命令会返回一个非零退出状态码:

#!/bin/bash
if read -t 5 -p "enter your name: " name 
then 
    echo "hello $name" 
else 
    echo "sorry, too slow! " 
fi

在这里插入图片描述
你可以用-n选项来指定计输入的字符数,当输入的字符达到预设的字符数时,就自动退出,将输入的数据赋给变量:

#!/bin/bash
read -n1 -p "do you want to continue [Y/N]?" answer
case $answer in 
Y | y)
    echo
    echo "fine, continue on…";;
N | n)
    echo
    echo OK, goodbye 
    exit;;
esac
echo "this is the end of the script"

在这里插入图片描述
本例中将-n选项和值1一起使用,告诉read命令在接受单个字符后退出。

14.6.3 隐藏方式读取

-s选项可以避免在read命令中输入的数据出现在显示器上(实际上,数据会被显示,只是read命令会将文本颜色设成跟背景色一样)。

read -s -p "Enter your password: " pass

14.6.4 从文件中读取

最后,也可以用read命令来读取Linux系统上文件里保存的数据。每次调用read命令,它都会从文件中读取一行文本。当文件中再没有内容时,read命令会退出并返回非零退出状态码。下面是一个按行读取文件的脚本:

#!/bin/bash
count=1
cat test_file | while read line
do
    echo "line $count: $line"
    count=$[ $count + 1 ]
done

在这里插入图片描述

第15章 呈现数据

15.1 理解输入和输出

15.1.1 标准文件描述符

Linux系统将每个对象当作文件处理。这包括输入和输出进程。Linux用文件描述符(file descriptor)来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次最多可以有九个文件描述符。出于特殊目的,bash shell保留了前三个文件描述符(0、1和2),见表15-1。
在这里插入图片描述

15.1.2 重定向错误

1. 只重定向错误

将该文件描述符值放在重定向符号前。该值必须紧紧地放在重定向符号前,否则不会工作。你在表15-1中已经看到,STDERR文件描述符被设成2。示例如下:
在这里插入图片描述

2. 重定向错误和数据

如果想重定向错误和正常输出,必须用两个重定向符号。
在这里插入图片描述
bash shell还提供了特殊的重定向符号&>,当使用&>符时,命令生成的所有输出都会发送到同一位置,包括数据和错误:
在这里插入图片描述

15.2 在脚本中重定向输出

15.2.1 临时重定向

如果有意在脚本中生成错误消息,可以将单独的一行输出重定向到STDERR。你所需要做的是使用输出重定向符来将输出信息重定向到STDERR文件描述符。在重定向到文件描述符时,你必须在文件描述符数字之前加一个&

echo "This is an error message" >&2

15.2.2 永久重定向

如果脚本中有大量数据需要重定向,你可以用exec命令告诉shell在脚本执行期间重定向某个特定文件描述符

exec 2>testerror

这个脚本用exec命令来将发给STDERR的输出重定向到文件testerror。

15.3 在脚本中重定向输入

你可以使用与脚本中重定向STDOUT和STDERR相同的方法来将STDIN从键盘重定向到其他位置。exec命令允许你将STDIN重定向到Linux系统上的文件中:

exec 0< testfile

这个命令会告诉shell它应该从文件testfile中获得输入,而不是STDIN,看下面的示例:

exec 0< testfile 
count=1 
while read line 
do 
	echo "Line #$count: $line" 
	count=$[ $count + 1 ] 
done

STDIN重定向到文件后,当read命令试图从STDIN读入数据时,它会到文件去取数据,而不是键盘。

15.4 创建自己的重定向

在shell中最多可以有9个打开的文件描述符。其他6个从3~8的文件描述符均可用作输入或输出重定向。

15.4.1 创建输出文件描述符

可以用exec命令来给输出分配文件描述符。和标准的文件描述符一样,一旦将另一个文件描述符分配给一个文件,这个重定向就会一直有效,直到你重新分配:

exec 3>test3_out
echo "this should be store in file test3_out"  >&3

在这里插入图片描述
也可以不用创建新文件,而是使用exec命令来将输出追加到现有文件中:exec 3>>test13out

15.4.2 重定向文件描述符

你可以将STDOUT的原来位置重定向到另一个文件描述符,然后再利用该文件描述符重定向回STDOUT。看下面的例子:

#!/bin/bash
#将3重定向至1,即显示器
exec 3>&1
#将1重定向至文件test_out,此时3还是定向至显示器
exec 1>test_out
echo "This should store in the output file"
#将1重定向至3,即和3一样重新定向至显示器
exec 1>&3
echo "Now things should be back to normal"

在这里插入图片描述

15.4.3 创建输入文件描述符

可以用和重定向输出文件描述符同样的办法重定向输入文件描述符。在重定向到文件之前,先将STDIN文件描述符保存到另外一个文件描述符,然后在读取完文件之后再将STDIN恢复到它原来的位置。

15.4.4 创建读写文件描述符

可以用同一个文件描述符对同一个文件进行读写。不过用这种方法时,你要特别小心。由于你是对同一个文件进行数据读写,shell会维护一个内部指针,指明在文件中的当前位置。任何读或写都会从文件指针上次的位置开始。如果不够小心,它会产生一些令人瞠目的结果。看看下面这个例子:

#!/bin/bash
#将3的输出输入都定向至文件testfile
exec 3<> testfile
read line <&3
echo "Read: $line"
echo "This is a test line" >&3

在这里插入图片描述
当脚本向文件中写入数据时,它会从文件指针所处的位置开始。read命令读取了第一行数据,所以它使得文件指针指向了第二行数据的第一个字符。在echo语句将数据输出到文件时,它会将数据放在文件指针的当前位置,覆盖了该位置的已有数据。

15.4.5 关闭文件描述符

要关闭文件描述符,将它重定向到特殊符号&-:

exec 3> &-

一旦关闭了文件描述符,就不能在脚本中向它写入任何数据,否则shell会生成错误消息。

15.5 列出打开的文件描述符

有时要记住哪个文件描述符被重定向到了哪里很难。为了帮助你理清条理,bash shell提供了lsof命令。lsof命令会列出整个Linux系统打开的所有文件描述符。这是个有争议的功能,因为它会向非系统管理员用户提供Linux系统的信息。要想以普通用户账户来运行它,必须通过全路径名来引用:/usr/sbin/lsof。该命令会产生大量的输出,但好在有大量的命令行选项和参数可以用来帮助过滤lsof的输出。最常用的有-p-d,前者允许指定进程ID(PID),后者允许指定要显示的文件描述符编号。要想知道进程的当前PID,可以用特殊环境变量$$(shell会将它设为当前PID)。-a选项用来对其他两个选项的结果执行布尔AND运算,这会产生如下输出。
在这里插入图片描述
FD一列表示文件描述符号以及访问类型(r代表读,w代表写,u代表读写)。

15.6 阻止命令输出

可以将输出重定向到一个叫作null文件的特殊文件。null文件跟它的名字很像,文件里什么都没有。shell输出到null文件的任何数据都不会保存,全部都被丢掉了。在Linux系统上null文件的标准位置是/dev/null。你重定向到该位置的任何数据都会被丢掉,不会显示:
在这里插入图片描述

15.7 创建临时文件

Linux系统有特殊的目录,专供临时文件使用。Linux使用/tmp目录来存放不需要永久保留的文件。
mktemp命令可以在/tmp目录中创建一个唯一的临时文件。shell会创建这个文件,但不用默认的umask值。它会将文件的读和写权限分配给文件的属主,并将你设成文件的属主。一旦创建了文件,你就在脚本中有了完整的读写权限,但其他人没法访问它(当然,root用户除外)。

15.7.1 创建本地临时文件

默认情况下,mktemp会在本地目录中创建一个文件。要用mktemp命令在本地目录中创建一个临时文件,你只要指定一个文件名模板就行了。模板可以包含任意文本文件名,但在文件名末尾必须加上6个X(大写)。mktemp命令会用6个字符码替换这6个X,从而保证文件名在目录中是唯一的:
在这里插入图片描述

15.7.2 在/tmp 目录创建临时文件

-t选项会强制mktemp命令来在系统的临时目录来创建该文件。在用这个特性时,mktemp命令会返回用来创建临时文件的全路径,而不是只有文件名:
在这里插入图片描述

15.7.3 创建临时目录

-d选项告诉mktemp命令来创建一个临时目录而不是临时文件。这样你就能用该目录进行任何需要的操作了,比如创建其他的临时文件。
在这里插入图片描述

15.8 记录消息

将输出同时发送到显示器和日志文件,这种做法有时候能够派上用场。你不用将输出重定向两次,只要用特殊的tee命令就行。tee命令相当于管道的一个T型接头。它将从STDIN过来的数据同时发往两处。一处是
STDOUT,另一处是tee命令行所指定的文件名:

tee filename

注意,默认情况下,tee命令会在每次使用时覆盖输出文件内容。如果你想将数据追加到文件中,必须用-a选项:
在这里插入图片描述

15.9 实例

脚本内容如下:

#!/bin/bash 
# read file and create INSERT statements for MySQL 
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}

脚本中出现了三处重定向操作。
while循环使用read语句从数据文件中读取文本。注意在done语句中出现的重定向符号:done < ${1},当运行程序test23时,$1代表第一个命令行参数。它指明了待读取数据的文件。read语句会使用IFS字符解析读入的文本,我们在这里将IFS指定为逗号。
脚本中另外两处重定向操作出现在同一条语句中:cat >> $outfile << EOF。这条语句中包含一个输出追加重定向(双大于号)和一个输入追加重定向(双小于号)。输出重定向将cat命令的输出追加到由$outfile变量指定的文件中。cat命令的输入不再取自标准输入,而是被重定向到脚本中存储的数据。EOF符号标记了追加到文件中的数据的起止。

第16章 控制脚本

16.1 处理信号

16.1.1 重温 Linux 信号

下表是常见的Linux信号:
在这里插入图片描述
在这里插入图片描述
默认情况下,bash shell会忽略收到的任何SIGQUIT (3)和SIGTERM (5)信号(正因为这样,交互式shell才不会被意外终止)。但是bash shell会处理收到的SIGHUP (1)和SIGINT (2)信号。如果bash shell收到了SIGHUP信号,比如当你要离开一个交互式shell,它就会退出。但在退出之前,它会将SIGHUP信号传给所有由该shell所启动的进程(包括正在运行的shell脚本)。通过SIGINT信号,可以中断shell。Linux内核会停止为shell分配CPU处理时间。这种情况发生时,shell会将SIGINT信号传给所有由它所启动的进程,以此告知出现的状况。

16.1.2 生成信号

bash shell允许用键盘上的组合键生成两种基本的Linux信号。

1. 中断进程

Ctrl+C组合键会生成SIGINT信号,并将其发送给当前在shell中运行的所有进程。

2. 暂停进程

Ctrl+Z组合键会生成一个SIGTSTP信号,停止shell中运行的任何进程。停止(stopping)进程跟终止(terminating)进程不同:停止进程会让程序继续保留在内存中,并能从上次停止的位置继续运行。

16.1.3 捕获信号

也可以不忽略信号,在信号出现时捕获它们并执行其他命令。trap命令允许你来指定shell脚本要监看并从shell中拦截的Linux信号。如果脚本收到了trap命令中列出的信号,该信号不再由shell处理,而是交由本地处理。trap命令的格式是:

trap commands signals

下面是个简单的例子:

# !/bin/bash
trap "echo 'sorry, i have trap singnal Ctrl + C' " SIGINT
echo This is a test script
count=1 
while [ $count -le 10 ] 
do 
 echo "Loop #$count" 
 sleep 1 
 count=$[ $count + 1 ] 
done 
echo "This is the end of the test script" 

本例中用到的trap命令会在每次检测到SIGINT信号时显示一行简单的文本消息。捕获这些信号会阻止用户用bash shell组合键Ctrl+C来停止程序。

16.1.4 捕获脚本退出

除了在shell脚本中捕获信号,你也可以在shell脚本退出时进行捕获。只要在trap命令后加上EXIT信号就行。

#!/bin/bash 
trap "echo Goodbye..." EXIT 
count=1 
while [ $count -le 5 ] 
do 
 echo "Loop #$count" 
 sleep 1 
 count=$[ $count + 1 ] 
done

16.1.5 修改或移除捕获

要想在脚本中的不同位置进行不同的捕获处理,只需重新使用带有新选项的trap命令:

#!/bin/bash 
trap "echo 'sorry, Ctrl+C is trapped'" SIGINT
count=1
while [ $count -le 5 ]; do
    echo "Loop $count"
    sleep 1
    count=$[ $count + 1 ]
done
# 修改捕获信号的处理
trap "echo 'I modify the trap'" SIGINT
count=1
while [ $count -le 5 ]; do
    echo "Loop $count"
    sleep 1
    count=$[ $count + 1 ]
done

也可以删除已设置好的捕获。只需要在trap命令与希望恢复默认行为的信号列表之间加上两个破折号就行了trap -- SIGINT:

#!/bin/bash 
trap "echo 'sorry, Ctrl+C is trapped'" SIGINT
count=1
while [ $count -le 5 ]; do
    echo "Loop $count"
    sleep 1
    count=$[ $count + 1 ]
done
trap -- SIGINT
echo "I just removed the trap"
count=1
while [ $count -le 5 ]; do
    echo "Loop $count"
    sleep 1
    count=$[ $count + 1 ]
done

16.2 以后台模式运行脚本

在后台模式中,进程运行时不会和终端会话上的STDIN、STDOUT以及STDERR关联

16.2.1 后台运行脚本

以后台模式运行shell脚本非常简单。只要在命令后加个&符就行了:

#!/bin/bash 
count=1
while [ $count -le 10 ]; do
    echo "Loop $count"
    sleep 1
    count=$[ $count + 1 ]
done

运行命令./test.sh &,结果如下:
在这里插入图片描述

第一行中的[1] 5443,方括号中的数字是shell分配给后台进程的作业号。下一个数是Linux系统分配给进程的进程ID(PID)。Linux系统上运行的每个进程都必须有一个唯一的PID。
当后台进程结束时,它会在终端上显示出一条消息:[1]+ Done ./test.sh。这表明了作业的作业号以及作业状态,还有用于启动作业的命令。
最好是将后台运行的脚本的STDOUT和STDERR进行重定向,避免杂乱无章的输出。
值得注意的是,每一个后台进程都和终端联系在一起。如果终端退出,那么后台进程也会随之退出。如果希望运行在后台模式的脚本在登出控制台后能够继续运行,需要借助于别的手段。

16.3 在非控制台下运行脚本

有时你会想在终端会话中启动shell脚本,然后让脚本一直以后台模式运行到结束,即使你退出了终端会话。这可以用nohup命令来实现。nohup命令运行了另外一个命令来阻断所有发送给该进程的SIGHUP信号。这会在退出终端会话时阻止进程退出。命令格式如下:nohup ./test.sh &
如果使用nohup运行了一个命令,该命令的输出会被追加到已有的nohup.out文件中。当运行位于同一个目录中的多个命令时一定要当心,因为所有的输出都会被发送到同一个nohup.out文件中,结果会让人摸不清头脑。

16.4 作业控制

启动、停止、终止以及恢复作业的这些功能统称为作业控制。通过作业控制,就能完全控制
shell环境中所有进程的运行方式了。

16.4.1 查看作业

jobs命令用于查看shell当前正在处理的作业。
脚本如下:

#!/bin/bash 
echo "process id: $$"
count=1
while [ $count -le 10 ]; do
    echo "Loop $count"
    sleep 10
    count=$[ $count + 1 ]
done

首先从命令行中启动脚本,然后使用Ctrl+Z组合键来停止脚本。
然后还是使用同样的脚本,利用&将另外一个作业作为后台进程启动,并将输出重定向至本地文件。
最后使用jobs显示这两个已停止/运行中的作业信息,如下:
在这里插入图片描述
图中带加号的作业会被当做默认作业,带减号的作业成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业。
jobs命令可以使用一些命令行参数:
在这里插入图片描述

16.4.2 重启停止的作业

要以后台模式重启一个作业,可用bg命令加上作业号, 不加作业号,则表示该作业为默认作业:
在这里插入图片描述

16.5 调整谦让度

调度优先级(scheduling priority)是内核分配给进程的CPU时间(相对于其他进程)。在Linux系统
中,由shell启动的所有进程的调度优先级默认都是相同的。
调度优先级是个整数值,从-20(最高优先级)到+19(最低优先级)。默认情况下,bash shell以优先级0来启动所有进程。

16.5.1 nice 命令

nice命令允许你设置命令启动时的调度优先级。要让命令以更低的优先级运行,只要用nice的-n命令行来指定新的优先级级别:
在这里插入图片描述
nice命令阻止普通系统用户来提高命令的优先级,要提供命令的优先级,需要使用root账号。

16.5.2 renice 命令

有时你想改变系统上已运行命令的优先级。这正是renice命令可以做到的。它允许你指定运行进程的PID来改变它的优先级:
在这里插入图片描述

renice命令会自动更新当前运行进程的调度优先级。和nice命令一样,renice命令也有一
些限制:

  • 只能对属于你的进程执行renice
  • 只能通过renice降低进程的优先级
  • root用户可以通过renice来任意调整进程的优先级

如果想完全控制运行进程,必须以root账户身份登录或使用sudo命令

16.6 定时运行作业

Linux系统提供了多个在预选时间运行脚本的方法:at命令和cron表。每个方法都使用不同的技
术来安排脚本的运行时间和频率。

16.6.1 用 at 命令来计划执行作业

at命令允许指定Linux系统何时运行脚本。at的守护进程atd会以后台模式运行,检查作业队列来运行作业。大多数Linux发行版会在启动时运行此守护进程。atd守护进程会检查系统上的一个特殊目录(通常位于/var/spool/at)来获取用at命令提交的作业。默认情况下,atd守护进程会每60秒检查一下这个目录。有作业时,atd守护进程会检查作业设置运行的时间。如果时间跟当前时间匹配,atd守护进程就会运行此作业。

1. at命令的格式

at命令的基本格式非常简单:

at [-f filename] time

默认情况下,at命令会将STDIN的输入放到队列中。你可以用-f参数来指定用于读取命令(脚本文件)的文件名。time参数指定了Linux系统何时运行该作业。
at命令能识别多种不同的时间格式:

  • 标准的小时和分钟格式,比如10:15
  • AM/PM指示符,比如10:15 PM
  • 特定可命名时间,比如now、noon、midnight或者teatime(4 PM)
  • 标准日期格式,比如MMDDYY、MM/DD/YY或DD.MM.YY。
  • 文本日期,比如Jul 4或Dec 25,加不加年份均可
  • 你也可以指定时间增量:当前时间+25 min、10:15+7天

在你使用at命令时,该作业会被提交到作业队列(job queue)针对不同优先级,存在26种不同的作业队列。作业队列通常用小写字母a-z和大写字母A-Z来指代。作业队列的字母排序越高,作业运行的优先级就越低。默认情况下,at的作业会被提交到a作业队列。如果想以更高优先级运行作业,可以用-q参数指定不同的队列字母。

2. 获取作业的输出

当作业在Linux系统上运行时,Linux系统会将提交该作业的用户的电子邮件地址作为输出。使用e-mail作为at命令的输出极其不便。at命令利用sendmail应用程序来发送邮件。如果你的系统中没有安装sendmail,那就无法获得任何输出!因此在使用at命令时,最好在脚本中对STDOUTSTDERR进行重定向,如下:

#!/bin/bash 
echo "This script ran at $(date +%B%d,%T)" > test.out 
echo >> test.out 
sleep 5 
echo "This is the script's end..." >> test.out

如果不想在at命令中使用邮件或重定向,最好加上-m选项来屏蔽作业产生的输出信息。

3. 列出等待的作业

atq命令可以查看系统有哪些作业在等待

4. 删除作业

一旦知道了哪些作业在作业队列中等待,就能用atrm命令来删除等待中的作业:atrm + 作业号

16.6.2 安排需要定期执行的脚本

Linux系统使用cron程序来安排要定期执行的作业。cron程序会在后台运行并检查一个特殊的
表(被称作cron时间表),以获知已安排执行的作业

1. cron时间表

cron时间表采用一种特别的格式来指定作业何时运行。其格式如下:

min hour dayofmonth month dayofweek command

命令列表必须指定要运行的命令或脚本的全路径名:

15 10 * * * /home/rich/test.sh > test.out
2. 构建cron时间表

Linux提供了crontab命令来处理cron时间表。要列出已有的cron时间表,可以用-l选项。
要为cron时间表添加条目,可以用-e选项。

3. 浏览cron目录

有4个基本目录:hourly、daily、monthly和weekly。比如,如果脚本需要每天运行一次,只要将脚本复制到daily目录,cron就会每天执行它。

4. anacron程序

如果某个作业在cron时间表中安排运行的时间已到,但这时候Linux系统处于关机状态,那么这个作业就不会被运行。当系统开机时,cron程序不会再去运行那些错过的作业。要解决这个问题,许多Linux发行版还包含了anacron程序。
如果anacron知道某个作业错过了执行时间,它会尽快运行该作业。这意味着如果Linux系统关机了几天,当它再次开机时,原定在关机期间运行的作业会自动运行。
anacron程序只会处理位于cron目录的程序,比如/etc/cron.monthly。它用时间戳来决定作业是否在正确的计划间隔内运行了。每个cron目录都有个时间戳文件,该文件位于/var/spool/ anacron
anacron时间表的基本格式和cron时间表略有不同:

period delay identifier command

period条目定义了作业多久运行一次,以天为单位。delay条目会指定系统启动后anacron程序需要等待多少分钟再开始运行错过的脚本。identifier条目是一种特别的非空字符串,如cron-weekly。它用于唯一标识日志消息和错误邮件中的作业。

16.6.3 使用新 shell 启动脚本

基本上,依照下列顺序所找到的第一个文件会被运行,其余的文件会被忽略:

  • $HOME/.bash_profile
  • $HOME/.bash_login
  • $HOME/.profile

因此,应该将需要在登录时运行的脚本放在上面第一个文件中。
每次启动一个新shell时,bash shell都会运行.bashrc文件。.bashrc文件通常也是通过某个bash启动文件来运行的。因为.bashrc文件会运行两次:一次是当你登入bash shell时,另一次是当你启动一个bash shell时。如果你需要一个脚本在两个时刻都得以运行,可以把这个脚本放进该文件中。

第17章 创建函数

17.1 基本的脚本函数

17.1.1 创建函数

有两种格式可以用来在bash shell脚本中创建函数。第一种格式采用关键字function,后跟分配给该代码块的函数名:

function name { 
	commands 
}

第二种简略方式:

name() { 
	commands 
}

17.1.2 使用函数

函数必须在使用前被定义。函数名必须是唯一的,如果你重定义了函数,新定义会覆盖原来函数的定义,这一切不会产生任何错误消息:

# !/bin/bash
func1() {
    echo "old func1"
}
func1
func1() {
    echo "new func1"
}
func1

17.2 返回值

bash shell会把函数当作一个小型脚本,运行结束时会返回一个退出状态码。有3种不同的方法来为函数生成退出状态码。

17.2.1 默认退出状态码

默认情况下,函数的退出状态码是函数中最后一条命令返回的退出状态码。在函数执行结束后,可以用标准变量$?来确定函数的退出状态码。
值得注意的是,你无法通过函数返回状态码来判断其是否运行成功,比如下面的例子:

# !/bin/bash
func() {
    ls -l badfile
    echo "func return"
}
func
echo "the exit status is: $?"

输出结果如下:
在这里插入图片描述
函数最后一条语句echo运行成功,该函数的退出状态码就是0,尽管其中ls命令并没有正常运行。

17.2.2 使用 return 命令

bash shell使用return命令来退出函数并返回特定的退出状态码,但使用时需注意:

  • 函数一结束就取返回值,如果在用$?变量提取函数返回值之前执行了其他命令,函数的返回值就会丢失。记住,$?变量会返回执行的最后一条命令的退出状态码。
  • 退出状态码必须是0~255,否则会产生一个错误。
# !/bin/bash
double() {
    read -p "enter a value: " value
    return $[ $value * 2 ]
}
double
echo "the new value is: $?"

17.2.3 使用函数输出

正如可以将命令的输出保存到shell变量中一样,你也可以对函数的输出采用同样的处理办法。可以用这种技术来获得任何类型的函数输出,并将其保存到变量中:

# !/bin/bash
double() {
    read -p "enter a value: " value
    echo $[ $value * 2 ]
}
result=$(double)
echo "the new value is: $result"

17.3 在函数中使用变量

17.3.1 向函数传递参数

bash shell会将函数当作小型脚本来对待。这意味着你可以像普通脚本那样向函数传递参数。
函数可以使用标准的参数环境变量来表示命令行上传给函数的参数。例如,函数名会在$0变量中定义,函数命令行上的任何参数都会通过$1$2等定义。也可以用特殊变量$#来判断传给函数的参数数目:

# !/bin/bash
Add() {
    if [ $# -eq 2 ] ; then
        echo $[ $1 + $2 ]
    else
        echo 0
    fi
}
result=$(Add 1 2)
echo "the result is: $result"

由于函数使用特殊参数环境变量作为自己的参数值,因此它无法直接获取脚本在命令行中的参数值。要在函数中使用这些值,必须在调用函数时手动将它们传过去:

# !/bin/bash
Add() {
    if [ $# -eq 2 ] ; then
        echo $[ $1 + $2 ]
    else
        echo 0
    fi
}

if [ $# -eq 2 ] ; then
    result=$(Add $1 $2)
    echo "the result is: $result"
fi

17.3.2 在函数中处理变量

1. 全局变量

全局变量是在shell脚本中任何地方都有效的变量。如果你在脚本的主体部分定义了一个全局变量,那么可以在函数内读取它的值。类似地,如果你在函数内定义了一个全局变量,可以在脚本的主体部分读取它的值。默认情况下,你在脚本中定义的任何变量都是全局变量

2. 局部变量

无需在函数中使用全局变量,函数内部使用的任何变量都可以被声明成局部变量。要实现这一点,只要在变量声明的前面加上local关键字就可以了:

# !/bin/bash

func() {
    local temp=$[ $value+5 ]
    result=$[ $temp * 2 ]
}

temp=4
value=6
func
echo "The result is $result"
echo "temp is $temp"

输出如下:
在这里插入图片描述
可见在func1函数中使用$temp变量时,并不会影响在脚本主体中赋给$temp变量的值。

17.4 数组变量和函数

17.4.1 向函数传数组参数

将数组变量当作单个参数传递的话,它不会起作用。如果你试图将该数组变量作为函数参数,函数只会取数组变量的第一个值:

# !/bin/bash

test() {
    local array=$1
    echo "the received array is ${array[*]}"
}

myarray=(1 2 3 4 5)
echo "the origin array is ${myarray[*]}"
test $myarray

在这里插入图片描述
要解决这个问题,你必须将该数组变量的值分解成单个的值,然后将这些值作为函数参数使用。在函数内部,可以将所有的参数重新组合成一个新的变量:

# !/bin/bash

addArray() {
    local sum=0
    local newArray=($(echo "$@"))
    for value in ${newArray[*]}; do
        sum=$[ $sum + $value ]
    done
    echo $sum
}

myArray=(1 2 3 4 5)
echo "the origin array is ${myArray[*]}"
args=$(echo ${myArray[*]})
result=$(addArray $args)
echo "the array add result is $result"

17.4.2 从函数返回数组

从函数里向shell脚本传回数组变量也用类似的方法。函数用echo语句来按正确顺序输出单个数组值,然后脚本再将它们重新放进一个新的数组变量中:

# !/bin/bash

echoArray() {
    local newArray=($(echo "$@"))
    echo ${newArray[*]}
}

myArray=(1 2 3 4 5)
echo "the origin array is ${myArray[*]}"
args=$(echo ${myArray[*]})
result=$(echoArray $args)
echo "the array echo result is ${result[*]}"

在这里插入图片描述

17.5 函数递归

下面是使用递归计算阶乘的经典例子:

# !/bin/bash

factorial() {
    if [ $1 -eq 1 ]; then
        echo 1  
    else
        echo $[ $(factorial $[ $1-1 ]) * $1 ]
    fi
}
echo "The factorial of $1 is: $(factorial $1)"

在这里插入图片描述

17.6 创建库

bash shell允许创建函数库文件,然后在多个脚本中引用该库文件。
使用函数库的关键在于source命令。source命令会在当前shell上下文中执行命令,而不是创建一个新shell。可以用source命令来在shell脚本中运行库文件脚本。这样脚本就可以使用库中的函数了。source命令有个快捷的别名,称作点操作符(dot operator)。要在shell脚本中运行myfuncs库文件,只需添加:. ./myfuncs
创建脚本myfuncs如下:

# !/bin/bash

add(){
    echo $[ $1+$2 ]
}

decr(){
    echo $[ $1-$2 ]
}

在test.sh中引用此脚本:

# !/bin/bash
. ./myfuncs.sh
echo "add $1 $2 result is $(add $1 $2)"
echo "decr $1 $2 result is $(decr $1 $2)"

在这里插入图片描述

17.7 在命令行上使用函数

17.7.1 在命令行上创建函数

一种方法是采用单行方式定义函数。当在命令行上定义函数时,你必须记得在每个命令后面加个分号,这样shell就能知道在哪里是命令的起止了:

doubleit() { read -p "enter value:" value; echo $[ $value * 2 ]; }
doubleit 

另一种方法是采用多行方式来定义函数。在定义时,bash shell会使用次提示符来提示输入更多命令。用这种方法,你不用在每条命令的末尾放一个分号,只要按下回车键就行,在函数的尾部使用花括号,shell就会知道你已经完成了函数的定义:
在这里插入图片描述

17.7.2 在.bashrc 文件中定义函数

在命令行上直接定义shell函数的明显缺点是退出shell时,函数就消失了。一个非常简单的方法是将函数定义在一个特定的位置,这个位置在每次启动一个新shell的时候,都会由shell重新载入。最佳地点就是.bashrc文件。

17.8 实例

函数的应用绝不仅限于创建自己的函数自娱自乐。在开源世界中,共享代码才是关键,而这一点同样适用于脚本函数。你可以下载大量各式各样的函数,并将其用于自己的应用程序中。

第 18 章 图形化桌面环境中的脚本编程

第 19 章 初识sed和gawk

shell脚本最常见的一个用途就是处理文本文件。如果想在shell脚本中处理任何类型的数据,你得熟悉Linux中的sedgawk工具。

19.1 文本处理

sed编辑器被称作流编辑器(stream editor),和普通的交互式文本编辑器恰好相反。在交互式文本编辑器中(比如vim),你可以用键盘命令来交互式地插入、删除或替换数据中的文本。流编辑器则会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。
sed编辑器会执行下列操作:

  1. 一次从输入中读取一行数据
  2. 根据所提供的编辑器命令匹配数据
  3. 按照命令修改流中的数据
  4. 将新的数据输出到STDOUT

由于命令是按顺序逐行给出的,sed编辑器只需对数据流进行一遍处理就可以完成编辑操作。这使得sed编辑器要比交互式编辑器快得多,你可以快速完成对数据的自动修改。
sed命令的格式:sed options script file
在这里插入图片描述

1. 在命令行定义编辑器命令

默认情况下,sed编辑器会将指定的命令应用到STDIN输入流上:
在这里插入图片描述
这个例子在sed编辑器中使用了s命令。s命令会用斜线间指定的第二个文本字符串来替换第一个文本字符串模式。
重要的是,要记住,sed编辑器并不会修改文本文件的数据。它只会将修改后的数据发送到STDOUT。如果你查看原来的文本文件,它仍然保留着原始数据。

2. 在命令行使用多个编辑器命令

要在sed命令行上执行多个命令时,只要用-e选项就可以了。命令之间必须用分号隔开,并且在命令末尾和分号
之间不能有空格:

 sed -e 's/brown/green/; s/dog/cat/' data.txt

在这里插入图片描述

3. 从文件中读取编辑器命令

如果有大量要处理的sed命令,那么将它们放进一个单独的文件中通常会更方便一些。可以在sed命令中用-f选项来指定文件:
在这里插入图片描述

19.1.2 gawk 程序

gawk程序是Unix中的原始awk程序的GNU版本。gawk程序让流编辑迈上了一个新的台阶,它提供了一种编程语言而不只是编辑器命令。你可以用它做下面的事情:

  • 定义变量来保存数据
  • 使用算术和字符串操作符来处理数据
  • 使用结构化编程概念(比如if-then语句和循环)来为数据处理增加处理逻辑
  • 通过提取数据文件中的数据元素,将其重新排列或格式化,生成格式化报告
1. gawk命令格式
gawk options program file

在这里插入图片描述

2. 从命令行读取程序脚本

gawk程序脚本用一对花括号来定义。由于gawk命令行假定脚本是单个文本字符串,你还必须将脚本放到单引号中。下面的例子在命令行上指定了一个简单的gawk程序脚本:

 gawk '{print "Hello World!"}'

如果尝试运行这个命令,你可能会有些失望,因为什么都不会发生。原因在于没有在命令行上指定文件名,所以gawk程序会从STDIN接收数据。在运行这个程序时,它会一直等待从STDIN输入的文本。如果你输入一行文本并按下回车键,gawk会对这行文本运行一遍程序脚本。跟sed编辑器一样,gawk程序会针对数据流中的每行文本执行程序脚本。由于程序脚本被设为显示一行固定的文本字符串,因此不管你在数据流中输入什么文本,都会得到同样的文本输出:
在这里插入图片描述
要终止这个gawk程序,你必须表明数据流已经结束了。bash shell提供了一个组合键来生成EOF(End-of-File)字符。Ctrl+D组合键会在bash中产生一个EOF字符。这个组合键能够终止该gawk程序并返回到命令行界面提示符下。

3. 使用数据字段变量

gawk的主要特性之一是其处理文本文件中数据的能力。它会自动给一行中的每个数据元素分配一个变量。默认情况下,gawk会将如下变量分配给它在文本行中发现的数据字段:

  • $0代表整个文本行
  • $1代表文本行中的第1个数据字段
  • $2代表文本行中的第2个数据字段
  • $n代表文本行中的第n个数据字段

在文本行中,每个数据字段都是通过字段分隔符划分的。gawk中默认的字段分隔符是任意的空白字符(例如空格或制表符)。如果你要读取采用了其他字段分隔符的文件,可以用-F选项指定:
在这里插入图片描述

4. 在程序脚本中使用多个命令

gawk编程语言允许你将多条命令组合成一个正常的程序。要在命令行上的程序脚本中使用多条命令,只要在命令之间放个分号即可:

 echo "My name is Rich" | gawk '{$4="Christine"; print $0}'

也可以用次提示符一次一行地输入程序脚本命令。

5. 从文件中读取程序

跟sed编辑器一样,gawk编辑器允许将程序存储到文件中,然后再在命令行中引用,只需要通过-f选项指定文件名即可:

在这里插入图片描述

6. 在处理数据前/后运行脚本

gawk还允许指定程序脚本何时运行。有时可能需要在处理数据前后运行脚本,可以听过BEGINEND关键字来实现。程序会强制gawk在读取数据前执行BEGIN关键字后指定的程序脚本,在读完数据后执行END脚本:

gawk 'BEGIN {print "The data File Contents:"}; {print $0}; END {print "End of file"}' data.txt

在这里插入图片描述

19.2 sed 编辑器基础

成功使用sed编辑器的关键在于掌握其各式各样的命令和格式,它们能够帮助你定制文本编辑行为。

19.2.1 更多的替换选项

1. 替换标记

替换命令在替换多行中的文本时能正常工作,但默认情况下它只替换每行中出现的第一处。要让替换命令能够替换一行中不同地方出现的文本必须使用替换标记(substitution flag)。替换标记会在替换命令字符串之后设置:

s/pattern/replacement/flags

有4种可用的替换标记:

  • 数字,表明新文本将替换第几处模式匹配的地方
  • g,表明新文本将会替换所有匹配的文本
  • p,表明原先行的内容要打印出来
  • w file,将替换的结果写到文件中

p替换标记会打印与替换命令中指定的模式匹配的行。这通常会和sed的-n选项一起使用。-n选项将禁止sed编辑器输出。但p替换标记会输出修改过的行。将二者配合使用的效果就是只输出被替换命令修改过的行。

# !/bin/bash
cat data.txt
echo "test 0"
sed -n 's/test/trial/p' data.txt
echo "test 1"
sed 's/test/trial/' data.txt
echo "test 2"
sed 's/test/trial/2' data.txt
echo "test 3"
sed 's/test/trial/g' data.txt
echo "test 4"
sed 's/test/trial/w test.txt' data.txt
cat test.txt

在这里插入图片描述

2. 替换字符

有时你会在文本字符串中遇到一些不太方便在替换模式中使用的字符。Linux中一个常见的例子就是正斜线(/)。替换文件中的路径名会比较麻烦,因为你需要给每一个正斜线前加一个反斜线, 可读性很差:

 sed 's/\/bin\/bash/\/bin\/csh/' /etc/passwd

要解决这个问题,sed编辑器允许选择其他字符来作为替换命令中的字符串分隔符,比如下面的例子使用!作为分隔符:

sed 's!/bin/bash!/bin/csh!' /etc/passwd

19.2.2 使用地址

默认情况下,在sed编辑器中使用的命令会作用于文本数据的所有行。如果只想将命令作用于特定行或某些行,则必须用行寻址(line addressing)。在sed编辑器中有两种形式的行寻址:

  • 以数字形式表示行区间
  • 用文本模式来过滤出行

两种形式都使用相同的格式来指定地址:[address] command, 也可以将特定地址的多个命令分组:

address { 
 command1 
 command2 
 command3 
}
1. 数字方式的行寻址

sed编辑器会将文本流中的第一行编号为1,然后继续按顺序为接下来的行分配行号。在命令中指定的地址可以是单个行号,或是用起始行号、逗号以及结尾行号指定的一定区间范围内的行,如果想将命令作用到文本中从某行开始的所有行,可以用特殊地址——美元符$

sed '2s/dog/cat/' data.txt
sed '2,3s/dog/cat/' data.txt
#从第2行开始的所有行
sed '2,$s/dog/cat/' data1.txt
2. 使用文本模式过滤器

sed编辑器允许指定文本模式来过滤出命令要作用的行:/pattern/command。必须用正斜线将要指定的pattern封起来。sed编辑器会将该命令作用到包含指定文本模式的行上:

 sed '/Samantha/s/bash/csh/' /etc/passwd

在这里插入图片描述
其中pattern还支持正则表达式。

3. 命令组合

如果需要在单行上执行多条命令,可以用花括号将多条命令组合在一起。sed编辑器会处理地址行处列出的每条命令:

 sed '2{ 
	s/fox/elephant/ 
	s/dog/cat/ 
}' data1.txt

19.2.3 删除行

删除命令d名副其实,它会删除匹配指定寻址模式的所有行。使用该命令时要特别小心,如果你忘记加入寻址模式的话,流中的所有文本行都会被删除。当和指定地址一起使用时,可以从数据流中删除特定的文本行:

sed '3d' data.txt
sed '3,5d' data.txt
sed '3,$d' data.txt
# 模式匹配特性也适用于删除命令
sed '/number 1/d' data.txt

也可以使用两个文本模式来删除某个区间内的行,但这么做时要小心。你指定的第一个模式会“打开”行删除功能,第二个模式会“关闭”行删除功能。sed编辑器会删除两个指定行之间的所有行(包括指定的行)。而且需要注意的是,因为只要sed编辑器在数据流中匹配到了开始模式,删除功能就会打开,即可能多次开始删除模式:
在这里插入图片描述

19.2.4 插入和附加文本

如你所期望的,跟其他编辑器类似,sed编辑器允许向数据流插入和附加文本行。两个操作的区别可能比较让人费解:

  • 插入(insert)命令(i)会在指定行前增加一个新行
  • 附加(append)命令(a)会在指定行后增加一个新行

这两条命令的费解之处在于它们的格式。它们不能在单个命令行上使用。你必须指定是要将行插入还是附加到另一行。格式如下:

sed '[address]command\ 
new line'

new line中的文本将会出现在sed编辑器输出中你指定的位置:
在这里插入图片描述
在这里插入图片描述
如果你有一个多行数据流,想要将新行附加到数据流的末尾,只要用代表数据最后一行的美元符$就可以了:

 sed '$a\ 
> This is a new line of text.' data.txt

要插入或附加多行文本,就必须对要插入或附加的新文本中的每一行使用反斜线,直到最后一行:

 sed '1i\ 
> This is one line of new text.\ 
> This is another line of new text.' data.txt

19.2.5 修改行

修改(change)命令允许修改数据流中整行文本的内容。它跟插入和附加命令的工作机制
一样,你必须在sed命令中单独指定新行:
在这里插入图片描述

19.2.6 转换命令

转换(transform)命令(y)是唯一可以处理单个字符的sed编辑器命令。转换命令格式如下:

[address]y/inchars/outchars/

转换命令会对inchars和outchars值进行一对一的映射。inchars中的第一个字符会被转换为outchars中的第一个字符,第二个字符会被转换成outchars中的第二个字符。这个映射过程会一直持续到处理完指定字符。如果inchars和outchars的长度不同,则sed编辑器会产生一条错误消息:
在这里插入图片描述
转换命令是一个全局命令,也就是说,它会文本行中找到的所有指定字符自动进行转换,而不会考虑它们出现的位置。

19.2.7 回顾打印

另外有3个命令也能用来打印数据流中的信息:

  • p命令用来打印文本行
  • 等号(=)命令用来打印行号
  • l(小写的L)命令用来列出行
# p配合-n使用,只打印匹配的行
sed -n '/number 3/p' data.txt
# 只打印2-3行
sed -n '2,3p' data.txt
# 打印行号
sed '=' data1.txt

列出(list)命令(l)可以打印数据流中的文本和不可打印的ASCII字符。任何不可打印字符要么在其八进制值前加一个反斜线,要么使用标准C风格的命名法,比如\t,来代表制表符。

19.2.8 使用 sed 处理文件

1. 写入文件

w命令用来向文件写入行:[address]w filename
filename可以使用相对路径或绝对路径,但不管是哪种,运行sed编辑器的用户都必须有文件的写权限。地址可以是sed中支持的任意类型的寻址方式,例如单个行号、文本模式、行区间或文本模式。下面的例子是将数据流中的前两行打印到另一个文本文件中:
在这里插入图片描述

2. 从文件读取数据

读取命令r允许你将一个独立文件中的数据插入到数据流中:[address]r filename
在这里插入图片描述

第20章 正则表达式

在shell脚本中成功运用sed编辑器和gawk程序的关键在于熟练使用正则表达式。

20.1 什么是正则表达式

20.1.1 定义

正则表达式是你所定义的模式模板(pattern template),主要用于文本匹配。

20.1.2 正则表达式的类型

使用正则表达式最大的问题在于有不止一种类型的正则表达式。Linux中的不同应用程序可能会用不同类型的正则表达式。
正则表达式是通过正则表达式引擎(regular expression engine)实现的。在Linux中,有两种流行的正则表达式引擎:

  • POSIX基础正则表达式(basic regular expression,BRE)引擎
  • POSIX扩展正则表达式(extended regular expression,ERE)引擎

sed只支持BRE,gawk支持ERE

20.2 定义 BRE 模式

20.2.1 纯文本

正则表达式模式都区分大小写。这意味着它们只会匹配大小写也相符的模式。

20.2.2 特殊字符

正则表达式识别的特殊字符包括:.*[]^${}\+?|()
不能在文本模式中单独使用这些字符,如果要用某个特殊字符作为文本字符,就必须转义。在转义特殊字符时,你需要在它前面加一个反斜线\来告诉正则表达式引擎应该将接下来的字符当作普通的文本字符:
在这里插入图片描述

最后要注意,尽管正斜线不是正则表达式的特殊字符,要使用正斜线,也需要进行转义。

20.2.3 锚字符

默认情况下,当指定一个正则表达式模式时,只要模式出现在数据流中的任何地方,它就能匹配。有两个特殊字符可以用来将模式锁定在数据流中的行首或行尾。

1. 锁定在行首

脱字符(^)定义从数据流中文本行的行首开始的模式。如果模式出现在行首之外的位置,正则表达式模式则无法匹配:
在这里插入图片描述
需要注意的是如果你将脱字符^放到模式开头之外的其他位置,那么它就跟普通字符一样,不再是特殊字符了。

2. 锁定在行尾

跟在行首查找模式相反的就是在行尾查找。特殊字符美元符$定义了行尾锚点。

3. 组合锚点

在一些常见情况下,可以在同一行中将行首锚点和行尾锚点组合在一起使用:
在这里插入图片描述
还有一种常用方式,就是将两个锚点直接组合在一起,之间不加任何文本,这样过滤出数据流中的空白行:

 sed '/^$/d' test.txt

20.2.4 点号字符

特殊字符点号用来匹配除换行符之外的任意单个字符。它必须匹配一个字符,如果在点号字符的位置没有字符,那么模式就不成立:
在这里插入图片描述

20.2.5 字符组

可以定义用来匹配文本模式中某个位置的一组字符。使用方括号来定义一个字符组。方括号中包含所有你希望出现在该字符组中的字符:
在这里插入图片描述

20.2.6 排除型字符组

在正则表达式模式中,也可以反转字符组的作用。可以寻找组中没有的字符,而不是去寻找组中含有的字符。要这么做的话,只要在字符组的开头加个脱字符^
在这里插入图片描述

20.2.7 区间

可以用单破折线符号在字符组中表示字符区间。只需要指定区间的第一个字符、单破折线以及区间的最后一个字符就行了:

[0-9] [a-z] [a-ch-m]

20.2.8 特殊的字符组

除了定义自己的字符组外,BRE还包含了一些特殊的字符组,可用来匹配特定类型的字符:
在这里插入图片描述

20.2.9 星号

在字符后面放置星号表明该字符必须在匹配模式的文本中出现0次或多次:

# !/bin/bash
# e出现0次
echo "ik" | sed -n '/ie*k/p' 
# e出现1次
echo "iek" | sed -n '/ie*k/p'
# e出现2次
 echo "ieek" | sed -n '/ie*k/p'
# e出现3次
 echo "ieeek" | sed -n '/ie*k/p'

星号还能用在字符组上。它允许指定可能在文本中出现多次的字符组或字符区间:

echo "baaeeet" | sed -n '/b[ae]*t/p'

20.3 扩展正则表达式

20.3.1 问号

问号类似于星号,不过有点细微的不同。问号表明前面的字符可以出现0次或1次,但只限于此。它不会匹配多次出现的字符。

20.3.2 加号

加号是类似于星号的另一个模式符号,但跟问号也有不同。加号表明前面的字符可以出现1次或多次,但必须至少出现1次。如果该字符没有出现,那么模式就不会匹配

20.3.3 使用花括号

ERE中的花括号允许你为可重复的正则表达式指定一个上限。这通常称为间隔(interval),可以用两种格式来指定区间:

  • m:正则表达式准确出现m次
  • m, n:正则表达式至少出现m次,至多n次

这个特性可以精确调整字符或字符集在模式中具体出现的次数:

# !/bin/bash
echo "bt" | awk --re-interval '/be{1}t/ {print $0}' 
echo "bet" | awk --re-interval '/be{1}t/ {print $0}' 
echo "beet" | awk --re-interval '/be{1}t/ {print $0}' 
echo "beeet" | awk --re-interval '/be{1,3}t/ {print $0}' 
echo "beeeet" | awk --re-interval '/be{1,3}t/ {print $0}' 

20.3.4 管道符号

管道符号允许你在检查数据流时,用逻辑OR方式指定正则表达式引擎要用的两个或多个模式。如果任何一个模式匹配了数据流文本,文本就通过测试。如果没有模式匹配,则数据流文本匹配失败:

expr1|expr2|...

比如:

 echo "The cat is asleep" | gawk '/cat|dog/{print $0}'

20.3.5 表达式分组

正则表达式模式也可以用圆括号进行分组。当你将正则表达式模式分组时,该组会被视为一个标准字符。可以像对普通字符一样给该组使用特殊字符:

 echo "Sat" | gawk '/Sat(urday)?/{print $0}'
 echo "Saturday" | gawk '/Sat(urday)?/{print $0}'

第 21 章 sed进阶

21.1 多行命令

在使用sed编辑器的基础命令时,你可能注意到了一个局限。所有的sed编辑器命令都是针对单行数据执行操作的。在sed编辑器读取数据流时,它会基于换行符的位置将数据分成行。sed编辑器根据定义好的脚本命令一次处理一行数据,然后移到下一行重复这个过程。有时需要对跨多行的数据执行特定操作。
sed编辑器包含了三个可用来处理多行文本的特殊命令:

  • N:将数据流中的下一行加进来创建一个多行组(multiline group)来处理
  • D:删除多行组中的一行
  • P:打印多行组中的一行

21.1.1 next 命令

1. 单行的next命令

小写的n命令会告诉sed编辑器移动到数据流中的下一文本行,而不用重新回到命令的最开始再执行一遍。
看下面的例子,你有个数据文件,共有5行内容,其中的两行是空的。目标是删除首行之后的空白行,而留下最后一行之前的空白行。如果写一个删掉空白行的sed脚本,你会删掉两个空白行:
在这里插入图片描述
解决办法是用n命令。在这个例子中,脚本要查找含有单词header的那一行。找到之后,n命令会让sed编辑器移动到文本的下一行,也就是那个空行。这时,sed编辑器会继续执行命令列表,该命令列表使用d命令来删除空白行。sed编辑器执行完命令脚本后,会从数据流中读取下一行文本,并从头开始执行命令脚本。因为sed编辑器再也找不到包含单词header的行了。所以也不会有其他行会被删掉:
在这里插入图片描述
注:此命令在mac中似乎无法执行

2. 合并文本行

21.1.2 多行删除命令

sed编辑器提供了多行删除命令D,它只删除模式空间中的第一行。该命令会删除到换行符(含换行符)为止的所有字符。

21.1.3 多行打印命令

多行打印命令(P)沿用了同样的方法。它只打印多行模式空间中的第一行。这包括模式空间中直到换行符为止的所有字符。

21.2 保持空间

模式空间(pattern space)是一块活跃的缓冲区,在sed编辑器执行命令时它会保存待检查的文本。
sed编辑器有另一块称作保持空间(hold space)的缓冲区域。在处理模式空间中的某些行时,可以用保持空间来临时保存一些行。有5条命令可用来操作保持空间:
在这里插入图片描述

21.3 排除命令

感叹号命令!用来排除(negate)命令,也就是让原本会起作用的命令不起作用:

# 不打印包含header的行
sed -n '/header/!p' data2.txt

21.4 改变流

通常,sed编辑器会从脚本的顶部开始,一直执行到脚本的结尾(D命令是个例外,它会强制sed编辑器返回到脚本的顶部,而不读取新的行)。sed编辑器提供了一个方法来改变命令脚本的执行流程,其结果与结构化编程类似。

21.4.1 分支

分支命令b的格式如下:[address]b [label]
address参数决定了哪些行的数据会触发分支命令。label参数定义了要跳转到的位置。如果没有加label参数,跳转命令会跳转到脚本的结尾。
在这里插入图片描述
上面例子中只有第一行和最后一行执行了相应的字符替换, 2-3行被跳过。
要是不想直接跳到脚本的结尾,可以为分支命令定义一个要跳转到的标签。标签以冒号开始,最多可以是7个字符长度::label
跳转命令指定如果文本行中出现了first,程序应该跳到标签为jump1的脚本行。如果分支命令的模式没有匹配,sed编辑器会继续执行脚本中的命令,包括分支标签后的命令(因此,所有的替换命令都会在不匹配分支模式的行上执行)。
在这里插入图片描述

21.4.2 测试

类似于分支命令,测试(test)命令(t)也可以用来改变sed编辑器脚本的执行流程。测试命令会根据替换命令的结果跳转到某个标签,而不是根据地址进行跳转。如果替换命令成功匹配并替换了一个模式,测试命令就会跳转到指定的标签。如果替换命令未能匹配指定的模式,测试命令就不会跳转。跟分支命令一样,在没有指定标签的情况下,如果测试成功,sed会跳转到脚本的结尾。
测试命令使用与分支命令相同的格式:[address]t [label]
在这里插入图片描述

21.5 模式替代

21.5.1 &符号

&符号可以用来代表替换命令中的匹配的模式。不管模式匹配的是什么样的文本,你都可以在替代模式中使用&符号来使用这段文本:

 echo "The cat sleeps in his hat." | sed 's/.at/"&"/g'

21.5.2 替代单独的单词

sed编辑器用圆括号来定义替换模式中的子模式。你可以在替代模式中使用特殊字符来引用每个子模式。替代字符由反斜线和数字组成。数字表明子模式的位置。sed编辑器会给第一个子模式分配字符\1,给第二个子模式分配字符\2,依此类推。此中模式通常和通配符配合使用:

echo "That furry cat is pretty" | sed 's/furry \(.at\)/\1/'
echo "That furry hat is pretty" | sed 's/furry \(.at\)/\1/'

21.6 在脚本中使用 sed

21.6.1 使用包装脚本

可以将sed编辑器命令放到shell包装脚本(wrapper)中,不用每次使用时都重新键入整个脚本。包装脚本充当
着sed编辑器脚本和命令行之间的中间人角色:

#!/bin/bash 
# 反转文本行
sed -n '1!G; h ; $p' $1 

在这里插入图片描述

21.6.2 重定向 sed 的输出

sed编辑器会将脚本的结果输出到STDOUT上。你可以在shell脚本中使用各种标准方法对sed编辑器的输出进行重定向。可以在脚本中用$()将sed编辑器命令的输出重定向到一个变量中,以备后用。下面的例子使
用sed脚本来向的阶乘计算结果添加逗号:

#!/bin/bash 
# Add commas to number in factorial answer 
# 
factorial=1 
counter=1 
number=$1 
# 
while [ $counter -le $number ] 
do 
 factorial=$[ $factorial * $counter ] 
 counter=$[ $counter + 1 ] 
done 
# 
result=$(echo $factorial | sed '
:start
s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
t start') 
# 
echo "The result is $result"

在这里插入图片描述

第 22 章 gawk进阶

22.1 使用变量

gawk编程语言支持两种不同类型的变量:内建变量与自定义变量

22.1.1 内建变量

1. 字段和记录分隔符变量

在这里插入图片描述
变量FS和OFS定义了gawk如何处理数据流中的数据字段。默认情况下,gawk将OFS设成一个空格,你可以通过设置OFS变量,在输出中使用任意字符串来分隔字段:
在这里插入图片描述
FIELDWIDTHS变量允许你不依靠字段分隔符来读取记录。在一些应用程序中,数据并没有使用字段分隔符,而是被放置在了记录中的特定列。这种情况下,必须设定FIELDWIDTHS变量来匹配数据在记录中的位置。一旦设置了FIELDWIDTH变量,gawk就会忽略FS变量,并根据提供的字段宽度来计算字段:
在这里插入图片描述
变量RS和ORS定义了gawk程序如何处理数据流中的字段。默认情况下,gawk将RS和ORS设为换行符。默认的RS值表明,输入数据流中的每行新文本就是一条新纪录。

2. 数据变量

在这里插入图片描述
跟shell变量不同,在脚本中引用gawk变量时,变量名前不加美元符:
在这里插入图片描述

22.1.2 自定义变量

gawk自定义变量名可以是任意数目的字母、数字和下划线,但不能以数字开头,变量名区分大小写。

1. 在脚本中给变量赋值

在gawk程序中给变量赋值跟在shell脚本中赋值类似,都用赋值语句:

gawk 'BEGIN{testing="this is a test"; print testing; testing=45; print testing}'
2. 在命令行上给变量赋值

这个特性可以让你在不改变脚本代码的情况下就能够改变脚本的行为。使用命令行参数来定义变量值会有一个问题。在你设置了变量后,这个值在代码的BEGIN部分不可用。可以用-v命令行参数来解决这个问题。

在这里插入图片描述

22.2 处理数组

gawk编程语言使用关联数组提供数组功能。关联数组跟数字数组不同之处在于它的索引值可以是任意文本字符串。你不需要用连续的数字来标识数组中的数据元素。相反,关联数组用各种字符串来引用值。每个索引字符串都必须能够唯一地标识出赋给它的数据元素。

22.2.1 定义数组变量

可以用标准赋值语句来定义数组变量。数组变量赋值的格式如下:var[index] = element,其中var是变量名,index是关联数组的索引值,element是数据元素值:

#!/bin/bash 
gawk 'BEGIN{
    capition["Illinois"]="SpringField"
    print capition["Illinois"]
}'

22.2.2 遍历数组变量

要在gawk中遍历一个关联数组,可以用for语句的一种特殊形式:

for (var in array) 
{ 
	statements 
}

重要的是记住这个变量中存储的是索引值而不是数组元素值。可以将这个变量用作数组的索引,轻松地取出数据元素值:

#!/bin/bash 
gawk 'BEGIN{
    var["a"] = 1
    var["b"] = 2
    var["c"] = 3
    var["d"] = 4
    for (key in var){
        print "Key:",key,"- Value:",var[key]
    }
}'

注意,索引值不会按任何特定顺序返回,但它们都能够指向对应的数据元素值。

22.2.3 删除数组变量

从关联数组中删除数组索引要用一个特殊的命令: delete array[index]:

#!/bin/bash 
 gawk 'BEGIN{ 
    var["a"] = 1 
    var["b"] = 2 
    for (key in var){ 
        print "Key:",key," - Value:",var[key] 
    } 
    delete var["b"] 
    print "---" 
    for (key in var){
        print "Key:",key," - Value:",var[key] 
    }
}'

22.3 使用模式

gawk程序支持多种类型的匹配模式来过滤数据记录,这一点跟sed编辑器大同小异。

22.3.1 正则表达式

在使用正则表达式时,正则表达式必须出现在它要控制的程序脚本的左花括号前:

在这里插入图片描述
gawk程序会用正则表达式对记录中所有的数据字段进行匹配,包括字段分隔符:
在这里插入图片描述
这并不总是件好事。它可能会造成如下问题:当试图匹配某个数据字段中的特定数据时,这些数据又出现在其他数据字段中。

22.3.2 匹配操作符

匹配操作符(matching operator)允许将正则表达式限定在记录中的特定数据字段。匹配操作符是波浪线~。可以指定匹配操作符、数据字段变量以及要匹配的正则表达式:``

$2 ~ /^data2/

$2变量代表记录中的第二个数据字段。这个表达式会过滤出第二个字段以文本data2开头的所有记录:
在这里插入图片描述
这可是件强大的工具,gawk程序脚本中经常用它在数据文件中搜索特定的数据元素。
你也可以用!符号来排除正则表达式的匹配:

$1 !~ /expression/

22.3.3 数学表达式

除了正则表达式,你也可以在匹配模式中用数学表达式。举个例子,如果你想显示所有属于root用户组(组ID为0)的系统用户,可以用这个脚本:

 gawk -F: '$4 == 0{print $1}' /etc/passwd

可以使用任何常见的数学比较表达式:

  • x == y
  • x <= y
  • x < y
  • x >= y
  • x > y

也可以对文本数据使用表达式,但必须小心。跟正则表达式不同,表达式必须完全匹配:

gawk -F, '$1 == "data11"{print $1}' test.txt

22.4 结构化命令

gawk编程语言支持常见的结构化编程命令。

22.4.1 if-else语句

格式如下:

if (condition){
    statement1
}else if (condition){
    statement2
}else {
    statement3
}

实例:

#!/bin/bash 
 gawk '{ 
    if ($1 > 20){
        print $1
    }else if($1 > 10){
        x = $1 * 2
        print x
    }
    else{
        x = $1 * 3
        print x
    }
}' test.txt

在这里插入图片描述

22.4.2 while 语句

格式如下:

while (condition) 
{ 
 statements 
}

gawk编程语言支持在while循环中使用break语句和continue语句,允许你从循环中跳出。

#!/bin/bash 
#求三个数的平均数
gawk '{ 
    total = 0 
    i = 1 
    while (i < 4) 
    { 
        total += $i 
        i++ 
    } 
    avg = total / 3 
    print "Average:",avg 
}' test.txt

在这里插入图片描述

22.4.3 do-while 语句

do 
{ 
	statements 
} while (condition)

这种格式保证了语句会在条件被求值之前至少执行一次。

22.4.4 for 语句

风格类似c语言

for( variable assignment; condition; iteration process){
	statements
}

2.5 格式化打印

使用printf可以格式化打印,用法与C语言类似:

printf "format string", var1, var2 . . .

在这里插入图片描述
实例:

#!/bin/bash 
gawk '{ 
    total = 0 
    for (i=1; i<4; i++){
        total = total + $i
    }
    avg = total / 3 
    printf "Average: %5.1f\n",avg 
}' test.txt

在这里插入图片描述

22.6 内建函数

此处只粗略列出内建函数,详细用法不在此举例。要了解更多gawk内建函数,可参考其官方文档。

22.6.1 数学函数

在这里插入图片描述

22.6.2 字符串函数

在这里插入图片描述
在这里插入图片描述

22.6.3 时间函数

在这里插入图片描述

22.7 自定义函数

22.7.1 定义函数

要定义自己的函数,必须用function关键字,函数名必须能够唯一标识函数。可以在调用的gawk程序中传给这个函数一个或多个变量:

function name([variables]) 
{ 
	statements 
}
function myrand(limit) 
{ 
 return int(limit * rand()) 
}

22.7.2 使用自定义函数

在定义函数时,它必须出现在所有代码块之前(包括BEGIN代码块)。乍一看可能有点怪异,但它有助于将函数代码与gawk程序的其他部分分开。

#!/bin/bash 
gawk '
function myprint() { 
    printf "%d - %d\n", $1, $3
} 
{ 
    myprint() 
}' test.txt

在这里插入图片描述

22.7.3 创建函数库

gawk提供了一种途径来将多个函数放到一个库文件中,这样你就能在所有的gawk程序中使用了。
首先,你需要创建一个存储所有gawk函数的文件test1.gawk:

function myprint() { 
    printf "%d - %d\n", $1, $3
} 

需要使用-f命令行参数来使用它们, 很遗憾,不能将-f命令行参数和内联gawk脚本放到一起使用,不过可以在同一个命令行中使用多个-f参数。
再建一个gawk脚本文件test2.gawk:

myprint() 

最后使用下面命令执行:

gawk -f test1.gawk -f test2.gawk test.txt

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值