第三十六章: 其他命令

36.1 组命令和子shell

bash允许将命令组合到一起使用, 有两种方式, 分别是组命令和子shell:

# 组命令
{ command1; command2; [command3; ...] }
# 子shell
(command1; command2; [command3; ...])

需要注意的是, 在实现组命令时必须使用一个空格将花括号与命令分开, 并且在闭花括号前使用分号或是换行来结束最后的命令.

36.1.1 执行重定向

可以使用组命令和子shell来管理重定向:

ls -l > output.txt
echo "Listing of foo.txt" >> output.txt
cat foo.txt >> output.txt

# 等价于
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
# 或是
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt

当创建命令管道时, 通常将多条命令的结果输出到一条流中, 这比较有用:

{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr

在下面的脚本中我们将使用组命令, 看几个与关联数组结合使用的编程技巧. 这个脚本称为 array-2, 当给定一个目录名, 打印出目录中的文件列表, 伴随着每个文件的文件所有者和组所有者. 在文件列表的末尾, 脚本打印出属于每个所有者和组的文件数目. 这里我们看到的(为简单起见而缩短的)结果, 是给定脚本的目录为 /usr/bin 的时候:

[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)

脚本代码如下:

#!/bin/bash

# array-2: Use arrays to tally file owners

declare -A files file_group file_owner groups owners

if [[ ! -d "$1" ]]; then
   echo "Usage: array-2 dir" >&2
   exit 1
fi

for i in "$1"/*; do
   owner=$(stat -c %U "$i")
   group=$(stat -c %G "$i")
    files["$i"]="$i"
    file_owner["$i"]=$owner
    file_group["$i"]=$group
    ((++owners[$owner]))
    ((++groups[$group]))
done

# List the collected files
{ for i in "${files[@]}"; do
printf "%-40s %-10s %-10s\n" \
"$i" ${file_owner["$i"]} ${file_group["$i"]}
done } | sort
echo

 List owners
echo "File owners:"
{ for i in "${!owners[@]}"; do
printf "%-10s: %5d file(s)\n" "$i" ${owners["$i"]}
done } | sort
echo

# List groups
echo "File group owners:"
{ for i in "${!groups[@]}"; do
printf "%-10s: %5d file(s)\n" "$i" ${groups["$i"]}
done } | sort

36.1.2 进程替换

组命令和子shell有一个主要的不同: 子shell在当前shell的子拷贝中执行命令, 而组命令在当前shell里面执行命令. 通常情况下组命令更合适, 除非脚本真的需要子shell.

在之前的章节中, 我们接触到了一个子shell环境导致的问题实例:

echo "foo" | read

以上的命令并不能对REPLY变量进行赋值, 因为read命令是在子shell中执行的. 由于总是在子shell中执行管道中的命令, 所以任何变量赋值都会遇到这个问题. shell提供了一种称为进程替换的外部扩展方式来解决这个问题:

实现进程替换的方式有两种, 一种是产生标准输出的进程:

<(list)

另一种是吸纳标准输入的进程:

>(list)

这里的list是一系列命令. 为了解决上述read命令的问题, 可以像如下方式那样来使用进程替换:

read < <(echo "foo")

进程替换允许把子shell的输出当作一个普通的文件, 目的是为了重定向. 事实上这是一种扩展形式, 我们可以查看它的真实值:

[me@linuxbox ~]$ echo <(echo "foo")
/dev/fd/63

进程替换通常结合带有read的循环使用, 例如如下代码:

#!/bin/bash
# pro-sub : demo of process substitution
while read 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 | tail -n +2)

产生的输出如下:

[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

36.2 trap

bash提供了trap机制来让脚本响应信号, 语法如下:

trap argument signal [signal...]

这里的argument是作为命令被读取的字符串, 而signal是对信号量的说明, 该信号量会触发解释命令的执行. 例如:

#!/bin/bash
# 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

当脚本收到 SIGTERM或是SIGINT信号时, 脚本定义的trap将执行echo命令.

通常我们使用shell函数来代替命令, 将代码修改如下:

#!/bin/bash
# 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

类UNIX系统通常在/tmp目录下创建临时文件, 为了给临时文件一个不可预知的文件名, 可以使用如下代码:

tempfile=/tmp/$(basename $0).$$.$RANDOM

这会创建一个包含程序名的文件, 其后是进程ID, 然后是一个1~32767之间的整数.

更好的办法是使用 mktemp程序来命名和创建临时文件:

tempfile=$(mktemp /tmp/foobar.$$.XXXXXXXXXX)

mktemp使用模板作为参数来创建文件名, 该模板包含一系列的X字符, mktemp用随机的字母和数字来替换这些X字符. mktemp构造了一个临时文件名, 同时也创建了这个文件.

如果是普通用户执行的脚本, 较好的做法是避免使用 /tmp 目录, 而在用户主目录下为临时文件创建一个目录, 代码如下:

[[ -d $HOME/tmp ]] || mkdir $HOME/tmp

36.3 异步执行

bash提供了wait命令让父脚本暂停, 直到指定的进程(例如子脚本)结束.

36.3.1 wait命令

父脚本如下:

#!/bin/bash
# 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."

子脚本如下:

#!/bin/bash
# async-child : Asynchronous execution demo (child)
echo "Child: child is running..."
sleep 5
echo "Child: child is done. Exiting."

$! 变量的值总是包含后台中最后一次运行的进程ID. 输出如下:

[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.

36.4 命名管道

大多数类UNIX系统支持创建一种叫做命名管道的特殊类型的文件, 使用命名管道可以建立两个进程之间的通信, 并且可以像其他类型的文件一样使用.

命名管道的工作方式和文件相似, 命名管道实际上是两块先进先出的缓冲区, 数据从一端进入, 从另一端出去. 可以通过如下方式使用命名管道:

process1 > named_pipe
process2 < named_pipe

# 等价于
process1 | process2

36.4.1 设置命名管道

可以使用mkfifo命令创建命名管道:

mkfifo pipe1

命名管道文件的属性字段的第一个字母是p.

36.4.2 使用命名管道

打开两个终端, 在第一个终端中输入:

ls -l > pipe1

这时命令会挂起, 因为命名管道被阻塞. 在第二个终端输入:

cat < pipe1

可以看到第一个终端中命令的结果出现在第二个终端中, 同时第一个终端中的ls命令结束.

36.5 本章结尾语

36.6 扩展阅读

  • bash 手册页的 复合命令 部分包含了对组命令和子 shell 表示法的详尽描述.

  • bash 手册也的 EXPANSION 部分包含了一小部分进程替换的内容.

  • 《高级 Bash 脚本指南》也有对进程替换的讨论:

http://tldp.org/LDP/abs/html/process-sub.html

  • 《Linux 杂志》有两篇关于命令管道的好文章. 第一篇源于1997年9月:

http://www.linuxjournal.com/article/2156

  • 和第二篇源于2009年3月:

http://www.linuxjournal.com/content/using-named-pipes-fifos-bash

results matching ""

    No results matching ""