在我们旅程的最后一章,我们将看看一些零碎的东西。虽然我们在前面的章节中已经介绍了很多内容,但还有许多bash功能我们还没有介绍。大多数都相当模糊,主要对那些将bash集成到Linux发行版中的人有用。然而,有一些虽然不常用,但对某些编程问题有帮助。我们将在这里介绍它们。
bash允许将命令组合在一起。这可以通过两种方式之一完成,要么使用组命令(group command),要么使用子shell(subshell)。我们在【第6章.重定向】中介绍了组命令,正如我们所记得的,组命令使用以下语法:
{ command1; command2; [command3; ...] }
子shell使用类似的语法:
(command1; command2; [command3;...])
组命令用大括号括住其命令,子shell使用小括号。值得注意的是,由于bash实现组命令的方式,大括号必须用空格与命令隔开,最后一个命令必须在结束大括号之前用分号或换行符终止。
那么,组命令和子shell有什么好处呢?虽然它们有一个重要的区别(我们稍后会讲到),但它们都可以用来管理重定向。让我们考虑一个对多个命令执行重定向的脚本段。
ls -l > output.txt
echo "Listing of foo.txt" >> output.txt
cat foo.txt >> output.txt
这很简单。三个命令的输出被重定向到一个名为 output.txt 的文件。使用组命令,我们可以编写如下代码:
xxxxxxxxxx
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
使用subshell也是类似的:
xxxxxxxxxx
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt
使用这种技术,我们节省了一些打字时间,但组命令或子shell真正闪耀的地方是管道。在构建命令管道时,将多个命令的结果合并到一个流中通常很有用。组命令和子shell使这变得容易。
xxxxxxxxxx
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr
在这里,我们组合了三个命令的输出,并将它们通过管道传输到 lpr
的输入中,以生成打印报告。
虽然组命令和子shell看起来很相似,都可以用于组合流进行重定向,但两者之间存在重要区别。组命令在当前shell中执行其所有命令,而子shell(顾名思义)在当前shell的子副本中执行其命令。这意味着环境(包括shell函数、别名和变量等)被复制并提供给shell的新实例。这个环境副本与子进程的正常工作方式不同,因为子shell继承了父进程环境的整个副本,而子进程只继承了父shell的导出变量。当子shell退出时,其环境副本将丢失,因此对子shell环境所做的任何更改(包括变量赋值)也将丢失。
最重要的是,子shell,像子进程一样,与组命令不同,不能改变父shell的环境。让我们演示一下;首先使用组命令:
xxxxxxxxxx
[me@linuxbox ~]$ foo="Original Value"
[me@linuxbox ~]$ { foo="Altered Value"; echo $foo; }
Altered Value
[me@linuxbox ~]$ echo $foo
Altered Value
接下来,我们将对子shell执行相同的步骤:
xxxxxxxxxx
[me@linuxbox ~]$ foo="Original Value"
[me@linuxbox ~]$ ( foo="Altered Value"; echo $foo )
Altered Value
[me@linuxbox ~]$ echo $foo
Original Value
正如我们所看到的,组命令能够修改当前shell中 foo
的值,而subshell则不能。当我们想要更改目录时,子shell的这一特性非常方便。当我们在子shell中执行 cd
命令时,当前工作目录在子shell的持续时间内会发生变化,但在返回父shell后,当前工作文件夹保持不变。
xxxxxxxxxx
[me@linuxbox ~]$ pwd
/home/me
[me@linuxbox ~]$ ( cd /usr/bin; pwd )
/usr/bin
[me@linuxbox ~]$ pwd
/home/me
然而,一般来说,除非脚本需要子shell,否则组命令比子shell更可取。组命令速度更快,所需内存更少。
在下面的脚本中,我们将使用组命令,并查看可以与关联数组结合使用的几种编程技术。这个名为 array-2
的脚本在给定目录名称时,会打印目录中文件的列表以及文件所有者和组所有者的名称。在列表末尾,脚本会打印出属于每个所有者和组的文件数量。在这里,我们看到了当脚本被指定到 /usr/bin 目录时的结果(为简洁起见而压缩):
x[me@linuxbox ~]$ array-2 /usr/bin
/usr/bin/2to3-2.6 root root
/usr/bin/2to3 root root
/usr/bin/a2p root root
/usr/bin/abrowser root root
/usr/bin/aconnect root root
/usr/bin/acpi_fakekey root root
/usr/bin/acpi_listen root root
/usr/bin/add-apt-repository root root
.
.
.
/usr/bin/zipgrep root root
/usr/bin/zipinfo root root
/usr/bin/zipnote root root
/usr/bin/zip root root
/usr/bin/zipsplit root root
/usr/bin/zjsdecode root root
/usr/bin/zsoelim root root
File owners:
daemon : 1 file(s)
root : 1394 file(s)
File group owners:
crontab : 1 file(s)
daemon : 1 file(s)
lpadmin : 1 file(s)
mail : 4 file(s)
mlocate : 1 file(s)
root : 1380 file(s)
shadow : 2 file(s)
ssh : 1 file(s)
tty : 2 file(s)
utmp : 2 file(s)
以下是脚本的列表(带行号):
xxxxxxxxxx
1 #!/bin/bash
2
3 # array-2: Use arrays to tally file owners
4
5 declare -A files file_group file_owner groups owners
6
7 if [[ ! -d "$1" ]]; then
8 echo "Usage: array-2 dir" >&2
9 exit 1
10 fi
11
12 for i in "$1"/*; do
13 owner="$(stat -c %U "$i")"
14 group="$(stat -c %G "$i")"
15 files["$i"]="$i"
16 file_owner["$i"]="$owner"
17 file_group["$i"]="$group"
18 ((++owners[$owner]))
19 ((++groups[$group]))
20 done
21
22 # List the collected files
23 { for i in "${files[@]}"; do
24 printf "%-40s %-10s %-10s\n" \
25 "$i" "${file_owner["$i"]}" "${file_group["$i"]}"
26 done } | sort
27 echo
28
29 # List owners
30 echo "File owners:"
31 { for i in "${!owners[@]}"; do
32 printf "%-10s: %5d file(s)\n" "$i" "${owners["$i"]}"
33 done } | sort
34 echo
35
36 # List groups
37 echo "File group owners:"
38 { for i in "${!groups[@]}"; do
39 printf "%-10s: %5d file(s)\n" "$i" "${groups["$i"]}"
40 done } | sort
让我们来看看这个脚本的机制:
第5行:必须使用 declare
命令并使用 -A
选项创建关联数组。在这个脚本中,我们创建了五个数组,如下所示:
files
包含目录中文件的名称,按文件名索引file_group
包含每个文件的组所有者,按文件名索引file_owner
包含每个文件的所有者,按文件名索引groups
包含属于索引组的文件数量owner
包含属于索引所有者的文件数量第7-10行:这些行检查是否传递了有效的目录名作为位置参数。如果没有,将显示一条使用消息,脚本将以退出状态1退出。
第12-20行:这些行遍历目录中的文件。使用 stat
命令,第13行和第14行提取文件所有者和组所有者的名称,并使用文件名称作为数组索引将值分配给它们各自的数组(第16行和第17行)。同样,文件名本身被分配给 files
数组(第15行)。
第18-19行:属于文件所有者和组所有者的文件总数加1。
第22-27行:输出文件列表。这是通过使用 “${array[@]}”
参数展开来完成的,该参数展开为整个数组元素列表,每个元素都被视为一个单独的单词。这允许文件名可能包含嵌入式空格。还要注意,整个循环都用大括号括起来,从而形成一个组命令。这允许将循环的整个输出通过管道传输到 sort
命令中。这是必要的,因为关联数组元素的扩展没有排序。
第29-40行:这两个循环类似于文件列表循环,除了它们使用 “${!array[@]}”
扩展,该扩展扩展到数组索引列表,而不是数组元素列表。
我们在【第28章.读取键盘输入】中看到了子shell环境问题的一个例子,当时我们发现管道中的读取命令并不像我们直观预期的那样工作。总结一下,如果我们构建一个这样的管道:
xxxxxxxxxx
echo "foo" | read
echo $REPLY
REPLY
变量的内容始终为空,因为 read
命令是在子shell中执行的,当子shell终止时,其 REPLY
副本将被销毁。
因为管道中的命令总是在子shell中执行,所以任何分配变量的命令都会遇到这个问题。幸运的是,shell提供了一种称为过程替换(process substiution)的奇特扩展形式,可用于解决这个问题。
进程替换以两种方式表示。
对于产生标准输出的流程,它看起来像这样:
<(list)
或者,对于接收标准输入的流程,它看起来像这样:
>(list)
其中 list 是命令列表。
为了解决 read
的问题,我们可以使用如下的进程替换:
xxxxxxxxxx
read < <(echo "foo")
echo $REPLY
进程替换允许我们将子shell的输出视为普通文件进行重定向。事实上,由于它是一种扩张形式,我们可以检验它的真正价值。
xxxxxxxxxx
[me@linuxbox ~]$ echo <(echo "foo")
/dev/fd/63
通过使用 echo
查看扩展的结果,我们看到子shell的输出是由一个名为 /dev/fd/63 的文件提供的。
进程替换通常用于包含 read
的循环。下面是一个 read
循环的示例,它处理子shell创建的目录列表的内容:
xxxxxxxxxx
# pro-sub: demo of process substitution
while read -r attr links owner group size date time filename; do
cat << _EOF_
Filename: $filename
Size: $size
Owner: $owner
Group: $group
Modified: $date $time
Links: $links
Attributes: $attr
_EOF_
done < <(ls -l --time-style="+%F %H:%m"| tail -n +2)
该循环对目录列表的每一行执行 read
。列表本身在脚本的最后一行生成。此行将流程替换的输出重定向到循环的标准输入。tail
命令包含在进程替换管道中,以消除列表中不需要的第一行。
执行时,脚本会产生如下输出:
xxxxxxxxxx
[me@linuxbox ~]$ pro-sub | head -n 20
Filename: addresses.ldif
Size: 14540
Owner: me
Group: me
Modified: 2009-04-02 11:12
Links: 1
Attributes: -rw-r--r--
Filename: bin
Size: 4096
Owner: me
Group: me
Modified: 2009-07-10 07:31
Links: 2
Attributes: drwxr-xr-x
Filename: bookmarks.html
Size: 394213
Owner: me
Group: me
eval
构建命令eval
内置是一个奇怪而神秘的命令。简单地说,它需要一个参数列表,将它们组合成一个字符串,并将其传递给shell执行。所以问题自然变成了这是为了什么?我们为什么要用这个?
在shell脚本中,有些情况下我们需要在运行时动态构造命令, eval
允许我们这样做。让我们做一个实验。虽然我们没有明确地介绍它,但可以将命令放入字符串变量中,然后依靠参数展开将变量展开为命令:
xxxxxxxxxx
[me@linuxbox ~]$ cmd="echo foo"
[me@linuxbox ~]$ $cmd
foo
[me@linuxbox ~]$
现在让我们看看当我们在命令中包含变量时会发生什么:
xxxxxxxxxx
[me@linuxbox ~]$ str=abcde
[me@linuxbox ~]$ cmd="echo $str"
[me@linuxbox ~]$ $cmd
abcde
[me@linuxbox ~]$
这符合我们的预期,但让我们看看当我们为 str
赋值时会发生什么。
xxxxxxxxxx
[me@linuxbox ~]$ str=ABCDE
[me@linuxbox ~]$ $cmd
abcde
[me@linuxbox ~]$
结果没有改变。这是有道理的,因为我们在分配 cmd
时已经对 $str
执行了参数扩展。但是,如果我们想稍后扩展变量呢?我们可以将命令括在单引号中,以抑制初始变量赋值过程中的参数扩展。
xxxxxxxxxx
[me@linuxbox ~]$ cmd='echo $str'
[me@linuxbox ~]$ $cmd
$str
[me@linuxbox ~]$
虽然我们在分配 cmd
时没有得到参数扩展,但在执行 cmd
时也没有得到。这样做的原因是,即使扩展($cmd
)导致可以进一步扩展的内容($str
),shell也只执行一次扩展。
如果我们使用 eval
,我们会得到额外的扩展级别。
xxxxxxxxxx
[me@linuxbox ~]$ str=abcde
[me@linuxbox ~]$ cmd='echo $str'
[me@linuxbox ~]$ eval "$cmd"
abcde
[me@linuxbox ~]$ str=ABCDE
[me@linuxbox ~]$ eval "$cmd"
ABCDE
[me@linuxbox ~]$
这向我们展示了 eval
的功能。它将参数连接到一个字符串中,对字符串的内容执行波浪号、参数和路径名扩展,然后将字符串传递给当前shell(不创建子shell)执行。
小心 eval
eval
命令名声不好。这源于对使用 eval
向shell脚本添加安全漏洞有多容易的担忧。如果提供给 eval
的字符串来自外部源(即用户输入),则必须注意确保字符串中没有未经授权的命令(称为代码注入攻击)。始终验证给定 eval
的字符串的内容。
在下面的脚本中,我们将使用 eval
来帮助找到单词谜题的可能答案。
对于那些不熟悉这款流行游戏的人来说,维基百科是这样描述的:
Wordle is a web-based word game created and developed by Welsh software engineer Josh Wardle. Players have six attempts to guess a five-letter word, with feedback given for each guess in the form of colored tiles indicating when letters match or occupy the correct position.
Wordle是一款基于网络的文字游戏,由威尔士软件工程师Josh Wardle创建和开发。玩家有六次尝试猜测一个五个字母的单词,每次猜测都会以彩色瓷砖的形式给出反馈,指示字母何时匹配或占据正确的位置。
基本上,当我们猜测时,游戏会告诉我们猜测中的任何字母是否存在于秘密单词中,以及猜测中的字母是否与秘密单词中的位置匹配。因此,每次我们猜测时,我们都会了解更多关于这个秘密单词的信息,直到我们(希望)能够弄清楚它的身份。游戏的目标是以最少的尝试次数猜测单词。
辅助脚本输出一个单词列表,与到目前为止的猜测相匹配。为了做到这一点,给脚本一个简单的正则表达式,告诉脚本哪些字母是已知的,以及它们在秘密单词中的位置。此外,我们还提供了一个已知包含在秘密单词中的字母列表,以及已知不出现在秘密单词里的字母列表。
假设秘密单词是“about”,我们猜测是“bloat”。Wordle会告诉我们“o”和“t”出现在单词中各自的位置,并且单词中还包含未知位置的“a”和“b”,此外,秘密单词不包含“l”。在这种情况下,我们将以这种方式调用脚本:
xxxxxxxxxx
[me@linuxbox ~]$ eval-wordle ..o.t +a +b -l
第一个参数是一个五个字符的正则表达式,其中“o”和“t”位于各自的位置。接下来是秘密单词中存在的已知字符和已知不是秘密单词的字母。脚本响应如下:
xxxxxxxxxx
abort
about
2
表明词典中只有两个单词符合选择标准。
该脚本的工作原理是根据选择标准过滤字典(/usr/share/dict/words),然后显示剩下的内容。为此,我们需要构建一个长的多部分管道,其长度取决于命令行上提供的位置参数的数量。通过这种方式,我们不需要临时文件来保存中间结果。
xxxxxxxxxx
1 #!/bin/bash
2 # eval-wordle - demonstrate eval by solving wordle puzzles
3 PROGNAME=${0##*/}
4 DICTIONARY=/usr/share/dict/words
5 usage() {
6 printf "%s\n" "Usage: ${PROGNAME} [-h|--help]"
7 printf "%s\n" " ${PROGNAME} regex +-char..."
8 }
9 help_message() {
10 cat << _EOF_
11 $PROGNAME - Wordle Helper
12 $(usage)
13 Options:
14 -h, --help Display this help message and exit.
15 Arguments:
16 The first argument must be a five character regular
17 expression at minimum of '.....' representing five
18 unknown characters in the answer. This is followed by
19 zero or more character known to be either present or
20 absent from the answer. These are expressed as either
21 +char for letters known to be present or -char for
22 letters known to not be in the answer.
23 Example:
24 $PROGNAME a.... +o -v
25 This means we know that the answer starts with 'a' in
26 the first postion and also contains an 'o' but does
27 not contain a 'v'.
_EOF_
28 return
29 }
30 add_plus() { # Add a letter
31 local char="$1"
32 echo " | grep $char"
33 return
34 }
35 add_minus() { # Sutraact a letter
36 local char="$1"
37 echo " | grep -v $char"
38 return
39 }
40 # Parse command-line
41 if [[ "$1" == "-h" || "$1" == "--help" ]]; then
42 help_message
43 exit 0
44 fi
45 if (( "${#1}" == 5 )); then
49 known_chars="$1"
50 shift
48 else
46 echo "First argument must be a 5 character regex." >&2
47 exit 1
51 fi
52 cmd="LANG=C grep '^.....$' $DICTIONARY \
53 | grep -v '[[:punct:]]' \
54 | grep -v '[[:upper:]]' \
55 | grep '$known_chars'"
56 while [[ -n "$1" ]]; do
57 case "$1" in
58 -[[:alpha:]])
59 cmd="${cmd}$(add_minus "${1:1}")"
60 ;;
61 +[[:alpha:]])
62 cmd="${cmd}$(add_plus "${1:1}")"
63 ;;
64 *)
65 echo "Invalid argument '$1'" >&2
66 exit 1
67 ;;
68 esac
69 shift
70 done
71 eval "$cmd | tee >(wc -l)"
让我们复习一下脚本的有趣之处。
+letter
或 -letter
时,我们都会删除前导加号或减号,并调用相应的函数将管道的下一个元素添加到命令字符串中。tee
中, tee
将列表传递到标准输出(以便在屏幕上显示)和一个“文件”,该文件实际上是在进程替换中运行的wc程序。狡猾(tricky)。在【第10章.进程】中,我们看到了程序如何对信号做出响应。我们也可以将此功能添加到我们的脚本中。虽然我们到目前为止编写的脚本不需要这种功能(因为它们的执行时间很短,也不创建临时文件),但更大、更复杂的脚本可能会受益于信号处理例程。
当我们设计一个庞大而复杂的脚本时,重要的是要考虑如果用户在脚本运行时注销或关闭计算机会发生什么。当发生此类事件时,将向所有受影响的进程发送信号。反过来,代表这些进程的程序可以执行操作,以确保程序的正确有序终止。例如,我们编写了一个脚本,在执行过程中创建了一个临时文件。为了良好的设计,我们会让脚本在完成工作后删除文件。如果收到指示程序将提前终止的信号,让脚本删除文件也是明智之举。
bash为此提供了一种机制,称为 trap (陷阱)。陷阱是通过名为 trap
的内置命令来实现的。 trap
使用以下语法:
trap argument signal [signal...]
其中 argument 是一个将被读取并视为命令的字符串,signal 是触发执行解释命令的信号的规范。
这里有一个简单的例子:
xxxxxxxxxx
# trap-demo: simple signal handling demo
trap "echo 'I am ignoring you.'" SIGINT SIGTERM
for i in {1..5}; do
echo "Iteration $i of 5"
sleep 5
done
此脚本定义了一个陷阱,该陷阱将在脚本运行时每次收到 SIGINT
或 SIGTERM
信号时执行 echo
命令。当用户试图按 Ctrl-c 停止脚本时,程序的执行看起来像这样:
xxxxxxxxxx
[me@linuxbox ~]$ trap-demo
Iteration 1 of 5
Iteration 2 of 5
^CI am ignoring you.
Iteration 3 of 5
^CI am ignoring you.
Iteration 4 of 5
Iteration 5 of 5
正如我们所看到的,每次用户试图中断程序时,都会打印消息。
构造一个字符串来形成一个有用的命令序列可能会很尴尬,因此通常的做法是指定一个shell函数作为命令。在这个例子中,为每个要处理的信号指定了一个单独的shell函数:
xxxxxxxxxx
# trap-demo2: simple signal handling demo
exit_on_signal_SIGINT () {
echo "Script interrupted." 2>&1
exit 0
}
exit_on_signal_SIGTERM () {
echo "Script terminated." 2>&1
exit 0
}
trap exit_on_signal_SIGINT SIGINT
trap exit_on_signal_SIGTERM SIGTERM
for i in {1..5}; do
echo "Iteration $i of 5"
sleep 5
done
此脚本具有两个 trap
命令,每个信号一个。反过来,每个陷阱都指定了一个在接收到特定信号时要执行的shell函数。请注意,每个信号处理功能中都包含 exit
命令。如果没有 exit
,脚本将在完成函数后继续。
当用户在执行此脚本期间按 Ctrl-c 时,结果如下:
xxxxxxxxxx
[me@linuxbox ~]$ trap-demo2
Iteration 1 of 5
Iteration 2 of 5
^CScript interrupted.
除了所有程序都可以使用的信号外, trap
内置还支持几个内部和bash特定的信号。特别感兴趣的是 EXIT
和 ERR
。正如人们所料,当脚本终止时, EXIT
陷阱会被激活。每当命令(除某些例外)以非零退出状态退出时, ERR
陷阱就会激活。这就像我们在【第30章】中看到的 set -e
选项的一个更有用的版本。下面我们有一个简短的 EXIT
和 ERR
陷阱演示脚本。请注意第5行的故意错误:
xxxxxxxxxx
1 #!/bin/bash
2 # trap-demo3 - demonstrate ERR and EXIT signal handling
3 trap "echo \"There is an error.\"" ERR
4 trap "echo \"The program has ended.\"" EXIT
5 echox "Running..."
6 read -r -p "Say something... " something
7 echo "$something"
此脚本定义陷阱,然后尝试显示一行文本,但 echo
命令拼写错误。脚本继续下一行,等待用户输入。最后,它显示用户输入的任何内容。
当我们运行此脚本时,我们得到以下内容:
xxxxxxxxxx
[me@linuxbox ~]$ trap-demo3
./trap-demo3: line 8: echox: command not found
There is an error.
Say something...
The program has ended.
我们得到的第一件事是shell中关于我们拼写错误的命令的错误消息。接下来是来自 ERR
陷阱的消息,显示它已被激活。接下来会出现 read
提示,如果我们按 ENTER 键,将输出一个空行,EXIT
陷阱消息将宣布程序已结束。
脚本中包含信号处理程序的一个原因是删除脚本可能创建的临时文件(用于在执行过程中保存中间结果)。命名临时文件是一门艺术。传统上,类Unix系统上的程序在 /tmp 目录中创建临时文件,这是一个用于此类文件的共享目录。然而,由于目录是共享的,这带来了一定的安全问题,特别是对于以超级用户权限运行的程序。除了为暴露给系统所有用户的文件设置适当权限的明显步骤外,为临时文件赋予不可预测的文件名也很重要。这避免了被称为临时竞争攻击(temp race attack)的漏洞。创建不可预测(但仍然具有描述性)名称的一种方法是这样做:
tempfile=/tmp/$(basename $0).$$.$RANDOM
这将创建一个文件名,由程序名称、进程ID(PID)和随机整数组成。但是请注意, $RANDOM
shell变量只返回1-32767范围内的值,从计算机的角度来看,这不是一个很大的范围,因此该变量的单个实例不足以克服确定的攻击者。
更好的方法是使用 mktemp
程序(不要与 mktemp
标准库函数混淆)来命名和创建临时文件。 mktemp
程序接受模板作为用于构建文件名的参数。模板应包含一系列“X”字符,这些字符由相应数量的随机字母和数字替换。“X”字符序列越长,随机字符序列就越长。以下是一个示例:
tempfile=$(mktemp /tmp/foobar.$.XXXXXXXXXXX)
这将创建一个临时文件,并将其名称分配给变量 tempfile
。模板中的“X”字符被随机字母和数字替换,因此最终文件名(在本例中,还包括用于获取PID的特殊参数$$的扩展值)可能如下:
/tmp/foobar.6593.UOZuvM6654
对于由普通用户执行的脚本,明智的做法是避免使用 /tmp 目录,并在用户的主目录中为临时文件创建一个目录,使用如下代码行:
[[ -d $HOME/tmp ]] || mkdir $HOME/tmp
有时需要同时执行多个任务。我们已经看到,所有现代操作系统即使不是多用户操作系统,也至少是多任务操作系统。脚本可以被构造为以多任务方式运行。
通常,这涉及启动一个脚本,然后在父脚本继续运行的同时,启动一个或多个子脚本来执行额外的任务。然而,当一系列脚本以这种方式运行时,可能会出现保持父级和子级协调的问题。也就是说,如果父或子依赖于另一个脚本,并且一个脚本必须等待另一个完成任务才能完成自己的任务,该怎么办?
bash有一个内置命令来帮助管理这样的异步执行。wait
命令使父脚本暂停,直到指定的进程(即子脚本)完成。
wait
我们将首先演示 wait
命令。为此,我们需要两个脚本。
首先是父脚本:
xxxxxxxxxx
# async-parent: Asynchronous execution demo (parent)
echo "Parent: starting..."
echo "Parent: launching child script..."
async-child &
pid=$!
echo "Parent: child (PID= $pid) launched."
echo "Parent: continuing..."
sleep 2
echo "Parent: pausing to wait for child to finish..."
wait "$pid"
echo "Parent: child is finished. Continuing..."
echo "Parent: parent is done. Exiting."
第二个是子脚本:
xxxxxxxxxx
# async-child: Asynchronous execution demo (child)
echo "Child: child is running..."
sleep 5
echo "Child: child is done. Exiting."
在这个例子中,我们看到子脚本很简单。真正的动作是由家长执行的。在父脚本中,子脚本被启动并置于后台。通过为pid变量分配 $!
shell参数的值来记录子脚本的进程ID,该参数将始终包含放入后台的最后一个作业的进程ID。
父脚本继续执行,然后使用子进程的PID执行等待命令。这会导致父脚本暂停,直到子脚本退出,此时父脚本结束。
执行时,父脚本和子脚本会产生以下输出:
xxxxxxxxxx
[me@linuxbox ~]$ async-parent
Parent: starting...
Parent: launching child script...
Parent: child (PID= 6741) launched.
Parent: continuing...
Child: child is running...
Parent: pausing to wait for child to finish...
Child: child is done. Exiting.
Parent: child is finished. Continuing...
Parent: parent is done. Exiting.
在大多数类Unix系统中,可以创建一种称为 named pipe (命名管道)的特殊类型的文件。命名管道用于在两个进程之间创建连接,可以像其他类型的文件一样使用。它们不太受欢迎,但很好了解。
有一种称为 client-server(CS,客户端-服务器)的通用编程架构,它可以使用命名管道等通信方法,以及网络连接等其他类型的进程间通信(interprocess communication)。
当然,最广泛使用的客户端-服务器系统类型是与web服务器通信的web浏览器。web浏览器充当客户端,向服务器发出请求,服务器用网页响应浏览器。【即所谓BS,browser-server】
命名管道的行为类似于文件,但实际上形成先进先出(FIFO)缓冲区。与普通(未命名)管道一样,数据从一端进入,从另一端出来。使用命名管道,可以设置如下内容:
process1 > named_pipe
和:
process2 < named_pipe
它的行为如下:
process1 | process2
首先,我们必须创建一个命名管道。这是使用 mkfifo
命令完成的。
xxxxxxxxxx
[me@linuxbox ~]$ mkfifo pipe1
[me@linuxbox ~]$ ls -l pipe1
prw-r--r-- 1 me me 0 2009-07-17 06:41 pipe1
在这里,我们使用 mkfifo
创建一个名为 pipe1
的命名管道。使用 ls
,我们检查文件,发现 attributes
字段中的第一个字母是“p”,表示它是一个命名管道。
为了演示命名管道的工作原理,我们需要两个终端窗口(或者两个虚拟控制台)。在第一个终端中,我们输入一个简单的命令,并将其输出重定向到指定的管道。
xxxxxxxxxx
[me@linuxbox ~]$ ls -l > pipe1
按下 Enter 键后,命令将显示为挂起。这是因为还没有任何东西从管道的另一端接收数据。当这种情况发生时,据说管道堵塞了。一旦我们将一个进程附加到另一端,并且它开始从管道读取输入,这个条件就会清除。使用第二个终端窗口,我们输入以下命令:
xxxxxxxxxx
[me@linuxbox ~]$ cat < pipe1
从第一个终端窗口生成的目录列表作为 cat
命令的输出出现在第二个终端中。一旦第一个终端中的 ls
命令不再被阻止,它就会成功完成。
好了,我们的旅程到此结束。现在唯一要做的就是练习,练习,练习。虽然我们在徒步旅行中走了很多路,但还有更多值得探索的地方。有成千上万的命令行程序等待被发现和享受。开始在 /usr/bin 中挖掘,看看!
对于仍然渴望更多内容的读者,请查看我的后续书籍《Linux命令行冒险》(Adventures with the Linux Command Line),该书可在LinuxCommand.org上免费下载。