Linux Shell管道调用用户定义函数(使shell支持map函数式特性)

时间:2021-06-03 20:25:00

Linux中有一个管道的概念,常用来流式的处理文本内容,比如一个文件对其中的每一行应用好几个操作,出于两个方面的考虑可能需要在管道中使用用户定义函数:

1. 刚需: 内置的sed/awk之类的可能没法满足我们的需求,只能使用用户定义函数

2. 代码质量: 如果是流式操作很多很长,那么可能就需要将其进行拆分,将相关的部分封装为一个函数,然后流式调用函数,这样程序的可读性更好,也更容易维护

在管道中上一个程序的标准输出会被放到下一个程序的标准输入,处在管道中的程序需要做的就是读取标准输入中的东西进行处理。

下面是一个在管道中使用用户定义函数的例子,print函数不断地从标准输入流中读取数据然后放到标准输出流中:

#! /bin/bash

print(){
while read line
do
echo "$line"
done
} cat original.data | print

上面的例子是比较简单的,那么现在需求又改了,假设读入的每行都是一个数字,需要对数字加1并保存到一个新的文件,那么对上面的脚本进行扩充:

#! /bin/bash

add(){
while read line
do
echo $(($line+1))
done
} save(){
while read line
do
echo $line >> result.data
done
} print(){
while read line
do
echo "$line"
done
} # cat original.data | print
cat original.data | add | save

上面的脚本虽然功能实现了,但是有冗余代码,在函数add和save中重复了循环读取输入流数据的代码,现在重构一下,将重复读取的部分抽象出来为一个新的函数map,此函数接受一个可以处理单行的函数的名字function_name,map函数每读取一行就调用function_name函数,将读取到的内容作为参数传入,如果处理单行的函数function_name还有输出,则还可以作为下一个管道命令的输入,这个处理模型类似于函数式编程中的map的概念,即这里通过一个小技巧使得shell支持部分函数式的功能。

现在按照上面的思想来对脚本进行改造:

#! /bin/bash

add(){
echo $(($1+1))
} save(){
echo $1 >> result.data
} print(){
echo "$1"
} map(){
function_name=$1
while read line
do
$function_name "$line"
done
} # cat original.data | print
cat original.data | map "add" | map "save"

现在看起来好多了,并且程序的可读性也得到了保障,但是如果我想对add传入一个参数指定究竟要加几怎么办呢,又或者我想在save函数自定义保存到位置又该怎么办呢?

这个很简单,在map中取完第一个参数之后就没再用了,直接shift参数然后直接传递即可,再重构一下代码:

#! /bin/bash

add(){
echo $(($1+$2))
} save(){
echo $1 >> $2
} print(){
echo "$1"
} map(){
function_name=$1
shift
while read line
do
$function_name "$line" $@
done
} # cat original.data | print
cat original.data | map "add" 2 | map "save" "result.data"

看起来似乎很完美了,但还能做得更好吗?

如果map方法需要在其它地方调用怎么办?一种方法是将map抽取为utils.sh或者functional.sh之类的库,然后在使用的时候引入它即可,比如新建functional.sh的文件:

##################################################
#
#
# shell 函数式库
#
#
################################################## # $1 函数名
# $[2,] 传递给$1函数的参数
map(){
function_name=$1
shift
while read line
do
$function_name "$line" $@
done
}

使用的时候source functional.sh即可使用map函数。

或者更细粒度的,直接将map抽取为一个文件(以后如果有其它的类似函数也都会抽取为单独的文件),并且放到PATH中,这样使用的时候无需引入,直接就可以使用,比如创建一个文件叫做map:

##################################################
#
#
# functional.map
#
#
################################################## # $1 函数名
# $[2,] 传递给$1函数的参数
map(){
function_name=$1
shift
while read line
do
$function_name "$line" $@
done
} map #@

但是这种方式有个陷阱就是一定要记得每次使用时保证map在$PATH中。

上面就是一步一步优化封装了一个小小的map函数使得shell支持map特性,虽然shell是一门面向字符串的语言,但是稍微对其封装就可以使其具备一些高级语言的特性,提高开发效率,使得程序可读性更好。

.