1、概述
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:
- Linux提供了cat、ls、copy等命令与操作系统交互;
- go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
- 容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
- git、npm等也是大家比较熟悉的工具
尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
2、基础知识
命令行程序主要涉及内容:
- 命令
- 命令行参数
- 选项:长格式、短格式
- IO:stdin、stdout、stderr、管道、重定向
- 环境变量
3、Golang 的支持
通过pflag包来获取命令行参数。
pflag.IntVarP(&(args.startPage),"startPage","s",-1,"start page")
pflag.IntVarP(&(args.endPage),"endPage","e",-1,"end page")
pflag.IntVarP(&(args.pageLen),"pageLen","l",72,"the length of page")
pflag.BoolVarP(&(args.pageType),"pageType","f",false,"page type")
pflag.StringVarP(&(args.outDestination),"outDestination","d","","print destination")
pflag.Parse()
args_left:=pflag.Args() // 其余参数
if len(args_left) > 0 {
args.inFile=args_left[0]
} else {
args.inFile=""
}
4、开发实践
要求
使用golang开发开发 Linux 命令行实用程序中的selpg。
- 请按文档使用 selpg章节要求测试你的程序;
- 请使用pflag替代goflag以满足Unix命令行规范,参考:Golang之使用Flag和Pflag;
- golang文件读写、读环境变量,请自己查os包;
- “-dXXX” 实现,请自己查os/exec库,例如案例Command,管理子进程的标准输入和输出通常使用io.Pipe,具体案例见Pipe
- 请自带测试程序,确保函数等功能正确
selpg介绍
selpg是从文本输入选择页范围的实用程序。该输入可以来自作为最后一个命令行参数指定的文件,在没有给出文件名参数时也可以来自标准输入。
selpg首先处理所有的命令行参数。在扫描了所有的选项参数(也就是那些以连字符为前缀的参数)后,如果 selpg 发现还有一个参数,则它会接受该参数为输入文件的名称并尝试打开它以进行读取。如果没有其它参数,则 selpg 假定输入来自标准输入。
指令格式:selpg -sNumber -eNumber [ -f | -lNumber ][ -dDestination ] [ input_filename ]
详情可看文档。
设计与实现
首先,先要安装spf13/pflag。
go get github.com/spf13/pflag
根据指令的各个参数,可以创建一个结构体来储存。
type selpg_args struct{
startPage int
endPage int
inFile string
pageLen int
pageType bool // true for -f, false for -lNumber
outDestination string
}
对于命令行中输入的参数,需要通过pflag来解析,解析完之后存储到结构体当中。
(ps:为什么不能用flag而是要用pflag呢?因为用flag来解析参数的话,参数后面的值需要用空格' '
或者等号'='
来隔开,那么指令就会变成selpg -s Number -e Number [ -f | -l Number ][ -d Destination ] [ input_filename ]
或是selpg -s=Number -e=Number [ -f | -l=Number ][ -d=Destination ] [ input_filename ]
,这样并不满足Unix命令行规范。)
因而,程序的设计可以分为如下步骤:
解析参数、检查参数、处理输入、输出
解析参数
func getArgs(args *selpg_args){
pflag.IntVarP(&(args.startPage),"startPage","s",-1,"start page")
pflag.IntVarP(&(args.endPage),"endPage","e",-1,"end page")
pflag.IntVarP(&(args.pageLen),"pageLen","l",72,"the length of page")
pflag.BoolVarP(&(args.pageType),"pageType","f",false,"page type")
pflag.StringVarP(&(args.outDestination),"outDestination","d","","print destination")
pflag.Parse()
args_left:=pflag.Args() // 其余参数
if len(args_left) > 0 {
args.inFile=args_left[0]
} else {
args.inFile=""
}
check_args(args)
}
主要考验的是对pflag包的使用。
检查参数
func check_args(args *selpg_args){
if args==nil{
fmt.Fprintf(os.Stderr,"\n[Error]The args is nil!Please check your program!\n\n")
os.Exit(0)
}else if(args.startPage==-1)||(args.endPage==-1){
fmt.Fprintf(os.Stderr,"\n[Error]The startPage and endPage is not allowed empty!Please check your command!\n\n")
os.Exit(0)
}else if (args.startPage<0)||(args.endPage<0){
fmt.Fprintf(os.Stderr,"\n[Error]The startPage and endPage is not negative!Please check your command!\n\n")
os.Exit(0)
}else if args.startPage>args.endPage{
fmt.Fprintf(os.Stderr,"\n[Error]The startPage can not be bigger than the endPage!Please check your command!\n\n")
os.Exit(0)
}
if args.pageType==false&&args.pageLen<1 {
fmt.Fprintln(os.Stderr,"\n[Error]You should input valid page length!\n\n")
os.Exit(0)
}
}
主要是对-s
和-e
两个强制选项做判断,还有就是-l
里面定义的行数不能小于1。
处理输入
func processInput(args *selpg_args){
var reader *bufio.Reader
if args.inFile=="" {
reader=bufio.NewReader(os.Stdin)
} else{
fileIn,err:=os.Open(args.inFile)
defer fileIn.Close()
if err!=nil {
os.Stderr.Write([]byte("Open file error.\n"))
os.Exit(0)
}
reader=bufio.NewReader(fileIn)
}
output(reader,args)
}
输入的处理主要是要判断是标准输入亦或是文件源输入,最后一个命令行参数指定源文件,但有时候缺少该参数时用的就是标准输入。
输出
writer:=bufio.NewWriter(os.Stdout)
lineCtr:=0
pageCtr:=1
endSign:='\n'
if args.pageType==true {
endSign='\f'
}
for{
strLine,errRead:=reader.ReadBytes(byte(endSign))
if errRead!=nil {
if errRead==io.EOF {
writer.Flush()
break
} else{
os.Stderr.Write([]byte("Read bytes from reader failed.\n"))
os.Exit(0)
}
}
if pageCtr>=args.startPage&&pageCtr<=args.endPage {
_,errWrite:=writer.Write(strLine)
if errWrite!=nil {
fmt.Println(errWrite)
os.Stderr.Write([]byte("Write bytes to out failed.\n"))
os.Exit(0)
}
}
if args.pageType==true {
pageCtr++
} else{
lineCtr++
}
if args.pageType==false&&lineCtr==args.pageLen {
lineCtr=0
pageCtr++
}
if pageCtr>args.endPage {
writer.Flush()
break
}
}
首先就要区分-f
类型和-lNumber
类型,一个读到'\f'
就分页,一个是读取pageLen行再分页。在ReadBytes的时候可以根据情况来决定相应的终止符号,读到相应的页(从startPage到endPage)就输出便可。
同时,还要处理-d
参数,表示送到相应的打印机打印,但是因为我这里没有连接打印机,因而只是另外写了一个程序去模拟打印机。
var cmd *exec.Cmd=nil
var stdin io.WriteCloser=nil
writer:=bufio.NewWriter(os.Stdout)
if args.outDestination!="" {
cmd=exec.Command(args.outDestination)
var pipeErr error;
stdin,pipeErr=cmd.StdinPipe()
if pipeErr!=nil {
fmt.Println(pipeErr)
os.Exit(0)
}
startErr:=cmd.Start()
if startErr!=nil {
fmt.Println(startErr)
os.Exit(0)
}
}
基本思路就是通过io.Pipe来管理子进程的标准输入和输出,把内容传给子进程,再让子进程把内容输出到一个特定的文件里面。相应的程序在lp1.go里面实现。
ps:lp1.go的实现
首先,通过mkdir $GOPATH/src/github.com/github-user/lp1 -p
来创建一个新目录,在新目录里面创建新文件lp1.go
,编写程序。
package main
import(
"bufio"
"io"
"os"
)
func main(){
reader:=bufio.NewReader(os.Stdin)
file,openErr:=os.OpenFile("./lp1.txt",os.O_RDWR|os.O_CREATE|os.O_APPEND,0644)
if openErr!=nil {
panic(openErr)
}
writer:=bufio.NewWriter(file)
for{
line,errRead:=reader.ReadBytes('\n')
if errRead!=nil {
if errRead==io.EOF {
break
} else {
os.Stderr.Write([]byte("Read bytes from reader fail\n"))
os.Exit(0)
}
}
_,errWrite:=writer.Write(line)
if errWrite!=nil {
os.Stderr.Write([]byte("Write bytes to file fail\n"))
os.Exit(0)
}
writer.Flush()
}
}
无非就是打开目标文件,把从selpg.go
传来的“标准输入”读取出来,写进目标文件里面。
编写完文件之后,需要执行go install github.com/github-user/lp1
,方便后续在任何路径都能直接通过lp1
来直接调用这个程序,(同样也使得这个模拟的打印机更加逼真)。
selpg.go
也是如此,执行go install github.com/github-user/selpg
,后续就能直接在终端直接通过selpg -sNumber -eNumber [ -f | -lNumber ][ -dDestination ] [ input_filename ]
这样的指令格式来直接调用自己的程序。
原本,-d
参数的处理应该是可以另外用一个函数来实现的,但是考虑到跟前面的输出函数有太多的相似之处,在这里我就直接将它们合为一起了。完整函数如下:
func output(reader *bufio.Reader,args *selpg_args){
var cmd *exec.Cmd=nil
var stdin io.WriteCloser=nil
writer:=bufio.NewWriter(os.Stdout)
if args.outDestination!="" {
cmd=exec.Command(args.outDestination)
var pipeErr error;
stdin,pipeErr=cmd.StdinPipe()
if pipeErr!=nil {
fmt.Println(pipeErr)
os.Exit(0)
}
startErr:=cmd.Start()
if startErr!=nil {
fmt.Println(startErr)
os.Exit(0)
}
}
lineCtr:=0
pageCtr:=1
endSign:='\n'
if args.pageType==true {
endSign='\f'
}
for{
strLine,errRead:=reader.ReadBytes(byte(endSign))
if errRead!=nil {
if errRead==io.EOF {
writer.Flush()
break
} else{
os.Stderr.Write([]byte("Read bytes from reader failed.\n"))
os.Exit(0)
}
}
if pageCtr>=args.startPage&&pageCtr<=args.endPage {
_,errWrite:=writer.Write(strLine)
if errWrite!=nil {
fmt.Println(errWrite)
os.Stderr.Write([]byte("Write bytes to out failed.\n"))
os.Exit(0)
}
if stdin!=nil {
_,errWrite:=stdin.Write(strLine)
if errWrite!=nil {
fmt.Println(errWrite)
os.Stderr.Write([]byte("Write bytes to out failed.\n"))
os.Exit(0)
}
}
}
if args.pageType==true {
pageCtr++
} else{
lineCtr++
}
if args.pageType==false&&lineCtr==args.pageLen {
lineCtr=0
pageCtr++
}
if pageCtr>args.endPage {
writer.Flush()
break
}
}
if stdin!=nil {
stdin.Close()
}
if cmd!=nil {
if err:=cmd.Wait();err!=nil {
fmt.Println(err)
os.Exit(0)
}
}
}
相关指令的测试
selpg -s1 -e1 input_file
2.selpg -s1 -e1 < input_file
这里同样也是72行为一页,但在这里,最后一个参数并不是作为selpg的源输入文件参数,当前selpg读取的是标准输入,只不过是这时候标准输入已被shell/内核重定向为来自“input_file”而不是显式命名的文件名参数。
这是shell自行实现了的,因而并不在我们程序的实现范围之内。
3.other_command | selpg -s10 -e20
这个也是shell自行实现好了的,“other_command”的标准输出被shell/内核重定向至 selpg 的标准输入。因为总共也不够72行(总共才1页),因而要输出第10到第20页的话就是空值。
4.selpg -s10 -e20 input_file >output_file
因为我的in.txt总共才101行,把第1页到第3页输出到out.txt的话,实际上也就输出了两页。这里同样地,shell也已经帮我们实现好了输出的重定向,标准输出被shell/内核重定向至“output_file”。
5.selpg -s10 -e20 input_file 2>error_file
输入一个错误的指令,就能看到错误信息输出到err.txt里面了。所有的错误消息被shell/内核重定向至“error_file”。请注意:在“2”和“>”之间不能有空格;这是 shell 语法的一部分(请参阅“man bash”或“man sh”)。
6.selpg -s10 -e20 input_file >output_file 2>error_file
out.txt里面新得到的内容跟先前的测试一样,同时err.txt里面的内容被清空了。
7.selpg -s10 -e20 input_file >output_file 2>/dev/null
写至标准输出的内容被重定向到了out.txt,写至标准错误的内容被丢弃。
8.selpg -s10 -e20 input_file >/dev/null
9.selpg -s10 -e20 input_file | other_command
同时,这里的other_command还可以设置为先前自行写的模拟lp1程序。
10.selpg -s10 -e20 input_file 2>error_file | other_command
1.selpg -s10 -e20 -l66 input_file
2.selpg -s10 -e20 -f input_file
(这里,我在第72行的后面加了一个’\f’。)
3.selpg -s10 -e20 -dlp1 input_file
4.selpg -s10 -e20 input_file > output_file 2>error_file &
由于打印的内容太少,一下子就执行完了。
编写测试程序
为了方便,遇到了困难,这里并没有用go test,而是直接写了一个selpgTest.go
程序。
同时,排除上诉语义差不多的指令,还有shell已经实现好了的输入/输出重定向,一般来说就对-l
、-f
、-d
来做测试就差不多了。懒
package main
import (
"fmt"
"os"
"os/exec"
)
func main(){
var N,count int
fileIn,err:=os.Open("in.txt")
fileOut,err:=os.Open("out.txt")
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
dataIn:=make([]byte, 1000)
dataOut:=make([]byte, 1000)
N,err=fileIn.Read(dataIn)
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
cmd:=exec.Command("bash","-c","selpg -s1 -e1 in.txt >out.txt")
if err = cmd.Run(); err != nil {
fmt.Print(err)
fmt.Print('\n')
}
count,err=fileOut.Read(dataOut)
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
count=0
for i:=0;count<72;i++ {
if dataIn[i]!=dataOut[i] {
fmt.Println("Failed at test1.")
os.Exit(0)
}
if dataIn[i]=='\n' {
count++;
}
}
cmd=exec.Command("bash","-c","selpg -s1 -e1 -f in.txt >out.txt")
if err = cmd.Run(); err != nil {
fmt.Print(err)
fmt.Print('\n')
}
fileOut,err=os.Open("out.txt")
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
count,err=fileOut.Read(dataOut)
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
count=0
for i:=1;count<1;i++ {
if dataIn[i]!=dataOut[i] {
fmt.Println("Failed at test2.")
}
if dataIn[i]=='\f' {
count++;
}
}
cmd=exec.Command("bash","-c","selpg -s2 -e2 -l66 in.txt >out.txt")
if err = cmd.Run(); err != nil {
fmt.Print(err)
fmt.Print('\n')
}
fileOut,err=os.Open("out.txt")
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
count,err=fileOut.Read(dataOut)
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
count=0
i:=0
for ;count<66;i++ {
if dataIn[i]=='\n' {
count++;
}
}
count=0
for j:=0;count<66&&i<N; {
if dataIn[i]!=dataOut[j] {
fmt.Println("Failed at test3.")
os.Exit(0)
}
if dataIn[i]=='\n' {
count++;
}
i++;
j++
}
cmd=exec.Command("bash","-c","selpg -s2 -e2 -l66 -dlp1 in.txt >out.txt")
if err = cmd.Run(); err != nil {
fmt.Print(err)
fmt.Print('\n')
}
fileOut,err=os.Open("lp1.txt")
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
count,err=fileOut.Read(dataOut)
if err!=nil {
fmt.Print(err)
fmt.Print('\n')
}
count=0
for i=0;count<66;i++ {
if dataIn[i]=='\n' {
count++;
}
}
count=0
for j:=0;count<66&&i<N; {
if dataIn[i]!=dataOut[j] {
fmt.Printf("%d %d %q %q\n",i,j,dataIn[i],dataOut[j])
fmt.Println("Failed at test4.")
os.Exit(0)
}
if dataIn[i]=='\n' {
count++;
}
i++;
j++
}
fmt.Println("PASS.")
}
大致思路就是,通过Command的方式,将指令传到shell去执行,然后比对输出文件内容即可。最终执行程序,输出PASS
即为通过。