0%

Linux命令行与shell脚本编程大全

缘起

​ 室友在某次项目中海量的机械重复操作,使我意识到使用脚本自动化处理能够节省多少的时间。这本书一看就很厚实,但是参考机械重复操作耗费的时间,这点前期投入不值一提。

​ 在这里记录一些自己不太熟悉的内容,当做备忘录查询

第一部分

第二章

​ Ubuntu 使用的是GNOME Terminal 仿真器,几个能够提高效率的快捷键。

名称 快捷键 描述
启动终端 Ctrl + Alt + T Ubuntu Unity 桌面环境中 快速访问GNOME终端
Open Terminal Shift+Ctrl+N 在新的GNOME Terminal窗口中启动一个新的shell会话
Open Tab Shift+Ctrl+T 在现有的GNOME Terminal窗口的新标签中启动一个新的shell会话
Close Tab Shift+Ctrl+W 关闭当前标签中的会话
Close Window Shift+Ctrl+Q 关闭当前的GNOME Terminal会话
名称 快捷键 描述
Copy Shift+Ctrl+C 将所选的文本复制到GNOME的剪贴板中
Paste Shift+Ctrl+V 将GNOME剪贴板中的文本粘贴到会话中
名称 快捷键 描述
Zoom In Ctrl++ 逐步增大窗口显示字号
Zoom Out Ctrl+- 逐步减小窗口显示字号
Normal Size Ctrl+0 恢复默认字号

第三章 基本bash shell 命令

​ Linux会在根驱动器上创建一些特别的目录,我们称之为挂载点(mount point)。

​ 挂载点是虚拟目录中用于分配额外存储设备的目录。虚拟目录会让文件和目录出现在这些挂载点目录中,然而实际上它们却存储在另外一个驱动器中。

ls -l(long 长列表) -F (斜杠区分目录) -R (递归显示)

可以加模式匹配

​ ls -l my_scr?pt

​ ls -l my_s*t

防止文件被覆盖,最好加-i ,交互在覆盖前确认

mv -i

cp -i

链接文件

ln -s

ln

rmdir 只能删除空目录

rm -r 递归删除 -f 强制删除不询问 -i 删除前确认

常用file确认文件类型

less is more,less 命令查看文件内容,翻页

tail /head -5 或者 -n 5

cat -n (显示行号,包括空行) -b (显示行号,不算空行)

第四章 更多bash shell 监控和处理

Linux系统中使用的GNU ps 命令支持3种不同类型的命令行参数:
Unix风格的参数,前面加单破折线;
BSD风格的参数,前面不加破折线;
GNU风格的长参数,前面加双破折线。

ps 命令

参数 描述
-f 显示完整格式的输出
-e 显示所有进程
-l 长格式输出,比 -f 多出
l BSD风格,进程状态更清晰
–forest 用层级结构显示出进程和父进程之间的关系

top 命令

键入f允许你选择对输出进行排序的字段,

键入d允许你修改轮询间隔。

键入q可以退出 top 。

kill 命令

kill pid

kill -s HUP pid (-s 选择其他信号)

killall 进程名(可用通配符)

​ killall http*

mount 命令

mount 命令提供如下四部分信息:
媒体的设备文件名
媒体挂载到虚拟目录的挂载点
文件系统类型
已挂载媒体的访问状态

手动挂载

mount -t type device directory

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

卸载设备

umount [directory | device ]

如果有任何程序正在使用设备上的文件,系统就不会允许你卸载它:

可用lsof命令获得使用它的进程信息,然后在应用中停止使用该设备或停止该进程。lsof命令的用法很简
单:lsof /path/to/device/node,或者lsof /path/to/mount/point

df du 和 dd命令

disk free

df -h

disk usage

du -h

sort 数据排序

sort -n 识别数字 -M 用三字符月份名按月份排序

-t 分割符号, -k 列数 规定排序字段

sort -t ‘:’ -k 3 -n /etc/passwd

grep 搜索数据

grep [options] pattern [file]

如果要进行反向搜索(输出不匹配该模式的行),可加 -v 参数。

如果要显示匹配模式的行所在的行号,可加 -n 参数。

如果只要知道有多少行含有匹配的模式,可用 -c 参数。

如果要指定多个匹配模式,可用 -e 参数来指定每个模式。

grep -e t -e f file1

使用正则表达式

grep [tf] file1

tar 归档数据

tar -zxvf filename.tgz 解压

tar -zcvf [newfilename.tar.gz] [objectlist….] 压缩

tar -cvf 不压缩

第五章 理解shell

bash 参数

参 数 描 述
-i 启动一个能够接收用户输入的交互shell
-l 以登录shell的形式启动
-r 启动一个受限shell,用户会被限制在默认目录中

(command;….) 创建进程列表,生成子shell执行命令

echo $BASH_SUBSHELL 结果为0表示不存在子shell执行命令,为1 则是子shell执行

& 后台执行

jobs -l 查看后台作业

协程,但看起来不常用

corproc name { command; }

当外部命令执行时,会创建出一个子进程。

就算衍生出子进程或是创建了子shell,你仍然可以通过发送信号与其沟通,这一点无论是
在命令行还是在脚本编写中都是极其有用的。发送信号(signaling)使得进程间可以通过
信号进行通信。

内建命令和外部命令的区别在于前者不需要使用子进程来执行。它们已经和shell编译成了一
体,作为shell工具的组成部分存在。

可以利用 type 命令来了解某个命令是否是内建的

type -a 查看多种实现

有些命令有多种实现。例如 echo 和 pwd 既有内建命令也有外部命令。

history

显示最近执行过的命令

!! 最新命令

当输入 !! 时,bash首先会显示出从shell的历史记录中唤回的命令。然后执行该命令。
命令历史记录被保存在隐藏文件.bash_history中。

bash命令的历史记录是先存放在内存中,当shell退出时才被写入到历史文件中。

要实现强制写入,需要使用 history 命令的 -a选 项。

要想强制重新读取.bash_history文件,更新终端会话的历史记录,可以使用 history -n 命令。

!20 执行第20号命令

alias 命令别名

alias -p 查看已经存在的命令别名

alias li=’ls -li’

因为命令别名属于内部命令,一个别名仅在它所被定义的shell进程中才有效。

第六章 环境变量

env / printenv 展示 全局环境变量

set 命令会显示为某个特定进程设置的所有环境变量,包括局部变量、全局变量
以及用户定义变量。

创建局部环境变量

my_variable=”hello world”

变量名、等号和值之间没有空格

创建全局环境变量

先创建一个局部环境变量,然后再把它导出到全局环境中(export var_name)

删除环境变量

unset var_name

在 unset 命令中引用环境变量时,记住不要使用 $

如果要用到变量,使用 $

如果要操作变量,不使用 $ 。

这条规则的一个例外就是使用 printenv 显示某个变量的值。

不是所有的默认环境变量都会在运行 set 命令时列出。尽管这些都是默
认环境变量,但并不是每一个都必须有一个值。

PATH变量

PATH=$PATH:/home/christine/Scripts

#导出

export PATH=$PATH:/home/christine/Scripts

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

1
2
3
4
5
/etc/profile
$HOME/.bash_profile
$HOME/.bashrc
$HOME/.bash_login
$HOME/.profile

shell会按照按照下列顺序,运行第一个被找到的文件,余下的则被忽略:

1
2
3
$HOME/.bash_profile
$HOME/.bash_login
$HOME/.profile

注意,这个列表中并没有$HOME/.bashrc文件。这是因为该文件通常通过其他文件运行的

如果bash是作为交互式shell启动的,它就不会访问/etc/profile文件,只会检查用户HOME目录中的.bashrc文件。

.bashrc文件有两个作用:一是查看/etc目录下通用的bashrc文件,二是为用户提供一个定制自己的命令别名(参见第5章)和私有脚本函数(将在第17章中讲到)的地方。

最后一种shell是非交互式shell。系统执行shell脚本时用的就是这种shell。

bash shell提供了 BASH_ENV 环境变量。当shell启动一个非交互式shell进
程时,它会检查这个环境变量来查看要执行的启动文件。

持久化环境变量

最好是在**/etc/profile.d目录中创建一个以.sh结尾的文件**。把所有新的或修改过的全局环境变
量设置放在这个文件中。

在大多数发行版中,存储个人用户永久性bash shell变量的地方是**$HOME/.bashrc**文件。

但如果设置了 BASH_ENV 变量,那么记住,除非它指向的是
$HOME/.bashrc,否则你应该将非交互式shell的用户变量放在别的地方。

图形化界面组成部分(如GUI客户端)的环境变量可能需要在另外一些配置文件中设置

数组变量

移植性不好,不常用

1
2
3
4
5
6
7
mytest=(one two three four five)
echo $mytest # 只会显示$mytest[0]
echo ${mytest[*]}

mytest[2]=seven # 修改

unset mytest[2] # 删除,使得这个位置的变量为空

第七章 文件权限

用户管理

/etc/passwd文件的字段包含了如下信息:
登录用户名
用户密码
用户账户的UID(数字形式)
用户账户的组ID(GID)(数字形式)
用户账户的文本描述(称为备注字段)
用户HOME目录的位置
用户的默认shell

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

useadd

useradd -D 查看添加用户默认值

在创建新用户时,如果你不在命令行中指定具体的值, useradd 命令就会使用 -D 选项所显示
的那些默认值。这个例子列出的默认值如下:
 新用户会被添加到GID为 100 的公共组;
 新用户的HOME目录将会位于/home/loginname;
 新用户账户密码在过期后不会被禁用;
 新用户账户未被设置过期日期;
 新用户账户将bash shell作为默认shell;
 系统会将/etc/skel目录下的内容复制到用户的HOME目录下;
 系统为该用户账户在mail目录下创建一个用于接收邮件的文件。

修改默认值和创建新用户修改参数见书P128 P129

userdel 删除用户,但不会清除用户文件

-r 参数, userdel 会删除用户的HOME目录以及邮件目录

image-20221222211243363

chage 命令的日期值可以用下面两种方式中的任意一种:
 YYYY-MM-DD格式的日期
 代表从1970年1月1日起到该日期天数的数值

-I 设置密码过期到锁定账户的天数

chage 命令中有个好用的功能是设置账户的过期日期。有了它,你就能创建在特定日期自动
过期的临时用户,再也不需要记住删除用户了!

usermod

-c 修改备注字段, -e 修改过期日期, -g 修改默认的登录组

-l 修改用户账户的登录名。
-L 锁定账户,使用户无法登录。
-p 修改账户的密码。
-U 解除锁定,使用户能够登录
-G 组名,将该组添加到用户的属组的列表里,不会影响默认组。

用户组管理

/etc/group文件有4个字段:

 组名
 组密码
 GID
 属于该组的用户列表

group add

groupadd shared(组名)

usermod -G shared rich
usermod 命令的 -G 选项会把这个新组添加到该用户账户的组列表里。

groupmod

GID(加 -g 选项)或组名(加 -n 选项)。

groupmod -n sharing shared

文件权限

输出结果的第一个字段就是描述文件和目录权限的编码。这个字段的第一个字符代表了对象的类型:
 - 代表文件
 d 代表目录
 l 代表链接
 c 代表字符型设备
 b 代表块设备
 n 代表网络设备

rwx

若没有某种权限,在该权限位会出现单破折线。这3组权限分别对应对象的3个安全级别:
 对象的属主
 对象的属组
 系统其他用户

umask

第一位代表了一项特别的安全特性,叫作粘着位(sticky bit)

先获取这3个 rwx 权限的值,然后将其转换成3位二进制值

umask 值只是个掩码。它会屏蔽掉不想授予该安全级别的权限。

对文件来说,全权限的值是 666 (所有用户都有读和写的权限);而对目录来说,则是 777 (所有用户都有读、写、执行权限)

用全权限值减去掩码值就是创建新文件或者目录的权限值

目录读权限:表示用户可以用ls命令将目录下的具体子目录和文件罗列出来。

目录写权限:表示用户可以在该目录下可创建子目录或者文件。

目录执行权限:表示可以用cd进入该目录

chmod

1
[ugoa…][[+-=][rwxXstugo…]

第一组字符定义了权限作用的对象:
 u 代表用户
 g 代表组
 o 代表其他
 a 代表上述所有

现有权限基础上增加权限(+),还是在现有权限基础上移除权限(-),或是将权限设置成后面的值(=)

 s :运行时重新设置UID或GID。
 u :将权限设置为跟属主一样。
 g :将权限设置为跟属组一样。
 o :将权限设置为跟其他用户一样。

chown chgrp

chown 命令用来改变文件的属主,
chgrp 命令用来改变文件的默认属组。

chown options owner[.group] file

1
2
3
#example
chown dan newfile
chown dan.shared newfile

共享文件

想让其他人也能访问文件,要么改变其他用户所在安全组的访问权限,要么就给文件分配一个包含
其他用户的新默认属组。如果你想在大范围环境中创建文档并将文档与人共享,这会很烦琐。
幸好有一种简单的方法可以解决这个问题。

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

SGID位对文件共享非常重要。启用SGID位后,你可以强制在一个共享目录下创建的新文件都属于该目录的属组,这个组也就成为了每个用户的属组。

为了让这个环境能正常工作,所有组成员都需把他们的 umask 值设置成文件对属组成员可写。在前面的例子中, umask 改成了 002 ,所以文件对属组是可写的。

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

image-20221222224248450

第八章 管理文件系统

文件系统通过索引节点号而不是文件全名及路径来标识文件。

ext文件系统名称中的extended部分来自其跟踪的每个文件的额外数据,包括:
 文件名
 文件大小
 文件的属主
 文件的属组
 文件的访问权限
 指向存有文件数据的每个硬盘块的指针

日志文件系统

日志文件系统为Linux系统增加了一层安全性。它不再使用之前先将数据直接写入存储设备
再更新索引节点表的做法,而是先将文件的更改写入到临时文件(称作日志,journal)中。

在数据成功写到存储设备和索引节点表之后,再删除对应的日志条目。

image-20221222230009221

写时复制文件系统

ZFS是一个稳定的文件系统,与Resier4、Btrfs和ext4势均力敌。

创建分区

fdisk

sudo fdisk -l 查看信息

sudo fdisk /dev/sdb(新插入的硬盘)

comand: m (help) , n (创建新分区) , p(展示信息),q(不保存退出),w(保存退出)

创建文件系统

image-20221223141859380

可用type 命令寻找

sudo mkfs.ext4 /dev/sdb1

sudo mount -t ext4 /dev/sdb1 /mnt/my_partition

逻辑卷管理

image-20221223143341085

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#从硬盘 上定义物理卷 pv

sudo fdisk /dev/sdb (t 修改类型, 8e lvm系统) 产生新的分区sdb1

sudo pvcreate /dev/sdb1

sudo pvdisplay /dev/sdb1

#将物理卷加入卷组 vg
sudo vgcreate Vol1 /dev/sdb1
sudo vgdisplay Vol1

#从卷组创建逻辑卷 lv
sudo lvcreate -l 100%FREE -n lvtest Vol1
sudo lvdisplay Vol1

# 逻辑卷可视为物理卷,创建文件系统,并挂载使用
sudo mkfs.ext4 /dev/Vol1/lvtest
sudo mount /dev/Vol1/lvtest /mnt/my_partition

修改逻辑卷的命令,不会用,做个记录就好

image-20221223150543794

第九章 安装软件程序

包管理系统 PMS,广泛使用的PMS基础工具是 dpkg 和 rpm

基于 Debian 的系统

dpkg 命令是基于Debian系PMS工具的核心。包含在这个PMS中的其他工具有:
 apt-get
 apt-cache
 aptitude

aptitude show package_name

所有跟某个特定软件包相关的所有文件的列表

dpkg -L package_name

同样可以进行反向操作,查找某个特定文件属于哪个软件包。注意,在使用的时候必须用绝对文件路径。
dpkg –search absolute_file_name

aptitude search package_name

在每个包名字之前都有一个 p 或 i 。如果看到一个 i ,说明这个包现在已经安装到了你
的系统上了。如果看到一个 p 或 v ,说明这个包可用,但还没安装。

aptitude safe-upgrade

还有一些不那么保守的软件升级选项:
 aptitude full-upgrade
 aptitude dist-upgrade

要想只删除软件包而不删除数据和配置文件,可以使用 aptitude 的 remove 选项。要删除软
件包和相关的数据和配置文件,可用 purge 选项。

sudo aptitude purge wine

要看软件包是否已删除,可以再用 aptitude 的 search 选项。如果在软件包名称的前面看到
一个 c ,意味着软件已删除,但配置文件尚未从系统中清除;如果前面是个 p 的话,说明配置文件
也已删除。

第十章 编辑器

ctrl + s 是锁定屏幕,

ctrl + q 可以解锁

ctrl + z 放入后台,使用 fg job_num 可以解决

啥都不说了,用gedit就得了

第二部分

第十一章 基本脚本

引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不要使用美元符。

命令替换

有两种方法可以将命令输出赋给变量:
 反引号字符( ` )
 $() 格式

example:

​ $(date)

命令替换会创建一个子shell来运行对应的命令。子shell(subshell)是由运行该脚本的shell
所创建出来的一个独立的子shell(child shell)。正因如此,由该子shell所执行命令是无法
使用脚本中所创建的变量的。

在命令行提示符下使用路径 ./ 运行命令的话,也会创建出子shell;要是运行命令的时候
不加入路径,就不会创建子shell。如果你使用的是内建的shell命令,并不会涉及子shell。
在命令行提示符下运行脚本时一定要留心!

IO重定向

如果输出文件已经存在了,重定向操作符会用新的文件数据覆盖已有文件。

可以用双大于号(>>)来追加数据

输入重定向 <

wc 命令可以对对数据中的文本进行计数。默认情况下,它会输出3个值:
 文本的行数
 文本的词数
 文本的字节数

内联输入重定向符号是远小于号(<<)。

command << marker
data
marker

image-20221223215409327

管道

command1 | command2

Linux系统实际上会同时运行这两个命令,在系统内部将它们连接起来。

在第一个命令产生输出的同时,输出会被立即送给第二个命令。

数据传输不会用到任何中间文件或缓冲区。

管道最流行的用法之一是将命令产生的大量输出通过管道传送给 more 命令。
通过将输出管道连接到 more 命令,可以强制输出在一屏数据显示后停下来。

数学运算

expr 命令,太拉胯,还是不要用了

在bash中,在将一个数学运算结果赋给某个变量时,可以用美元符和方括号( $[ operation ] )将数学表达式围起来。

bash shell数学运算符只支持整数运算。若要进行任何实际的数学计算,这是一个巨大的限制。

z shell(zsh)提供了完整的浮点数算术操作。

bc解决方案,-q 快速启动

variable=$(echo “options; expression” | bc)

最好的办法是使用内联输入重定向,它允许你直接在命令行中重定向数据。在shell脚本中,
你可以将输出赋给一个变量。

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

你还会注意到,在这个例子中,你可以在bash计算器中赋值给变量。这一点很重要:在bash
计算器中创建的变量只在bash计算器中有效,不能在shell脚本中使用

退出脚本

必须在其运行完毕后立刻查看或使用 $? 变量。它的值会变成由shell所执行的最后一条命令的退出状态码。

image-20221223222203304

默认情况下,shell脚本会以脚本中的最后一个命令的退出状态码退出。

exit 命令允许你在脚本结束时指定一个退出状态码,也可以在 exit 命令的参数中使用变量。

退出状态码被缩减到了0~255的区间。shell通过模运算得到这个结果。一个值的模就是被除
后的余数。最终的结果是指定的数值除以256后得到的余数。

1
2
#查看退出状态
echo $?

第十二章 使用结构化命令

if-then

最基本的结构化命令就是 if-then 语句。 if-then 语句有如下格式。
if command
then
commands
fi

如果该命令的退出状态码(参见第11章)是 0(该命令成功运行),位于 then 部分的命令就会被执行。

如果该命令的退出状态码是其他值, then部分的命令就不会被执行,bash shell会继续执行脚本中的下一个命令。

fi 语句用来表示 if-then语句到此结束。

有时你可能不想看到错误信息。第15章将会讨论如何避免这种情况。

你可能在有些脚本中看到过 if-then 语句的另一种形式:
if command; then
commands
fi
通过把分号放在待求值的命令尾部,就可以将 then 语句放在同一行上了,这样看起来更
像其他编程语言中的 if-then 语句。

if-then-else

if command
then
commands
else
commands
fi

if then elif then

if command1
then
commands
elif command2
then
more commands
fi

还可以嵌套多个 elif 和 else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以继续将多个 elif 语句串起来,形成一个大的 if-then-elif 嵌套组合。
if command1
then
command set 1
elif command2
then
command set 2
elif command3
then
command set 3
elif command4
then
command set 4
else
command set 5
fi

每块命令都会根据命令是否会返回退出状态码 0 来执行。记住,bash shell会依次执行 if 语句,
只有第一个返回退出状态码 0 的语句中的 then 部分会被执行。

test命令

test 命令提供了在 if-then 语句中测试不同条件的途径。如果 test 命令中列出的条件成立,test 命令就会退出并返回退出状态码 0 。

这样 if-then 语句就与其他编程语言中的 if-then 语句以类似的方式工作了。

1
2
3
4
if test condition
then
commands
fi

如果不写 test 命令的 condition 部分,它会以非零的退出状态码退出,并执行 else 语句块。

bash shell提供了另一种条件测试方法,无需在 if-then 语句中声明 test 命令。

1
2
3
4
if [ condition ]
then
commands
fi

第一个方括号之后和第二个方括号之前必须加上一个空格,否则就会报错。

test 命令可以判断三类条件:
 数值比较
 字符串比较
 文件比较

image-20221224160901450

但是涉及浮点值时,数值条件测试会有一个限制。

bash shell只能处理整数。如果你只是要通过 echo 语句来显示这个结果,那没问题。但是,在基于数字的函数中就不行了

image-20221224161653901

大于号和小于号必须转义,否则shell会把它们当作重定向符号,把字符串值当作文件名;

大于和小于顺序和 sort 命令所采用的不同

比较测试中使用的是标准的ASCII顺序,根据每个字符的ASCII数值来决定排序结果。

sort命令使用的是系统的本地化语言设置中定义的排序顺序。

这个变量并未在shell脚本中定义过,所以它的字符串长度仍然为0

image-20221224163611079

复合条件测试

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

fi-then 高级特性

双括号在比较过程中使用高级数学表达式

(( expression ))

image-20221224204654127

不需要将双括号中表达式里的大于号转义。这是双括号命令提供的另一个高级特性

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

[[ expression ]]

case 命令

1
2
3
4
5
case variable in
pattern1 | pattern2 ) commands1 ;;
pattern3 ) commands2 ;;
*) default commands ;;
esac

case 命令会将指定的变量与不同模式进行比较。如果变量和模式是匹配的,那么shell会执行
为该模式指定的命令。

第十三章 更多结构化命令

for 循环

1
2
3
4
for var in list
do
commands
done

只要你愿意,也可以将 do 语句和 for 语句放在同一行,
但必须用分号将其同列表中的值分开: for var in list; do 。

var会一直保持最后一次迭代的值(除非你修改了它)

读取复杂值,带引号等

有两种办法可解决这个问题:
 使用转义字符(反斜线)来将单引号转义;
 使用双引号来定义用到单引号的值。

记住, for 循环假定每个值都是用空格分割的。如果有包含空格的数据值,你就陷入麻烦了。
在某个值两边使用双引号时,shell并不会将双引号当成值的一部分。

从变量读取值

通常shell脚本遇到的情况是,你将一系列值都集中存储在了一个变量中,然后需要遍历变量
中的整个列表。也可以通过 for 命令完成这个任务。

1
2
3
#向变量中存储的已有文本字符串尾部添加文本的一个常用方法
list="Alabama Alaska Arizona Arkansas Colorado"
list=$list" Connecticut"

从命令读取值

for state in $(cat $file)

默认情况下,bash shell会将下列字符当作字段分隔符:
 空格
 制表符
 换行符

可以在shell脚本中临时更改 IFS 环境变量的值来限制被bash shell当作字段
分隔符的字符。

IFS=$’\n’

用通配符读取目录

1
2
3
4
for file in /home/rich/.b* /home/rich/badtest
do
......
done

在Linux中,目录名和文件名中包含空格当然是合法的。要适应这种情况,应该将 $file 变量用双引号圈起来。

C 语言风格for 循环

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

尽管可以使用多个变量,但你只能在 for 循环中定义一种条件。

1
2
3
4
5
6
#!/bin/bash
# multiple variables
for (( a=1, b=10; a <= 10; a++, b-- ))
do
echo "$a - $b"
done

while 循环

它会在每次迭代的
一开始测试 test 命令。在 test 命令返回非零退出状态码时, while 命令会停止执行那组命令。

1
2
3
4
while test command
do
other commands
done

while 命令中定义的 test command 和 if-then 语句(参见第12章)中的格式一模一样。

while 命令的关键在于所指定的 test command 的退出状态码必须随着循环中运行的命令而改变。

while 命令允许你在 while 语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。

until 命令

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

1
2
3
4
until test commands
do
other commands
done

break

在处理多个循环时, break 命令会自动终止你所在的最内层的循环。

有时你在内部循环,但需要停止外部循环。 break 命令接受单个命令行参数值

其中 n 指定了要跳出的循环层级。默认情况下, n 为 1 ,表明跳出的是当前的循环。

如果你将n 设为 2 , break 命令就会停止下一级的外部循环

处理循环输出

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

实例

使用 read 命令读取文件中的各行

第十四章 处理用户输入

命令行参数

位置参数变量是标准的数字: $0 是程序名, $1 是第一个参数, $2 是第二个参数,依次类推,直到第九个参数 $9 。

$0 参数获取shell在命令行启动的脚本名

当传给 $0 变量的实际字符串不仅仅是脚本名,而是完整的脚本路径时,
变量 $0 就会使用整个路径。

在shell脚本中使用命令行参数时要小心些。如果脚本不加参数运行,可能会出问题

在使用参数前一定要检查其中是否存在数据。

参数个数

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

这个变量还提供了一个简便方法来获取命令行中最后一个参数 即 image-20221227161845392

image-20221227161755238

抓取所有数据

1
2
3
4
5
6
7
$* 和 $@ 变量可以用来轻松访问所有的参数。这两个变量都能够在单个变量中存储所有的命令行参数。

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

$@ 变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。
这样你就能够遍历所有的参数值,得到每个参数。这通常通过 for 命令完成。

移动变量

在使用 shift 命令时,默认情况下它会将每个参数变量向左移动一个位置。所以,变量 $3
的值会移到 $2 中,变量 $2 的值会移到 $1 中,而变量 $1 的值则会被删除(注意,变量 $0 的值,也就是程序名,不会改变)。

你也可以一次性移动多个位置,只需要给 shift 命令提供一个参数,指明要移动的位置数就行了。

shift 2

通过使用 shift 命令的参数,就可以轻松地跳过不需要的参数。

处理选项

分离参数和选项使用 –

使用 case $1 shift 格式逐一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

使用 getopt 命令

格式 getopt optstring parameters

在 optstring 中列出你要在脚本中用到的每个命令行选项字母。

然后,在每个需要参数值的选项字母后加一个冒号。

getopt 命令会基于你定义的 optstring 解析提供的参数。

1
2
3
4
$ getopt ab:cd -a -b test1 -cd -e test2 test3
getopt: invalid option -- 'e'
-a -b test1 -c -d -- test2 test3
# -q 忽略报错

set 命令的选项之一是双破折线( – ),它会将命令行参数替换成 set 命令的命令行值。

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

getopt 命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。

使用更高级的 getopts

getopts 命令(注意是复数)内建于bash shell。

每次调用它时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出
并返回一个大于0的退出状态码。这让它非常适合用解析命令行所有参数的循环中。

getopts 命令的格式如下:
getopts optstring variable

有效的选项字母都会列在 optstring 中,
如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,可以在 optstring 之前加一个冒号。
getopts 命令将当前参数保存在命令行中定义的 variable 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while getopts :ab:c opt
do
case "$opt" in
a) ;;
b) echo "Found the -b option, with value $OPTARG" ;; # 选项参数
.....
*) ;;
esac

done

# opt处理结束后,处理param
shift $[ $OPTIND - 1 ]
......

getopts 命令解析命令行选项时会移除开头的单破折线,所以在 case 定义中不用单破折线。

getopts 命令会用到两个环境变量。
如果选项需要跟一个参数值, OPTARG 环境变量就会保存这个值。
OPTIND 环境变量保存了参数列表中 getopts 正在处理的参数位置。

1
2
3
4
5
# 可以在参数值中包含空格。
./test19.sh -b "test1 test2" -a
# 另一个好用的功能是将选项字母和参数值放在一起使用,而不用加空格。
./test19.sh -abtest1
# getopts 还能够将命令行上找到的所有未定义的选项统一输出成问号。

getopts 命令知道何时停止处理选项,并将参数留给你处理。

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

注意 OPTIND 需要减一才能 匹配上处理的参数

标准化选项

image-20221227152446122

获取用户输入

基本读取

read 命令

1
2
3
4
echo -n "Enter your name: "
read name

read -p "Please enter your age: " age

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

也可以不指定变量,read 命令会将它收到的任何数据都放进特殊环境变量 REPLY 中

超时和限长

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

让 read 命令来统计输入的字符数。当输入的字符达到预设的字符数时,就自动退出,将输入的数据赋给变量。

隐藏方式读取

-s 选项可以避免在 read 命令中输入的数据出现在显示器上

从文件中读取

每次调用 read 命令,它都会从文件中读取一行文本。

当文件中再没有内容时, read 命令会退出并返回非零退出状态码。

1
2
3
4
cat test | while read line
do
......
done

第十五章 呈现数据

每个进程一次最多可以有九个文件描述符。出于特殊目的,bash shell保留了前三个文件描述符( 0 、 1 和 2 )

image-20221227163036310

在使用输入重定向符号( < )时,Linux会用重定向指定的文件来替换标准输入文件描述符。

追加到某个文件。这可以用 >> 符号来完成

STDERR 并不会随着 STDOUT 的重定向而发生改变。使用脚本时,
你常常会想改变这种行为,尤其是当你希望将错误消息保存到日志文件中的时候。

重定向错误

1
ls -al test badtest test2 2> test5

用这种方法,shell会只重定向错误消息

重定向错误和数据

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

将 STDERR 和 STDOUT 的输出重定向到同一个输出文件。当使用 &> 符时,命令生成的所有输出都会发送到同一位置,包括数据和错误。

1
ls -al test test2 test3 badtest &> test7

相较于标准输出,bashshell自动赋予了错误消息更高的优先级。这样你能够集中浏览错误信息了。

脚本中重定向输出

临时重定向行输出

使用输出重定向符来将输出信息重定向到 STDERR 文件描述符

在重定向到文件描述符时,你必须在文件描述符数字之前加一个 &

1
2
3
4
# shellscript 中
echo "This is an error" >&2
# 使用shellscript
./test8 2> test9

永久重定向脚本中的所有命令

取而代之,你可以用 exec 命令告诉shell在脚本执行期间重定向某个特定文件描述符

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

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# redirecting output to different locations
exec 2>testerror
# 仍然显示在stdout
echo "This is the start of the script"
echo "now redirecting all output to another location"
exec 1>testout
# stdout 重定向 输出在testout中
echo "This output should go to the testout file"

# errout重定向在文件testerror中
echo "but this should go to the testerror file" >&2

在脚本中重定向输入*

1
2
3
4
5
6
exec 0< testfile
# 逐行读取
while read line
do
......
done

这是在脚本中从待处理的文件中读取数据的绝妙办法。Linux系统管理员的一项日常任务就
是从日志文件中读取数据并处理。这是完成该任务最简单的办法。

创建自己的重定向

临时的输出重定向

1
2
3
4
5
exec 3>test13out
echo "and this should be stored in the file" >&3

# 也可以不用创建新文件,而是使用 exec 命令来将输出追加到现有文件中。
exec 3>>test13out

临时的输入重定向

1
2
3
4
exec 6<&0
exec 0< testfile

exec 0<&6

可以用同一个文件描述符对同一个文件进行读写,但是读写维护同一个文件偏移指针,所以很容易覆盖原有内容。

使用完自己创建文件描述符后,需要手动关闭

1
2
#要关闭文件描述符,将它重定向到特殊符号 &-
exec 3>&-

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

如果随后你在脚本中打开了同一个输出文件,shell会用一个新文件来替换已有文件。#追加符号无效

losf 查询Linux系统打开的文件描述符

最常用的有 -p 和 -d ,前者允许指定进程ID(PID),后者允许指定要显示的文件描述符编号

要想知道进程的当前PID,可以用特殊环境变量 $$ (shell会将它设为当前PID)。 -a 选项用来
对其他两个选项的结果执行布尔 AND 运算,这会产生如下输出。

1
lsof -a -p $$ -d 0,1,2

阻止命令输出

可以将 STDERR 重定向到一个叫作null文件的特殊文件。

在Linux系统上null文件的标准位置是/dev/null。你重定向到该位置的任何数据都会被丢掉,不会显示。

由于/dev/null文件不含有任何内容,程序员通常用它来快速清除现有文件中的数据,而不用先删除文件再重新创建。

1
2
3
4
5
6
7
$ cat testfile
This is the first line.
This is the second line.
This is the third line.
$ cat /dev/null > testfile
$ cat testfile
$

这是清除日志文件的一个常用方法,因为日志文件必须时刻准备等待应用程序操作。

创建临时文件

mktemp

mktemp 命令可以在/tmp目录中创建一个唯一的临时
文件。shell会创建这个文件,但不用默认的 umask 值(参见第7章)。它会将文件的读和写权限分
配给文件的属主,并将你设成文件的属主。

1
tempfile=$(mktemp test19.XXXXXX)