SHELL(bash)脚本编程三:重定向

这一篇中,我们介绍了一点关于输入输出重定向和管道的基础知识,本篇将继续重定向的话题。 在开始前,先说一说shell中的引用

引用

和许多编程语言一样,bash也支持字符的转义,用来改变字符的原有含义,使得一些元字符(如&)可以出现在命令中。 bash中有三种类型的引用,相互之间稍有不同: 第一种是反斜线(\),用来转义紧随其后的一个字符

[root@centos7 temp]# echo \$PATH
$PATH
[root@centos7 temp]# 

第二种是单引号(''),它禁止对包含的文本进行解析。 第三种是双引号(""),它阻止部分解析,但是允许一些单词(word)的展开。 在双引号中仍保持特殊含义的字符包括:

$ ` \ !
#其中$(扩展符:变量扩展,数学扩展,命令替换)和`(命令替换)保持它们的特殊意义;
#双引号中反斜线\只有在其后跟随的是如下字符时才保持其特殊意义:$ ` " \ ! <newline>;
#默认时,感叹号!(历史扩展,下篇叙述)只用在交互式shell中,脚本中无法进行历史记录和扩展。
# 如第一篇所述,双引号中位置变量和数组变量使用@和*时,含义有所区别:
# "$@"和"${array[@]}"扩展之后每一个元素都是单独的单词
# "$*"和"${array[*]}"扩展之后是一个整体

bash中还有一种特殊的引用:$'string'。其中字符串string内反斜线转义的字符有特殊含义,遵循ANSI C标准,部分解释见这里 例子:

[root@centos7 ~]# echo $'\u4f60\u597d\uff0c\u4e16\u754c\uff01'
你好,世界!
[root@centos7 ~]#

重定向

在以下的描述中如果数字n省略,第一个重定向操作符号是<,则此重定向指标准输入(文件描述符0),如果第一个重定向操作符号是>,则此重定向指标准输出(文件描述符1)。 跟在重定向操作符后面的word会经过扩展。

1、输入重定向

[n]<word

2、输出重定向

[n]>word

word的扩展结果文件会被命令的输出所覆盖(文件不存在会被创建)。通过内置命令set设置了noclobber选项的bash进程在使用重定向操作符>时,不会覆盖后面的文件。使用操作符>|可以强制覆盖。

3、追加输出重定向

[n]>>word

4、重定向标准输出和标准错误

&>word
>&word

两种写法同理,相当于>word 2>&1

5、追加重定向标准输出和标准错误

&>>word

相当于>>word 2>&1

6、以读写的方式打开文件

[n]<>word

以上的重定向中word的扩展结果不能为多个,且只能是文件。一条命令中多个重定向出现的先后顺序很重要,但某个重定向处于命令的位置是无关紧要的。

#!/bin/bash
#多个重定向出现的顺序有时会影响结果
#标准输出和标准错误都重定向至文件file
ls hello file >file 2>&1
#标准错误输出至终端,标准输出重定向至文件
ls hello file 2>&1 >file
#重定向出现的位置无关紧要。下面三条命令等价:
head -1 </etc/passwd >>newfile
>>newfile head -1 </etc/passwd
head</etc/passwd>>newfile -1
#查看验证
cat newfile

执行:

[root@centos7 ~]# ./test.sh 
ls: 无法访问hello: 没有那个文件或目录
root:x:0:0:root:/root:/bin/bash
root:x:0:0:root:/root:/bin/bash
root:x:0:0:root:/root:/bin/bash

7、Here Documents

<<[-]word
    here-document
delimiter

此处的word不能扩展,如果word中有任何字符被引用(如前引用部分),delimiterword去除引用后剩余的字符,并且here-document中的词都不会被shell解释。如果word没有被引用,here-document中的词可以经历变量扩展命令替换数学扩展(和双引号的情况类似)。 如果重定向操作符是<<-,那么处于here-document中的开头tab字符将会被删除。

8、Here Strings

<<<word

这里word的扩展结果会作为字符串被重定向。 脚本举例:

#!/bin/bash
VAR='hello'
#Here Documents
cat <<EOF >file
#文档内容不会被作为注释
不被引用时变量可以在文档内被扩展:
$VAR world
EOF
cat file
#Here Strings
echo ${parameter:=$[`tr "," "+" <<<"1,2,3"`]}
#变量临时作用域
IFS=':' read aa bb cc <<<"11:22:33"
echo -e "$aa $bb $IFS $cc"

执行结果:

[root@centos7 ~]# ./test.sh   
#文档内容不会被作为注释
不被引用时变量可以在文档内被扩展:
hello world
6
11 22  
 33
[root@centos7 ~]# 

9、复制文件描述符

[n]<&word   #复制输入文件描述符
[n]>&word   #复制输出文件描述符

这里的word扩展后的值必须为数字,表示复制此文件描述符到n,如果word扩展的结果不是文件描述符,就会出现重定向错误。如果word的值为-,则表示关闭文件描述符n[n]>&word这里有一个特殊情况:如果n省略且word的结果不是数字,则表示重定向标准错误和标准输出(如前所述)。

10、转移文件描述符

[n]<&digit- #转移输入文件描述符
[n]>&digit- #转移输出文件描述符

这两种表示移动文件描述符digit到文件描述符n,移动后文件描述符digit被关闭。 由于bash中重定向只在当前命令中有效,命令执行完毕后,重定向被撤销。可以使用内置命令exec使重定向在整个脚本有效。 脚本举例:

#!/bin/bash
#打开输入文件描述符3,并关联文件file
exec 3<file
#先将文件描述符复制给标准输入,cat命令从标准输入读取到文件file的内容
cat <&3
#关闭文件描述符3
exec 3<&-

#打开3号和4号描述符作为输出,并且分别关联文件。
exec 3>./stdout
exec 4>./stderr
#转移标准输出到3号描述符,关闭原来的1号文件描述符。
exec 1>&3-
#转移标准错误到4号描述符,关闭原来的2号文件描述符。
exec 2>&4-
#命令的标准输出将写入文件./stdout,标准错误写入文件./stderr
ls file newfile
#关闭两个文件描述符
exec 3>&-
#关闭的时候重定向符号是>还是<都没关系
exec 4<&-

#定义远端主机及端口
host=10.0.1.251
port=80
#以读写的方式打开文件描述符5并关联至文件(此文件代表一条到远端的TCP链接)
if ! exec 5<>/dev/tcp/$host/$port
then
    exit 1
fi
#测试链接可用性
echo -e "GET / http1.1\n" >&5
#获取输出
cat <&5
#关闭文件描述符
exec 5<&-

执行结果:

[root@centos7 ~]# ./test.sh 
#我是文件file的内容
<!DOCTYPE html... #余下部分是http响应信息
...
[root@centos7 ~]# 
[root@centos7 ~]# cat stderr 
ls: 无法访问newfile: 没有那个文件或目录
[root@centos7 ~]# cat stdout 
file
[root@centos7 ~]#

coproc

上一篇中我们描述了coproc命令的语法,这里给出用例:

#!/bin/bash
#简单命令
#简单命令使用不能通过NAME指定协进程的名字
#此时进程的名字统一为:COPROC。(也预示着同一时间只能有一个简单命令的协进程)
coproc cat file
#协进程PID
echo $COPROC_PID
#转移协进程的输出文件描述符到标准输入,并供cat命令使用:
cat <&${COPROC[0]}-

#复合命令
#对于命名协进程,其后的命令必须是复合命令
coproc ASYNC while read line
do
    if [ "$line" == "break" ];then
        break
    else
        awk -F: '{print $1}' <<<"$line"
    fi
done
#传递数据到异步程序(sed命令在文件底部追加了字符串"break")
sed '$abreak' /etc/passwd >&${ASYNC[1]}
#获得输出
while read -u ${ASYNC[0]} user_name
do
    echo $user_name
done

执行结果:

[root@centos7 ~]# ./test.sh 
28653
命令的标准输出和标准输入通过双向管道分别连接到当前shell的两个文件描述符,
然后文件描述符又分别赋值给了数组元素NAME[0]和NAME[1]
root
bin
daemon
...
[root@centos7 ~]#

管道

管道是进程间通信的主要手段之一。linux管道分为两种:匿名管道命名管道。 通过控制操作符||&连接命令时所创建的管道都是匿名管道匿名管道只能用于具有亲缘关系的进程之间。 命名管道可以用在两个不相关的进程之间,可以使用命令mknodmkfifo来创建命名管道。 我们已经见过很多匿名管道的例子,这里举一个利用命名管道控制并发进程数的例子:

#!/bin/bash
#进程个数
NUM=10
tmpfile="$$.fifo"
#生成临时命名管道
[ -e $tmpfile ] && exit || mkfifo $tmpfile
#以读写的方式打开文件描述符5,并关联至命名管道
exec 5<>$tmpfile
#删除临时命名管道文件
rm $tmpfile
#写入指定数量的空行供read使用
while((NUM-->0))
do
    echo
done >&5
 
for IP in `cat ip.list`
do
    #read命令每次读取一行输入,保证了同一时间有10个如下复合命令在运行
    read
    {
        #统计IP在日志文件access.log出现的次数
        grep -c $IP access.log >>result.txt
        echo
    #命令运行结束后仍写入一个空行至文件描述符5
    #结尾的符号&保证此复合命令在后台运行
    } >&5 &
done <&5
#内置命令wait的作用是等待子进程的结束
wait
#关闭文件描述符5
exec 5>&-

执行略。 当然,这里的for循环中执行的复合命令可以替换为任意需要并发执行的任务。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券