第1章 初识linux shell

首先linux可划分为4部分:linux内核,GNU工具,图形化桌面环境,应用软件。下面逐一介绍前3部分:

内核

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

  • 系统内存管理:内核通过硬盘上的存储空间来实现虚拟内存,这块区域称为交换空间(swap space)。内核不断地在交换空间和实际的物理内存之间反复交换虚拟内存中的内容。这使得系统以为它拥有比物理内存更多的可用内存。
  • 软件程序管理:Linux操作系统将运行中的程序称为进程。内核创建了第一个进程(称为init进程)来启动系统上所有其他进程。当内核启动时,它会将init进程加载到虚拟内存中。内核在启动任何其他进程时,都会在虚拟内存中给新进程分配一块专有区域来存储该进程用到的数据和代码。一些Linux发行版使用一个表来管理在系统开机时要自动启动的进程。在Linux系统上,这个表通常位于专门文件/etc/inittab中。另外一些系统(比如Ubuntu )则采用/etc/init.d目录,将开机时启动或停止某个应用的脚本放在这个目录下。这些脚本通过/etc/rcX.d目录下的入口(entry)启动, 这里的X代表运行级(run level)。运行级为1时,只启动基本的系统进程以及一个控制台终端进程,我们称之为单用户模式。标准的启动运行级是3。在这个运行级上,大多数应用软件,比如网络支持程序,都会启动。另一个Linux中常见的运行级是5。在这个运行级上系统会启动图形化的X Window系统,允许用户通过图形化桌面窗口登录系统。
  • 硬件设备管理:Linux系统将硬件设备当成特殊的文件,称为设备文件。设备文件有3种分类:字符型设备文件, 块设备文件,网络设备文件
  • 文件系统管理:标准linux文件系统如下图所示,Linux服务器所访问的所有硬盘都必须格式化成表1-1所列文件系统类型中的一种。

2022-05-30T08:33:42.png

GNU工具

GNU组织(GNU是GNU’s Not Unix的缩写)开发了一套完整的Unix工具,但没有可以运行它们的内核系统。这些工具是在名为开源软件(open source software,OSS)的软件理念下开发的。

linux桌面环境

  1. X Window系统
  2. KDE桌面
  3. GNOME桌面
  4. Unity桌面
  5. Xfce桌面(和KDE很像,轻量级桌面)

linux发行版参考下图:

2022-05-30T08:36:15.png

2022-05-30T08:36:24.png

第2章 走进shell

访问CLI(命令行界面)通常有以下方法:

  • 通过Linux控制台终端访问CLI
  • 通过图形化终端仿真器访问CLI
  • 使用GNOME终端仿真器
  • 使用Konsole终端仿真器
  • 使用xterm终端仿真器

第3章 基本的bash shell命令

命令手册

man命令用来访问命令手册获取帮助,使用方法很简单,如man ls。不记得具体命令可用man -k keyword来搜索。

man手册页有对应的内容区域,区域编号及其分配内容如下:

1:可执行程序或shell命令
2:系统调用
3:库调用
4:特殊文件
5:文件格式与约定
6:游戏
7:概览,约定及杂项
8:超级用户和系统管理员命令
9:内核例程

可用man section cmd来访问对应区域的手册,如man 7 hostname。也可用man 1 intro来查看第1部分内容简介。此外info命令也可用来显示命令帮助。

浏览文件系统

2022-05-30T08:43:08.png

基本操作文件和目录的命令:

cd:切换目录(.和..代表当前目录和父目录)
ls:列出目录内容,常用选项有-aliR等
touch:创建文件,-a改变访问时间
cp:复制文件,-r递归复制
ln:创建链接
mv:移动/重命名文件
rm:删除文件,-r递归
mkdir:创建目录
rmdir:删除目录(只能删空目录)

查看文件内容命令:

file:显示文件类型
cat:显示文件内容(全部),常用选项有-nTb
more:查看文件内容(分页)
less:查看文件内容(分页)
head:查看文件内容(部分),常用-n 行数
tail:查看文件内容(部分),常用-n

第4章 更多的bash shell命令

探查进程

ps命令用来列出系统的进程状况,其参数有三种不同的风格:

Unix风格:ps -ef(列出系统上运行的所有进程),ps -l 以长格式输出
BSD风格:ps x 显示所有进程,ps l 长格式输出
GNU长参数风格:ps –forest 显示系统的层级信息

实时监测进程

top命令,键入q退出

结束进程

kill命令可通过进程ID(PID)给进程发送信号,大多数编写完善的程序都能接收和处理标准Unix进程信号,即:

2022-05-30T08:46:49.png

默认情况下,kill命令会向命令行中所有PID发送TERM信号,但如果有不服管教的进程的话,就需要强制终止,使用kill -s可以指定其他信号,如kill -s HUP 15275,也可以用kill -9 15275强制终止进程。

killall命令支持通过进程名来结束进程,如killall -r http*,谨慎使用。

监测磁盘空间

mount命令会提供以下4种信息:

媒体的设备文件名
媒体挂载到虚拟目录的挂载点
文件系统类型
已挂载媒体的访问状态

下面是手动挂载媒体设备的基本命令:

mount -t type device directory

手动将U盘/dev/sdb1挂载到/media/disk,可用下面的命令:

mount -t vfat /dev/sdb1 /media/disk

mount命令常用选项-o,支持添加以逗号分隔的额外选项,如重新挂载根目录为可读写:

mount -o rw,remount /

umount命令用来卸载设备,使用方法非常简单:

umount [directory | device]

df命令可以查看所有已挂载磁盘的使用情况, 常用选项-h

du命令可以查看某个目录(默认当前目录)的磁盘使用情况,常用选项有:

-c:显示出所有已列出文件总的大小
-h:以用户易读格式输出
-s:只显示每个参数的总计大小

处理数据文件:

sort命令可以排序大数据文件,常用选项有:

-n:以数字排序,如1 2 34等
-h:以易读数字排序,如1K,2M等
-M:以月份排序,如JAN<DEC
-r:逆向排序

此外,-k和-t参数在对按字段分隔的数据进行排序时非常有用,例如/etc/passwd文件。可以用-t 参数来指定字段分隔符,然后用-k参数来指定排序的字段。举个例子,要对 /etc/passwd根据用户ID进行数值排序,可以这么做:

sort -t ':' -k 3 -n /etc/passwd

grep命令可以用来搜索数据,常用选项有:

-n:显示匹配行号
-c:统计匹配次数
-e:指定(更多)匹配模式
-v:反向搜索

tar命令用来归档压缩文件,常用tar -zcvf target.tgz dir压缩,tar -zxvf解压,常用选项还有:

-t:列出归档文件内容
-p:保留所有文件权限
-d:从已有归档文件中删除
-r:追加文件到已有归档文件末尾

第5章 理解shell

shell的子父关系

登录后在命令行输入bash或zsh将打开一个子shell,子shell只有一部分环境继承了父shell,可以使用ps -f命令看出是否产生了子shell,如:

2022-05-30T08:56:47.png

可以看出在输入bash后产生了一个子shell,即bash,因为它的PPID(父进程ID号)是和登录shell zsh的进程ID是一样的。也可以用先前介绍的命令ps –forest来显示出进程的层级关系。

进程列表

在一行中运行一系列指定命令,只需把每条命令用分号隔开即可,这称为命令列表,如:

pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls

上述命令加上一个小括号,便变成了进程列表,即:

(pwd ; ls ; cd /etc ; pwd ; cd ; pwd ; ls)

进程列表会产生子shell,命令列表不会产生子shell;进程列表是一种命令分组,也可以使用花括号将命令括起来产生命令分组,但这种方式也不会产生子shell。

可以使用一个环境变量判断是否产生了子shell,在bash里,这个环境变量是BASH_SUBSHELL,其值是当前子shell的个数。

在shell脚本中,经常使用子shell进行多进程处理。但是采用子shell的成本不菲,会明显拖慢处理速度。在交互式的CLI shell会话中,子shell同样存在问题。它并非真正的多进程处理,因为终端控制着子shell的I/O。

别出心裁的子shell用法

在交互式shell中,一个高效的子shell用法就是使用后台模式。在后台模式中运行命令可以在处理命令的同时让出CLI,以供他用。使用后台模式只需在命令末尾加上一个&即可,如sleep 3000&,该命令会输出一个括在方括号中的后台作业号和一个进程id。jobs命令可以显示出当前运行在后台模式中的所有用户的进程(作业),jobs -l 还可以显示出进程的id号。

在CLI中运用子shell的创造性方法之一就是将进程列表置入后台模式。你既可以在子shell中进行繁重的处理工作,同时也不会让子shell的I/O受制于终端。使用tar(参见第4章)创建备份文件是有效利用后台进程列表的一个更实用的例子:

(tar -cf Rich.tar /home/rich ; tar -cf My.tar /home/christine)&

协程可以同时做两件事。它在后台生成一个子shell,并在这个子shell中执行命令。如:coproc sleep 3000

理解shell的内建命令

外部命令,有时候也被称为文件系统命令,是存在于bash shell之外的程序。它们并不是shell程序的一部分。外部命令程序通常位于/bin、/usr/bin、/sbin或/usr/sbin中。

当外部命令执行时,会创建出一个子进程。这种操作被称为衍生(forking)。外部命令ps很方便显示出它的父进程以及自己所对应的衍生子进程。

内建命令和外部命令的区别在于前者不需要使用子进程来执行。它们已经和shell编译成了一体,作为shell工具的组成部分存在。不需要借助外部程序文件来运行。可以使用type [-a]或which命令来查看命令类型,type -a显示了命令的每一种实现,which只显示了外部命令文件。

一个有用的内建命令是history,输入!!可以唤出历史命令中的最后一条命令(上一条命令),输入!23可以唤出历史命令中的第23条命令。

命令历史记录被保存在隐藏文件.bash_history中,它位于用户的主目录中。这里要注意的是,bash命令的历史记录是先存放在内存中,当shell退出时才被写入到历史文件中。可以在退出shell会话之前强制将命令历史记录写入.bash_history文件。要实现强制写入,需要使用history命令的-a选项。

如果你打开了多个终端会话,仍然可以使用history -a命令在打开的会话中向.bash_history文件中添加记录。但是对于其他打开的终端会话,历史记录并不会自动更新。这是因为.bash_history文件只有在打开首个终端会话时才会被读取。要想强制重新读取.bash_history文件,更新终端会话的历史记录,可以使用history -n命令。

第6章 使用linux环境变量

6.1 什么是环境变量

bash shell用一个叫作环境变量(environment variable)的特性来存储有关shell会话和工作环境的信息(这也是它们被称作环境变量的原因)。这项特性允许你在内存中存储数据,以便程序或shell中运行的脚本能够轻松访问到它们。这也是存储持久数据的一种简便方法。 环境变量一般分为全局变量和局部变量。

要查看全局环境变量,可使用envprintenv命令,查看单个环境变量只能使用printenv命令

局部环境变量只能在定义它们的进程中可见。set命令可显示某个特定进程设置的所有环境变量,包括局部变量,全局变量以及所有用户自定义变量。

6.2 设置环境变量

要设置全局变量,只需设置一个局部变量,再用export命令将其导出即可; 子shell中修改环境变量的值不会影响父shell中该变量的值,子shell中使用export命令也不会影响父shell的环境变量的值。

6.3 删除环境变量

unset命令用来删除环境变量,同样,这不会影响到父shell中的环境变量。

6.4 默认bash shell环境变量

2022-05-30T09:18:03.png

2022-05-30T09:18:13.png

2022-05-30T09:18:27.png

2022-05-30T09:18:38.png

6.5 设置PATH环境变量

PATH环境变量用来定义命令和程序的查找目录。常用办法是将当前目录加入PATH环境变量:

PATH=$PATH:.

6.6 定位系统环境变量

登录shell

当你登录Linux系统时,bash shell会作为登录shell启动。登录shell会从5个不同的启动文件里读取命令:

/etc/profile
$HOME/.bash_profile
$HOME/.bashrc
$HOME/.bash_login
$HOME/.profile

/etc/profile文件是系统上默认的bash shell的主启动文件。系统上的每个用户登录时都会执行这个启动文件。

另外4个文件为用户自定义文件,shell默认会按照下列顺序寻找文件,找到一个后立刻执行,剩下的都忽略:

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

注意上面没有.bashrc文件,因为这个文件通常会通过其他文件运行。

交互式shell

例如你输入bash命令时进入的子shell,这种情况下不会去读取etc/profile文件。

非交互式shell

例如执行shell脚本时,bash shell提供了BASH_ENV环境变量。当shell启动一个非交互式shell进程时,它会检查这个环境变量来查看要执行的启动文件。如果有指定的文件,shell会执行该文件里的命令,这通常包括shell脚本变量设置。如果该变量未设置,子shell将继承父shell的环境变量。

环境变量持久化

显然,将alias命令别名等设置在.bashrc文件中即可。

第7章 理解linux文件权限

7.1 linux的安全性

/etc/passwd 文件,在笔者的ubuntu系统上,这个文件内容大概如下:

2022-05-30T09:34:32.png

以冒号分隔的各个字段分别为:登录用户名:用户密码:用户账户的UID(数字形式):用户账户的组ID(GID)(数字形式) : 用户账户的文本描述(称为备注字段) : 用户HOME目录的位置 : 用户的默认shell

/etc/shadow 文件用来专门保留用户与其密码相关的信息。类似与/etc/passwd文件,各个字段分别为:

  • 与/etc/passwd文件中的登录名字段对应的登录名
  • 加密后的密码
  • 自上次修改密码后过去的天数密码(自1970年1月1日开始计算)
  • 多少天后才能更改密码
  • 多少天后必须更改密码
  • 密码过期前提前多少天提醒用户更改密码
  • 密码过期后多少天禁用用户账户
  • 用户账户被禁用的日期(用自1970年1月1日到当天的天数表示)
  • 预留字段给将来使用

添加新用户

使用useradd命令可方便添加用户并为新用户设置环境,系统默认值被设置在/etc/default/useradd文件中,使用useradd -D可查看这些的默认设置:

2022-06-02T03:41:55.png

有意思的是默认设置中的SKEL变量,创建时系统会将该变量值所在目录下(图中为/etc/skel)的所有文件(.bashrc等)复制到新用户的家目录,这允许你为新用户指定配置文件模板。

改变默认设置很简单:

useradd -D -b default_home
useradd -D -e expiration_date
useradd -D -f inactive # 密码过期到账户被禁用的天数
useradd -D -g group
useradd -D -s shell

创建新用户时命令行参数可覆盖默认设置,常用参数如:

useradd test -m -d /home/new -s /bin/bash -p $(openssl passwd -crypt "test_passwd") -e 12/10/2021

-m 表示为用户创建家目录 ,-d 指定用户家目录,-s 指定shell ,-p 指定加密后的密码。-e 指定用户账户将在2021年12月10日过期,格式也可为2021-12-10

删除用户

userdel 命令用来删除用户,常用选项为 -r 用来删除用户的家目录和邮件池; -f 用来强制删除一些文件

修改用户

  • usermod:选项大致和useradd一样,另外常用的有 -L 锁定账户,使其无法登录 -U 解锁账户 -l 修改登录用户名
  • passwd:直接跟用户名即可更改密码,默认为当前用户修改密码,-e 可指定强制使密码到期, -l/u 可锁定,解锁密码, -n 3 指定3天后才能修改密码,-x 5 指定5天后密码到期。
  • chpasswd:如果需要为系统中的大量用户修改密码,chpasswd命令可以事半功倍。chpasswd命令能从标准输入自动读取登录名和密码对(由冒号分割)列表,给密码加密,然后为用户账户设置。你也可以用重定向命令来将含有userid:passwd对的文件重定向给该命令。
  • chsh:chsh -s /bin/zsh 可修改登录shell,注意参数只能使用绝对路径
  • chfn:用来修改用户信息
  • chage:-E 指定过期日期,格式为2021-12-10 或 12/10/2021,-l 显示账户,密码过期信息。

7.2 使用linux组

/etc/group 文件:各字段为 组名:组密码:GID:属于该组的用户列表

组密码允许非组内成员通过它临时成为该组成员。这个功能并不很普遍,但确实存在。千万不能通过直接修改/etc/group文件来添加用户到一个组,要用usermod命令(在7.1节中介绍过)。在添加用户到不同的组之前,首先得创建组。

注:用户账户列表某种意义上有些误导人。你会发现,在列表中,有些组并没有列出用户。这并不是说这些组没有成员。当一个用户在/etc/passwd文件中指定某个组作为默认组时,用户账户不会作为该组成员再出现在/etc/group文件中。多年以来,被这个问题难倒的系统管理员可不是一两个呢。

创建与修改组:

  • groupadd shared命令用来创建新组shared
  • groupmod -n sharing shared命令可重命名该组
  • groupdel sharing 可删除该组

7.3 理解文件权限

对于ls -l 命令的输出,各个字段说明如下图:

2022-06-02T03:53:19.png

第一个字段有6种情况:-代表文件, d代表目录 , l代表链接 ,c代表字符型设备, b代表块设备, n代表网络设备

rwx分别代表读写执行权限,对应的数值分别为4,2,1

默认文件权限来自umask, umask可设置来屏蔽某些敏感权限,默认情况下umask为022, 则新创建的文件权限为666-022=644, 即-rw-r–r–, 新创建的目录权限为777-022=755, 即drwxr-x–x。

7.4 改变安全性设置

改变文件权限

使用chmod 755 test.sh或使用更复杂的模式:(注:-R选项可将权限设置递归作用于目录上)

chmod ugoa…[rwxXstugo] file

其中ugoa分别代表属主,属组,其他,全部; 而除了rwx外,其他的设置分别表示如下:

X:如果对象是目录或者它已有执行权限,赋予执行权限
s:运行时重新设置UID或GID
t:保留文件或目录
u:将权限设置为跟属主一样
g:将权限设置为跟属组一样
o:将权限设置为跟其他用户一样。

改变所属关系

使用chown命令可改变文件属主, 如chown -R -h clare test.sh; -R 表示递归处理, -h选项可以改变该文件的所有符号链接文件的所属关系。

使用chgrp命令可改变文件属组, 如chgrp shared test.sh.

7.5 共享文件

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

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

SGID位对文件共享非常重要。启用SGID位后,你可以强制在一个共享目录下创建的新文件都属于该目录的属组,这个组也就成为了每个用户的属组。 SGID可通过chmod命令设置。它会加到标准3位八进制值之前(组成4位八进制值),或者在符号模式下用符号s。例如:

2022-06-02T09:08:10.png

第8章 管理文件系统

8.1 探索linux文件系统

ext(extended filesystem)文件系统:

它为Linux提供了一个基本的类Unix文件系统:使用虚拟目录来操作硬件设备,在物理设备上按定长的块来存储数据。ext文件系统采用名为索引节点的系统来存放虚拟目录中所存储文件的信息。索引节点系统在每个物理设备中创建一个单独的表(称为索引节点表)来存储这些文件的信息。存储在虚拟目录中的每一个文件在索引节点表中都有一个条目。ext文件系统名称中的extended部分来自其跟踪的每个文件的额外数据,包括:

  • 文件名
  • 文件大小
  • 文件的属主
  • 文件的属组
  • 文件的访问权限
  • 指向存有文件数据的每个硬盘块的指针

此外,还有ext2,ext3,ext4,Reiser文件系统,JFS文件系统等文件系统。

8.2 操作文件系统

创建分区

有时候,创建新磁盘分区最麻烦的事情就是找出安装在Linux系统中的物理磁盘。Linux采用了一种标准格式来为硬盘分配设备名称,但是你得熟悉这种格式。对于老式的IDE驱动器,Linux使用的是/dev/hdx。其中x表示一个字母,具体是什么要根据驱动器的检测顺序(第一个驱动器是a,第二个驱动器是b,以此类推)。对于较新的SATA驱动器和SCSI驱动器,Linux使用/dev/sdx。其中的x具体是什么也要根据驱动器的检测顺序(和之前一样,第一个驱动器是a,第二个驱动器是b,以此类推)。在格式化分区之前,最好再检查一下是否正确指定了驱动器。

使用fdisk命令来管理分区,例如在笔者的腾讯云系统上:fdisk /dev/vda; 注:可使用ls /dev来查看硬盘设备名称。随后输入m来查看命令帮助。

fdisk中常用单字符命令有:p 显示存储设备的详细信息;n 创建新分区;l 列出可用分区类型;m 列出帮助。

使用n命令创建分区后使用w命令来将更改保存到设备上。在将数据存储到分区之前,你必须用某种文件系统对其进行格式化,这样Linux才能使用它。每种文件系统类型都用自己的命令行程序来格式化分区。表8-3列出了本章中讨论的不同文件系统所对应的工具。

2022-06-02T04:08:14.png

使用以下命令来挂载文件系统后就可以在新的分区中保存文件和目录了(注意这是在笔者的系统上, 你的系统中可能不是/dev/vda3)!

sudo mkfs.ext4 /dev/vda3 # 使用ext4文件系统格式化新创建的分区
sudo mkdir /mnt/my_partition # 在虚拟目录中创建挂载点
sudo mount -t ext4 /dev/vda3 /mnt/my_partition # 使用ext4文件系统类型将新分区挂载到挂载点

fsck命令能够检查和修复大部分类型的Linux文件系统。

8.3 逻辑卷管理

逻辑卷管理的核心在于如何处理安装在系统上的硬盘分区。在逻辑卷管理的世界里,硬盘称作物理卷(physical volume,PV)。每个物理卷都会映射到硬盘上特定的物理分区。多个物理卷集中在一起可以形成一个卷组(volume group,VG)。逻辑卷管理系统将卷组视为一个物理硬盘,但事实上卷组可能是由分布在多个物理硬盘上的多个物理分区组成的。卷组提供了一个创建逻辑分区的平台,而这些逻辑分区则包含了文件系统。整个结构中的最后一层是逻辑卷(logical volume,LV)。逻辑卷为Linux提供了创建文件系统的分区环境,作用类似于到目前为止我们一直在探讨的Linux中的物理硬盘分区。Linux系统将逻辑卷视为物理分区。

2022-06-02T04:14:04.png

图8-1中的卷组横跨了三个不同的物理硬盘,覆盖了五个独立的物理分区。在卷组内部有两个独立的逻辑卷。Linux系统将每个逻辑卷视为一个物理分区。每个逻辑卷可以被格式化成ext4文件系统,然后挂载到虚拟目录中某个特定位置。注意,图8-1中,第三个物理硬盘有一个未使用的分区。通过逻辑卷管理,你随后可以轻松地将这个未使用分区分配到已有卷组:要么用它创建一个新的逻辑卷,要么在需要更多空间时用它来扩展已有的逻辑卷。

Linux LVM是由Heinz Mauelshagen开发的,于1998年发布到了Linux社区。它允许你在Linux上用简单的命令行命令管理一个完整的逻辑卷管理环境。

最初的Linux LVM允许你在逻辑卷在线的状态下将其复制到另一个设备。这个功能叫作快照。在备份由于高可靠性需求而无法锁定的重要数据时,快照功能非常给力。遗憾的是,LVM1只允许你创建只读快照。一旦创建了快 照,就不能再写入东西了。

LVM2提供的另一个引人注目的功能是条带化(striping)。有了条带化,可跨多个物理硬盘创建逻辑卷。当Linux LVM将文件写入逻辑卷时,文件中的数据块会被分散到多个硬盘上。每个后继数据块会被写到下一个硬盘上。

LVM镜像是一个实时更新的逻辑卷的完整副本。当你创建镜像逻辑卷时,LVM会将原始逻辑卷同步到镜像副本中。根据原始逻辑卷的大小,这可能需要一些时间才能完成。一旦原始同步完成,LVM会为文件系统的每次写操作执行两次写入——一次写入到主逻辑卷,一次写入到镜像副本。

定义物理卷:

创建分区后使用t命令更改分区类型为Linux LVM, 可根据提示使用L命令找到该类型对应的序号。然后可使用下列命令来创建物理卷:

sudo pvcreate /dev/vda3 # 创建物理卷
sudo pvdisplay /dev/vda3 # 显示已创建的物理卷信息

创建卷组:

sudo vgcreate Voll /dev/vda3 # 创建卷组
sudo vgdisplay Voll # 显示已创建的卷组信息

创建逻辑卷:

sudo lvcreate -l 100%FREE -n lvtest Vol1 # 创建逻辑卷并命令为lvtest,指定其逻辑区段占比为100%
sudo lvdisplay Voll # 列出已创建的逻辑卷信息

创建文件系统:

sudo mkfs.ext4 /dev/Vol1/lvtest
sudo mount /dev/Voll/lvtest /mnt/my_partition

修改LVM:

Linux LVM的好处在于能够动态修改文件系统,因此最好有工具能够让你实现这些操作。在Linux有一些工具允许你修改现有的逻辑卷管理配置。

2022-06-02T04:25:10.png

第9章 安装软件程序

9.1 包管理基础

各种主流Linux发行版都采 用了某种形式的包管理系统来控制软件和库的安装。PMS利用一个数据库来记录各种相关内容:

  • Linux系统上已安装了什么软件包
  • 每个包安装了什么文件
  • 每个已安装软件包的版本。

PMS工具及相关命令在不同的Linux发行版上有很大的不同。Linux中广泛使用的两种主要的PMS基础工具是dpkg和rpm。基于Debian的发行版(如Ubuntu和Linux Mint)使用的是dpkg命令;基于Red Hat的发行版(如Fedora、openSUSE及Mandriva)使用的是rpm命令。

9.2 基于Debian的系统

到目前为止,最常用的命令行工具是aptitude,这是有原因的。aptitude工具本质上是apt工具和dpkg的前端。dpkg是软件包管理系统工具,而aptitude则是完整的软件包管理系统。

aptitude常用选项:

  • aptitude show package # 显示某个特定包的详细信息
  • aptitude install package # 安装软件包
  • aptitude search package # 搜素软件包
  • aptitude safe-upgrade
  • aptitude full-upgrade
  • aptitude dist-upgrade
  • aptitude purge wine # 删除软件包和相关的数据和配置文件
  • aptitude remove wine # 只删除软件包,不删除相关数据和配置文件

以下命令可以显示某个软件包所有相关的文件列表以及查询文件对应的软件包:

dpkg -L vim-common # 查询相关文件列表
dpkg --search /usr/share/doc/vim-common # 反向查询

软件仓库:

aptitude默认的软件仓库位置是在安装Linux发行版时设置的。具体位置存储在文件/etc/apt/sources. list中。

使用下面的结构来指定仓库源。

deb (or deb-src) address distribution_name package_type_list

deb或deb-src的值表明了软件包的类型。deb值说明这是一个已编译程序源,而deb-src值则说明这是一个源代码的源。address条目是软件仓库的Web地址。distribution_name条目是这个特定软件仓库的发行版版本的名称。package_type_list条目可能并不止一个词,它还表明仓库里面有什么类型的包。你可以看到诸如main、restricted、universe和partner这样的值。

9.3 基于 Red Hat 的系统

类似地,如果需要找出系统上的某个特定文件属于哪个软件包:yum provides file_name

要删除软件和它所有的文件,就用erase选项: yum erase package_name

有时在安装多个软件包时,某个包的软件依赖关系可能会被另一个包的安装覆盖掉。这叫作损坏的包依赖关系(broken dependency)。如果系统出现了这个问题,先试试下面的命令: yum clean all

或者列出依赖关系:yum deplist package_name

如果这样仍未解决问题,还有最后一招: yum update –skip-broken

9.4 从源码安装

下载源码 -> tar解压 -> 认真阅读readme文件 -> make or make install 即可。

第10章 使用编辑器

10.1 vim编辑器

vi编辑器是Unix系统最初的编辑器。它使用控制台图形模式来模拟文本编辑窗口,允许查看文件中的行、在文件中移动、插入、编辑和替换文本。尽管它可能是世界上最复杂的编辑器(至少讨厌它的人是这么认为的),但其拥有的大量特性使其成为Unix管理员多年来的支柱性工具。

使用readlink –f命令可以立即找出链接中的最后一环:

readlink -f /usr/bin/vi

安装了vim后即可输入vim test.sh来进入编辑器环境,vim编辑器有两种操作模式:普通模式和编辑模式

在插入模式下,vim会将你在当前光标位置输入的每个键都插入到缓冲区。普通模式下按下i键就可以进入插入模式。要退出插入模式回到普通模式,按下键盘上的退出键(ESC键,也就是Escape键)就可以了

在普通模式中,可以用方向键在文本区域移动光标(只要vim能正确识别你的终端类型)。如果你恰巧在一个古怪的没有定义方向键的终端连接上,也不是完全没有希望。vim中有用来移动光标的命令:

  • h:左移一个字符
  • j:下移一行(文本中的下一行)
  • k:上移一行(文本中的上一行)
  • l:右移一个字符
  • PageDown(或Ctrl+F):下翻一屏
  • PageUp(或Ctrl+B):上翻一屏
  • G:移到缓冲区的最后一行
  • num G:移动到缓冲区中的第num行
  • gg:移到缓冲区的第一行

保存退出

vim编辑器在普通模式下有个特别的功能叫命令行模式。命令行模式提供了一个交互式命令行,可以输入额外的命令来控制vim的行为。要进入命令行模式,在普通模式下按下冒号键。光标会移动到消息行,然后出现冒号,等待输入命令。在命令行模式下有几个命令可以将缓冲区的数据保存到文件中并退出vim。

  • q:如果未修改缓冲区数据,退出
  • q!:取消所有对缓冲区数据的修改并退出
  • w filename:将文件保存到另一个文件中
  • wq:将缓冲区数据保存到文件中并退出。

编辑数据

在普通模式下,vim编辑器提供了一些命令来编辑缓冲区中的数据。表10-1列出了一些常用的vim编辑命令。

2022-06-02T04:36:41.png

复制粘贴:

复制文本则要稍微复杂点。vim中复制命令是y(代表yank)。可以在y后面使用和d命令相同的第二字符(yw表示复制一个单词,y$表示复制到行尾)。在复制文本后,把光标移动到你想放置文本的地方,输入p命令。复制的文本就会出现在该位置。普通模式下可按v选择操作区域,按u撤销操作,按C-r恢复操作。

查找替换:

可以使用vim查找命令来轻松查找缓冲区中的数据。要输入一个查找字符串,在普通模式下按下斜线(/) 键。光标会跑到消息行,然后vim会显示出斜线。在输入你要查找的文本后,按下回车键。按n或N来查找下一个/上一个

在普通模式下输入:(冒号)进入命令模式,在普通模式下输入以下内容即可完成替换。

  • :s/old/new/g:一行命令替换所有old
  • :n,ms/old/new/g:替换行号n和m之间所有old
  • :%s/old/new/g:替换整个文件中的所有old
  • :%s/old/new/gc:替换整个文件中的所有old,但在每次出现时提示。

10.2 nano 编辑器

10.3 emacs 编辑器

emacs编辑器是一款极其流行的编辑器,甚至比Unix出现的都早。开发人员对它爱不释手,于是就将其移植到了Unix环境中,现在也移植到了Linux环境中。

和vim编辑器的不同之处在于:使用vim时,你必须不停地从插入模式中进出,从而在输入命令和插入文本之间切换;而emacs编辑器只有一个模式。如果你输入可打印字符,emacs就将它插入到光标当前位置;如果你输入一个命令,emacs就执行命令。如果未能正确检测方向键,有一些命令可用来移动光标。

  • C-p:上移一行(文本中的前一行)。
  • C-b:左移一字符
  • C-f:右移一字符
  • C-n:下移一行(文本中的下一行)。

还有一些命令能够让光标在文本中进行较长距离的跳跃(C表示ctrl,M表示alt)。

  • M-f:右移到下个单词
  • M-b:左移到上个单词
  • C-a:移至行首
  • C-e:移至行尾
  • M-a:移至当前句首
  • M-e:移至当前句尾
  • M-v:上翻一屏
  • C-v:下翻一屏
  • M-<:移至文本的首行
  • M->:移至文本的尾行

保存并退出:

  • C-x C-s:保存当前缓冲区到文件
  • C-z:退出emacs并保持在这个会话中继续运行,以便你切回
  • C-x C-c:退出emacs并停止该程序。

你会注意到这些功能中有两个需要两次键命令。C-x命令叫作扩展命令(extend command)。这为我们提供了另外一组命令。

编辑数据:

  • M-Backspace:剪切光标当前所在位置之前的单词
  • M-d:剪切光标当前所在位置之后的单词
  • C-k:剪切光标当前所在位置至行尾的文本
  • M-k:剪切光标当前所在位置至句尾的文本。

可按C-@来选择操作区域,可按C-/来撤销操作

复制粘贴:

当你用剪切命令剪切了某个数据后,将光标移动到你要粘贴数据的位置,用C-y来粘贴。这会将文本从临时区域取出并将其粘贴在光标所在的位置。C-y命令会取出最后一个剪切命令存下的文本。如果你执行了多个剪切命令,可以用M-y命令来循环选择它们。

查找替换:

在emacs编辑器中查找文本可用C-s和C-r命令。C-s命令是在会从缓冲区域中从光标当前位置到缓冲区尾部执行前向查找,而C-r命令会是从在缓冲区域中从光标从当前所在位置到缓冲区头部执行后向查找。

要用新字符串来替换一个已有文本字符串,就必须用M-x命令。这个命令需要一个文本命令和参数。该文本命令是replace-string。输入该命令并按下回车键,emacs会询问要替换的已有字符串。输入之后,再按一次回车键,emacs会询问用来替换的新字符串。

10.4 KDE 系编辑器

10.5 GNOME 编辑器

使用gedit命令即可,需要桌面环境。

第11章 构建基本脚本

创建shell脚本文件:

使用编辑器创建文件,赋予文件可执行权限,记得将文件所在目录加入PATH环境变量,否则你将需要使用绝对路径或相对路径来执行文件。

echo命令可用来输出数据,常用选项有-e 开启转义;-n 不自动换行。

除了环境变量,shell脚本还允许在脚本中定义和使用自己的变量。定义变量允许临时存储数据并在整个脚本中使用,从而使shell脚本看起来更像一个真正的计算机程序。用户变量可以是任何由字母、数字或下划线组成的文本字符串,长度不超过20个。用户变量区分大小写,所以变量Var1和变量var1是不同的。使用等号赋值,等号两边不能有多的空格,如myvar=1,不能是myvar = 1。shell中空语句是:(冒号)。

命令替换:

shell脚本中最有用的特性之一就是可以从命令输出中提取信息,并将其赋给变量。把输出赋给变量之后,就可以随意在脚本中使用了。这个特性在处理脚本数据时尤为方便。有两种方法可以实现这种功能:**使用反引号括住命令,或使用$(cmd)的形式**。如test=$(date),建议用这种形式,因为有些时候反引号容易被当作单引号。

注意:命令替换会创建一个子shell来运行对应的命令。子shell(subshell)是由运行该脚本的shell所创建出来的一个独立的子shell(child shell)。正因如此,由该子shell所执行命令是无法使用脚本中所创建的变量的。在命令行提示符下使用路径./运行命令的话,也会创建出子shell;要是运行命令的时候不加入路径,就不会创建子shell。如果你使用的是内建的shell命令,并不会涉及子shell。在命令行提示符下运行脚本时一定要留心!

重定向

使用>, >>, <, <<, 如:date > test.txt (不追加,会覆盖原来文件内容); date >> test.txt (追加至文件尾部); wc < test.txt (从文件中读取输入并计数)

其中,内联输入重定向符号是远小于号(<<)。除了这个符号,你必须指定一个文本标记来划分输 入数据的开始和结尾。任何字符串都可作为文本标记,但在数据的开始和结尾文本标记必须一致,并且结尾开头不能有任何空格,否则会被当做heredoc的内容。在命令行上使用内联输入重定向时,shell会用PS2环境变量中定义的次提示符(参见第6章) 来提示输入数据。如下图:

2022-06-02T05:04:16.png

管道

管道允许你将一个命令的输出重定向到另一个命令的输入,一个经典例子是:

cat text.txt | sort | more # 将text.txt的文件内容读取并进行排序并以分页形式显示

不要以为由管道串起的两个命令会依次执行。Linux系统实际上会同时运行这两个命令,在系统内部将它们连接起来。在第一个命令产生输出的同时,输出会被立即送给第二个命令。数据传输不会用到任何中间文件或缓冲区。

执行数学运算:

  • expr 3 + 5 或expr 3 * 5, 注意*必须进行转义,并且操作符两边必须至少有一个空格。
  • 使用方括号,如echo $[3+2*5], 注意bash中不支持浮点运算,zsh中支持。

浮点解决方案:

使用bc命令来执行浮点运算,如:result=$(echo “scale=4; 3.44 / 5” | bc) 或者使用<<将多行计算重定向到bc。

可使用exit n命令来指定脚本退出状态码,退出码参考值如下;

2022-06-02T05:13:55.png

第12章 使用结构化命令

if-then-elif-else-fi 结构。与其他编程语言不同的是,判断的条件是命令的返回状态码,为0则真,否则为假

使用test命令:if test $var;then echo yes; fi

使用方括号和test命令等效:if [ $var ]; then echo yes;fi 但要注意方括号和表达式之间至少要有一个空格

数值比较(不支持浮点数)

2022-06-02T05:20:05.png

字符串比较(> 和 < 需要转义)

2022-06-02T05:22:00.png

文件比较

2022-06-02T05:22:24.png

if-then的高级特性:用于数字表达式的双括号;用于高级字符串处理的双方括号

如:if (($a**2 > 90)); if [[ $s1 =~ $s2 ]]; # 判断s1是否包含s2 if [[ “$USER” == c* ]]; # 正则匹配

注意:双括号中不需要转义,括号与表达式之间不需要留空格,双方括号与表达式间需要空格

case语句

2022-06-02T05:25:42.png

;;相当于break语句, *相当于匹配default

第13章 更多的结构化命令

for val in list; do cmd; done # for循环中默认用空格区分每一个list中的值,所以含空格的值应被双引号包围,含单引号等shell特殊字符的应该用转义符号转义。

控制上述字段分隔符的是特殊的环境变量IFS,叫作内部字段分隔符(internal field separator) IFS环境变量定义了bash shell用作字段分隔符的一系列字符。默认情况下,bash shell会将下列字符当作字段分隔符:空格,制表符,换行符。

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

如果要指定多个IFS字符,只要将它们在赋值行串起来就行。 IFS=$’\n’:;” 这个赋值会将换行符、冒号、分号和双引号作为字段分隔符。如何使用IFS字符解析数据没有任何限制。

C语言风格的for循环:

for (( a = 1; a < 10; a++)); do echo $a; done # 这允许你使用和c语言一样的for结构,但后续引用变量仍需加$

while循环: while [ $a == “true” ] ; do echo yes; done

until循环: until [ $a -ge 3 ]; do echo yes; done

跳出循环可用break n; n表示循环深度,默认为1表示跳出当前循环,2表示上层循环。

跳过循环可用continue n语句。同break一样,该命令接收一个参数n。

可在循环末尾将输出重定向或使用管道处理输出。

两个例子, 寻找可执行文件:

#!/usr/bin/bash
IFS=:
for folder in $PATH; do
  echo "$folder: "
  for file in $folder/*; do
    if [ -x $file ]; then
      echo -e "\t$file"
    fi
  done
done

创建多个用户账户:

#!/usr/bin/bash 
# process new user accounts 
input="users.csv" 
while IFS=',' read -r userid name 
do 
echo "adding $userid" 
useradd -c "$name" -m $userid 
done < "$input"

第14章 处理用户输入

命令行参数:

$0表示当前运行脚本的名称(可能包含绝对路径,我们可使用basename来只获得脚本文件名)如下图所示:

2022-06-02T05:28:44.png

$1, $2, $3, … 分别表示传递给脚本或函数的第n个参数,当参数超过9个时,需要使用花括号表明变量的边界:${10}, ${11}。

参数计数与获取最后一个参数

$#表示参数的数量,要引用最后一个参数可用 **${!#}** ,不可用${$#}.

获取所有参数

$*变量会将命令行上提供的所有参数当作一个单词保存。这个单词包含了命令行中出现的每一个参数值。基本上$*变量会将这些参数视为一个整体,而不是多个个体。

$@变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。这样你就能够遍历所有的参数值,得到每个参数。这通常通过for命令完成。二者区别只在用双引号引用时,如图:

2022-06-02T05:30:04.png

移动变量

使用shift命令来将参数左移,经典遍历和跳过参数的方法如图:

2022-06-02T05:30:27.png

分离参数和选项

你会经常遇到想在shell脚本中同时使用选项和参数的情况。Linux中处理这个问题的标准方式是用特殊字符来将二者分开,该字符会告诉脚本何时选项结束以及普通参数何时开始。对Linux来说,这个特殊字符是双破折线(–)。shell会用双破折线来表明选项列表结束。在双破折线之后,脚本就可以放心地将剩下的命令行参数当作参数,而不是选项来处理了。例如删除文件名为”-test.sh”的文件:rm -rf — -test.sh

实现上述分离参数和选项也很简单:

#!/bin/bash 
# extracting 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 
# 
count=1 
for param in $@ 
do 
echo "Parameter #$count: $param" 
count=$[ $count + 1 ] 
done

在遇到双破折线时,脚本用break命令来跳出while循环。由于过早地跳出了循环,我们需要再加一条shift命令来将双破折线移出参数变量。随后shell会把所有双破折线后的变量当作参数。

合并选项

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

getopt optstring parameters

例如:

getopt ab:cd -a -b test1 -cd test2 test3

以上命令产生输出:-a -b test1 -c -d — test2 test3

optstring定义了四个有效选项字母:a、b、c和d。冒号(:)被放在了字母b后面,因为b选项需要一个参数值。当getopt命令运行时,它会检查提供的参数列表(-a -b test1 -cd test2 test3),并基于提供的optstring进行解析。注意,它会自动将-cd选项分成两个单独的选项,并插入双破折线来分隔行中的额外参数。如果参数列表中出现了optstring中没有的变量,getopt命令会产生错误输出,可用-q选项禁止显示该错误消息。

set命令的选项之一是双破折线(–),它会将命令行参数替换成set命令的命令行值。然后,该方法会将原始脚本的命令行参数传给getopt命令,之后再将getopt命令的输出传给set命令,用getopt格式化后的命令行参数来替换原始的命令行参数,看起来如下所示:

set — $(getopt -q ab:cd “$@”)

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

2022-06-02T05:32:34.png

如上图所示,遗憾的是,第二次传递的参数“test1 test2”并为得到正确处理,getopt命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。幸而还有另外一个办法能解决这个问题。

与getopt不同,前者将命令行上选项和参数处理后只生成一个输出,而getopts命令能够和已有的shell参数变量配合默契。每次调用它时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于0的退出状态码。这让它非常适合用解析命令行所有参数的循环中。其命令格式为:

getopts optstring variable

optstring值类似于getopt命令中的那个。有效的选项字母都会列在optstring中,如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,可以在optstring之前加一个冒号。getopts命令将当前参数保存在命令行中定义的variable中。getopts命令会用到两个环境变量。如果选项需要跟一个参数值,OPTARG环境变量就会保存这个值。OPTIND环境变量保存了参数列表中getopts正在处理的参数位置。这样你就能在处理完选项之后继续处理其他命令行参数了,注意此时case语句中不带单破折号,因为getopts已经在内部处理它了。

getopts命令知道何时停止处理选项,并将参数留给你处理。在getopts处理每个选项时, 它会将OPTIND环境变量值增一。在getopts完成处理时,你可以使用shift命令和OPTIND值来移动参数。

使用示例如图:

2022-06-02T05:34:31.png

可见此时不仅支持含空格的参数,还支持参数和选项之间无空格,这对使用者非常友好。

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

2022-06-02T05:35:39.png

获得用户输入

read命令从标准输入(键盘)或另一个文件描述符中接受输入。在收到输入后,read命令会将数据放进一个变量。下面是read命令的用法示例脚本,主要选项有-n -s -p -t

#!/usr/bin/bash

# 读取多个变量,空格或tab隔开
read -p "input: " p1 p2 p3
echo $p1 $p2 $p3
# 未指定变量,则保存到REPLY变量
read -p "Enter name:"
echo $REPLY
read -p "Enter some numbers:"
echo $REPLY

# 设置等待用户输入的超时
if read -t 5 -p "please enter your name (in 5 seconds):"; then
    echo "Hello $REPLY"
else
    echo
    echo "Sorry, too slow!"
fi

# 设置读取n个字符自动结束输入, 这里输入一个字符后无需回车即结束输入
read -n1 -p "Do you want to continue? [y/n] " ch
case $ch in
    Y|y) echo
        echo "fine, continue on..." ;;
    N|n) echo
        echo "ok, goodbye."
        exit;;
esac

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

最后从文件中读取,只需把cat命令用管道传给含read的while命令即可:

#!/usr/bin/bash
c=1
cat test.txt | while read line; do
  echo $c: $line
  let c++
done

第15章 呈现数据

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

默认情况下,STDERR文件描述符会和STDOUT文件描述符指向同样的地方(尽管分配给它们的文件描述符值不同)。也就是说,默认情况下,错误消息也会输出到显示器输出中。

重定向标准错误和标准输出示例(2> 和 1> 是一个整体,不能有空格!):

ls -al test test2 test3 badtest 2> test6 1> test7

当使用&>符(这是一个整体,不能有空格)时,命令生成的所有输出都会发送到同一位置,包括数据和错误:

ls -al test test2 test3 badtest &> test7

为了避免错误信息散落在输出文件中,相较于标准输出,bash shell自动赋予了错误消息更高的优先级。这样你能够集中浏览错误信息了,因此上述命令在test7文件中错误信息将集中在文件头部。

临时重定向

echo “this is an error message.” >&2

此处>和&2之间不能有空格,默认情况下,Linux会将STDERR导向STDOUT。但是,如果你在运行脚本时重定向了STDERR,脚本中所有导向STDERR的文本都会被重定向。

永久重定向

如果脚本中有大量数据需要重定向,那重定向每个echo语句就会很烦琐。取而代之,你可以用exec命令告诉shell在脚本执行期间重定向某个特定文件描述符: exec 1>testout

exec命令会启动一个新shell并将STDOUT文件描述符重定向到文件。脚本中发给STDOUT的所有输出会被重定向到文件。

类似地,在脚本中可以重定向标准输入到文件: exec 0<testfile

创建自己的重定向

在脚本中重定向输入和输出时,并不局限于这3个默认的文件描述符。我曾提到过,在shell中最多可以有9个打开的文件描述符。其他6个从3~8的文件描述符均可用作输入或输出重定向。你可以将这些文件描述符中的任意一个分配给文件,然后在脚本中使用它们。例如(类比c语言,将&看作取址符即可):

exec 3>testfile; echo “this will be redirected to testfile” >&3

于是我们可以重定向stdout后再恢复输出到stdout:

#!/usr/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"

创建文件描述符

#!/bin/bash 
# testing input/output file descriptor 
exec 3<> testfile 
read line <&3 
echo "Read: $line" 
echo "This is a test line" >&3

这个例子用了exec命令将文件描述符3分配给文件testfile以进行文件读写,注意使用<>时文件指针在文件首部。接下来,它通过分配好的文件描述符,使用read命令读取文件中的第一行,然后将这一行显示在STDOUT上。最后,它用echo语句将一行数据写入由同一个文件描述符打开的文件中。结果有些意外:

2022-06-02T05:44:45.png

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

关闭文件描述符

如果你创建了新的输入或输出文件描述符,shell会在脚本退出时自动关闭它们。然而在有些情况下,你需要在脚本结束前手动关闭文件描述符。这只需执行语句:exec 3>&-之后你就不能向该文件描述符写数据了。关闭文件描述符后如果要再次打开文件应考虑文件指针的位置,避免数据写入顺序错乱。

如果我们想追加数据到文件描述符对应的文件中,不能用 echo “again” >>&3,只能在分配文件描述符时选择追加,如:exec 3>>testfile; echo “again” >&3

注意:文件描述符和重定向操作符间不能有空格,重定向操作符和&之间不能有空格。

列出打开的文件描述符

如下所示:

#!/usr/bin/bash
exec 3> testfile3 
exec 6> testfile6
lsof -a -p $$ -d 0,1,2,3,6 # -p指定进程id,$$表示当前进程id,-d指定文件描述符

你可以通过将输出或错误重定向到/dev/null,所有重定向到该文件的数据都会被丢弃。

创建临时目录和临时文件

#!/bin/bash 
# using a temporary directory 
tempdir=$(mktemp -d dir.XXXXXX) 
cd $tempdir 
tempfile1=$(mktemp temp.XXXXXX) 
tempfile2=$(mktemp temp.XXXXXX) 
exec 7> $tempfile1 
exec 8> $tempfile2 
echo "Sending data to directory $tempdir" 
echo "This is a test line of data for $tempfile1" >&7 
echo "This is a test line of data for $tempfile2" >&8

mktemp命令选项-d创建目录,还可用-t选项指定返回创建文件的绝对路径(默认只返回文件名)。创建时shell会将X换成随机字符,保证了文件或目录的唯一性,至少要使用三个X。

记录消息:

tee命令相当于管道的一个T型接头。它将从STDIN过来的数据同时发往两处。一处是STDOUT,另一处是tee命令行所指定的文件名:

date | tee logfile -a

这样你即能将数据呈现给用户又能将数据保存起来供日后使用,-a指定追加内容(默认是覆盖)

实例脚本:

#!/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}

这个脚本很简短,直接从csv文件中读取用户数据,然后将insert语句写入sql文件中,相当实用。

第16章 控制脚本

处理信号

重温最常见的linux系统信号:

信号 值 描述
1 SIGHUP 挂起进程
2 SIGINT 终止进程
3 SIGQUIT 停止进程
9 SIGKILL 无条件终止进程
15 SIGTERM 尽可能终止进程
17 SIGSTOP 无条件停止进程,不是终止进程
18 SIGTSTP 停止或暂停进程,但不终止进程
19 SIGCONT 继续运行停止的进程

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

产生信号

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

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

当用Ctrl+Z组合键时,shell会通知你进程已经被停止了。shell将shell中运行的每个进程称为作业,并为每个作业分配唯一的作业号。它会给第一个作业分配作业号1,第二个作业号2,以此类推

捕获信号

使用trap命令来捕获并处理信号:

#!/bin/bash 
# Removing a set trap 
# 
trap "echo ' Sorry... Ctrl-C is trapped.'" SIGINT 
# 
count=1
while [ $count -le 5 ] 
do 
echo "Loop #$count" 
sleep 1 
count=$[ $count + 1 ] 
done 
# 
# Remove the trap 
trap -- SIGINT 
echo "I just removed the trap"
# or recover the default behavior of the signal
# trap - SIGINT 
# 
count=1 
while [ $count -le 5 ] 
do 
echo "Second Loop #$count" 
sleep 1 
count=$[ $count + 1 ] 
done

以后台模式运行脚本

只需在命令后加入&即可,最好将后台运行脚本的输出全部重定向。如果终端会话退出,那么后台进程也会随之退出

nohup命令运行了另外一个命令来阻断所有发送给该进程的SIGHUP信号。这会在退出终端会话时阻止进程退出。例如:

nohup ~/test.sh & # 命令别名不可用,注意环境变量的继承关系

和普通后台进程一样,shell会给命令分配一个作业号,Linux系统会为其分配一个PID号。区别在于,当你使用nohup命令时,如果关闭该会话,脚本会忽略终端会话发过来的SIGHUP信号。

作业控制

使用jobs命令来显示后台作业:

2022-06-02T06:32:52.png

你可能注意到了jobs命令输出中的加号和减号。带加号的作业会被当做默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号,该作业会被当成作业控制命令的操作对象。当前的默认作业完成处理后,带减号的作业成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业,不管shell中有多少个正在运行的作业。

在bash作业控制中,可以将已停止的作业(未终止,通常为按下ctrl+z的作业)作为后台进程或前台进程重启。前台进程会接管你当前工作的终端,所以在使用该功能时要小心了。只需使用bg或fg命令(加上作业号)即可。

调整谦让度

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

使用nice命令和renice命令来调整优先级,但普通用户不能增加优先级,只能renice自己的进程:

nice -n 10 nohup ~/test.sh &
renice -n 5 -p 465545

定时任务

使用at命令指定任务执行时间:

at -f test.sh 20.15

时间格式可以为:

10.15 pm
now, noon, midnight, teatime(4 pm)
mm/dd/yy, mmddyy, dd.mm.yy
jul 4, dec 25等
now + 7 (min | hour | day | year)

使用atq可查看at任务,使用atrm可删除任务。

使用cron执行周期任务:

使用crontab -e编辑cron表,详见:linux设置定时任务-crontab使用方法

第17章 创建函数

函数必须先定义后使用,函数名必须唯一,否则新定义会覆盖旧定义且不会产生错误信息。定义函数可用可选关键字function或直接写函数名即可,如:

#!/bin/bash 
# demonstrating the local keyword 
function func1 { 
local temp=$[ $value + 5 ] 
result=$[ $temp * 2 ] 
} 
temp=4 
value=6 
func1 
echo "The result is $result" 
if [ $temp -gt $value ] 
then 
echo "temp is larger" 
else 
echo "temp is smaller" 
fi

注意:此处result变量在函数内定义,但函数外仍能访问,即在脚本中定义的变量都是全局变量,为了避免这个影响,使用local关键字来声明这是个局部变量,如上所示,函数外temp依然等于4。

使用return语句可以从函数中带值返回(退出函数),但你必须**马上使用$?**才能捕获到这个返回值,因为$?保留的是上一条命令的返回值,可以随时用该变量检查上条命令是否执行成功。

数组参数获取与返回

#!/bin/bash
# adding values in an array
function addarray {
    local sum=0
    local newarray
    newarray=($(echo "$@"))
    for value in ${newarray[*]}
    do
        sum=$[ $sum + $value ]
    done
    echo $sum

}
myarray=(1 2 3 4 5)
echo "The original array is: ${myarray[*]}"
arg1=$(echo ${myarray[*]})
result=$(addarray $arg1)
echo "The result is $result"

如果你试图将该数组变量作为函数参数,函数只会取数组变量的第一个值。要解决这个问题,你必须将该数组变量的值分解成单个的值,然后将这些值作为函数参数使用。在函数内部,可以将所有的参数重新组合成一个新的变量。

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

#!/bin/bash
# returning an array value
function arraydblr {
    local origarray
    local newarray
    local elements
    local i
    origarray=($(echo "$@"))
    newarray=($(echo "$@"))
    elements=$[ $# - 1 ]
    for (( i = 0; i <= $elements; i++ ))
        {
            newarray[$i]=$[ ${origarray[$i]} * 2 ]

        }
    echo ${newarray[*]}

}
myarray=(1 2 3 4 5)
echo "The original array is: ${myarray[*]}"
arg1=$(echo ${myarray[*]})
result=($(arraydblr $arg1))
echo "The new array is: ${result[*]}"

递归函数(很少使用)

#!/bin/bash
# using recursion
function factorial {
    if [ $1 -eq 1 ]
    then
        echo 1
    else
        local temp=$[ $1 - 1 ]
        local result=$(factorial $temp)
        echo $[ $result * $1 ]
    fi
}

read -p "Enter value: " value
result=$(factorial $value)
echo "The factorial of $value is: $result"

创建库

其实就是创建一个包含很多常用函数的shell脚本,然后在新脚本中使用source myfun.sh 或 . myfun.sh来读取该shell,之后你就可以使用这些常用函数了。

source命令会在当前shell上下文中执行命令,而不是创建一个新shell,因此可以继承当前shell的环境变量

一个好办法是把自定义常用函数定义在~/.bashrc中,这样不管是交互式shell还是新启动的shell都可以使用。

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

创建菜单

我们可以写一个菜单函数来显示并读取用户的输入,但有时使用select命令会更方便。select命令会将每个列表项显示成一个带编号的选项,然后为选项显示一个由PS3环境变量定义的特殊提示符。例如:

#!/bin/bash
# using select in the menu
function diskspace {
    clear
    df -k

}
function whoseon {
    clear
    who

}
function memusage {
    clear
    cat /proc/meminfo

}
PS3="Enter option: "
select option in "Display disk space" "Display logged on users" "Display memory usage" "Exit program"
do
    case $option in
        "Exit program")
            break ;;
        "Display disk space")
            diskspace ;;
        "Display logged on users")
            whoseon ;;
        "Display memory usage")
            memusage ;;
        *)
            clear
            echo "Sorry, wrong selection";;
    esac
done
clear

实测这个菜单用起来不是很好,想要个性化一点,还是自己写一个吧!

制作窗口

主要使用dialog包,使用apt install dialog来安装即可,常用控件如下:

msgbox部件

dialog --title Testing --msgbox "This is a test" 10 20

yesno部件

dialog --title "Please answer" --yesno "Is this thing on?" 10 20 # yes返回0,no返回1

inputbox部件

dialog --inputbox "Enter your age:" 10 20 2>age.txt # ok返回0, cancel返回1

textbox部件

dialog --textbox /etc/passwd 15 45 # 只显示一个exit按钮

menu部件

dialog --menu "Sys Admin Menu" 20 30 10 1 "Display disk space" 
2 "Display users" 3 "Display memory usage" 4 "Exit" 2> test.txt # 20 30 分别指定宽和高 10指定一页最多显示的菜单项

fselect部件

dialog --title "Select a file" --fselect $HOME/ 10 50 2>file.txt # 使用空格键将选中的文件加入文本框中

最后来个简单菜单例子实现先前的管理员功能:

#!/bin/bash
# using dialog to create a menu
temp=$(mktemp -t test.XXXXXX)

temp2=$(mktemp -t test2.XXXXXX)
function diskspace {
    df -k > $temp
    dialog --textbox $temp 20 60

}
function whoseon {
    who > $temp
    dialog --textbox $temp 20 50

}
function memusage {
    cat /proc/meminfo > $temp
    dialog --textbox $temp 20 50

}
while [ 1 ]
do
    dialog --menu "Sys Admin Menu" 20 30 10 1 "Display disk space" 2 "Display users" 3 "Display memory usage" 0 "Exit" 2> $temp2 # 将用户选择的值写进临时文件供判断使用
    if [ $? -eq 1 ]
    then
        break
    fi
    selection=$(cat $temp2)
    case $selection in
        1)
            diskspace ;;
        2)
            whoseon ;;
        3)
            memusage ;;
        0)
            break ;;
        *)
            dialog --msgbox "Sorry, invalid selection" 10 30
    esac
done
clear

rm -f $temp 2> /dev/null
rm -f $temp2 2> /dev/null

结合先前几个部件我们已经能够写出很易用的图形化脚本了。

KDE环境

主要使用的是kdialog包,它和dialog包很像,区别是它使用STDOUT来输出值,而不是STDERR。

GNOME 环境

主要使用的是gdialog和zenity,其中zenity是大多数GNOME桌面Linux发行版上最常见的包,zenity命令行程序与kdialog和dialog程序的工作方式有些不同。许多部件类型都用另外的命令行选项定义,而不是作为某个选项的参数。举个例子:

#!/bin/bash
# using zenity to create a menu
temp=$(mktemp -t temp.XXXXXX)
temp2=$(mktemp -t temp2.XXXXXX)

function diskspace {
    df -k > $temp
    zenity --text-info --title "Disk space" --filename=$temp --width 750 --height 10
}

function whoseon {
    who > $temp
    zenity --text-info --title "Logged in users" --filename=$temp --width 500 --height 10
}

function memusage {
    cat /proc/meminfo > $temp
    zenity --text-info --title "Memory usage" --filename=$temp --width 300 --height 500
}

while [ 1 ]
do
    zenity --list --radiolist --title "Sys Admin Menu" --column "Select" --column "Menu Item" FALSE "Display diskspace" FALSE "Display users" FALSE "Display memory usage" FALSE "Exit" > $temp2
    if [ $? -eq 1 ]
    then
        break
    fi
    selection=$(cat $temp2)
    case $selection in
        "Display disk space")
            diskspace ;;
        "Display users")
            whoseon ;;
        "Display memory usage")
            memusage ;;
        Exit)
            break ;;
        *)
            zenity --info "Sorry, invalid selection"
    esac
done

第19章 初识sed和gawk

sed编辑器

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

sed编辑器可以根据命令来处理数据流中的数据,这些命令要么从命令行中输入,要么存储 在一个命令文本文件中。sed编辑器会执行下列操作:

(1) 一次从输入中读取一行数据。

(2) 根据所提供的编辑器命令匹配数据。

(3) 按照命令修改流中的数据。

(4) 将新的数据输出到STDOUT。

sed命令的格式: sed options script file

script参数指定了应用于流数据上的单个命令。如果需要用多个命令,要么使用-e选项在命令行中指定,要么使用-f选项在单独的文件中指定。

执行替换

sed -e 's/brown/green/; s/dog/cat/' data1.txt 
# -e指定执行不止一条编辑命令,s表示替换,也可把编辑命令放入文件,用-f指定即可。

替换标记

sed 's/brown/green/2' data.txt # 2 表示每行匹配到的第2个地方替换,不指定时只替换每行的第一次匹配
sed 's/brown/green/g' data.txt # g 表示每行匹配到的所有地方进行替换,即全文替换
sed -n 's/brown/green/p' data.txt # -n 表示不将结果输出,p 表示打印出匹配行,二者结合将只显示匹配行
sed -n 's/brown/green/w file' data.txt # w file 表示将结果保存到file中,会覆盖file,且只将匹配到的行保存(-n无法影响)

替换特殊字符

sed 's!/bin/bash!/bin/csh!' /etc/passwd # 将!作为替换分割符,/将不再需要转义,也可使用其他分隔符,非常方便

使用行寻址

sed -n '2s/dog/cat/gp' # 默认sed会作用于所有行,这样你可以指定sed只作用于第2行,或者'2,5s/dog/cat/gp'指定行区间
sed -n '2,$s/dog/cat/gp' # 指定行区间为第2行到最后一行

使用模式过滤

# 例如,如果你只想修改clare用户的登录shell:
sed -n '/clare/s/bash/zsh/p' # 此处必须使用/, 类似于vim的搜索,先搜索到指定行,再指定sed只作用于指定行

命令组合

sed -n '3,5{s/red/green/ ; s/dog/cat/p}' test.txt # 只需将命令扩在花括号中,分号或在命令行中换行隔开即可

删除行

sed '/1 this/d' test.txt # 删除匹配到的行,你可使用'd'直接删除所有行,也可以指定行区间
sed '/1/, /3/d' test.txt 
# 此处指定两个匹配模式,逗号隔开,但你指定的第一个模式会“打开”行删除功能,第二个模式会“关闭”行删除功能。
# sed编辑器会删除两个指定行之间的所有行(包括指定的行),谨慎使用
# 当使用两个匹配模式时,第一个模式表示打开执行的操作,第二个模式表示结束上述操作,不熟悉请勿使用这种模式

插入和追加文本

使用i和a来执行插入和追加,插入是在行前插入,追加则是在行后追加:

sed '3i\this is a new line' test.txt # 在第3行前插入数据,注意i后的\表示续行符,不使用也行,但使用了看起来更清楚
sed '3a\this is another new line' test.txt # 在第3行后追加数据,使用$a则表示在最后一行后追加
sed '/red/i\new line\n' test.txt # 在指定匹配到的行前插入数据,此处还支持末尾的换行符

修改行

sed '3c\this is a new line' test.txt # 修改第3行的数据,使用$c则表示修改最后一行,使用区间则只会将区间的数据全部替换

转换命令

sed '3,$y/123/789/' test.txt # 将指定行的123分别映射为789,只支持字符转换,两个字符串长度必须一致

打印

最后看看使用p,=,l来打印行:

sed -n '3, ${ /this/p; s/this/that/gp }' test.txt # 先将匹配的行打印,替换后再打印,实现对比效果
sed -n '3, ${ /this/=; /this/p; s/this/that/gp }' # =命令可以打印出匹配到的行号,这样更容易知道匹配了哪些行
sed -n '3, ${ /this/l; s/this/that/gp }' # l(list)可以打印不可见字符,如\t,\n等

读取命令

使用读取命令r我们可以将一个占位文本替换为一个文件中的内容:

2022-06-02T08:10:23.png

gawk程序

gawk程序是Unix中的原始awk程序的GNU版本。gawk程序让流编辑迈上了一个新的台阶,它提供了一种编程语言而不只是编辑器命令。

gawk程序的基本格式如下: gawk options program file

可用选项有:

-F fs:指定字段分隔符
-f file:从指定file读取程序
-v var=value:指定一个gawk变量及其值
-mf N:指定要处理的最大字段数
-mr N:指定要处理的最大行数
-w keyword:指定兼容模式或警告等级

gawk自动给每行元素分配一个变量:$0代表整个文本行,$1, $n代表第1,第n个数据字段

显示所有用户:

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

可以使用BEGIN和END指定要在数据处理前或数据处理后执行的语句,如:

BEGIN { 
print "The latest list of users and shells" 
print " UserID \t Shell" 
print "-------- \t -------" 
FS=":" 
} 
{ 
print $1 " \t " $7 
} 
END { 
print "This concludes the listing" 
}

将上述代码保存为script.gawk,然后执行该脚本:

gawk -f script.gawk /etc/passwd

这将打印出一份用户及其shell的报告,第22章将进一步介绍gawk的高级使用方法。

第20章 正则表达式

正则表达式是你所定义的模式模板(pattern template),Linux工具可以用它来过滤文本。Linux工具(比如sed编辑器或gawk程序)能够在处理数据时使用正则表达式对数据进行模式匹配。如果数据匹配模式,它就会被接受并进一步处理;如果数据不匹配模式,它就会被滤掉。

使用正则表达式最大的问题在于有不止一种类型的正则表达式。Linux中的不同应用程序可能会用不同类型的正则表达式。这其中包括编程语言(Java、Perl和Python)、Linux实用工具(比如sed编辑器、gawk程序和grep工具)以及主流应用(比如MySQL和PostgreSQL数据库服务器)

正则表达式是通过正则表达式引擎(regular expression engine)实现的。正则表达式引擎是一套底层软件,负责解释正则表达式模式并使用这些模式进行文本匹配。在Linux中,有两种流行的正则表达式引擎:

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

大多数Linux工具都至少符合POSIX BRE引擎规范,能够识别该规范定义的所有模式符号。遗憾的是,有些工具(比如sed编辑器)只符合了BRE引擎规范的子集。这是出于速度方面的考虑导致的,因为sed编辑器希望能尽可能快地处理数据流中的文本。gawk程序则用ERE引擎来处理它的正则表达式模式。

正则表达式的特殊字符包括(意味着你必须转义):

.*[]^${}+?|()

锁定行首和行尾

echo "this is a great book" | sed -n '/^this/p' # ^表示指定数据必须出现在行首才匹配
echo "this ^ is a great book" | sed -n '/this ^/p' # ^未在行首时不需要转义也能匹配
echo "this is a great book" | sed -n '/book$/p' 
# $表示数据必须出现在行尾才匹配,注意是从后往前匹配,若是books则无法匹配,同样,$未出现在行尾时也不需要转义。
echo -e "this is a test line\n\n\n another line" | sed -n '/^$/p'
# ^$是匹配空行的好办法

点号特殊字符

echo "at ten o'clock we'll go home" | sed -n '/.at/p' # .必须匹配除换行符外的任意一个字符,否则就不能成立
echo "this test is at line four" | sed -n '/.at/p' # 这会匹配,因为在正则表达式中空格是普通字符

字符组

使用方括号来定义字符组,字符组必须有一个字符来匹配相应的位置

echo "Yes" | sed -n '/[Yy][Ee][Ss]/p' # yes的任何大小写都将匹配
echo "yes" | sed -n '/[^y][Ee][Ss]/p' # 使用了脱字符后将匹配不属于该字符组的情况,此处yes将不能匹配
echo "this is a fat cat and a hat" | sed -n '/[a-ch-m]at/p' # 此处只匹配a-c,h-m的情况,所以fat不会被匹配

除了自定义字符组,BRE支持以下特殊字符组:

  • [[:alpha:]]:匹配任意字母,不区分大小写
  • [[:alnum:]]:匹配0-9,a-z,A-Z
  • [[:blank:]]:匹配空格或制表符
  • [[:digit:]]:匹配0-9
  • [[:lower:]]:匹配a-z
  • [[:upper:]]:匹配A-Z
  • [[:print:]]:匹配任意可打印字符
  • [[:punct:]]:匹配标点符号
  • [[:space:]]:匹配任意空白字符:空格,制表符,NL,FF,VT和CR

星号

echo "Yeeees" | sed -n '/Ye*s/p' # *表示字符e可以出现0次或多次
echo "this is a great book" | sed -n '/a.*book/p' # .*表示任意字符出现任意次
echo "this is a teesesesst" | sed -n '/te[es]*t/p' # 这表示es的任意组合或者不出现都能匹配

扩展正则表达式

POSIX ERE模式包括了一些可供Linux应用和工具使用的额外符号。gawk程序能够识别ERE模式,但sed编辑器不能。除非你使用sed的-r选项开启该支持

问号

echo "Yeeees" | sed -r -n '/Ye?s/p' # ?表示字符e可以出现0次或1次
echo "this is a great book" | sed -r -n '/boo[ks]?/p' # ks中只能一个字符出现0次或1次,都出现或其中一个出现两次则不行

加号和问号很类似,它表示模式出现至少1次或多次,又称作贪婪匹配

花括号

ERE中的花括号允许你为可重复的正则表达式指定一个上限。这通常称为间隔(interval)。可以用两种格式来指定区间:{m}表示表达式准确出现m次,{m,n}表示至少出现m次,最多出现n次

默认情况下,gawk程序不会识别正则表达式间隔。必须指定gawk程序的–re-interval命令行选项才能识别正则表达式间隔:

echo "beet" | gawk --re-interval '/be{1}t/{print $0}' # e只能准确出现一次,此处不匹配
echo "beet" | sed -rn '/be{1,2}t/p' # e能出现1次到2次,所以此处能匹配

管道符号

echo "He has a hat." | gawk '/[ch]at|dog/{print $0}' # 此处能匹配cat, hat, 和dog

表达式分组

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

echo "Sat" | gawk '/Sat(urday)?/{print $0}'
echo "Saturday" | gawk '/Sat(urday)?/{print $0}' # 此处能处理Saturday及其缩写Sat
echo "i have a cat and a dog." | sed -rn '/(cat|dog)?\./p' # 此处表示匹配句号前有0个或1个的cat或dog

实例

目录文件计数:

#!/bin/bash

count=0
for dir in $(echo $PATH | sed 's/:/ /g'); do
    if [ -d $dir ]; then
        for j in $(ls $dir); do
            let count++
        done
        echo "directory $dir has $count files."
        count=0
    fi
done

电子邮件验证:

邮件地址基本格式为: username@hostname

username值可用字母数字字符以及这些特殊字符: 点号(.),单破折号(-),加号(+),下划线(_)

对于username及@符号,我们可以这样匹配:

^([a-zA-Z0-9_-.+]+)@

hostname模式使用同样的方法来匹配服务器名和子域名:

([a-zA-Z0-9_-.]+)

对于顶级域名,有一些特殊的规则。顶级域名只能是字母字符,必须不少于二个字符(国家或地区代码中使用),并且长度上不得超过五个字符。下面就是顶级域名用的正则表达式模式:

.([a-zA-Z]{2,5})$

连在一起则有:

^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$

第21章 sed进阶

多行命令

n(next)命令用来定位到下一行,例如你想删除含有单词header的下一行数据:

sed '/header/{n ; d}' data1.txt

多行版本的next命令(用大写N)会将下一文本行添加到模式空间中已有的文本后(换行符后)。

如果想要查找的短语分散在两行(这种情况并不少见),就需要用到N命令,考虑如下字段:

On Tuesday, the Linux System

Administrator’s group meeting will be held.

All Desktop Users should attend.

你想查找特定的双词短语System Administrator,可以这样做:

sed 'N ; s/System.Administrator/Desktop User/' data3.txt

使用了通配符.来匹配空格或换行这两种情况,如果你想保留换行符,则在N命令后先加一个保留换行符的替换语句即可,如果短语在最后一行,N命令会错过它,因为没有新的文本行加入模式空间,可以这样解决:

sed ' 
> s/System Administrator/Desktop User/ 
> N 
> s/System\nAdministrator/Desktop\nUser/ 
> ' data4.txt # 注意>表示按下回车的次提示符, 此处不可直接按按钮复制使用

单行删除命令d当和N一起使用时,将删除模式空间中的两行,只删除一行需用D命令

同理,多行打印命令P和p差异大同小异

保持空间

sed编辑器有另一块称作保持空间(hold space)的缓冲区域。在处理模式空间中的某些行时,可以用保持空间来临时保存一些行。有5条命令可用来操作保持空间,见表21-1。

2022-06-02T08:32:29.png

假设我们有文件data2.txt, 内容如下:

This is the header line. 
This is the first data line. 
This is the second data line. 
This is the last line.

执行命令:

sed -n '/first/ {h ; p ; n ; p ; g ; p }' data2.txt

输出结果如下:

This is the first data line.

This is the second data line.

This is the first data line.

可见,我们可以通过保持空间在模式空间和保持空间中来回移动文本行,这是个有用的开端。

排除命令

使用感叹号可以使命令的作用反转,例如1p将打印第一行,而1!p将打印除了第一行的所有行,举个例子,你想反转数据流中的所有行,不使用tac命令:

sed -n '{1!G ; h ; $p }' data2.txt

使用1!G来禁止保持空间附加到模式空间的第一行,否则输出将多出一个空行,上述命令过程如图:

2022-06-02T08:34:42.png

改变流

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

分支命令:

2022-06-02T08:35:42.png

测试命令

2022-06-02T08:37:08.png

模式替代

&符号可以用来代表替换命令中的匹配的模式。不管模式匹配的是什么样的文本,你都可以在替代模式中使用&符号来使用这段文本。这样就可以操作模式所匹配到的任何单词了。如:

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

&符号会提取匹配替换命令中指定模式的整个字符串。有时你只想提取这个字符串的一部分。

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

echo "The System Administrator manual" | sed 's/\(System\) Administrator/\1 User/' 
# 结果为:The System User manual

另一个经典妙用,在大数字中插入逗号:

2022-06-02T08:39:08.png

在脚本中使用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" 
#

创建sed实用工具

sed '$!G' data2.txt # 加倍行距,排除最后一行
sed '/^$/d ; $!G' data6.txt # 先删除空白行,再加倍行距,避免出现过多空白行
sed '=' data2.txt | sed 'N; s/\n/ /' # 增加行号,=将默认显示行号独占一行,使用N使其显示在同一行
sed '/./,/^$/!d' data8.txt # 删除连续空行,只保留一个空行
sed '/./,$!d' data9.txt # 删除开头的空白行
sed '{ 
:start 
/^
\n*$/{$d; N; b start } 
}' data.txt # 删除尾部的空白行
sed 's/<[^>]*>//g ; /^$/d' data11.txt # 删除html标签和空白行

再看这个例子:

2022-06-02T08:40:38.png

这个脚本会首先检查这行是不是数据流中最后一行。如果是,退出(quit)命令会停止循环。N命令会将下一行附加到模式空间中当前行之后。如果当前行在第10行后面,11,$D命令会删除模式空间中的第一行。这就会在模式空间中创建出滑动窗口效果。因此,这个sed程序脚本只会显示出data7.txt文件最后10行。

第22章 gawk进阶

使用变量

2022-06-02T08:41:55.png

FS和OFS使用例子

FS和OFS使用例子

如果设置了FIELDWIDTHS,gawk就会忽略FS变量,如:

echo "1005.3247596.37" | gawk 'BEGIN{FIELDWIDTHS="3 5 2 5"}{print $1,$2,$3,$4}'

RS使用举例

cat > test.txt << EOF
Riley Mullen 
123 Main Street 
Chicago, IL 60601 
(312)555-1234

Frank Williams 
456 Oak Street 
Indianapolis, IN 46201 
(317)555-9876
EOF
gawk 'BEGIN{FS="\n"; RS=""} {print $1,$4}' test.txt

上例中gawk把文件中的每行都当成一个字段,把空白行当作记录分隔符。

2022-06-02T08:44:25.png

可以通过下列例子来体会这些内建变量的作用:

gawk 'BEGIN{print ARGC,ARGV[1]}' data1
gawk 'BEGIN{ 
print ENVIRON["HOME"]; print ENVIRON["PATH"] }'
gawk 'BEGIN{FS=":"; OFS=":"} {print $1,$NF}' /etc/passwd # NF变量让你轻松引用最后一个字段的值
# 注意, $是引用字段时使用,不是使用变量,gawk中引用变量不需要加$, 如上面第1, 2行所示
# FNR变量为当前处理的记录数计数,参数提供多个文件时,其值处理完每个文件后都会被重置,NR变量同理,但不重置

你也可以使用自定义变量,并且可以在命令行上实时给变量赋值,经典用法是先用-v参数指定变量的值,然后-f指定脚本,你指定的变量值将在指定脚本中的BEGIN部分也可用。

处理数组

gawk支持关联数组,并使用这种格式遍历数组:for (var in array) { statements },使用delete语句删除数组变量

使用模式

echo "data11,data12\ndata21,data22" | gawk 'BEGIN{FS=","} /11/{print $1}'

匹配模式必须出现在处理语句花括号的左边,也可以使用字段分隔符进行匹配,但可能出现意想不到的结果

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

gawk -F: '$1 ~ /rich/{print $1,$NF}' /etc/passwd # 表示在第一个字段中查找匹配,此处即查找用户名含rich的用户
# $1 !~ /expression/ 也可以这样排除查找到的记录

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

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

结构化命令

gawk支持类c的if-else; while; for(i=0;i<3;i++); do-while结构化语句,举个例子,求数据行中每一行的平均值:

echo "1 2 3\n4 5 6\n7 8 9" | gawk '{total=0; for (i=1; $i!=$NF; i++) { total += $i }; total+=$NF; avg=total/i; print avg;}' # 注意循环条件,需加上最后一个字段的值

格式化打印

采用类c的printf即可进行格式化打印

内建函数

2022-06-02T08:48:34.png

2022-06-02T09:19:31.png

2022-06-02T08:48:54.png

2022-06-02T08:49:19.png

2022-06-02T08:49:38.png

自定义函数

使用function name([args])来定义函数,先定义后使用,可多次使用-f选项引用函数库

最后修改:2022 年 06 月 02 日
如果觉得我的文章对你有用,请随意赞赏