第 1 章:构建基本脚本

1.1 使用多个命令

shell 脚本的关键在于输入多个命令并处理每个命令的结果,甚至需要将一个命令的结果传给另一个命令。shell 可以让你将多个命令串起来,一次执行完成。如果要两个命令一起运行,可以把它们放到同一行中,彼此用分号隔开

date 命令先运行,显示当前日期,后面紧跟 who 命令,显示当前是谁登录到了系统上。

1.2 创建 shell 脚本

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

#!/usr/bin/bash

在通常的 shell 脚本中,井号(#)用作注释行。shell 并不会处理 shell 脚本中的注释行。然而,shell 脚本文件的第一行是个例外,# 后面的感叹号会告诉 shell 用哪个 shell 来运行脚本。

我们可以通过 which 命令找到 shell 的位置。

然后创建一个文件输入我们的命令,记得在第一行写上 shell 的位置。

现在我们还无法直接运行脚本,因为 shell 会通过 PATH 环境变量来查找命令,而我们创建的文件并不在 PATH 环境变量中。要让 shell 找到我们的文件只需采用以下两种做法之一:

  • 将 shell 脚本文件锁出的目录添加到 PATH 环境变量中;
  • 在提示符中用绝对或相对文件路径来引用 shell 脚本文件。

虽然现在 shell 可以找到脚本文件,但是还有一个问题,shell 指明你没有执行文件的权限。

通过查看权限我们可以发现,我们对这个文件并没有执行权限,这是由于 umask 变量在 Ubuntu 中被设置成了 022,所以系统创建的文件并没有相关的权限。这时我们需要通过 chmod 命令赋予文件属主执行文件的权限,之后我们就可以使用这个脚本了。

11.3 显示消息(echo)

可以通过 echo 命令将自己的消息输出在控制台显示器上,如果在 echo 命令后加上字符串,该命令就能显示出这个文本字符串。

默认情况下,echo 不需要使用引号将要显示的文本字符串划定出来,但是如果字符串中出现引号的话,就需要使用单引号或者双引号(和字符串中出现的引号不同)来划定文本字符串。

echo 的 -n 参数可以使输出不换行。

1.4 使用变量

1.4.1 环境变量

shell 维护着一组环境变量,用来记录特定的系统信息。可以用 set 命令来显示一份完整的当前环境变量列表。

在脚本中,你可以在环境变量名称前加上美元符($)来使用这些环境变量。

上述也是三种使用变量的方法,echo 命令中的环境变量会在脚本运行时替换成当前值。$ 符号类似转义符我们可以通过使用 \$ 将其视为一个普通字符。

1.4.2 用户变量

shell 脚本还允许在脚本中定义和使用自己的变量,定义变量允许临时存储数据并在整个脚本中使用。shell 中使用等号将值赋给用户变量,但是需要注意的是,在变量、等号和值之间不能出现空格。

如果要将一个变量赋值给另一个变量,也必须要加上美元符。

没有美元符的话,shell 会将变量名解释称普通的文本字符串。

1.4.3 命令替换

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

  • 反引号字符(`)—— 美式键盘的破浪线
testing=`date`
  • $() 格式
testing=$(date)

shell 会允许命令替换符号中的命令,并将其暑促赋给变量 testing,注意赋值等号和命令替换字符之间没有空格。

1.5 重定向输入和输出

有时候你想要保存某个命令的输出而不仅仅是让它显示在显示器上,bash shell 提供了几个操作符,可以将命令的输出重定向到另一个位置(比如文件)。重定向可以用于输入,也可以用于输出,可以将文件重定向到命令输入。

1.5.1 输出重定向

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

command > outputfile

这样在显示器上出现的命令输出会被保存到指定的输出文件中。

重新向操作符会用新的文件数据覆盖已有文件(如果文件已存在),但是如果你不想覆盖原文件的内容,而是想追加到已有文件中,此时可以使用双大于号(>>)来追加数据。

1.5.2 输入重定向(< 和 <<)

输入重定向和输出重定向正好相反,输入重定向是将文件的内容重定向到命令。输入重定向符是小于号(<):

comman < inputfile

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

这个例子说明,test 有 2 行、11 个单词以及 60 字节。

另一种输入重定向是内联输入重定向。这种方法无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据就可以了。其符号是双小于号(<<),除了这个符号,还必须指定一个文本标记(任何字符串都可)来划分输入数据的开始和结尾,但是开始和结尾的文本标记必须一致。

command << marker
data
<< marker

如下所示。

在命令行上使用内联输入重定向时,shell 会使用次提示符来提示输入数据,次提示符会持续提示,直到输入了文本标记的那个字符串。

1.6 管道

有时候需要将也给命令的输出作为另一个命令的输入。着虽然可以通过重定向来实现,但是却十分笨拙。事实上我们可以将一个命令重定向到另一个命令,这个过程叫作管道连接

管道符号是(|),管道被放在命令之间,将一个命令的输出重定向到另一个命令中:

command1 | command2

在第一个命令产生输出的同时,输出会被立即送给第二个命令,数据传输不会用到任何中间文件或缓冲区。

可以在一条命令中使用任意多条管道,可以持续地将命令的输出通过管道传给其他命令来细化操作。

1.7 执行数学运算

1.7.1 expr 命令

expr 命令允许在命令行上处理数学表达式以及一些逻辑判断,虽然比较笨拙。

需要注意的是,expr 命令操作符在 shell 中另有含有(比如星号),当它们出现在 expr 命令中时,会得到非预想的结果。要解决这个问题,我们需要对那些容易被 shell 错误解释的字符使用反斜线(反斜线)将其标出来。

如果要将一个数学算式的结果赋给另一个变量,同样也需要使用命令替换来获取 expr 命令的输出:

1.7.2 使用方括号

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

在使用方括号来计算公式时,就不用担心 shell 会误解乘号或其他符号,shell 直到它不是通配符,因为它在括号内。

说明:bash shell 数学运算符只支持整数运算,z shell(zsh)提供了完整的浮点数算数操作。

1.7.3 浮点解决方案(bc)

为了克服 bash 中数学运算的整数限制,最常见的方案是用内置的 bash 计算器 bc。

  1. bc 的基本用法

bash 计算器实际上是一种编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该表达式,最后返回结果。bash 计算器能够识别:

  • 数字(整数和浮点数)
  • 变量(简单变量和数组)
  • 注释(以 # 或 C 语言中的 /* */ 开始的行)
  • 表达式
  • 编程语句(例如 if-then 语句)
  • 函数

如果 Linux 系统中没有 bc,可以通过 sudo apt-get bc 安装 bc。

之后就可以在 shell 提示符下通过 bc 命令访问 bash 计算器,quit 退出 bash 计算机。

浮点运算是由内置变脸 scale 控制的,必须将这个值设置为你希望在计算结果中保留的小数位。scale 变量的默认值是 0,在 sacle 值被设置之前,bash 计算器的计算结果不包含小数位。

一旦变量被定义,你就可以在整个 bash 计算器会话中使用该变量,print 语句允许你打印变量和数字。

  1. 在脚本中使用 bc

我们可以通过命令替换运行 nv,并将输出赋给一个变量,其基本格式如下:

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

第一部分 options 允许你设置变量,如果不止一个变量,可以用逗号分开。expression 参数定义了通过 bc 执行的数学表达式。

这个方法适合较短的运算,当运算设计更多数字时,在一个命令行中列出多个表达式就会比较麻烦。虽然 bc 可以识别输入重定向,允许你将一个文件重定向到 bc 命令来处理,但是你还得将表达式存放到文件中。

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

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

EOF 文本字符串表示了内联重定向数据的起止。记住,这仍然需要命令替换符号将 bc 命令的输出赋给变量。现在就可以将所有 bash 计算器设计的部分都放到同一个脚本文件的不同行。

将选项和表达式放在脚本的不同行中可以人处理过程变得清晰,提高易读性。不过需要知道的是,在 bash 计算器中创建的变量只在 bash 计算器中有效,不能在 shell 脚本中使用。

1.8 退出脚本

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

1.8.1 查看退出状态码($?)

Linux 提供了一个专门的变量 $? 来保存上一个已执行命令的退出状态码,对于需要检查的命令,必须在其运行完毕后立刻查看其退出状态码。

一个成功结束的命令的退出状态码是 0,如果一个命令结束时有错误,退出状态码就是一个整数值。常见的退出状态码如下:

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

1.8.2 exit 命令

默认情况下,shell 脚本会议脚本中最后的一个命令的退出状态码退出。你可以改变这种默认行为,返回自己的退出状态码,exit 命令允许你在脚本结束时指定一个退出状态码。退出状态码的区间是 0~255,所以如果超出了这个范围,其结果就是指定的数值对 256 求模的结果。


第 2 章:使用结构化命令

2.1 使用 if-then 语句

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

if command
thencommands
fi

其他编程语言中,if 后面的灯饰求值结果为 true 或 false,但是 bash shell 的 if 语句会执行 if 后面的那个命令,如果该命令的退出状态码是 0(该命令运行成功),则位于 then 部分的命令才会被执行。如果该命令的退出状态码是其他值,then 部分的命令就不会被执行。fi 语句表示 if-then 语句到此结束。

在本例中,if 语句行有一个不能工作的命令,由于这是个错误命令,所以它会产生一个非零的退出码,bash shell 就会跳过 then 部分的 echo 语句。

2.2 if-then-else 语句

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

if command
thencommands
elsecommands
fi

当 if 语句中的命令返回退出码为 0 时,执行 then 部分的代码,否则执行 else 部分的代码。

2.3 嵌套 if

同样的 shell 中也支持嵌套 if 格式,以及 if-then-elif 格式,elif 使用另一个 if-then 语句延续 else 部分,其格式为:

if command1
thencommands
elif command2commands
elsecommands
fi

记住,在 elif 语句中,紧跟其后的 else 语句属于 elif 代码块,它们并不属于之前的 if-else 代码块。

2.4 test 命令

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

test condition

condition 时 test 命令要测试的一些列参数和值。当用在 if-then 语句中时,其类似于下述格式:

if test condition
thencommands
fi

如果不写 test 命令的 condition 部分,它会以非零的退出状态码退出。

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

if [ condition ]
thencommands
fi

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

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

2.4.1 数值比较

test 命令的数值比较参数如下表所示:

比较 描述
n1 -eq n2 检查 n1 是否与 n2 相等
n1 -ge n2 检查 n1 是否大于或等于 n2
n1 -gt n2 检查 n1 是否大于 n2
n1 -le n2 检查 n1 是否小于或等于 n2
n1 -lt n2 检查 n1 是否小于 n2
n1 -ne n2 检查 n1 是否不等于 n2

数值条件测试可以用在数字和变量上。但是涉及到浮点时,数值条件测试有一个限制。

记住,bash shell 只能处理整数,除非你只是通过 echo 语句来显示这个结果。

2.4.2 字符串比较

下标列出了可用的字符串比较功能:

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

字符串的相等和不等,和其他程序相似,就不过多解释。

  1. 字符串顺序

当使用测试条件的大于或小于功能时,需要注意这两个问题:

  • 大于号和小于号必须转移,否则 shell 会把它们当作重定向符号,把字符串值当作文件名;
  • 大于和小于顺序和 sort 命令有所不同。在比较测试中使用的时标准 ASCII 码顺序,根据每个字符的 ASCII 数值来决定排序结果,也就是说大写字母被认为是小于小写字母的;test 命令来说,本地化设置制定了排序顺序中小写字母出现在大写字母前。


3. 字符串大小

-n 和 -z 可以检查一个变量是否含有数据。如果一个变量没有在 shell 脚本中定义过,它的长度默认为 0。

2.4.3 文件比较

文件比较时 shell 编程中最强大、也是用的最多的比较形式,它允许你测试 Linux 文件系统上文件和目录的状态。下标列出了这些比较:

比较 描述
-d file 检查 file 是否存在,并是一个目录
-e file 检查 file 是否存在
-f file 检查 file 是否存在,并是一个文件
-r file 检查 file 是否存在,并可读
-s file 检查 file 是否存在,并非空
-w file 检查 file 是否存在,并可写
-x file 检查 file 是否存在,并可执行
-O file 检查 file 是否存在,并属于当前用户所有
-G file 检查 file 是否存在,并且默认组与当前用户相同
file1 -nt file2 检查 file1 是否比 file2 新
file1 -ot file2 检查 file1 是否比 file2 旧

这些测试条件使你能够在 shell 脚本中检查文件系统中的文件,它们经常出现在需要进行文件访问的脚本中。

这里简单选几个,其他的用法类似。

  1. 检查所属关系

-O 比较可以测试出你是否是文件的属主。.

我们可以发现 /etc/passwd 的属主是 root,而这个脚本是运行在普通账户账户下,所以测试失败。

  1. 检查默认属组关系

-G 比较会检查文件的默认组,如果它匹配了用户的默认组,则测试成功。需要注意的是 -G 比较只会检查默认组而非用户所属的所有组

  1. 检查文件日期

-nt 和 -ot 是用来对两个文件的创建日期进行比较。需要注意的是,这两个比较不会先检查文件是否存在,如果文件不存在,会返回一个错误的结果。所以在尝试使用这两个比较命令之前,必须先确认文件是否存在。

2.5 复合条件测试

if-then 语句允许使用布尔逻辑来组合测试:

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

第一种布尔运算使用 AND 布尔运算符来组合两个条件,只有两个条件都满足时,then 部分命令才会执行。

第二种布尔运算使用 OR 布尔运算符来组合两个条件,只要任意条件为 TRUE,then 部分就会执行。

2.6 if-then 的高级特性

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

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

2.6.1 用于数学表达式的双括号

双括号命令允许你在比较过程中使用高级数学表达式。test 命令只能在比较重使用简单的算术操作。双括号命令提供了更多的数学符号,其格式如下:

(( expression ))

expression 可以时任意的数学赋值或比较表达式,除了 test 命令使用的标准数学运算符,还有下述运算符:

符号 描述 符号 描述
val++ 后增 val-- 后减
++val 先增 --val 先减
! 逻辑求反 ~ 位求反
** 幂运算
<< 左位移 >> 右位移
& 位布尔和 | 位布尔或
&& 逻辑和 || 逻辑或

可以在 if 语句中使用双括号命令,也可以在脚本中的普通命令里使用。

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

2.6.2 用于高级字符串处理功能的双方括号

双括号命令提供了针对字符串比较的高级特性,其格式为:

[[ expression ]]

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

说明:双方括号在 bash shell 中可以使用,但是并不是所有 shell 都支持双方括号。

2.7 case 命令

case 命令可以让你不用写出所有的 elif 语句来不停得检查同一个变量的值了,case 命令会采用列表格式来检查单个变量的多个值:

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

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

case 命令提供了一个更清晰的方法来为变量每个可能的值定制不同的选项。


第 3 章:更多的结构化命令

3.1 for 命令

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

for var in list
docommands
done

3.1.1 读取列表中的值

每次 for 命令遍历值列表,它都会将列表中的下个值赋给 var。var 可以像 for 命令语句中其他脚本变量一样使用。在最后一次迭代后,car 变量的值会在 shell 脚本的剩余部分一致保持有效,直到你修改它。

3.1.2 读取列表中的复杂值

shell 看到了列表值中的单引号会尝试使用它们来定义一个单独的数据值。有两种办法可以解决:

  • 使用转义字符(反斜线)来将单引号转义;
  • 使用双引号来定义用到的单引号。

for 命令用空格来划分列表中的每个值,如果在单独的数据值中有空格,就必须用双引号将这些值圈起来。

3.1.3 从变量读取列表

在 shell 中也可以将一系列值先存储在一个变量中,然后再通过这个变量遍历整个列表。

注意,代码用了另一个赋值语句向 list 变量包含的已有列表中添加(或者说拼接)了一个值。这是向变量中存储的已有文本字符串尾部添加文本的一个常用办法。

3.1.4 从命令读取值

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

3.1.5 更改字段分隔符

设置字段分隔符的环境变量是 IFS,叫作内部字段分隔符。IFS 环境变量定义了 bash shell 用作字段分隔符的一些列字符。默认情况下,bash shell 会将下列字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

如果 bash shell 在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中的一个新数据字段的开始。我们可以在 shell 脚本中临时更改 IFS 环境变量的值来限制被 bash shell 当作字段分隔符的字符。

警告:有时候我们在一个地方修改 IFS 之后,在其他地方可能需要继续沿用 IFS 的默认值,所以安全的做法是在改变 IFS 之前保存原来的 IFS 值,之后在恢复。

IFS.OLD=$IFS
IFS='\n'
...
IFS=$IFS.OLD

这样可以保证在脚本的后续操作中使用的是 IFS 默认值。

如果要指定多个 IFS 字符,只需要将它们在赋值行串起来即可。

IFS=$'\n':;

这个赋值会将换行符、冒号、分号作为字段分隔符。

3.1.6 用通配符读取目录

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

3.2 C 语言风格的 for 命令

3.2.1 C 语言的 for 命令

bash shell 支持一种类似 C 语言风格的 for 循环,其基本格式为:

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

它使用了 C 语言风格的变量引用方式而不是 shell 风格的变量引用方式:

for (( a = 1; a < 10; a++ ))

注意,一些与 bash shell 标准的 for 命令不同的地方:

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

可以参考下面的例子。

3.2.2 使用多个变量

C 风格的 for 命令也允许未迭代使用多个变量,可以为每个变量定义不同的迭代过程。虽然可以用多个变量,但是只能在 for 循环中定义一种条件。

3.3 while 命令

3.3.1 while 的基本格式

while 命令的基本格式是:

while test commands
doother commands
done

while 命令中定义的 test command 和 if-then 语句中的格式一模一样。可以使用任何普通的 bash shell 命令,或者使用 test 命令进行条件测试。

while 命令的关键在于所指定的 test command 的退出状态码必须随着循环中的运行命令而改变,如果退出状态码不发生改变,while 循环将一直进行下去。

3.3.2 使用多个测试命令

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

这说明,在含有多个命令的 while 语句中,在每次迭代中所有的测试命令都会被执行,包括测试命令失败的最后一次迭代。

3.4 until 命令

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

until test commands
doother commands
done

until 命令和 while 命令除了工作方式相反,其他完全相同。

3.5 嵌套循环

循环语句可以在循环内使用任意类型的命令,这也包括其他循环命令。这种循环叫作嵌套循环。被嵌套的循环(也称内部循环)会在外部循环的每次迭代中遍历以便它有所的值。

3.6 控制循环

有两个命令可以帮助我们控制循环内部情况:

  • break 命令
  • continue 命令

3.6.1 break 命令

break 命令用来退出任意类型的循环,通常与 if-then 配合使用,下面是使用 break 命令的几种情况。

  1. 跳出单个循环

当处理单个循环时,在 shell 中执行 break 命令,它便会尝试跳出当前正在执行的循环。如果跳出了当前循环,剩余的循环就不会再执行了。

  1. 跳出内部循环

在除了嵌套循环时,break 命令会跳出当前所在的循环,如果在内层循环中使用 break 命令,会跳出本次内层循环,但是不会影响外层循环。

  1. 跳出外部循环

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

break n

其中 n 指定了要跳出的循环层级。默认情况下,n 为 1,表明跳出的时当前循环。如果将 n 设置为 2,break 命令就会停止下一级的外部循环。

注意,当 shell 执行了 break 命令后,外部循环也停止了。

3.6.2 continue 命令

continue 命令可以提前中止某次循环的命令,但并不会完全终止整个循环,同样 continue 通常也配合 if-then 执行。

continue 命令也允许通过命令行参数指定要继续执行哪一级循环:

continue n

其中 n 定义了要继续的循环层级。

3.8 处理循环的输出

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

shell 会将 for 命令的结果重定向到文件 output.txt 中,而不是显示在屏幕上。这种方法同样适用于将循环的结果通过管道接给另一个命令。

state 值并没有在 for 命令列表中以特定次序列出。for 命令的输出传给了 sort 命令,该命令会改变 for 命令输出结果的顺序,之后再将排序好的结果重定向到 output.txt 中。

3.9 一个实例 —— 查找可执行文件

当你从命令行中运行一个程序的时候,Linux 系统会搜索一系列目录来查找对应的文件。这些目录被定义在环境变量 PATH 中,如果你向找出系统中有哪些可执行文件可供使用,只需要扫描 PATH 环境变量中的所有目录就可以了。

#!/usr/bin/bashIFS=:
for folder in $PATH
doecho "$folder:"for file in $folder/*doif [ -x $file ]thenecho "   $file"fidone
done

我们首先创建了一个 for 循环,对环境变量 PATH 中的目录进行迭代,然后用另一个 for 循环来得带特定目录的所有文件,最后检查各个文件是否具有可执行权限。


第 4 章:处理用户输入

4.1 命令行参数

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

./addem 10 30

本例,向脚本 addem 传递了两个命令行参数(10 和 30)。脚本会通过特殊的变量来处理命令行参数。

4.1.1 读取参数

bash shell 会将一些称为位置参数的特殊变量分配给输入到命令行中的所有参数。位置参数变量是标准的数字:$0 是程序名,$1 是第一个参数,$2是第二个参数,以此类推,如果超过 9 个,必须在数字周围加上花括号,比如 ${10}。

4.1.2 读取脚本名

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

但是这有一个潜在的问题,如果使用另一个命令来运行 shell 脚本,命令会和脚本名混在一起出现在 $0 参数中。还有一个问题,当传给 $0 变量的实际字符串不仅仅是脚本名,而是完整的脚本路径时,变量 $0 就会使用整个路径。

我们可以使用 basename 命令解决上述问题,该命令会返回不包含路径的脚本名。

4.1.3 测试参数

在脚本中使用参数之前一定要事先确定参数中是否存在数据,如果不存在数据还使用了此参数的话,脚本很有可能会产生错误消息。

4.2 特殊参数变量

4.2.1 参数统计

特殊变量 $# 含有脚本运行时携带的命令行参数的个数。这样就无需测试每个参数是否存在了。

这个变量还提供了一个简便方法来获取命令行中最后一个参数,而完全不需要知道实际上到底用了多少个参数。但是需要注意的是, ${ $# } 并不能代表最后一个命令行参数变量。

这表明我们不能再花括号内使用美元符,当我们将美元符换成感叹号时,它就可以正常运行了。还有一点需要知道,如果我们没有传递任何命令行参数,${ !# } 会返回命令行用到的脚本名。

4.2.2 抓取所有数据

$* 和 $@ 变量可以用来访问所有的参数,这两个变量都能在单个变量中存储所有的命令行参数。

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

$@ 变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。

由此可见 $* 变量会将所有参数当成单个参数;$@ 变量会单独处理每个参数。

4.3 移动变量

bash shell 的 shift 命令能够用来操作命令行参数,该命令会根据它们的相对位置来移动命令行参数。

shift 命令默认情况会将每个参数变量向左移动一个为止。变量 $2 的值会移动到 $1 变量中,而 $1 的值则会被删除。
注意:变量 $0 的值,也就是程序名,不会发生改变。

这也是一种变量命令行参数的方法,尤其是在不知道有多少参数的时候,这样只用操作第一个参数即可。

Tips:使用 shift 命令时,如果某个参数被移出,它的值就被丢弃了,且无法再恢复。

也可以一次性移动多个位置,只需要给 shift 命令提供一个参数,指明要移动的位置数即可:

shift n

4.4 处理选项

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

4.4.1 查找选项

在命令行上,命令行选项紧跟在脚本名之后,和命令行参数一样。事实上,你可以向处理命令行参数一样处理命令行选项。

  1. 处理简单选项

我们可以使用 shift 依次处理脚本程序接待的命令行参数,我们也可以这样处理命令行选项。

2. 分离参数和选项

Linux 可以通过特殊字符双破折线(--)将选项和参数分开,该字符会告诉脚本何时选项结束以及普通参数何时开始。

3. 处理带值选项

有些选项会带上一个额外的参数值,其格式类似:

file -a value

当命令行选项要求额外参数时,脚本也必须可以检查并正确处理。

现在 shell 脚本中已经有了处理命令行选项的基本能力,但是还不能合并选项。比如使用 -ac。如果脚本想要对用户更友好点,也需要为用户提供这种特性。

4.4.2 使用 getopt 命令

getopt 命令可以识别命令行参数,从而可以更方便的解析它们。

  1. 命令的格式

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

getopt optstring parameters

optstring 是这个过程的关键,它定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值。

首先,在 optstring 中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。

参考下面的例子:

optstring 定义了四个有效选项字母:a、b、c 和 d,冒号被放在了字母 b 后面,说明 b 需要一个参数。当 getopt 运行时,它会检查提供的参数列表(-a -b test1 -cd test2 test3),并基于 optstring 进行解析。

如果指定了也给不在 optstring 中的选项,默认情况下,getopt 命令会产生一条错误信息。如果想忽略这条信息,可以在命令后加 -q 选项。

2. 在脚本中使用 getopt

我们可以用 getopt 命令生成的格式化后的版本来替换已有的命令行选项和参数。用 set 命令(set 命令可以处理 shell 中的各种变量)可以做到。

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

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

然后原始的命令行参数变量的值会被 getopt 命令的输出替换。

现在我们可以使用合并的选项了,但是现在还存在这样的问题,如第二个例子,getopt 并没有办法处理带有空格和引号的参数。它会将空格当作参数分隔符,而不是根据双引号将二者当作要给参数。

4.4.3 使用更高级的 getopts(注意是复数)

getopts 命令内置于 bash shell,它比 getopt 多了一些扩展功能。getopt 将命令行上选项和参数处理后只生成一个输出,而 getopts 每次只处理命令行上检测到的一个参数。处理完所有参数后,它会退出并返回一个大于 0 的退出状态码。其格式如下:

getopts optstring variable

optstring 要求类似于 getopt 中的。如果要去掉错误消息的话,可以在 optstring 之前加上一个冒号。getopts 命令将当前参数保存在命令行中定义的 variable 中。

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

getopts 命令,可以在参数值中包含空格;还可以将选项字母和参数值放在一起使用,而不用加空格。

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

现在,你就拥有了一个能在所有脚本中使用的全功能命令行选项和参数处理工具。

#!/usr/bin/bashwhile getopts :ab:c opt
docase "$opt" ina) echo "-a option" ;;b) echo "-b option, with parameter value $OPTARG" ;;c) echo "-c option" ;;*) echo "$opt is not an option" ;;esac
doneshift $[ $OPTIND - 1 ]count=1
for param in "$@"
doecho "Parameter #$count: $param"count=$(expr $count + 1)
done

4.5 将选项标准化

下表显示了 Linux 中用到的一些命令行选项的常用含义。

选项 描述
-a 显示所有对象
-c 生成一个计数
-d 指定一个目录
-e 扩展一个对象
-f 指定输入数据的文件
-h 显示命令的帮助信息
-i 忽略文本大小写
-l 产生输出的长格式版本
-n 使用非交互模式(批处理)
-o 将所有输出重定向到指定的输出文件
-q 以安静模式运行
-r 递归地处理目录和文件
-s 以安静模式运行
-v 生成详细输出
-x 排除某个对象
-y 对所有问题回答 yes

4.6 获取用户输入

4.6.1 基本的读取(read)

read 命令从标准输入(键盘)或另一个文件描述符中接受输入。在收到输入后,read 命令会将数据放进一个变量。read 命令包含了 -p 选项,允许你直接在 read 命令行中指定提示符。

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

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

4.6.2 超时处理(-t)

使用 read 时,脚本可能会抑制等待脚本用户的输入。如果不管是否有数据输入,脚本都必须执行,此时你可以使用 -t 选项来指定也给计时器。-t 选项制定了 read 命令等待输入的秒数。当计时器过期后,read 命令会返回一个非零退出状态码。

4.6.3 隐藏方式读取(-s)

如果你不希望在屏幕上显示输入信息,可以使用 -s 选项(实际上,数据会被像是,只是 read 命令会将文本颜色设置成跟背景颜色一样)。

4.6.4 从文件中读取

read 命令也可以读取 Linux 系统上文件保存的数据。每次调用 read 命令,它都会从文件中读取一行文本。当文件中再没有内容时,read 命令会退出并返回非零状态码。可以通过 cat 命令和管道来读取文件中的内容。


第 5 章:呈现数据

5.1 理解输入和输出

目前直到的两种显示脚本输出的方法:

  • 在显示器屏幕上显示输出(echo)
  • 将输出重定向到文件中(> 或 >>)

但是这两种方法是将数据全部显示,有时我们只希望将一部分数据显示出来。

5.1.1 标准文件描述符

Linux 系统将每个对象当作文件处理。这包括输入和输出进程,Linux 用文件描述符来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次最多可以有九个文件描述符。

bash shell 包流量前三个文件描述符,如下表:

文件描述符 缩写 描述
0 STDIN 标准输入
1 STDOU 标准输出
2 STDERR 标准错误
  1. STDIN

STDIN 文件描述符代表 shell 的标准输入。对终端界面来说,标准输入是键盘。shell 从 STDIN 文件描述符对应的键盘获得输入,在用户输入时处理每个字符。

在使用输入重定向符号(<)时,Linux 会用重定向指定的文件来替换标准输入文件描述符。它会读取文件并提取数据,就如同它时键盘上键入的。

  1. STDOUT

STDOUT 文件描述符代表 shell 的标准输出。在终端界面上,标准输出就是终端显示器。shell 的所有输出(包括 shell 中运行的程序和脚本)会被定向到标准输出中,也就是显示器中。

默认情况下,大多数 bash 命令会将输出导向 STDOUT 文件描述符。当然也可以通过输出重定向符,将显示到显示器的所有输出重定向到指定的重定向文件中。

  1. STDERR

shell 通过特殊的 STDERR 文件描述符来处理错误消息。STDERR 文件描述符代表 shell 的标准错误输出。shell 或 shell 中运行的程序和脚本出错时生成的错误消息都会发送到这个位置。

默认情况下,STDERR 文件描述符回合 STDOUT 文件描述符只想同样的地方(也就是显示器上),尽管分配给它们的文件描述值不同。

但是 STDERR 并不会随着 STDOUT 的重定向而发生改变。

可以看到当命令生成错误消息时,shell 并未将错误消息重定向到输出重定向文件中,而是将错误消息显示早显示器屏幕上。

5.1.2 重定向错误

  1. 只重定向错误

STDERR 文件描述符被设置为 2,所以将该文件描述符值放在重定向符号前(该值和重定向符号之间没有空格),就可以只重定向错误消息。

2. 重定向错误和数据(>、2> 或 &>)

如果想将错误和数据存入两个不同的文件,必须用两个重定向符号。需要在符号前面放上待重定向数据所对应的文件描述符,然后指向用于保存数据的输出文件。

可以注意到我们只有文件 content1 和 content2 并没有文件 content3,所以该命令将查到的消息重定向到文件 good,将错误消息重定向到文件 bad。

如果想将 STDERR 和 STDOUT 的输出重定向到同一个文件,可以使用重定向符号 &>。

可以注意到的是,错误信息会更先出现在文件中,相较于标准输出,bash shell 赋予了错误消息更高的优先级。

5.2 在脚本中重定向输出

在脚本中重定向输出有两种:

  • 临时重定向行输出
  • 永久重定向脚本中所有命令

5.2.1 临时重定向

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

echo "This is an error message" >&2

这行会在脚本的 STDERR 文件描述符所指的位置显示文本,而不是通常的 STDOUT。

现在 STDOUT 显示的文本显示在了屏幕上,而 STDERR 显示的文本本我们重定向到了输出文件中。

5.2.2 永久重定向(exec)

如果有大量数据需要重定向,那么重定向每个 echo 语句就会十分麻烦,此时可以使用 exec 命令告诉 shell 在脚本执行期间重定向某个特定文件描述符。

假如我们重定向了 STDOUT,我们仍然可以将 echo 语句的输出发给 STDERR。

需要注意的是一旦重定向了 STDOUT 或 STDERR,就很难再将它们重定向回原来的位置。

5.3 在脚本中重定向输入(exec)

也可以将 STDIN 从键盘重定向到其他位置。exec 命令允许你将 STDIN 重定向到 linux 系统上的文件中:

exec 0< testfile

这个命令会告诉 shell 它应该从文件 testfile 中获取输入,而不是从键盘上。

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

但是需要注意的是,这样会跳过文件的最后一行。

5.4 创建自己的重定向

shell 最多可以有 9 个文件描述符。其余 3~8 的文件描述符都可以用作输入或输出重定向,可以将这些文件描述符中的任意一个分配给文件,然后再脚本中使用它们。

5.4.1 创建输出文件描述符

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

5.4.2 重定向文件描述符

现在介绍怎么恢复已重定向的文件描述符。你可以分配领域给文件描述符给标准文件描述符,反之亦然。这意味着你可以将 STDOUT 原来位置重定向到另一个文件描述符,然后再利用该文件描述符重定向回 STDOUT

5.4.3 创建输入文件描述符

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

5.4.4 创建读写文件描述符

shell 也允许使用单个文件描述符来作为输入和输出,也就是说可以用同一个文件描述符对同一个文件进行读写。

shell 会在文件内维护一个内部指针,指明在文件中的当前位置。任何读或写都从文件指针上次的位置开始。参考下面的例子:

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

5.4.5 关闭文件描述符(&-)

手动关闭文件描述符,可以将它重定向到特殊符号 &-,类似于:

exec 3>&-

该语句会关闭文件描述符 3,一旦关闭了文件描述符,就不能再脚本中使用它,否则 shell 会生成错误消息。

5.5 列出打开的文件描述符(lsof -a -p $$ -d)

lsof 命令会列出整个 Linux 系统打开的所有文件描述符。但是它会向非管理员用户提供 Linux 系统的信息(有的 Linux 系统隐藏了该命令)。如果想以普通用户来运行它,就必须通过全路径名来引用。

lsof 命令会产生大量的输出,它会显示当前 Linux 系统上打开的每个文件的有关信息,所有我们需要进行过滤。常用的选项有 -p 和 -d,前者允许指定进程 ID(PID),后者允许指定要显示的文件描述编号。

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

上例显示了当前进程的默认文件描述符(0,1 和 2)。lsof 的默认输出有 7 列信息,具体如下:

描述
COMMAND 正在运行的命令名的前 9 个字符
PID 进程的 PID
USER 进程属主的登录名
FD 文件描述符号以及访问类型(r 代表读,w 代表写,u 代表读写)
TYPE 文件的类型(CHR 代表字符型,BLK 代表块性,DIR 代表目录,REG 代表常规文件)
DEVICE 设备的设备号(主设备号和从设备号)
SIZE 如果有的话,表示文件的大小
NODE 本地文件的节点号
NAME 文件名

5.6 阻止命令输出

shell 输出到 null 文件的任何数据都不会保存,全部都会被丢掉。在 Linux 系统上 null 文件的标准位置是 /dev/null,重定向到该位置的任何数据都会被丢弃。

也可以在输入重定向中将 /dev/null 作为输入文件,可以用它来快速清楚现有文件中的数据,而不用先删除文件再重新创建。这也是清楚日志文件的一个常用方法。

5.7 创建临时文件

Linux 系统有一个特殊的目录 /tmp 目录,专供临时文件使用。大多数 Linux 发行版配置了系统在启动时自动删除 /tmp 目录的所有文件。系统上的任何用户都有权读写在 /tmp 目录中的文件。

有个特殊命令可以用来创建临时文件。mktemp 命令可以在 /tmp 目录中创建一个唯一的临时文件。shell 会创建这个文件,但不用默认的 umask 值,它会将文件的读和写权限分配给文件的属主,并将你设置为属主,而不让其他人访问它。

5.7.1 创建本地临时文件(mktemp)

mktemp 命令在本地目录中创建一个临时文件,你只要指定一个文件名模板就行,模板可以包含任意文本文件名,在文件名加上几个 x 或 X 就行了(具体数量和大小写不一定)。mktemp 命令会用等同于 x 数量的字符码替换这些 x,从而保证文件名在目录中是唯一的。


mktemp 命令的输出正是它所创建的文件的名字。如果要在脚本中使用 mktemp 命令,需要将文件名保存到变量中,这样就能在后面的脚本中引用了。

5.7.2 在 /tmp 目录创建临时文件(-t)

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

由于 mktemp 命令返回了全路径名,你可以在 Linux 系统上的任何目录下引用该临时文件,不管临时目录在哪里。

5.7.3 创建临时目录(-d)

-d 选项告诉 mktemp 命令来创建一个临时目录而不是临时文件。

5.8 记录消息(tee)

如果想将输出同时发送到显示器和日志文件,不需要将输出重定向两次,只需要使用 tee 命令。tee 命令想到与管道的一个 T 型结构,它将从 STDIN 过来的数据同时发往两处,一处是 STDOUT,另一处是 tee 命令行所指定的文件名:

tee filename

tee 会冲顶下那个来自 STDIN 的数据,因此可以用它配合管道命令来重定向命令输出。

如果想将数据追加到文件中,必须使用 -a 选项。

利用这个方法,既能将数据保存在文件中,也能将数据显示在屏幕上。


第 6 章:控制脚本

6.1 处理信号

Linux 利用信号与运行在系统中的进程进行通信。

6.1.1 Linux 信号

Linux 系统和应用程序可以生成超过 30 个信号,下表列出了一下常见的 Linux 信号:

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

默认情况下,shell 会忽略受到的任何 SIGQUIT(3) 和 SIGTERM(15) 信号(正因为这样,交互式 shell 才不会被意外终止)。但是 shell 会处理收到的 SIGHUP(1) 和 SIGINT(2) 信号。

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

6.1.2 生成信号

bash shell 允许使用键盘来生成两种基本的 Linux 信号。

  1. 中断进程

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

SIGINT 信号会终止 shell 中当前运行的进程。

  1. 暂停进程

Ctrl+Z 组合键会生成也给 SIGTSTP 信号,停止 shell 中运行的所有程序。停止程序会让程序继续保留在内存中,并能从上次停止的位置继续运行。

如果 shell 会话中有一个已停止的作业,在退出 shell 时,bash 会提醒你,并阻止你退出。

如果在有已停止作业存在的情况下,依旧像退出 shell,可以连续输入两边 exit 命令;或者使用 kill 命令来发送一个 SIGKILL 信号来终止它(通过 PID)。

终止作业后,不会得到任何回应,但下次如果你做了能够产生 shell 提示符的操作(比如回车),你就会看到一条消息,显示作业已经被终止了。每当 shell 产生一个提示符时,它就会显示 shell 中状态发生改变的作业的状态。

6.1.3 捕获信号(trap)

trap 命令允许指定 shell 脚本要监听并从 shell 中拦截的 Linux 信号。如果脚本收到了 trap 命令中列出的信号,该信号不再由 shell 处理,而是交由本地处理。其格式为:

trap commands signals

参考下面的例子。

本例中每次使用 Ctrl+C 组合键,脚本都会执行 trap 命令中指定的 echo 语句,而不是处理该信号并允许 shell 终止该脚本。

6.1.4 捕获脚本退出(EXIT)

要捕获 shell 脚本的退出,只要在 trap 命令后加上 EXIT 信号即可(不论脚本是如何结束的)。

6.1.5 修改或移除捕获

修改

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

删除

删除一设置好的捕获,只需要在 trap 命令与希望恢复默认行为的信号列表之间加上两个破折号就行了。移除信号捕获之后,脚本会按照默认行为来处理此信号。

6.2 以后台模式运行脚本

不是所有的进程都运行在终端显示器上,这些进程就是在后台运行的进程。

6.2.1 后台运行脚本(&)

以后台模式运行 shell 只需要在命令后加个 & 符号即可。

方括号中的数字是 shell 分配给后台进程的作业号,下一个数字是 Linux 系统分配给进程的进程 ID(PID)。Linux 系统上运行的每个进程都必须有一个唯一的 PID。

一旦系统显示了这些内容,新的命令行界面提示符就出现了。你之前执行的命令正在一后台模式安全的运行,当进程结束时,它会在终端上显示一条消息。表明了作业的作业号已经作业的状态,还有用于启用作业的命令

由于后台进程依旧会使用终端显示器来显示 STDOUT 和 STDERR 消息,如果此时我们使用的别的进程也需要在终端显示器上显示数据,此时脚本输出、输入命令以及命令输出会全部混在一起。

最好时将后台运行的脚本的 STDOUT 和 STDERR 进行重定向,以避免这种杂乱的输出。

6.2.2 进行多个后台作业

可以在命令行提示符下同时启动多个后台作业。每次启动新作业时,Linux 系统都会为其分配一个新的作业号和 PID。

需要注意的是,每一个后台进程都和终端会话(pts/0)终端联系在一起。如果终端会话退出,那么后台进程也会随之退出。如果希望运行在后台模式的脚本在登出控制台后能够继续运行,需要借助其他手段。

6.3 在非控制台下运行脚本(nohup)

nohup 命令运行了另外一个命令来阻断所有发送给该进程的 SIGHUP 信号,这会在退出终端会话时阻止进程退出,脚本会一直以后台模式运行到结束。

由于 nohup 命令会解除终端与进程的关联,进程也就不再同 STDOUT 和 STDERR 联系在一起。为了保存该命令产生的输出,nohup 命令会自动将 STDOUT 和 STDERR 的消息重定向到一个名为 nohup.out 的文件中。

说明:如果使用 nohup 运行了另一个命令,该命令的输出会被追加到已有的 nohup.out 文件中。当运行位于同一个目录中的多个命令时要小心,因为所有的输出都会被发送到同一 nohup.out 文件中。

nohup.out 文件包含了通常会发送到终端显示器上的所有输出。

6.4 作业控制

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

6.4.1 查看作业(jobs)

jobs 命令允许查看 shell 当前正在处理的作业。jobs 命令常见的命令行参数如下:

参数 描述
-l 列出进程的 PID 以及作业号
-n 之类出上次 shell 发出通知后改变了状态的作业
-p 只列出作业的 PID
-r 只列出运行中的作业
-s 只列出已停止的作业

参考下面的例子:

jobs 命令输出中带有加号和减号,带加号的作业会被当作默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号,该作业会被当成作业控制命令的操作对象。当前的默认作业完成处理后,带减号的作业成为下一个默认作业,任何时候都只有一个带加号的作业和一个带减号的作业

6.4.2 重启停止的作业(fg/bg)

可以将已停止的作业作为后台进程或前台进程重启,前台进程会接管你当前工作的终端,要以前台模式重启一个作业,可以使用 fg 命令加上作业号。要以后台模式重启一个作业,可以使用 bg 命令加上作业号。

6.5 调整谦让度

在多任务操作系统中,内核负责将 CPU 时间分配给系统上运行的每个进程。调度优先级时内核分配给进程的 CPU 时间。在 Linux 系统中,由 shell 启动的所有进程的调度优先级默认都是相同的。

调度优先级是个整数值,从 -20(最高优先级)到 +19(最低优先级)。默认情况下,bash shell 以优先级 0 来启动所有进程。

6.5.1 设置命令启动时的调度优先级(nice -n)

nice 命令允许你设置命令启动时的调度优先级,要让命令以耕地的优先级运行,只要用 nice 命令的 -n 命令行来指定新的优先级别。

需要注意的是,nice 命令会阻止普通系统用户来提高命令的优先级。

6.5.2 改变已运行命令的优先级(renice)

renice 命令允许指定运行进程的 PID 来改变它的优先级。

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

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

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

6.6 定时运行作业

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

at 命令允许指定 Linux 系统何时运行脚本。at 命令会将作业提交到队列中,指定 shell 何时运行改作业。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 命令会在第二天的那个时间运行指定的作业。

at 命令可以识别多种不同的时间格式:

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

在你使用 at 命令时,该作业会被提交到作业队列。作业队列会保存通过 at 命令提交的待处理的作业。针对不同优先级,存在 26 种不同的作业队列。作业队列通常用小写字母 a~z 和大写字母 A~Z 来指代。

作业队列的字母排序越高,作业运行的优先级就越低。默认情况下,at 的作业会被提交到 a 作业队列,可以使用 -q 参数指定不同的队列字母。

  1. 获取作业的输出

当作业在 Linux 系统上运行时,显示器并不会关联到该作业。取而代之的是,Linux 系统会将提交该作业的用户的电子邮件地址作为 STDOUT 和 STDERR。由于使用 e-mail 作为 at 命令的输出极其不便,因此在使用 at 命令时,最好在脚本中对 STDOUT 和 STDERR 进行重定向。

at 命令会显示分配给作业的作业号以及为作业安排的运行时间。-f 选项指定使用哪个脚本文件,now 指示 at 命令立刻执行该脚本。

  1. 列出等待的作业(atq)

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

作业列表中显示了作业号、系统运行改作业的日期和时间以及其所在的作业队列。

  1. 删除作业(atrm)

可以通过 atrm 命令来删除等待中的作业,只要指定想要删除的作业号就行。但是需要注意,只能删除自己提交的作业,不能删除其他人提交的作业。

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

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

  1. cron 时间表

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

min hour dayofmonth month dayofweek command

cron 时间表允许你用特定值、取值范围或者通配符来指定条目。例如,如果想在每天的 10:15 运行一个命令,可以用 cron 时间表条目:

15 10 * * * command

要制定在每周一 4:15 PM 运行的命令,可以使用:

15 16 * * 1 command
  1. 构建 cron 时间表(crontab)

每个系统用户(包括 root)都可以用自己的 cron 时间表来运行安排好的任务。Linux 提供了 crontab 命令来处理 cron 时间表。要列出已有的 cron 时间表,可以使用 -l 选项。

3. 浏览 cron 目录

如果创建的脚本对精确的执行时间要求不高,用预配置的 cron 脚本目录会更加方便。有 4 个基本目录:hourly、daily、monthly 和 weekly。

如果脚本需要每天运行依次,只要将脚本复制到 daily 目录,cron 就会每天执行它。

  1. anacron 程序

如果某个作业在 cron 时间表中安排运行的时间已到,但这时候 Linux 系统处于关机状态,那么这个作业就不会被运行。当系统开机时,cron 程序不会再去运行那些错过的作业。为了解决这个问题,许多 Linux 发行版还包含了 anacron 程序。

如果 anacron 知道某个作业错过了执行时间,它会尽快运行改作业。这意味着如果 Linux 系统关机了几天,当它再次开机时,原定在关机期间运行的作业会自动运行。

这个功能常用于进行常规日志维护的脚本。如果系统在脚本应该运行的时间刚好关机,日志文件就不会被整理,可能会变很大。通过 anacron,至少可以保证系统每次启动时整理日志文件。

anacron 程序只会处理位于 cron 目录的程序,比如 /etc/cron.monthly。它用时间戳来决定作业是否在正确的计划间隔内运行了。每个 cron 目录都有个时间戳文件,该文件位于 /var/spool/anacron。

6.6.3 使用新的 shell 启动脚本

如果每次运行脚本都能够启动一个新的 bash shell,将会非常方便。有时候,你希望为 shell 会话设置某些 shell 功能。基本上,依照下列顺序所找到的第一个文件会被运行,其余的文件会被忽略:

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

因此,应该将需要在登陆时运行的脚本放在上面第一个我呢见中。每次启动一个新 shell 时,bash shell 都会运行 .bashrc 文件。

可以这样验证:在主目录下的 .bashrc 文件中假如一条简单的 echo 语句,然后启动一个新的 shell。

首先输入 vim .bashrc,然后在 .bashrc 中加入一条 echo 语句,之后 wq 保存退出。

之后启动 bash。

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


第 7 章:创建函数

7.1 基本的脚本函数

函数是一个脚本代码块,可以为其命名并在代码中任何位置重用。要在脚本中使用该代码块时,只要使用所起的函数名就行。

7.1.1 创建函数

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

function name {commands
}

name 属性定义赋予函数的唯一名称。脚本中定义的每个函数都必须有一个唯一的名称。

第二种格式更接近于其他编程语言中定义函数的方式:

name () {commands
}

7.1.2 使用函数

要在脚本中使用函数,只需要像其他 shell 命令一样,在行中指定函数名就行了。

需要注意如下一些事项:

  • 如果在函数被定义前使用函数,会产生一条错误信息;
  • 函数名必须是唯一的,如果重定义了函数,新定义回覆盖原来函数的定义。

7.2 返回值

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

7.2.1 默认退出状态码

默认情况下,函数的退出状态码时函数中最后一条命令返回的退出状态码。在函数执行结束后,额可以用标准变量 $? 来确定函数的退出状态码。

7.2.2 使用 return 命令

bash shell 使用 return 命令来退出函数并返回特定的退出状态码。return 命令运行指定一个整数值来定义函数的退出状态码

当使用这种方法从函数中返回值时,请记住下面两点:

  • 函数一结束就返回值;
  • 退出状态码必须是 0~255
  • 如果在用 $? 变量提取返回值之前执行了其他命令,函数的返回值就会丢失。$? 变量回返回执行的最后一条命令的退出状态码。

7.2.3 使用函数输出

可以将函数的输出保存到 shell 变量中,这种技术可以获得任何类型的函数输出,并将其保存到变量中。

新函数会用 echo 语句来显示计算的结果,该脚本会获取 func 函数的输出,而不是查看退出状态码

说明:通过这种技术,还可以返回浮点值和字符串值。

7.3 在函数中使用变量

7.3.1 向函数传递参数

函数可以使用标准的参数环境变量来标识命令行上传给函数的参数($0、$1)。在脚本中指定函数时,必须将参数和函数放在同一行:

func 10 $pvalue=$(func 10 $p)

然后函数可以用参数环境变量来获得参数值。

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

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

7.3.2 在函数中处理变量

函数使用两种类型变量:

  1. 全局变量

全局变量是在 shell 脚本中任何地方都有效的变量。默认情况下,在脚本中定义的任何变量都是全局变量,在函数外定义的变量可在函数内正常访问。但是这也出现了这样一种情况,如果你在函数内修改了一个和函数外同名的变量,这也会对函数外的变量造成影响。参考下面例子:

事实上可能我们不想改变函数外的 temp 的值,只是不小心起了一个和函数外同名的变量,从而影响到了函数外的变量。

  1. 局部变量(local)

只要在变量声明前加上关键字 local 既可以了。local 关键字保证变量只局限在该函数中,如果脚本中在该函数之外有同样名字的变量,那么 shell 将保持这两个变量的值是分离的。

7.4 数组变量和寒素

7.4.1 像函数传递数组参数

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

在函数内部,数组仍然像其他数组一样使用。

7.4.2 从函数返回数组

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

该脚本用变量 $arg 将数组值传递给 func 函数,func 函数将该数组复制到一个局部数组变量中,然后对数据元素进行变量,并将每个元素翻倍,最后将结果存入一个新数组中。最后 func 函数将新数组通过 echo 语句输出出来。脚本用 func 的输出生成一个新数组。

7.5 函数递归

局部函数变量的一个特性是自成体系。除了从脚本命令行处获得的变量,自成体系的函数不需要使用任何外部资源。这个特性使得函数可以递归地调用,也就是说,函数可以调用自己来得到结果。通常递归函数都有一个最终可以迭代到的基准值。

下面是一个阶乘的例子:

7.6 创建库

bash shell 允许创建函数库文件,然后在多个脚本中引用该库文件。

第一步是创建一个包含脚本中所需函数的公用库文件。

第二步是,在用到这些函数的脚本文件中包含 myfuncs 库文件。但是需要注意,和环境变量一样,shell 函数仅在定义它的 shell 会话内有效。如果你在 shell 命令行界面的提示符下运行 myfuncs shell 脚本,shell 会创建一个新的 shell 并在其中运行这个脚本。

使用库函数的关键在于 source 命令(source 命令有个快捷别名,称作点操作符,其格式为:. ./myfuncs )。source 命令会在当前 shell 上下文中执行命令,而不是创建一个新 shell。

7.7 在命令行上使用函数

一旦在 shell 中定义了函数,就可以在整个系统中使用它了,无需担心脚本是不是在 PATH 环境变量中。重点是让 shell 能够识别这些函数。

7.7.1 在命令行上创建函数

有两种方法可以在命令行上定义一个函数。

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

第二种方式是采用多行方式来定义函数。在定义时,bash shell 会使用次提示符来提示输入更多命令。用这种方法,就不用再每条命令的末尾放一个分号了,只用按下回车就行。

警告:再命令行上创建函数时,如果你给函数起了跟内置命令或另一个命令相同的名字,函数将会覆盖原来的命令。

7.7.2 在 .bashrc 文件中定义函数

在命令行上直接定义 shell 函数的缺点是在退出 shell 时,函数就消失了。而在 .bashrc 文件定义函数,bash shell 在每次启动时都会在主目录下查找这个文件,不管是交互式 shell 还是从现有的 shell 中启动新的 shell。

可以直接在主目录下的 .bashrc 文件中定义函数。也可以在 .bashrc 文件中通过 source 命令将库文件中的函数添加到 .bashrc 脚本中,只要确保库文件的路径名正确就行。

第二部分:shell 脚本编程基础相关推荐

  1. Linux Shell脚本编程基础

    2 Linux Shell脚本编程基础 发表于: Linux, Shell, UNIX, 资源分享 | 作者: 谋万世全局者 标签: Linux,Shell,编程基础,脚本 本文作者:Leal 授权许 ...

  2. Linux基础篇--shell脚本编程基础

    Linux基础篇–shell脚本编程基础 本章内容概要  编程基础  脚本基本格式  变量  运算  条件测试  配置用户环境 7.1 编程基础 程序:指令+数据 程序编程风格:   过程式:以指令为 ...

  3. linux基础—课堂随笔_03 SHELL脚本编程基础

    shell脚本编程基础 条件选择:if语句 选择执行: 注意:if语句可嵌套 单分支 if(开头)判断条件:then 条件为真的分支代码  fi(结尾) 双分支 if(开头)判断条件:then 条件为 ...

  4. SHELL 脚本编程基础

    目录 前言 一. shell 概述 1.1 shell 和 shell 脚本 1.1.1 什么是shell 1.1.2 shell的作用 1.1.3 shell脚本是什么 1.1.4 shell脚本能 ...

  5. 《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---34

    以下为阅读<Linux命令行与shell脚本编程大全 第3版>的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下: 转载于:https://www.cnbl ...

  6. 《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---02

    以下为阅读<Linux命令行与shell脚本编程大全 第3版>的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下: 转载于:https://www.cnbl ...

  7. 《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---20

    以下为阅读<Linux命令行与shell脚本编程大全 第3版>的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下: 转载于:https://www.cnbl ...

  8. 《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---43

    以下为阅读<Linux命令行与shell脚本编程大全 第3版>的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下: 转载于:https://www.cnbl ...

  9. 《Linux命令行与shell脚本编程大全 第3版》Shell脚本编程基础---23

    以下为阅读<Linux命令行与shell脚本编程大全 第3版>的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下: 转载于:https://www.cnbl ...

  10. 算法及shell脚本编程基础

    bash存在多命令执行的特性,例如:# COMMAND1 $(COMMAND2):还有进程之间的通信(IPC):# COMMAND1 | COMMAND2- 一.命令执行结构与算法 命令执行中,存在顺 ...

最新文章

  1. [Java] Hashcode的作用
  2. linux系统下docker安装,Linux下Docker的安装与使用
  3. Ubuntu17.10 下配置caffe 仅CPU i386可以直接apt install caffe-cpu,但是怎么运行mnist代码我懵逼了...
  4. 【数字信号处理】相关函数 ( 能量信号 | 能量信号的互相关函数 | 能量信号的自相关函数 )
  5. Android 消息机制 Handler总结
  6. oracleasm 建立时出错
  7. kinmall分析百度亮剑区块链能否险中求胜?
  8. ERP和C4C中的function location
  9. python自带的idle输入python_打开python自带IDLE出的问题
  10. 浅谈Linux用户权限管理之三(文件与权限的设定)
  11. xgboost和lightGBM区别
  12. 《信号与系统学习笔记》—采样(一)
  13. 大数据项目开发进度(实时更新)
  14. html链接屏蔽了,js屏蔽广告/屏蔽网站元素/屏蔽网站超链接
  15. 玩战塔英雄不显示服务器,王者荣耀的这个问题,国家点名了仍没有改正过来!战塔英雄就没有...
  16. 学术英语(理工)第二版unit5课文翻译
  17. 人的一生应该追求什么东西呢
  18. 【Scrum模式语言3】完成的定义
  19. Protobuf(Protocol Buffer)在Unity中的简单应用
  20. html单元格合璧,你绝对不曾见过的Excel+DOS双剑合璧操作文件夹之7大妙招

热门文章

  1. (C语言网)那些让你起飞的计算机基础知识:学什么,怎么学?
  2. 微信支撑10亿用户背后核心技术:亿级流量Java并发与网络编程实战
  3. 文献阅读(41) Retrain-Less/DSIP
  4. TSDB写入与查询讲解,聚合(aggregator)与采样(downsample)讲解—以opentsdb为例
  5. packaging‘ with value ‘jar‘ is invalid Aggregator projects require ‘pom‘ as packaging
  6. 室内隔断房怎么装修?应该注意什么
  7. 山海鲸可视化凭借技术优势打造高性价比数字孪生
  8. python库:scapy使用
  9. Exception in thread “main“ java.sql.SQLException: Connections could not be acquired from the underly
  10. Java基础笔记6——File、流、枚举、断言