29、Bourne Shell 编程实用指南

Bourne Shell 编程实用指南

1. 字符串匹配

在 Bourne shell 中, case 条件语句不执行任何测试命令,也不评估退出代码,但它可以进行模式匹配。例如:

#!/bin/sh
case $1 in
    bye)
        echo Fine, bye.
        ;;
    hi|hello)
        echo Nice to see you.
        ;;
    what*)
        echo Whatever.
        ;;
    *)
        echo 'Huh?'
        ;;
esac

shell 按以下步骤执行此脚本:
1. 脚本将 $1 与每个用 ) 标记的 case 值进行匹配。
2. 如果某个 case 值与 $1 匹配,shell 将执行该 case 下面的命令,直到遇到 ;; ,此时它将跳转到关键字 esac
3. 条件语句以 esac 结束。

对于每个 case 值,可以匹配单个字符串(如上述示例中的 bye ),也可以使用 | 匹配多个字符串( hi|hello 表示如果 $1 等于 hi hello 则返回真),还可以使用 * ? 等模式(如 what* )。若要创建一个默认 case 来捕获所有未指定的 case 值,可以使用单个 * ,如上述示例的最后一个 case

注意 :每个 case 必须以两个分号 ;; 结尾,否则可能会出现语法错误。

2. 循环

Bourne shell 中有两种循环: for while

2.1 for 循环

for 循环(即“for each”循环)是最常见的。示例如下:

#!/bin/sh
for str in one two three four; do
    echo $str
done

在这个示例中, for in do done 是 shell 的关键字。shell 按以下步骤执行:
1. 将变量 str 定义为紧跟在关键字 in 后面的四个以空格分隔的值中的第一个( one )。
2. 执行 do done 之间的 echo 命令。
3. 返回 for 行,将 str 定义为下一个值( two ),执行 do done 之间的命令,并重复此过程,直到遍历完 in 后面的所有值。

此脚本的输出如下:

one
two
three
four

2.2 while 循环

Bourne shell 的 while 循环使用退出代码作为条件,类似于 if 语句。例如,以下脚本执行十次迭代:

#!/bin/sh
FILE=/tmp/whiletest.$$;
echo firstline > $FILE
while tail -10 $FILE | grep -q firstline; do
    # 向 $FILE 添加行,直到 tail -10 $FILE 不再显示 "firstline"
    echo -n Number of lines in $FILE:' '
    wc -l $FILE | awk '{print $1}'
    echo newline >> $FILE
done
rm -f $FILE

在这个例子中, grep -q firstline 的退出代码是测试条件。一旦退出代码不为零(即当 firstline 字符串不再出现在 $FILE 的最后十行中),循环将结束。

可以使用 break 语句退出 while 循环。Bourne shell 还有一个 until 循环,其工作方式与 while 循环完全相同,只是当遇到退出代码为零时退出循环,而不是退出代码不为零时。不过,通常不需要频繁使用 while until 循环。实际上,如果觉得需要使用 while 循环,可能应该使用像 awk 或 Python 这样的语言来代替。

3. 命令替换

Bourne shell 可以将命令的标准输出重定向回 shell 的命令行。也就是说,可以将一个命令的输出用作另一个命令的参数,或者通过将命令放在 $() 中来将命令的输出存储在 shell 变量中。

例如,以下示例将一个命令存储在变量 FLAGS 中:

#!/bin/sh
FLAGS=$(grep ^flags /proc/cpuinfo | sed 's/.*://' | head -1)
echo Your processor supports:
for f in $FLAGS; do
    case $f in
        fpu) MSG="floating point unit"
             ;;
        3dnow) MSG="3DNOW graphics extensions"
             ;;
        mtrr) MSG="memory type range register"
             ;;
        *) MSG="unknown"
             ;;
    esac
    echo $f: $MSG
done

这个示例在某种程度上比较复杂,它展示了在命令替换中既可以使用单引号,也可以使用管道。 grep 命令的结果被发送到 sed 命令(关于 sed 命令的更多信息,请参考后续内容), sed 命令将匹配 .*: 的内容删除,然后将结果传递给 head 命令。

不过,要避免过度使用命令替换。例如,在脚本中不要使用 $(ls) ,因为使用 shell 扩展 * 会更快。此外,如果想对通过 find 命令获得的多个文件名执行某个命令,考虑使用管道将结果传递给 xargs ,而不是使用命令替换,或者使用 -exec 选项。

注意 :传统的命令替换语法是将命令放在反引号( `)中,在许多 shell 脚本中会看到这种用法。 $()` 是一种新的语法,它是 POSIX 标准,通常更易于阅读和编写。

4. 临时文件管理

有时需要创建临时文件来保存后续命令要使用的结果。创建此类文件时,要确保文件名足够独特,以免其他程序意外写入。

可以使用 mktemp 命令来创建临时文件名。以下脚本展示了过去两秒内发生的设备中断:

#!/bin/sh
TMPFILE1=$(mktemp /tmp/im1.XXXXXX)
TMPFILE2=$(mktemp /tmp/im2.XXXXXX)
cat /proc/interrupts > $TMPFILE1
sleep 2
cat /proc/interrupts > $TMPFILE2
diff $TMPFILE1 $TMPFILE2
rm -f $TMPFILE1 $TMPFILE2

mktemp 的参数是一个模板,该命令会将 XXXXXX 转换为一组唯一的字符,并创建一个具有该名称的空文件。注意,此脚本使用变量名来存储文件名,这样如果想更改文件名,只需修改一行代码。

注意 :并非所有的 Unix 变体都自带 mktemp 命令。如果遇到可移植性问题,最好为操作系统安装 GNU coreutils 包。

使用临时文件的脚本还存在一个问题:如果脚本被中止,临时文件可能会被遗留下来。例如,在上述示例中,在第二个 cat 命令之前按下 Ctrl-C 会在 /tmp 中留下一个临时文件。应尽量避免这种情况,可以使用 trap 命令创建一个信号处理程序来捕获 Ctrl-C 生成的信号,并删除临时文件,如下所示:

#!/bin/sh
TMPFILE1=$(mktemp /tmp/im1.XXXXXX)
TMPFILE2=$(mktemp /tmp/im2.XXXXXX)
trap "rm -f $TMPFILE1 $TMPFILE2; exit 1" INT

在处理程序中使用 exit 可以明确终止脚本的执行,否则 shell 在执行信号处理程序后将继续正常执行。

注意 mktemp 可以不提供参数,如果不提供,模板将以 /tmp/tmp.. 为前缀。

5. Here 文档

如果想显示一大段文本或为另一个命令提供大量文本,不必使用多个 echo 命令,可以使用 shell 的 Here 文档功能。示例如下:

#!/bin/sh
DATE=$(date)
cat <<EOF
Date: $DATE
The output above is from the Unix date command.
It's not a very interesting command.
EOF

其中, <<EOF 告诉 shell 将后面的所有行重定向到 <<EOF 前面的命令(这里是 cat )的标准输入。当单独一行出现标记 EOF 时,重定向将中断。实际上,标记可以是任何字符串,但要确保在 Here 文档的开头和结尾使用相同的标记。此外,按照惯例,标记通常只使用大写字母。

注意 Here 文档中的 shell 变量 $DATE ,shell 会在 Here 文档中展开 shell 变量,这在显示包含许多变量的报告时特别有用。

6. 重要的 shell 脚本实用工具

有几个程序在 shell 脚本中特别有用。有些实用工具(如 basename )只有与其他程序一起使用时才真正实用,因此通常只在 shell 脚本中使用;而其他工具(如 awk )在命令行中也非常有用。

6.1 basename

如果需要删除文件名的扩展名或去除完整路径名中的目录部分,可以使用 basename 命令。在命令行中尝试以下示例,了解该命令的工作方式:

$ basename example.html .html
$ basename /usr/local/bin/example

在这两种情况下, basename 都返回 example 。第一个命令从 example.html 中删除后缀 .html ,第二个命令从完整路径中删除目录部分。

以下示例展示了如何在脚本中使用 basename 将 GIF 图像文件转换为 PNG 格式:

#!/bin/sh
for file in *.gif; do
    # 如果没有文件则退出
    if [ ! -f $file ]; then
        exit
    fi
    b=$(basename $file .gif)
    echo Converting $b.gif to $b.png...
    giftopnm $b.gif | pnmtopng > $b.png
done

6.2 awk

awk 不是一个单一用途的简单命令,实际上它是一种有效的编程语言。遗憾的是,目前 awk 的使用有点像一门失传的艺术,已被更全面的语言(如 Python)所取代。

有很多关于 awk 的书籍,例如 Alfred V. Aho、Brian W. Kernighan 和 Peter J. Weinberger 所著的《The AWK Programming Language》(Addison - Wesley,1988)。

尽管如此,很多人仍然使用 awk 来完成一项任务:从输入流中选择单个字段。例如:

$ ls -l | awk '{print $5}'

这个命令显示 ls 输出的第五个字段(即文件大小),结果是一个文件大小列表。

6.3 sed

sed (stream editor,流编辑器)是一个自动文本编辑器,它接收输入流(文件或标准输入),根据某个表达式对其进行修改,并将结果显示在标准输出上。在很多方面, sed 类似于 Unix 最初的文本编辑器 ed ,它有数十种操作、匹配工具和寻址功能。和 awk 一样,也有很多关于 sed 的书籍,例如 Arnold Robbins 所著的《sed & awk Pocket Reference, 2nd edition》(O’Reilly,2002)。

虽然 sed 是一个重要的程序,但详细分析超出了本文的范围,不过很容易了解它的工作方式。一般来说, sed 接收一个地址和一个操作作为参数。地址对应一组行,命令决定对这些行执行什么操作。

sed 一个非常常见的任务是用文本替换正则表达式。例如:

$ sed 's/exp/texto/'

如果想将 /etc/passwd 中的第一个冒号替换为 % ,并将结果发送到标准输出,可以这样做:

$ sed 's/:/%/' /etc/passwd

若要替换 /etc/passwd 中的所有冒号,在操作末尾添加修饰符 g

$ sed 's/:/%/g' /etc/passwd

以下命令逐行操作,读取 /etc/passwd ,删除第三行到第六行,并将结果发送到标准输出:

$ sed 3,6d /etc/passwd

在这个例子中, 3,6 是地址(行的范围), d 是操作(删除)。如果省略地址, sed 将对其输入流的所有行进行操作。 sed 最常见的两种操作可能是 s (搜索并替换)和 d

也可以使用正则表达式作为地址。以下命令删除任何与正则表达式 exp 匹配的行:

$ sed '/exp/d'

6.4 xargs

当需要对大量文件执行某个命令时,命令或 shell 可能会提示无法将所有参数放入其缓冲区。可以使用 xargs 来解决这个问题,它会对输入流中的每个文件执行命令。

很多人将 xargs find 命令一起使用。例如,以下脚本可以帮助验证当前目录树中所有以 .gif 结尾的文件是否真的是 GIF 图像(Graphic Interchange Format):

$ find . -name '*.gif' -print | xargs file

在上述示例中, xargs 执行 file 命令。不过,这种调用可能会导致错误或使系统面临安全问题,因为文件名可能包含空格或换行符。编写脚本时,使用以下格式,将 find 输出的分隔符和 xargs 的参数分隔符都改为空字符:

$ find . -name '*.gif' -print0 | xargs -0 file

xargs 会启动多个进程,因此如果有一长串文件,不要期望有很好的性能。

如果目标文件中有任何一个可能以单个连字符 - 开头,可能需要在 xargs 命令的末尾添加两个连字符 -- 。两个连字符 -- 可以告诉程序,后面的任何参数都是文件名,而不是选项。但要注意,并非所有程序都支持使用两个连字符。

使用 find 时,有一个替代 xargs 的方法: -exec 选项。不过,其语法有点复杂,需要提供 {} 来替换文件名,并使用字面分号 ; 来表示命令的结束。以下是仅使用 find 完成上述任务的方法:

$ find . -name '*.gif' -exec file {} \;

6.5 expr

如果在 shell 脚本中需要使用算术运算, expr 命令可以提供帮助(甚至可以执行一些字符串操作)。例如, expr 1 + 2 会显示 3 。(执行 expr --help 可以获取完整的操作列表。)

不过, expr 是一种笨拙且缓慢的执行数学运算的方式。如果频繁使用该命令,可能应该使用像 Python 这样的语言来代替 shell 脚本。

6.6 exec

exec 是 shell 的一个内置功能,它会用 exec 后面指定的程序替换当前的 shell 进程。它执行系统调用 exec() ,该调用在之前的相关内容中已经提到过。这个功能旨在节省系统资源,但要记住,一旦执行 exec ,就无法返回;当在 shell 脚本中执行 exec 时,脚本和执行该脚本的 shell 都将被新命令替换。

在 shell 窗口中测试这一点,可以尝试执行 exec cat 。按下 Ctrl - D Ctrl - C 结束 cat 程序后,窗口将消失,因为子进程不再存在。

6.7 子 shell

假设需要稍微更改 shell 的环境,但又不想让这种更改永久生效。可以使用 shell 变量来更改和恢复部分环境(如 path 或工作目录),但这是一种笨拙的方法。解决这类问题的简单方法是使用子 shell,即一个全新的 shell 进程,它可以专门用于执行一两个命令。新 shell 会复制原始 shell 的环境,当新 shell 结束时,对其 shell 环境所做的任何更改都将消失,原始 shell 将继续正常执行。

要使用子 shell,将子 shell 要执行的命令放在括号中。例如,以下行在 uglydir 目录中执行 uglyprogram 命令,同时保持原始 shell 不变:

$ (cd uglydir; uglyprogram)

以下示例展示了如何向 path 中添加一个组件,如果这种更改是永久性的,可能会导致问题:

$ (PATH=/usr/confusing:$PATH; uglyprogram)

使用子 shell 对环境变量进行一次性更改是一个非常常见的任务,甚至有一个现成的语法可以避免使用子 shell:

$ PATH=/usr/confusing:$PATH uglyprogram

管道和后台进程在子 shell 中也能正常工作。以下示例使用 tar 归档 orig 目录树中的所有文件,然后在新目录 target 中解压缩该文件,从而复制 orig 中的文件和文件夹(这很有用,因为它可以保留文件的所有权和权限信息,而且通常比使用 cp -r 命令更快):

$ tar cf - orig | (cd target; tar xvf -)

警告 :在执行此类命令之前,请仔细检查,确保目录 target 存在,并且与目录 orig 完全不同。

6.8 在脚本中包含其他文件

如果需要在 shell 脚本中包含另一个文件,可以使用点号( . )操作符。

综上所述,Bourne shell 提供了丰富的功能和工具,通过合理运用字符串匹配、循环、命令替换、临时文件管理、Here 文档以及各种实用工具,可以编写出功能强大且高效的 shell 脚本。同时,要注意各功能的使用细节和潜在问题,以确保脚本的正确性和可维护性。

7. 总结与实践建议

7.1 功能总结

为了更清晰地回顾本文介绍的 Bourne shell 相关功能,以下用表格形式进行总结:
| 功能分类 | 具体工具/语句 | 主要用途 | 示例代码 |
| — | — | — | — |
| 条件匹配 | case | 进行字符串模式匹配 | sh case $1 in bye) echo Fine, bye. ;; hi\|hello) echo Nice to see you. ;; what*) echo Whatever. ;; *) echo 'Huh?' ;; esac |
| 循环 | for | 遍历一组值 | sh for str in one two three four; do echo $str done |
| | while | 根据退出代码执行循环 | sh FILE=/tmp/whiletest.$$; echo firstline > $FILE while tail -10 $FILE \| grep -q firstline; do echo -n Number of lines in $FILE:' ' wc -l $FILE \| awk '{print $1}' echo newline >> $FILE done rm -f $FILE |
| 命令替换 | $() | 将命令输出用作参数或存储到变量 | sh FLAGS=$(grep ^flags /proc/cpuinfo \| sed 's/.*://' \| head -1) |
| 临时文件管理 | mktemp | 创建唯一的临时文件名 | sh TMPFILE1=$(mktemp /tmp/im1.XXXXXX) TMPFILE2=$(mktemp /tmp/im2.XXXXXX) |
| Here 文档 | << | 提供大量文本输入 | sh DATE=$(date) cat <<EOF Date: $DATE The output above is from the Unix date command. It's not a very interesting command. EOF |
| 实用工具 | basename | 去除文件名的扩展名或路径 | sh $ basename example.html .html $ basename /usr/local/bin/example |
| | awk | 从输入流中选择字段或进行编程 | sh $ ls -l \| awk '{print $5}' |
| | sed | 对文本进行替换、删除等操作 | sh $ sed 's/:/%/g' /etc/passwd |
| | xargs | 对大量文件执行命令 | sh $ find . -name '*.gif' -print0 \| xargs -0 file |
| | expr | 执行算术和字符串操作 | sh $ expr 1 + 2 |
| | exec | 替换当前 shell 进程 | sh $ exec cat |
| 子 shell | 括号 () | 临时更改 shell 环境 | sh $ (cd uglydir; uglyprogram) |
| 文件包含 | . | 在脚本中包含其他文件 | 未给出具体示例,一般形式为 . other_script.sh |

7.2 实践建议

  • 避免过度使用复杂功能 :虽然 Bourne shell 提供了丰富的功能,但在实际编写脚本时,应避免过度使用复杂的命令替换、嵌套循环等。例如,若需要频繁进行复杂的数学运算,优先考虑使用 Python 等更适合的编程语言,而不是依赖 expr 命令。
  • 注意可移植性 :不同的 Unix 变体可能对某些命令或语法的支持有所差异。如 mktemp 并非所有系统都自带,在编写脚本时要考虑到这一点,必要时安装 GNU coreutils 包以保证脚本的可移植性。
  • 错误处理 :在使用临时文件时,要考虑脚本意外中止的情况,使用 trap 命令捕获信号并删除临时文件,避免文件残留。同时,在执行可能影响系统重要数据的命令(如 tar 归档和解压缩)前,要仔细检查命令的参数,确保目标目录的正确性。
  • 代码可读性 :遵循编程规范,合理使用注释和空格,提高代码的可读性。例如,在复杂的命令替换或管道操作中,添加注释说明每个步骤的作用。

7.3 流程图示例

下面是一个简单的 for 循环处理文件的流程图,使用 mermaid 语法表示:

graph TD;
    A[开始] --> B[初始化变量];
    B --> C{是否还有文件};
    C -- 是 --> D[处理当前文件];
    D --> E[更新变量];
    E --> C;
    C -- 否 --> F[结束];

这个流程图展示了 for 循环在处理一组文件时的基本流程,先初始化变量,然后检查是否还有文件需要处理,如果有则处理当前文件并更新变量,继续循环;如果没有则结束循环。

7.4 进一步学习

Bourne shell 是一个强大的工具,但要深入掌握它,还需要不断实践和学习。可以阅读相关的书籍,如关于 awk sed 的专业书籍,了解更多高级用法。同时,在实际项目中多尝试使用不同的功能组合,积累经验,提高编写高效、健壮 shell 脚本的能力。

总之,通过合理运用 Bourne shell 的各种功能和实用工具,结合良好的编程习惯和错误处理机制,能够编写出满足各种需求的 shell 脚本,为系统管理和自动化任务提供有力支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值