学习 Linux,101: 自定义或编写简单脚本

利用 Linux 脚本的强大功能

学习如何使用标准的 shell 语法、循环和控制结构,以及成功或失败测试来自定义现有脚本或编写简单的新 bash 脚本。您可以使用本教程中的资料学习针对 Linux 系统管理员认证的 LPI 102 考试内容,或者仅为兴趣而学习。

Ian Shields, Linux 作家, Freelance

Ian Shields 是一位自由 Linux 作家。他已从位于北卡罗来纳州的 IBM 三角研究园退休。Ian 于 1973 年在澳大利亚堪培拉作为系统工程师加入 IBM,他在加拿大蒙特利尔和北卡罗莱拉州 RTP 从事过系统工程和软件开发工作。从上世纪 90 年代开始,他开始使用、开发 Linux 和进行相关的写作工作。他从澳大利亚国立大学获得了纯数学和相关专业的学士学位。他还拥有北卡罗来纳州立大学计算机科学方向的理科硕士和哲学博士学位。他喜欢定向赛跑和旅行。



2016 年 2 月 23 日

概述

在本教程中,学习自定义现有脚本或编写简单的新 bash 脚本。学习:

  • 使用标准的循环和控制结构
  • 使用命令替换
  • 测试来自命令的返回值来确定成功还是失败
  • 有条件地向超级用户发送邮件
  • 确保使用正确的 shell 解释您的脚本
  • 管理脚本的位置、所有权、执行和 suid 权利

使用 Linux shell 编程

在本教程中,我将通过 &&||来完善简单的命令执行和最小化测试。我将介绍如何使用 bash shell 控制结构为 shell 脚本增添强大的编程功能。首先将介绍如何执行您可赖以制定控制决策的各种测试。然后介绍如何使用 if-then-elseforwhilecase控制结构来利用这些测试结果。最后,我将介绍一些重要问题,关于谁有权利运行您的脚本,以及当您的脚本没有处于终端用户的直接控制下时,运行时如何通知超级用户(根用户)。

关于本系列

本教程系列将帮助学习 Linux 系统管理任务。您还可以使用这些教程中的资料对 Linux Professional Institute 的 LPIC-1:Linux 服务器专业认证考试进行备考。

请参阅 “学习 Linux,101:LPIC-1 学习路线图”,查看本系列中每部教程的描述和链接。这个路线图正在开发之中,它反映了 2015 年 4 月 15 日更新的 4.0 版 LPIC-1 考试目标。在完成这些教程中,会将它们添加到路线图中。

本教程帮助您对 Linux Server Professional (LPIC-1) 考试 102 的主题 105 中的目标 105.2 进行应考准备。该目标的权重为 4。

前提条件

要充分掌握本系列中的教程,您需要:

  • 掌握 Linux 的基本知识
  • 熟悉 GNU 和 UNIX®命令
  • 一个正常运行的 Linux 系统,您可以在该系统上练习本教程中介绍的命令

本教程以针对考试 101 的主题 103 的教程中介绍的材料为基础。此外,您还需要熟悉 “学习 Linux,101:自定义和使用 shell 环境” 中介绍的材料。

有时程序的不同版本会得到不同的输出格式,所以您的结果可能并不总是与这里给出的清单和图完全相同。本教程中的示例大部分都与发行版独立。除非另行说明,本文中的示例使用了 Ubuntu 15.10 和 4.2.0 内核。


变量赋值和算法

在学习任何编程语言时,都会学习如何将值赋给变量。在本系列前面的教程中,您学习了如何将字符串值赋给变量。Bash 支持使用整数的 shell 算法。您可以将一个表达式计算为算术值,并使用 let内建命令将它赋给一个变量。您可以明确将变量声明为整数变量,未来对它的赋值将会计算为整数表达式。 清单 1显示了两种方法的示例和一些细微区别。

清单 1. 变量赋值和算法
 ian@attic-u15:~$ x=3+4
 ian@attic-u15:~$ let y=5*10
 ian@attic-u15:~$ declare -i z=5*4/3
 ian@attic-u15:~$ echo $x $y $z
 3+4 50 6 
 ian@attic-u15:~$ # Use declare -p to show more information
 ian@attic-u15:~$ declare -p x y z
 declare -- x="3+4"
 declare -- y="50"
 declare -i z="6"

请注意,只有变量 z被声明为整数。

您可以在 shell 算法中使用大部分 C 或 C++ 算术运算符,包括逐位和逻辑运算符。您可以使用前和后增量运算符,以及常用的 C 或 C++ 幅值运算符,比如 +=&&=|=。如果需要将运算分组,可以使用圆括号。如果愿意的话,可以使用 letdeclare在一行中为多个变量赋值。如果希望在一个算术表达式中使用一个变量值,则不需要在变量名前使用 $,但是,如果您愿意的话,也可以这么做。 清单 2给出了 bash 中的更多算法例子。

清单 2. 更多算术赋值例子
 ian@attic-u15:~$ declare -i p q r
 ian@attic-u15:~$ let p=" x + 7 " q=" (y * 2**4) / 100 "
 ian@attic-u15:~$ q=" 2**z - (50 /3 ) + 7%4 "
 ian@attic-u15:~$ r=4
 ian@attic-u15:~$ r+=" q + ( 17 > 4) "
 ian@attic-u15:~$ echo $p $q $r
 14 51 56 
 ian@attic-u15:~$ declare -p p q r
 declare -i p="14"
 declare -i q="51"
 declare -i r="56"
 ian@attic-u15:~$ let t=3 u=p+q
 ian@attic-u15:~$ echo $t $u
 3 65 
 ian@attic-u15:~$ declare -p t u
 declare -- t="3"
 declare -- u="65"

请注意,=符号左边不能有空格,而且它的右边任何包含空格的内容都必须放在单引号或双引号中。您可以使用 (( ))结构来进行赋值,从而扩展这些规则。您不需要转义 (())之间的运算符。 清单 3显示了如果在错误的位置拥有空格会发生的情况,以及如何使用 (( ))来缓解该问题。

清单 3. 算法、空格和 (( ))
 ian@attic-u15:~$ declare -i t
 ian@attic-u15:~$ t= 3**3 % 5
 3**3: command not found 
 ian@attic-u15:~$ t = 3**3 % 5
 t: command not found 
 ian@attic-u15:~$ (( t = 3**3 % 5 ))
 ian@attic-u15:~$ echo $t
 2 
 ian@attic-u15:~$ # Logical expression using unescaped shell meta characters
 ian@attic-u15:~$ (( u = ( 3 > 5 ) || ( 4 < 6 ) ))
 ian@attic-u15:~$ echo $u
 1

测试

知道如何将值赋给变量和传递参数之后,您还需要知道如何测试这些值和参数。您已经知道 $?包含来自一个 shell 命令的返回状态。还可以设置该值,将它用于变量声明和赋值,以及我稍后将展示的测试。test命令是一个内建命令,它执行各种测试,并将返回状态设置为 0(成功或 true)或 1(失败或 false)。在本教程后面,我将展示如何使用返回状态来制定决策,比如在 if-then-else结构中。

test[

在以前的教程(“学习 Linux, 101:自定义和使用 shell 环境” 中的简单 add2path函数中,我介绍了 test命令,展示了在您的变量 PATH变量没有目录时如何添加它。参见 清单 4。

清单 4. add2path函数
 ian@attic-u15:~$ type  add2path
 add2path is a function 
 add2path () 
 { 
    local augpath augdir; 
    augpath=":$PATH:"; 
    augdir=":$1:"; 
    test "$augpath" = "${augpath/$augdir}" && PATH="$1:$PATH"
 }

根据表达式 expr的计算结果,test内建命令将会返回 0(true) 或 1(false)。您还可以使用方括号;test exprexpr ]是等效的。您可以显示 $?来检查返回值。然后可以像以前使用 && 和 || 一样使用返回值。或者您可以使用我将在本教程后面介绍的各种条件结构来测试返回值。 清单 5显示了一些简单的测试例子。

清单 5. 一些简单的测试
 ian@attic-u15:~$ test 3 -gt 4 && echo true || echo false
 false 
 ian@attic-u15:~$ [ "abc" != "def" ];echo $?
 0 
 ian@attic-u15:~$ [ "abc" = "def" ];echo $?
 1 
 ian@attic-u15:~$ test -d "$HOME" ;echo $?
 0

清单 5中的第一个示例使用 -gt运算符在两个文字值之间执行算术比较。第二和第三个示例使用了替代语法 [ ]来比较两个字符串相等还是不相等,然后在每种情况下回送 $?的值。最后一个示例使用 -d一元运算符来检查 HOME变量是否是一个目录的名称。

可以使用 -eq(相等)、-ne(不等)、-lt(小于)、-le(小于或等于)、-gt(大于)或 -ge(大于或等于)中的一个运算符来比较算术值。

可以使用 =来比较字符串是否相等,使用 !=比较字符串是否不等,并使用 <>确定第一个字符串排在第二个字符串之前还是之后。一元运算符 -z将会测试 null 字符串;如果一个字符串不是 null,-n或 no 运算符返回 true (0)。

<>运算符也被 shell 用来进行重定向,所以您必须使用 \<\>对它们进行转义。 清单 6显示了字符串测试的更多示例。

清单 6. 更多字符串测试
 ian@attic-u15:~$ test "abc" = "def" ;echo $?
 1 
 ian@attic-u15:~$ [ "abc" != "def" ];echo $?
 0 
 ian@attic-u15:~$ [ "abc" \< "def" ];echo $?
 0 
 ian@attic-u15:~$ [ "abc" \> "def" ];echo $?
 1 
 ian@attic-u15:~$ [ "abc" \< "abc" ];echo $?
 1 
 ian@attic-u15:~$ [ "abc" \> "abc" ];echo $?
 1 
 ian@attic-u15:~$ [ -z "abc" ]; echo $?
 1 
 ian@attic-u15:~$ [ -n "abc" ]; echo $?
 0

您可以在文件系统对象上使用许多测试。 表 1显示了一些常见的测试。如果测试的对象存在并具有指定的属性,则结果为 true (0)。

表 1. 常见文件测试
一元运算符特征
-d目录
-e-a存在
-f普通文件
-h-L符号链接
-p命名管道
-r可被您读取
-s不是 null
-S套接字
-w可被您写入
-N自上次读取以来已修改

您也可使用 表 2中所示的二元运算符来比较两个文件。

表 2. 文件比较测试
二元运算符特征
-nt测试文件 1 是否比文件 2 更新。此比较会使用修改时间戳。
-ot测试文件 1 是否比文件 2 更旧。此比较会使用修改时间戳。
-ef测试文件 1 是否是文件 2 的硬链接。

可以使用其他测试来检查文件的权限设置等方面。请参阅 bash 手册页了解更多的细节,或者使用 help test来查看 test内建命令的简略信息。您可以将 help命令用于其他内建命令。

您可以使用一元 -o运算符来测试各种 shell 选项是否已设置。如 清单 7所示,如果 -o 选项已设置,test -o option返回 true (0);否则它返回 false (1)。

清单 7. 测试 shell 选项
 ian@attic-u15:~$ # Setting and testing the unset option
 ian@attic-u15:~$ set +o nounset
 ian@attic-u15:~$ echo $MYTESTVAR

 ian@attic-u15:~$ [ -o nounset ];echo $?
 1 
 ian@attic-u15:~$ # You can also set/unset nounset using set -u or set +u
 ian@attic-u15:~$ set -u
 ian@attic-u15:~$ echo $MYTESTVAR
 bash: MYTESTVAR: unbound variable 
 ian@attic-u15:~$ test -o nounset; echo $?
 0

可以使用 -a二元选项来将表达式与逻辑与 (logical AND) 相组合,使用 -o二元选项来将表达式与逻辑或 (logical OR) 相组合。一元 !运算符对测试的含义求反。可使用圆括号来将表达式分组或覆盖默认优先级。请记住,shell 通常在一个子 shell 内运行括号之间的表达式,所以您必须使用 \(\)对圆括号进行转义,或者当您不想一个表达式在子 shell 内运行时,可以将这些运算符放在单引号或双引号中。 清单 8演示了 德·摩根定律在表达式上的应用。

清单 8. 组合和分组测试
 ian@attic-u15:~$ test "a" != "$HOME" -a 3 -ge 4 ; echo $?
 1 
 ian@attic-u15:~$ [ ! \( "a" = "$HOME" -o 3 -lt 4 \) ]; echo $?
 1 
 ian@attic-u15:~$ [ ! \( "a" = "$HOME" -o '(' 3 -lt 4 ')' ")" ]; echo $? 
 1
 ian@attic-u15:~$ # Be careful. ! has higher priority that -a or -o
 ian@attic-u15:~$ [ ! \( "a" = "$HOME" \) -o '(' 3 -lt 4 ')'  ]; echo $?
 0

test命令很强大,但转义的需求和字符串与算术比较之间的区别可能让它变得不实用。幸运的是,bash 有其他两种方式来设置算术和逻辑表达式的返回代码,如果您熟悉 C、C++ 或 Java 语法,那么它们看起来应该更自然一些。

来自 (( ))[[ ]] 的返回状态

您在本教程开头看到的 (( ))复合命令计算一个算术表达式,如果表达式计算为 0,则将退出状态设置为 1,或者如果表达式计算为非 0 值,则设置为 0。请注意,let命令基于最后一个参数计算为 0 还是非 0 值来设置返回状态。 清单 9显示了一些示例。

清单 9. 来自 (( )) 的返回状态
 ian@attic-u15:~$ let x=2 y=2**3 z=y*3;echo $? $x $y $z
 0 2 8 24 
 ian@attic-u15:~$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w
 0 3 8 16 
 ian@attic-u15:~$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w
 0 4 8 13 
 ian@attic-u15:~$ (( w - w )) ;echo $?
 1

[[ ]]复合命令执行一个条件表达式,并将返回状态设置为 0(true) 或 1(false)。与 (( ))一样,您可以为 [[ ]]复合命令使用更自然的语法来执行文件名和字符串测试。通过使用圆括号和逻辑运算符,您可以将 test命令可运行的测试组合在一起。参见 清单 10。

清单 10. 来自 [[ ]] 的返回状态
 ian@attic-u15:~$ [[ ( -d "$HOME" ) && ( -w "$HOME" ) ]]; echo $?
 0 
 ian@attic-u15:~$ [[ ( -d "$HOME" ) && ( -w "$HOME" ) ]] && 
 > echo "home is a writable directory"
 home is a writable directory

当使用 ==!=运算符时,您可以使用 [[ ]]复合命令在字符串上执行模式匹配。该匹配行为与 shell 通配符语法相同,如 清单 11中所示。

清单 11. 使用 [[ ]] 的通配符测试
 ian@attic-u15:~$ [[ "abc def .d,x--" == a[abc]*\ ?d* ]]; echo $?
 0 
 ian@attic-u15:~$ [[ "abc def c" == a[abc]*\ ?d* ]]; echo $?
 1 
 ian@attic-u15:~$ [[ "abc def d,x" == a[abc]*\ ?d* ]]; echo $?
 1

[[ ]]中,===拥有相同的含义,所以您可以使用任意一个。如果您希望模式是正则表达式而不是 shell 通配符语法,那么可以使用 =~。参见 清单 12。

清单 12. 使用 [[ ]] 的正则表达式模式匹配
 ian@attic-u15:~$ # Wildcard globbing does not match this pattern
 ian@attic-u15:~$ [[ "abc def c" == a[abc]*\ ?d* ]]; echo $? 
 1
 ian@attic-u15:~$ # But regular expression matching does
 ian@attic-u15:~$ [[ "abc def c" =~ a[abc]*\ ?d* ]]; echo $?
 0

您甚至可以在 [[ ]]复合命令内执行算术测试,但要小心。除非它们在一个嵌套的 (( ))复合命令内,否则 <>运算符会将操作数当作字符串来比较,并测试它们在当前核对序列中的顺序。 清单 13通过一些示例演示了这种行为。

清单 13. [[ ]] 中的算法测试
 ian@attic-u15:~$ # Set warning in case we use an unbound variable
 ian@attic-u15:~$ # Otherwise names are interpreted as strings
 ian@attic-u15:~$ set -u
 ian@attic-u15:~$ # First expression is false
 ian@attic-u15:~$ [[ "abc def d,x" == a[abc]*\ ?d* ]]; echo $?
 1 
 ian@attic-u15:~$ [[ "abc def d,x" == a[abc]*\ ?d* || (( 3 > 2 )) ]]; echo $?
 0 
 ian@attic-u15:~$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 -gt 2 ]]; echo $?
 0 
 ian@attic-u15:~$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 > 2 ]]; echo $?
 0 
 ian@attic-u15:~$ [[ "abc def d,x" == a[abc]*\ ?d* || a > 2 ]]; echo $?
 0 
 ian@attic-u15:~$ [[ "abc def d,x" == a[abc]*\ ?d* || a -gt 2 ]]; echo $?
 bash: a: unbound variable 
 ian@attic-u15:~$ # Restore default
 ian@attic-u15:~$ set +u

条件

您可以使用我目前展示的测试及 &&||控制运算符来完成大量编程。此外,bash 还包含更熟悉的 if-then-elsecase结构。在我展示这些结构和循环结构后,您的工具箱会变得充实得多。

使用 if-then-else语句

尽管您目前看到的测试仅返回 01值,但该命令可以返回其他值。本教程后面会介绍更多测试这些值的知识。

bash if命令是一个复合命令,它测试一次测试或命令的返回状态 ($?),并基于返回状态为 true (0) 还是 false(非 0)而进行分支。bash 中的 if命令有一个 then子句,其中包含在测试或命令返回 0时要执行的命令列表。该命令还有一个或多个可选的 elif子句。每个可选的 elif子句都有一项额外的测试和一个拥有关联的命令列表的 then子句。最后的一个 else子句和关联的命令列表是可选的。如果最初的测试和 elif子句中使用的任何测试的结果都不是 true,则运行最后的 else子句。需要一个终止 fi来标记结构的末尾处。

利用您目前在这些教程中学到的知识,现在可以构建一个简单的计算器来计算算术表达式,如 清单 14中所示。

清单 14. 使用 if-then-else计算表达式
 ian@attic-u15:~$ function mycalc ()
 > {
 >   local x
 >   if [ $# -lt 1 ]; then
 >     echo "This function evaluates arithmetic for you if you give it some"
 >   elif (( $* )); then
 >   let x="$*"
 >     echo "$* = $x"
 >   else
 >     echo "$* = 0 or is not an arithmetic expression"
 >   fi
 > } 
 ian@attic-u15:~$ mycalc 3 + 4
 3 + 4 = 7 
 ian@attic-u15:~$ mycalc 3 + 4**3
 3 + 4**3 = 67 
 ian@attic-u15:~$ mycalc 3 + (4**3 /2)
 bash: syntax error near unexpected token `('
 ian@attic-u15:~$ mycalc 3 + "(4**3 /2)"
 3 + (4**3 /2) = 35 
 ian@attic-u15:~$ mycalc xyz
 xyz = 0 or is not an arithmetic expression 
 ian@attic-u15:~$ mycalc xyz + 3 + "(4**3 /2)" + abc
 xyz + 3 + (4**3 /2) + abc = 35

计算器使用 local语句将 x声明为只能在 mycalc函数的范围内使用的局部变量。let内建命令有多个可能的选项,与和它密切相关的 declare命令一样。请查阅 bash 的手册页或使用 help let来了解更多的信息。

您已在 清单 14中看到,如果您的表达式使用了 shell 元字符,比如 ()*><,那么这些表达式必须正确转义。但是,您现在有一个方便的小计算器来计算算术表达式,就像 shell 一样。

请注意 清单 14中的最后两个示例。将 xyz传递给 mycalc并没有错,但除非您之前已将一个值赋给变量 xyz,否则它将计算为 0。在最后的例子中,该函数不够聪明,无法识别字符值来提醒您,xyzabc被静默地当作具有值 0的变量来处理。您可以使用一种字符串模式匹配测试,比如 [[ ! ("$*" == *[a-zA-Z]* ]](或针对您的语言环境的合适形式),以消除任何包含字母字符的表达式,但这会阻止您将 shell 变量用作输入。它还会阻止您在输入中使用十六进制表示法,因为十六进制表示法(比如 0x0f表示十进制树 15)可能包含字母。事实上,您可以在 shell 中(通过 base#表示法)使用最多 64 个 base 字符,所以您的输入可以合法地包含任何字母字符,以及 _@。对于八进制和十六进制的特殊情况,可以使用更常见的表示法,也就是说,在八进制数前面添加 0,在十六进制数前面添加 0x 或 0X。清单 15显示了一些示例。

清单 15. 使用不同的 base 字符来计算
 ian@attic-u15:~$ mycalc 015
 015 = 13 
 ian@attic-u15:~$ mycalc 0xff
 0xff = 255 
 ian@attic-u15:~$ mycalc 29#37
 29#37 = 94 
 ian@attic-u15:~$ mycalc 64#1az
 64#1az = 4771 
 ian@attic-u15:~$ mycalc 64#1azA
 64#1azA = 305380 
 ian@attic-u15:~$ mycalc 64#1azA_@
 64#1azA_@ = 1250840574 
 ian@attic-u15:~$ mycalc 64#1az*64**3 + 64#A_@
 64#1az*64**3 + 64#A_@ = 1250840574

对输入的其他处理不属于本教程的讨论范围,所以请审慎地使用您计算器。

elif语句很方便,可以帮助您简化脚本中的缩进。 清单 16展示了如何对 mycalc函数使用 type命令来显示 清单 14elif语句的等效形式。

清单 16. 类型 mycalc
 ian@attic-u15:~$ type mycalc
 mycalc is a function 
 mycalc () 
 { 
    local x; 
    if [ $# -lt 1 ]; then 
        echo "This function evaluates arithmetic for you if you give it some"; 
    else 
        if (( $* )); then 
            let x="$*"; 
            echo "$* = $x"; 
        else 
            echo "$* = 0 or is not an arithmetic expression"; 
        fi; 
    fi 
 }

Case 语句

在有多种可能性且希望基于某个值是否与某种特定可能性匹配来执行操作时,可以使用 case复合命令来简化测试。case复合命令以 case WORD in开始,以 esac(的反向拼写)结尾。每个 case包含一种模式或多个以 |分隔的模式,后跟 )、一个语句列表,最后是一对分号 (;;)。

例如,想象一个出售咖啡、无咖啡因咖啡 (decaf)、茶叶或苏打水的商店。 清单 17中的函数可用于确定对一个订单的响应。

清单 17. 使用 case命令
 ian@attic-u15:~$ type myorder
 myorder is a function 
 myorder () 
 { 
    case "$*" in 
        "coffee" | "decaf") 
            echo "Hot coffee coming right up"
        ;; 
        "tea") 
            echo "Hot tea on its way"
        ;; 
        "soda") 
            echo "Your ice-cold soda will be ready in a moment"
        ;; 
        *) 
            echo "Sorry, we don't serve that here"
        ;; 
    esac 
 } 
 ian@attic-u15:~$ myorder decaf
 Hot coffee coming right up 
 ian@attic-u15:~$ myorder tea
 Hot tea on its way 
 ian@attic-u15:~$ myorder milk
 Sorry, we don't serve that here

请注意,我们使用了 *来匹配任何还未被匹配的内容。

另一个与 case类似的 bash 结构是 select语句,这里没有介绍它。可以使用它将一个商品输出列表打印到终端,您的用户可以从该列表中进行选择。请参阅 bash 手册页或键入 help select来了解 select的更多信息。

当然,这样一个简单的饮品订购系统有许多问题;您不能一次订购两种饮品,而且该函数只能处理小写输入。您能否执行不区分大小写的匹配?答案是能,我将展示如何做。


返回值

Bash 有一个 shopt内建命令可用来设置或取消设置许多 shell 选项。其中一个选项是 nocasematch,如果设置了该选项,它会告诉 shell 在字符串匹配中忽略大小写。您的第一个想法可能是使用您在 test命令中学到的 -o操作数。不幸的是,nocasematch不是可以使用 -o测试的选项,所以您必须采用不同的方法。

您之前学到的测试不是能返回值的唯一测试。举例而言,if语句可测试基础 test命令的返回值是 true (0) 还是 false(非 0)。即使您使用了 test 以外的命令,成功和失败也分别由返回值 0和非零返回值表示。像大部分 UNIX 和 LInux 命令一样,shopt命令将会设置一个可以使用 $?检查的返回值。

掌握这项知识后,您现在可以测试 nocasematch选项,如果尚未设置它,请设置它,然后在您的函数终止时将该设置恢复为用户的首选项。shopt命令有 4 个方便的选项:-pqsu:打印当前值,不打印任何内容,设置该选项或取消设置该选项。-p-q选项设置一个返回值 0,用该值表示 shell 选项已设置,设置 1来表示它未设置。-p选项打印出了将该选项设置为当前值需要使用的命令,而 -q选项简单地将返回值设置为 01。 清单 18显示了您修改 myorder函数所需的基本用法示例,其中使用了您之前在 [[ ]]中看到的模式匹配。

清单 18. 使用 shopt
 ian@attic-u15:~$ # nocasematch starts out unset
 ian@attic-u15:~$ shopt -p nocasematch ; echo $?
 shopt -u nocasematch 
 1 
 ian@attic-u15:~$ # test it
 ian@attic-u15:~$ [[ "abc" = "ABC" ]] ;echo $?
 1 
 ian@attic-u15:~$ # set nocasematch
 ian@attic-u15:~$ shopt -s nocasematch ; echo $?
 0 
 ian@attic-u15:~$ # test the pattern again
 ian@attic-u15:~$ [[ "abc" = "ABC" ]] ;echo $?
 0 
 ian@attic-u15:~$ # restore nocasematch
 ian@attic-u15:~$ shopt -u nocasematch ; echo $?
 0

如 清单 19所示,修改后的 myorder函数现在可以使用来自 shopt的返回值来:

  1. 设置一个表示 nocasematch选项的当前状态的局部变量。
  2. 设置该选项。
  3. 返回 case命令。
  4. nocasematch选项重设为它的原始值。
清单 19. 测试来自 shopt命令的返回值
 ian@attic-u15:~$ type myorder
 myorder is a function 
 myorder () 
 { 
    local restorecase; 
    if shopt -q nocasematch; then 
        restorecase="-s"; 
    else 
        restorecase="-u"; 
        shopt -s nocasematch; 
    fi; 
    case "$*" in 
        "coffee" | "decaf") 
            echo "Hot coffee coming right up"
        ;; 
        "tea") 
            echo "Hot tea on its way"
        ;; 
        "soda") 
            echo "Your ice-cold soda will be ready in a moment"
        ;; 
        *) 
            echo "Sorry, we don't serve that here"
        ;; 
    esac; 
    shopt $restorecase nocasematch 
 } 
 ian@attic-u15:~$ shopt -p nocasematch
 shopt -u nocasematch 
 ian@attic-u15:~$ # nocasematch is currently unset
 ian@attic-u15:~$ myorder DECAF
 Hot coffee coming right up 
 ian@attic-u15:~$ myorder Soda
 Your ice-cold soda will be ready in a moment 
 ian@attic-u15:~$ shopt -p nocasematch
 shopt -u nocasematch 
 ian@attic-u15:~$ # nocasematch is unset again after running the myorder function

如果您想您的函数(脚本)返回其他函数或命令可以测试的值,那么可以在您的函数中使用 return 语句。 清单 20展示了如何为一种您可以销售的饮品返回 0,如果客户请求其他商品,则返回 1

清单 20. 设置您自己的函数返回值
 ian@attic-u15:~$ type myorder
 myorder is a function 
 myorder () 
 { 
    local restorecase; 
    rc=0; 
    if shopt -q nocasematch; then 
        restorecase="-s"; 
    else 
        restorecase="-u"; 
        shopt -s nocasematch; 
    fi; 
    case "$*" in 
        "coffee" | "decaf") 
            echo "Hot coffee coming right up"
        ;; 
        "tea") 
            echo "Hot tea on its way"
        ;; 
        "soda") 
            echo "Your ice-cold soda will be ready in a moment"
        ;; 
        *) 
            echo "Sorry, we don't serve that here"; 
            rc=1 
        ;; 
    esac; 
    shopt $restorecase nocasematch; 
    return $rc 
 } 
 ian@attic-u15:~$ myorder coffee;echo $?
 Hot coffee coming right up 
 0 
 ian@attic-u15:~$ myorder milk;echo $?
 Sorry, we don't serve that here 
 1

如果没有指定您自己的返回值,返回值将是执行的上一个命令的返回值。函数和脚本有一种在您从未考虑到的情况下被重用的倾向,所以一种好的做法是设置您自己的值。

命令可以返回 01以外的值,而且有时您需要额外的信息。例如,如果模式匹配,grep命令将会返回 0;如果不匹配,则会返回 1;但是,如果模式无效或文件规范与任何文件都不匹配,则会返回 2。如果需要区分成功 (0) 或失败(非 0)以外的返回值,可以使用 case命令或一个包含多个 elif部分的 if命令。


命令替换

如果将一个命令放在 $()之间或一对重音符 `之间,您可以将该命令的输出替换为另一个命令的输入。这种技术称为 命令替换。在需要嵌套命令替换时,可以采用 $()的形式。这种形式也使确定发生的情况变得更容易,因为圆括号有左右之分,但两个重音符是相同的。选择权在您手上,而且重音符仍然很常见。

我们常常将命令替换与循环结合使用(将在后面的 “循环” 中介绍)。但是,您还可以使用它来稍微简化 myorder函数。因为 shopt -p nocasematch打印您需要将 nocasematch选项设置为其当前值的命令,所以您只需保存该输出,然后在 case语句的末尾执行它。通过这么做,您会恢复 nocasematch选项,无论您是否更改了它。修改后的函数现在可能类似于 清单 21。请自行尝试它。

清单 21. 使用命令替换而不是返回值测试
 ian@attic-u15:~$ type myorder
 myorder is a function 
 myorder () 
 { 
    local restorecase=$(shopt -p nocasematch) rc=0; 
    shopt -s nocasematch; 
    case "$*" in 
        "coffee" | "decaf") 
            echo "Hot coffee coming right up"
        ;; 
        "tea") 
            echo "Hot tea on its way"
        ;; 
        "soda") 
            echo "Your ice-cold soda will be ready in a moment"
        ;; 
        *) 
            echo "Sorry, we don't serve that here"; 
            rc=1 
        ;; 
    esac; 
    $restorecase; 
    return $rc 
 } 
 ian@attic-u15:~$ shopt -p nocasematch
 shopt -u nocasematch 
 ian@attic-u15:~$ myorder DECAF
 Hot coffee coming right up 
 ian@attic-u15:~$ myorder TeA
 Hot tea on its way 
 ian@attic-u15:~$ shopt -p nocasematch
 shopt -u nocasematch

调试

如果您输入了函数定义且出现了输入错误,您想知道哪里出错了,您可能还想知道如何调试函数。幸运的是,您可以设置 -x选项在 shell 执行命令时跟踪它们和它们的参数。 清单 22展示了如何对来自 清单 21myorder函数使用此选项。

清单 22. 跟踪执行
 ian@attic-u15:~$ set -x
 ian@attic-u15:~$ myorder tea
 + myorder tea 
 ++ shopt -p nocasematch 
 + local 'restorecase=shopt -u nocasematch' rc=0 
 + shopt -s nocasematch 
 + case "$*" in 
 + echo 'Hot tea on its way'
 Hot tea on its way 
 + shopt -u nocasematch 
 + return 0 
 ian@attic-u15:~$ set +x
 + set +x

您可以对您的别名、函数或脚本使用此技术。如果需要更多的信息,可以添加 -v选项来获得详细的输出。


循环

Bash 和其他 shell 语言有 3 种循环结构与 C 语言中的循环结构比较相似。每种循环执行一个命令列表 0 次或更多次。命令列表放在单词 dodone之间,每个命令前都有一个分号。

for
for循环有两种形式。shell 脚本中的最常用的形式是迭代一组值,对每个值执行命令列表一次。这组值可能是空的,在这种情况下,不会执行命令列表。另一种形式更加类似于传统的 C for循环,它使用 3 个算术表达式来控制开始条件、步进函数和结束条件。
while
while循环该循环每次开始时计算一个条件,如果条件为 true,则执行命令列表。如果该条件最初不为 true,则从不执行这些命令。
until
until循环执行命令列表并在每次循环结束时计算一个条件。如果条件为 true,则再执行该循环一次。即使条件最初不为 true,这些命令也会至少执行一次。

测试的条件可以是一个命令列表。在这种情况下,将使用执行的 最后一个命令的返回值。清单 23演示了这些循环命令。

清单 23. 简单的 forwhileuntil循环
 ian@attic-u15:~$ for x in abd 2 "my stuff"; do echo $x; done 
 abd 
 2 
 my stuff 
 ian@attic-u15:~$ for (( x=2; x<5; x++ )); do echo $x; done 
 2 
 3 
 4 
 ian@attic-u15:~$ let x=3; while [ $x -ge 0 ] ; do echo $x ;let x--;done 
 3 
 2 
 1 
 0 
 ian@attic-u15:~$ let x=3; until echo -e "x=\c"; (( x-- == 0 )) ; do echo $x ; done 
 x=2 
 x=1 
 x=0

这些示例虽然不太自然,但它们演示了这些概念。您通常希望迭代一个函数或 shell 脚本的参数,或者命令替换所创建的一个列表。

在 “学习 Linux,101:自定义和使用 shell 环境” 中,您已经了解到 shell 可以 $*$@形式引用传递的参数列表,而且您是否引用这些表达式会影响对它们的解释方式。 表 3回顾了这些区别。

表 3. Shell 函数参数
参数用途
*从参数 1 开始的位置参数。如果在双引号内进行扩展,那么扩展结果将是一个单词,使用字段间分隔符 (IFS) 特殊变量的第一个字符来分离参数,如果 IFS 是 null,则没有中间空格。默认的 IFS 值是一个空白、制表符和换行符。如果 IFS 未设置,则使用的分隔符为空白,与默认 IFS 一样。
@从参数 1 开始的位置参数。如果在双引号内进行扩展,则每个参数变成一个单词,以便 "$@"等于 "$1""$2"……如果您的参数可能包含嵌入的空白,则使用此形式。

清单 24显示了一个函数,它打印出参数数量,然后依据 4 种替代选择来打印参数。

清单 24. 一个打印参数信息的函数
 ian@attic-u15:~$ type testfunc 
 testfunc is a function 
 testfunc () 
 { 
    echo "$# parameters"; 
    echo Using '$*'; 
    for p in $*; 
    do 
        echo "[$p]"; 
    done; 
    echo Using '"$*"'; 
    for p in "$*"; 
    do 
        echo "[$p]"; 
    done; 
    echo Using '$@'; 
    for p in $@; 
    do 
        echo "[$p]"; 
    done; 
    echo Using '"$@"'; 
    for p in "$@"; 
    do 
        echo "[$p]"; 
    done 
 }

清单 25展示了该函数的实际应用,在 IFS变量前面添加了一个额外的字符来方便函数执行。

清单 25. 使用 testfunc打印参数信息
 ian@attic-u15:~$ IFS="|${IFS}" testfunc abc "a bc" "1 2
 > 3"
 3 parameters 
 Using $* 
 [abc] 
 [a] 
 [bc] 
 [1] 
 [2] 
 [3] 
 Using "$*"
 [abc|a bc|1 2 
 3] 
 Using $@ 
 [abc] 
 [a] 
 [bc] 
 [1] 
 [2] 
 [3] 
 Using "$@"
 [abc] 
 [a bc] 
 [1 2 
 3]

请仔细分析区别,特别是引用形式和包含空格的参数,比如空白或换行字符。

breakcontinue命令

可以使用 break命令立即退出循环。如果您拥有嵌套循环,可以指定要分成的级别数。例如,如果您在一个 for内的另一个 for循环内有一个 until循环,而它们都在一个 while循环内,则 break 3会立即终止 until循环和两个 for循环,并将控制权返回给 while循环中的下一个指令。

可以使用 continue语句绕过命令列表中的剩余语句,直接转到循环的下一次迭代。 清单 26演示了 breakcontinue的使用。

清单 26. 使用 breakcontinue
 ian@attic-u15:~$ for word in red blue green yellow violet; do
 > if [ "$word" = blue ]; then continue; fi
 > if [ "$word" = yellow ]; then break; fi
 > echo "$word"
 > done
 red 
 green

再看一下 ldirs

是否还记得在 “学习 Linux,101:自定义和使用 shell 环境” 中,您是如何让 ldirs函数从一个长列表中提取文件名并确定它是否是一个目录?您开发的最后一个函数不是太糟,但前提是您拥有现在拥有的所有信息。您是否创建了同一个函数?或许没有。您知道如何使用 [ -d $name ]测试一个名称是否是一个目录,而且您知道 for复合命令。 清单 27给出了您可以编写 ldirs函数的另一种方法。

清单 27. 编写 ldirs的另一种方法
 ian@attic-u15:~$ type ldirs
 ldirs is a function 
 ldirs () 
 { 
    if [ $# -gt 0 ]; then 
        for file in "$@"; 
        do 
            [ -d "$file" ] && echo "$file"; 
        done; 
    else 
        for file in *; 
        do 
            [ -d "$file" ] && echo "$file"; 
        done; 
    fi; 
    return 0 
 } 
 ian@attic-u15:~$ cd developerworks/
 ian@attic-u15:~/developerworks$ ldirs
 my first article 
 readme 
 schema 
 templates 
 tools 
 web 
 xsl 
 ian@attic-u15:~/developerworks$ ldirs *s* tools/*
 my first article 
 schema 
 templates 
 tools 
 xsl 
 tools/java 
 ian@attic-u15:~/developerworks$ ldirs *www*

如果没有目录与您的条件匹配,新 ldirs函数会静默地返回。这不一定是您想要的。至少您的工具箱中现在有了另一个工具。


创建脚本

回想一下,myorder函数一次只能处理一种饮品。您现在可以将这个单一饮品函数与一个 for复合函数相结合,以迭代这些参数并处理多种饮品。这很简单,只需将您的函数放在一个文件中并添加 for指令。 清单 28演示了新的 myorder.sh 脚本。

清单 28. 使用 myorder.sh 订购多种饮品
 ian@attic-u15:~$ cat myorder.sh
 function myorder () 
 { 
    local restorecase=$(shopt -p nocasematch) rc=0; 
    shopt -s nocasematch; 
    case "$*" in 
        "coffee" | "decaf") 
            echo "Hot coffee coming right up"
        ;; 
        "tea") 
            echo "Hot tea on its way"
        ;; 
        "soda") 
            echo "Your ice-cold soda will be ready in a moment"
        ;; 
        *) 
            echo "Sorry, we don't serve that here"; 
            rc=1 
        ;; 
    esac; 
    $restorecase; 
    return $rc 
 } 

 for file in "$@"; do myorder "$file"; done 

 ian@attic-u15:~$ . myorder.sh coffee tea "milk shake"
 Hot coffee coming right up 
 Hot tea on its way 
 Sorry, we don't serve that here

您可以注意到,通过使用 .命令,会获取该脚本,在当前 shell 环境中运行它,而不是在它自己的 shell 中运行它。要运行一个脚本,必须获取它,或者必须使用 chmod +x命令将脚本文件标记为可执行,如 清单 29中所示。

清单 29. 让脚本可执行
 ian@attic-u15:~$ chmod +x myorder.sh
 ian@attic-u15:~$ ./myorder.sh coffee tea "milk shake"
 Hot coffee coming right up 
 Hot tea on its way 
 Sorry, we don't serve that here

您仍然必须提供脚本的完整或相对路径,除非将它放在位于 PATH上的目录中。


seqreadexec命令

Bash 和其他 shell 中有 3 个有用的命令,在脚本中常常会看到它们:seqreadexec

seq命令

seq命令生成一个具有指定的增量的数列。您指定至多 3 个参数:一个单独的结尾值;一个起点和一个重点;或者一个起点、增量和一个终点。如果未指定,增量和起点默认情况下为 1。增量可以为负值。可以使用 -s选项指定默认 \n以外的分隔符;如果需要的话,可以使用 -w选项获得等差数列。还可以使用 -f选项执行 printf风格的格式化。请参阅 seq手册页了解更多的细节。 清单 30显示了一些示例。

清单 30. 使用 seq生成数列
 ian@attic-u15:~$ seq 3
 1 
 2 
 3 
 ian@attic-u15:~$ seq -s " - " 7 10
 7 - 8 - 9 - 10 
 ian@attic-u15:~$ seq -w 2 7 19
 02 
 09 
 16 
 ian@attic-u15:~$ seq -s ' ' 2 -3 -8
 2 -1 -4 -7 
 ian@attic-u15:~$ seq 3 2

现在看看一个使用了目前介绍的一些概念的更有趣示例。您可能已在学校学过素数,而且可能听说过生成它们的方式,包括爱拉托逊斯筛法和试除法。我将在这个示例中使用试除法。思路是您通过将一个数除以更小的数来测试它是否是素数。显然,您只需要检查它是否可被更小的素数除尽,您需要测试的这个素数最大不能大于您测试的数的平方根。

清单 31显示了我的 primes.sh 脚本。该脚本在测试中使用了 [ ],在算法中使用了 (( )),在决策中使用了 if,还使用了 for循环,并使用 break命令来分解循环。我使用命令替换(使用重音符)来分配 seq命令的输出,将它作为 for命令要处理的值列表。

清单 31. 使用 seq和其他工具生成素数
 ian@attic-u15:~$ cat primes.sh
 #!/bin/bash 
 # Find all the positive primes up to $1 
 declare -i lastnum=0 
 # primelist will contain all prime values up to 
 # the square root of $1 
 primelist="2"
 # Only try to do something if we have a parameter 
 if [ $# -gt 0 ]; then 
  (( lastnum+= $1 )) 
  echo "Positive primes up to $lastnum"
  if [ $lastnum -ge 2 ]; then 
      echo "2"
      # Now only look at odd numbers greater than 2 
      for n in `seq 3 2  $lastnum` 
      do 
	  # Flag this one as prime till proven otherwise 
	  p=0 
	  for t in $primelist 
	  do 
	      (( remainder = n%t )) 
	      if [ $remainder -eq 0 ]; then 
		  p=1 
		  # Skip to next now we know not a prime 
		  break 
	      fi 
	  done 
	  if [ $p -eq 0 ]; then 
	      # Found a prime 
	      echo $n 
	      if (( lastnum > (n * n) )) ; then 
		  primelist="$primelist $n"
	      fi 
	   fi 
      done 
  fi 
 fi

将该脚本的代码粘贴到您自己的 Linux 系统中并尝试运行。 清单 32显示了一些示例输出。想想您可以如何修改此脚本来查找两个不同的数之间的素数,比如 10,000 和 10,500 之间。您能否或是否应该添加额外的错误检查或输入清理?

清单 32. 不超过 30 的素数
 ian@attic-u15:~$ ./primes.sh 30
 Positive primes up to 30 
 2 
 3 
 5 
 7 
 11 
 13 
 17 
 19 
 23 
 29

read命令

如果您想迭代一组数,那么 seq命令很有用,但是,如果您需要迭代来自终端或一个文件的输入,该怎么办?答案是使用 read命令,它从 stdin 读取一行,将它分解为标记,并将这些标记分配给一个或多个变量。 清单 33展示了如何将一行读入到 3 个数组变量中,然后使用 forseq打印结果。在读取第二个变量后,将输入行的剩余部分放在第三个变量 v[3]中。如果您希望将整行放在一个变量中,可以对 read使用单个变量。

清单 33. 使用 read命令
 ian@attic-u15:~$ read v[1] v[2] v[3]The quick brown fox jumps over the lazy dog
 ian@attic-u15:~$ for n in `seq 1 3`; do echo ${v[n]} ;done
 The 
 quick 
 brown fox jumps over the lazy dog

read命令有多个选项可用来设置行分隔符,在读取输入之前写出一个提示,读取至多指定数量个字符,等等。使用 help read查看简略摘要或使用 info bash read。在一些系统上,比如 Ubuntu 或 Debian,可能需要安装 bash-doc包才能获得 info格式的 bash 手册。

现在您已经知道如何从 stdin 读取一行,您可以将此命令与一个循环结构相结合 —通常为 while来迭代来自 stdin 的所有行。您可以尝试将此作为来自 清单 27ldirs函数的另一种方法。 清单 34给出了一次尝试的代码。

清单 34. 编写 ldirs的另一种方法
 ian@attic-u15:~$ type ldirs
 ldirs is a function 
 ldirs () 
 { 
    if [ $# -gt 0 ]; then 
        /bin/ls "$@" | while read l; do 
            [ -d "$l" ] && echo "$l"; 
        done; 
    else 
        /bin/ls | while read l; do 
            [ -d "$l" ] && echo "$l"; 
        done; 
    fi; 
    return 0 
 } 
 ian@attic-u15:~$ cd developerworks/
 ian@attic-u15:~/developerworks$ ldirs
 my first article 
 readme 
 schema 
 templates 
 tools 
 web 
 xsl 
 ian@attic-u15:~/developerworks$ ldirs *s* tools/*
 ian@attic-u15:~/developerworks$ # Oops! No output

分析来自 ls命令的输出,您将发现它没有显示完整路径。所以修改后的函数在没有参数时能正常运行,但有参数时可能失败。如果您返回来,使用前面的教程 “学习 Linux,101:自定义和使用 shell 环境” 中的函数,就会发现它也会遇到同样的问题,我当时没有指出这一事实。 清单 27中的 ldirs函数在使用参数时能够更好地运行,因为输入直接来自 shell 通配符和通配符替换,而不是来自 ls命令的格式化输出。

您现在已知道如何结合使用 readwhile循环,而且已经了解了编写 ldirs函数的 3 种不同方法。

exec命令

exec命令有两个用途。第一个是将控制权完全交给一个新程序,取代您当前运行的 shell,但不创建新进程。如果您想利用 bash shell 的强大功能来设置命令的复杂环境,那么可以这么做。使用 exec将您的 shell 替换为想要的命令后,您的用户无法返回到 shell 提示符(即使命令失败)。您可以在希望用户拥有有限且受控的系统访问权的地方使用 exec—例如在信息亭环境或图书馆目录终端上。

exec的第二种用法中,您没有指定命令。使用 exec从不同的文件句柄输入和输出到它们。为什么您想这么做?假设您想使用一个 while循环来读取一个文件,并计算总行数和空白行数。在 清单 34中,该过程是使用一个管道来完成的,其中 ls命令的输出传输到 while循环中。当 bash 运行一个管道时,它在一个子 shell 中运行它,对环境的任何更改均对调用环境不可见。 清单 35演示了该问题。

清单 35. 环境变量无法在管道中设置
 ian@attic-u15:~$ x=3 
 ian@attic-u15:~$ echo "abc" | while read n; do echo $n;x=4;done 
 abc 
 ian@attic-u15:~$ echo $x 
 3

如果您可以重定向来自指定文件的输入,而不使用 stdin,则不需要将 cat的输出传经 while循环。使用 exec重定向文件描述符很有用。 清单 36给出了一段统计一个指定文件中的总行数和空白行数的简单脚本。想想您可以如何修改该脚本来处理多个文件。

清单 36. 统计一个文件中的行数
 ian@attic-u15:~$ cat ./countlines.sh  
 #!/bin/bash 
 # Simple script to count lines in a file and also blank lines 

 if [ $# -gt 0 ]; then 
  if [ -f "$1" -a -r "$1" ] ; then 
      lines=0 
      blanklines=0 
      exec 3< "$1" # Redirect input to file descriptor 3 
      while read line <&3 # Read from fd 3 
      do { 
	  [ -z "$line" ] &&  (( blanklines ++  )) 
	  (( lines ++  )) 
      } 
      done 
      exec 3>&- # Restore input to stdin (fd 0) 
      echo "$1 has $lines lines of which $blanklines are blank"
  fi 
 fi 
 exit 0 
 ian@attic-u15:~$ ./countlines.sh  .bashrc 
 .bashrc has 120 lines of which 23 are blank

指定一个 shell

现在您有一些全新的 shell 脚本要处理,您可能会问它们能否在所有 shell 中运行。 清单 37显示了如果您在 Ubuntu 系统上首先使用 bash shell,然后使用 dash shell 来运行 myorder.sh shell 脚本,会发生什么情况。

清单 37. Shell 的区别
 ian@attic-u15:~$ ./myorder.sh tea soda
 Hot tea on its way 
 Your ice-cold soda will be ready in a moment 
 ian@attic-u15:~$ dash
 $ ./myorder.sh tea soda
 ./myorder.sh: 1: ./myorder.sh: Syntax error: "(" unexpected

结果并不好!

回想一下 “学习 Linux,101:自定义和使用 shell 环境” 中的介绍,单词 function在 bash 函数定义中是可选的,但未包含在 POSIX shell 规范中。dash 是一种比 bash 更小型、更轻量级的 shell,它不支持这个可选的特性。您无法保证您的潜在用户可能更喜欢哪个 shell,所以始终应确保您的脚本可移植到所有 shell 环境(这可能很困难),或者使用所谓的 shebang (#!) 来告诉 shell 在一个特定的 shell 中运行您的脚本。shebang 行必须是您的脚本的第一行,而且该行的剩余部分包含您的程序必须使用的 shell 的路径。所以您将对 myorder.sh 脚本使用 #!/bin/bash,如 清单 38中所示。

清单 38. 使用 shebang
 ian@attic-u15:~$ head -n3 myorder.sh
 #!/bin/bash 
 function myorder () 
 { 
 ian@attic-u15:~$ dash
 $ ./myorder.sh Tea Coffee
 Hot tea on its way 
 Hot coffee coming right up

您可以使用 cat命令来显示 /etc/shells,这是您系统上的 shell 列表。一些系统会列出未安装的 shell,而且一些列出的 shell(可能是 /dev/null)的存在可能只是为了确保 FTP 用户不会意外地离开他们的受限环境。如果您需要更改默认的 shell,可以使用 chsh命令,它会更新 /etc/passwd 中您的 userid 的条目。


Suid 权限和脚本位置

在早先的教程 “学习 Linux,101:管理文件权限和所有权” 中,您学习了如何更改文件的所有者和组,以及如何设置 suid 和 sgid 权限。一个包含这些权限集之一的可执行程序将在一个具有该文件的所有者 (suid) 或组 (suid) 的有效权限的 shell 中运行。因此根据设置的权限位,该程序将能够执行该所有者或组可以执行的任何操作。一些程序有合理的理由需要这么做。例如,passwd程序需要更新 /etc/shadow,chsh命令(您使用它更改默认 shell)需要更新 /etc/passwd。如果您为 ls使用了一个别名,列出这些程序可能会得到一个红色的、突出显示的列表来警告您,如 图 1中所示。这两个程序都设置了一个或多个 suid 位,因此就像所有者(在本例中为根用户)在运行它们一样。

图 1. 具有 suid 权限的程序
两个具有 suid 权限的程序的颜色输出

清单 39表明普通用户可运行这些程序和更新根用户拥有的文件。

清单 39. 使用 suid 程序
 jenni@attic-u15:~$ passwd
 Changing password for jenni. 
 (current) UNIX password: 
 Enter new UNIX password: 
 Retype new UNIX password: 
 passwd: password updated successfully 
 jenni@attic-u15:~$ cat /etc/shells
 # /etc/shells: valid login shells 
 /bin/sh 
 /bin/dash 
 /bin/bash 
 /bin/rbash 
 jenni@attic-u15:~$ chsh
 Password: 
 Changing the login shell for jenni 
 Enter the new value, or press ENTER for the default 
 Login Shell [/bin/bash]: /bin/dash
 jenni@attic-u15:~$ find /etc -mmin -4 -ls 2>/dev/null
 4325377   12 drwxr-xr-x 139 root     root        12288 Dec  1 22:47 /etc 
 4334839    4 -rw-r--r--   1 root     root         2304 Dec  1 22:47 /etc/passwd 
 jenni@attic-u15:~$ grep jenni /etc/passwd
 jenni:x:1001:1001:Jenni Aloi,,,:/home/jenni:/bin/dash

您可以为所有 shell 脚本设置 suid 和 sgid 权限,但大多数现代 shell 都会忽略脚本的这些位。您可以看到,shell 拥有一种强大的脚本语言,具有比本教程中介绍的更多的特性 —比如解释和执行任意表达式的能力。这些特性使 shell 成为了一个允许使用如此广泛的权限的不安全环境。所以如果您为一个 shell 脚本设置 suid 或 sgid 权限,不要期望该权限会在脚本运行时得到遵守。

在之前(参阅 清单 29),您更改了 myorder.sh 的权限,将它标记为可执行。但要运行该脚本,仍然需要通过添加 ./前缀来限定它的名称,除非您是在当前 shell 中获取它的。如果想要仅通过名称来运行一个 shell 脚本,该脚本必须在您的搜索路径上,该路径由 PATH变量表示。通常您不希望当前目录在您的路径上,因为这会带来潜在的安全风险。测试您的脚本并对它感到满意后,将它放在您的主目录中,或者如果它是个人脚本,可以将它放在 ~/bin 目录中,如果它要供系统上的其他用户使用,则将它放在 /usr/local/bin 中。如果您只使用 chmod +x来将它标记为可执行,那么它可以由每个人执行(所有者、组和所有用户),您通常希望这么做。如果您需要限制脚本,以便只有某个组的成员可以运行它,请参阅 “学习 Linux,101:管理文件权限和所有权。”

您可能已注意到,shell 程序(比如 bash 和 dash)通常位于 /bin 中而不是 /usr/bin 中。依据文件系统分层结构标准,/usr/bin 可位于在系统间共享的文件系统中,因此它可能在初始化时不可用。因此,一些函数(比如 shell)应位于 /bin 中,以便即使 /usr/bin 还未挂载,也可以使用它们。用户创建的脚本通常不需要位于 /bin(或 /sbin)中,因为这些目录中的程序应该已经为您提供了足够的工具来正常运行系统,达到您可以挂载 /usr 文件系统的状态。


向根用户发送邮件通知

假设在夜深人静您进入梦乡的时候,您的脚本正在运行您的系统上的一个管理任务。某个地方出错时会发生什么?幸运的是,将错误信息或日志文件通过邮件发送给自己、另一位管理员或根用户非常简单。只需将该消息传输到 mail命令,使用 -s选项添加一个主题行,如 清单 40中所示。

清单 40. 通过邮件将错误消息发送给用户
 ian@attic-u15:~$ echo "Midnight error message" | mail -s "Admin error" ian
 ian@attic-u15:~$ mail
"/var/mail/ian": 1 message 1 new 
 >N   1 Ian Shields        Tue Dec  1 23:08  13/423   Admin error 
 ? 1
 Return-Path: <ian@attic-u15> 
 X-Original-To: ian@attic-u15 
 Delivered-To: ian@attic-u15 
 Received: by attic-u15 (Postfix, from userid 1000) 
 id 6755C42740; Tue,  1 Dec 2015 23:08:57 -0500 (EST) 
 Subject: Admin error 
 To: <ian@attic-u15> 
 X-Mailer: mail (GNU Mailutils 2.99.98) 
 Message-Id: <20151202040857.6755C42740@attic-u15> 
 Date: Tue,  1 Dec 2015 23:08:57 -0500 (EST) 
 From: ian@attic-u15 (Ian Shields) 

 Midnight error message 
 ? d
 ? q
 Held 0 messages in /var/mail/ian

如果您需要通过邮件发送日志文件,可以使用 <重定向函数将它重定向为 mail命令的输入。如果您需要发送多个文件,可以使用 cat组合它们,然后将输出传输到 mail。在 清单 40中,邮件发送给了用户 ian,儿他恰好也在运行该命令,但管理脚本很有可能通过邮件直接发送给根用户或另一位管理员。跟平常一样,请参阅 mail的手册页,了解您可指定的其他选项。

对自定义和编写 bash 脚本的介绍到此就结束了。

参考资料

学习

讨论

  • 加入 developerWorks 中文社区,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=1027631
ArticleTitle=学习 Linux,101: 自定义或编写简单脚本
publish-date=02232016