一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
Linux管道操作符|
正如我另一篇文章说的那样,这是经常使用的一个命令:
cat system.log | grep hello
上面的命令,可以让我们在system.log里找到包含hello字符串的行,并输出到屏幕上。
什么叫模拟Linux管道操作符
管道操作符将一个命令分成前后两个部分,前面一个命令的标准输出会对接到后一个命令的标准输入。
以上说法就是最正确的描述。我们实际上就是来模拟这个过程。
这里需要注意的是,我们无法模拟形式上的东西,也就是说无法自定义一个符号来代替|,我们最终的效果如下。
当然,如果我们使用Golang来完整的开发一个类似`bash`的程序,比如说称之为`gosh`,我们当然随便使用任何符号来替换`|`。
这个文章里,我们会使用Golang开发一个简单的程序,称之为go-pipe,用如下的方法使用:
go-pipe "cat system.log" "grep hello"
也就是说go-pipe这个程序,接收两个命令行参数,然后这个程序来模拟管道,进而,最后的结果就和下面的指令一样:
cat system.log | grep hello
一切从新建Golang文件开始
一些基础的概念就不提了,比如说搭建Golang开发环境等等。
我们从main.go开始:
package main
func main() {
}
我们要接收两个命令行参数,那么这样即可:
arg1:=os.Args[1] // 这个可能是"cat system.log"
arg2:=os.Args[2] // 这个可能是"grep hello"
上面的代码没有正确处理错误,比如说,用户没有输入两个参数。这里不管,因为主题是模拟Linux管道操作符,错误处理啥的先忽略不提。
cat 和 grep 的启动顺序
由于要模拟两个程序的执行,我们必须先理清,这两个程序启动的时机。
- 是先启动
cat,再启动grep? - 还是 先启动
grep再启动cat? - 亦或 两者同时启动?
不妨假设system.log有1个G,那么试想,难道cat是把这个 1个G的内容全部读到内存里,然后再慢慢的输出给grep。
很显然,这是不好的。
所以说,这两个程序应该是同时执行,cat根据自身的buffer大小,将system.log的数据输出给grep。
所以,我们在go-pipe程序中,要并发的启动由两个参数所指定的程序cat system.log 和 grep hello。
在Golang中启动另一个程序
这个其实很好做到,官方库里自带了这个功能,这个功能基本上所有的编程语言都会自带,因为是一个基本的需求。
在Golang中,这样既可:
catcmd := exec.Command("cat", "system.log")
catcmd.Run()
如果没有更多的需求,用上面这样的代码就行了。
但是我们的需求是将 cat system.log这个命令行参数,作为一个指令进行启动。自然的,我们要分割一下字符串:
arg1 := os.Args[1]
arg1seg := strings.Split(arg1, " ")
arg1cmd := exec.Command(arg1seg[0], arg1seg[1:]...)
arg1cmd.Run()
上面的代码很清楚,不过有一点要提到:
exec.Command函数,第二个参数是一个可变长度的参数,这很好理解,因为一个指令的参数往往不止一个。例如:
cat system.log.1 system.log.2 |grep hello
上面的指令,cat就有两个参数。
采用 cmd.Start 来启动程序
一旦使用cmd.Run这个函数来启动程序的话,这个线程就会被卡住,直到这个程序结束。
所以我们换一个函数: cmd.Start。
arg1 := os.Args[1]
arg1seg := strings.Split(arg1, " ")
arg1cmd := exec.Command(arg1seg[0], arg1seg[1:]...)
arg1cmd.Start()
这样做的好处就是,Start 函数会立即返回,而不会卡住这个线程。
我们如法炮制,去接收第二个命令行参数:
arg1 := os.Args[1]
arg1seg := strings.Split(arg1, " ")
arg1cmd := exec.Command(arg1seg[0], arg1seg[1:]...)
arg2 := os.Args[2]
arg2seg := strings.Split(arg2, " ")
arg2cmd := exec.Command(arg2seg[0], arg2seg[1:]...)
arg1cmd.Start()
arg2cmd.Start()
此时此刻,我们就将两个命令行参数所制定的程序,启动了起来。 为什么将两个Start写在最后呢,这是因为,我们还要对接一下输入和输出。
对接输入和输出
回顾一下我们的目标:
就是让
cat system.log|grep hello
go-pipe "cat system.log" "grep hello"
上面两者的效果要是一样的。
所有光启动不行,还要将第一个程序的标准输出对接到第二个程序的标准输入。
看起来很简单,伪代码可能如下:
arg1 := os.Args[1]
arg1seg := strings.Split(arg1, " ")
arg1cmd := exec.Command(arg1seg[0], arg1seg[1:]...)
arg2 := os.Args[1]
arg2seg := strings.Split(arg2, " ")
arg2cmd := exec.Command(arg2seg[0], arg2seg[1:]...)
arg1cmd.Stdout = arg2cmd.Stdin // 对接
arg1cmd.Start()
arg2cmd.Start()
如上代码所示,第一个cmd的Stdout = 第二个cmd的Stdin,这样写,似乎很合理。
但是不好意思,代码提示错误:
错误很简单,就是因为两个变量类型不一致,不能直接赋值,那怎么办呢?
我们不能用上面那两个成员变量,应该用另外两个:
cmd1out, _ := arg1cmd.StdoutPipe() // 第一个cmd的标准输出
cmd2in, _ := arg2cmd.StdinPipe() // 第二个cmd的标准输入
接下来只要想办法,让这两个对接就行了。
用一个很简单的思维来思考,就是我们正在写的这个程序go-pipe其实就是cat和grep的中转站,我们在go-pipe中读取cat的输出,然后把这个内容写入grep程序,这样就行了。
buffer := make([]byte, 1024)
//
for {
n, err := cmd1out.Read(buffer)
if err != nil {
break
}
cmd2in.Write(buffer[:n])
}
上面的代码就是一个简单的中转站操作,就是读取 cmd1out 然后 写入 cmd2in。
不过上面的代码,有一个隐藏的bug,实际情况下会小概率发生,这里不提,文章最后提一下。
好了,在程序最后,我们需要Wait这两个cmd的操作,结束,否则,main函数就直接执行完了。
arg1cmd.Wait()
arg2cmd.Wait()
整体代码:
package main
import (
"os"
"os/exec"
"strings"
)
func main() {
arg1 := os.Args[1]
arg1seg := strings.Split(arg1, " ")
arg1cmd := exec.Command(arg1seg[0], arg1seg[1:]...)
arg2 := os.Args[2]
arg2seg := strings.Split(arg2, " ")
arg2cmd := exec.Command(arg2seg[0], arg2seg[1:]...)
arg2cmd.Stdout = os.Stdout
cmd1out, _ := arg1cmd.StdoutPipe()
cmd2in, _ := arg2cmd.StdinPipe()
arg1cmd.Start()
arg2cmd.Start()
//
buffer := make([]byte, 1024)
//
for {
n, err := cmd1out.Read(buffer)
if err != nil {
break
}
cmd2in.Write(buffer[:n])
}
arg1cmd.Wait()
arg2cmd.Wait()
}
此时此刻,我们准备好system.log文件:
然后:
go run main.go "cat system.log" "grep hello"
发现,确实能从system.log找到hello所在的行。
但是有一个问题, 程序卡住了。
这是为什么能?仔细思考以下整个过程。
发现,第二个程序, 也就是grep程序,应该就是卡住的原因,grep此时还在继续等待输入。
如果不确定的话,请使用ps aux|grep grep,来观察一下 grep hello 这个指令是否依然在运行。
其实很好办, 我们在for循环退出之后,马上关闭cmd2in就行了:
for {
n, err := cmd1out.Read(buffer)
if err != nil {
break
}
cmd2in.Write(buffer[:n])
}
cmd2in.Close()
现给出整体代码:
package main
import (
"os"
"os/exec"
"strings"
)
func main() {
arg1 := os.Args[1]
arg1seg := strings.Split(arg1, " ")
arg1cmd := exec.Command(arg1seg[0], arg1seg[1:]...)
arg2 := os.Args[2]
arg2seg := strings.Split(arg2, " ")
arg2cmd := exec.Command(arg2seg[0], arg2seg[1:]...)
arg2cmd.Stdout = os.Stdout
cmd1out, _ := arg1cmd.StdoutPipe()
cmd2in, _ := arg2cmd.StdinPipe()
arg1cmd.Start()
arg2cmd.Start()
//
buffer := make([]byte, 1024)
//
for {
n, err := cmd1out.Read(buffer)
if err != nil {
break
}
cmd2in.Write(buffer[:n])
}
cmd2in.Close()
arg1cmd.Wait()
arg2cmd.Wait()
}
如此一来,就能正常运行了。
中转站的小bug
细心的人肯定已经发现了。
cmd2in.Write(buffer[:n])
就是这一句有问题,因为Write函数,并不能确保将n个字节写入cmd2in里面去。
例如,如果n很大的时候,就不行。比如说n取个100M。我估计就写不进去。
Write函数,会返回实际写入成功的字节数,此时,我们需要继续将未写入的字节,继续写进去,也就是说,我们要搞一个循环,一直写,直到总共写入的字节==n。才行,大概的代码如下:
for {
n, err := cmd1out.Read(buffer)
if err != nil {
break
}
var totalWrite int
for {
if totalWrite == n {
break
}
tempn, _ := cmd2in.Write(buffer[totalWrite:n]) // 注意,这里开始下标变成了 totalWriten
totalWrite += tempn
}
}
好了,这里我们没有考虑Write函数的第二个返回值,也就是错误,实际项目代码里,这个还要考虑。