文章目录
使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg
一、概述:
1.1 CLI背景:
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:
- Linux提供了cat、ls、copy等命令与操作系统交互;
- go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
- 容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
- git、npm等也是大家比较熟悉的工具
1.2 任务要求:
- 请按文档 使用 selpg 章节要求测试你的程序
- 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
- golang 文件读写、读环境变量,请自己查 os 包
- “-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe
- 请自带测试程序,确保函数等功能正确
1.3 实现简介
selpg通过pflag进行参数读取,通过os包进行文件读取,实现文件之间和文件到命令行之间输入输出的自定命令程序
二、程序实现
2.1 selpgArgs 结构体
selpgArgs结构体实现参数结构的定义:
- startPage:开始读取的页号
- endPage:结束输出的页号
- inputFile: 输入文件
- outFile: 输出文件(在-d操作时使用
- PageType:文件读入形式,false 为按行读入,true为按页读入
- pagelength: 每页长度,单位:line
type selpgArgs struct {
startPage int
endPage int
inputFile string
pageLength int
pageType bool
outFile string
}
2.2 getArgs
getArgs:调用pflag包来进行命令函数的读取,必须参数为-s(startPage), -e(endPage)
func getArgs(arg *selpgArgs) {
pflag.IntVarP(&(arg.startPage), "startPage", "s", -1, "Start Page")
pflag.IntVarP(&(arg.endPage), "endPage", "e", -1, "End Page")
pflag.IntVarP(&(arg.pageLength), "pageLength", "l", 72, "PageLength")
pflag.StringVarP(&(arg.outFile), "outFile", "d", "", "Out File")
pflag.BoolVarP(&(arg.pageType), "pageType", "f", false, "PageType")
pflag.Parse()
argLeft := pflag.Args()
if len(argLeft) > 0 {
arg.inputFile = string(argLeft[0])
} else {
arg.inputFile = ""
}
}
2.3 checkArgs
checkArgs:用于检查用户输入的参数是否符合规范,并进行对应的报错输出
- 注意使用Fprintf,不能使用Printf等命令行输出,否则会在进行传输过程中,将对应的报错信息一并存储输出
func checkArgs(arg *selpgArgs) {
if (arg.startPage == -1) || (arg.endPage == -1) {
fmt.Fprintf(os.Stderr, "\nCommand Error! StartPage and EndPage is NULL! \n")
os.Exit(2)
} else if (arg.startPage <= 0) || (arg.endPage <= 0) {
fmt.Fprintf(os.Stderr, "\nStartPage or EndPage is negative! \n")
os.Exit(3)
} else if arg.startPage > arg.endPage {
fmt.Fprintf(os.Stderr, "\nCommand Error! StartPage is larger than EndPage!\n")
os.Exit(4)
} else if (arg.pageType == true) && (arg.pageLength != 72) {
fmt.Fprintf(os.Stderr, "\nCommand Error! Both use -l and -f!\n")
os.Exit(5)
} else if arg.pageLength <= 0 {
fmt.Fprintf(os.Stderr, "\nCommand Error! PageLength less than 1! \n")
os.Exit(6)
}
}
2.4 process
process:进行文件的读入,并调用output函数进行对应输出
- 首先判断arg的inputfile是否为空,决定输入端为命令行终端读入或者是文件读入
- 最后调用output函数进行对应数据的输出
func process(arg *selpgArgs) {
var fin *os.File
if arg.inputFile == "" {
fin = os.Stdin
} else {
_, errFileExits := os.Stat(arg.inputFile)
if os.IsNotExist(errFileExits) {
fmt.Fprintf(os.Stderr, "\n[Error]: input file \"%s\" does not exist\n", arg.inputFile)
os.Exit(7)
}
fin, _ = os.Open(arg.inputFile)
}
output(fin, arg)
}
2.5 output
output:是selpg设计中最主要的函数,主要进行文件输出的实现
- 首先使用bufio包进行输入文件fin的缓冲
buf := bufio.NewReader(fin)
- 之后判断outfile是否为空,如果为空则使用终端作为输出,否则为-d的调用(这里应该使用调用打印机打印,执行exec.Command执行lp命令调用打印机来打印对应文件,但是个人没有打印机,而且太菜鸡,之前配环境好像更改了默认设置,使用cups-pdf过程中存在bug,放(zi)弃(bi)了,所以选用执行cat命令,并通过Pipe实现安全的数据传输,将对应结果直接输出到output文件中)。
var err error
var fout io.WriteCloser
cmd := &exec.Cmd{}
if len(arg.outFile) == 0 {
fout = os.Stdout
} else {
//cmd = exec.Command("lp", " -d ", arg.outFile)
cmd = exec.Command("cat")
cmd.Stdout, err = os.OpenFile(arg.outFile, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
fmt.Fprintf(os.Stderr, "\ncould not open file %s\n", arg.outFile)
os.Exit(7)
}
fout, err = cmd.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "\ncould not open pipe to \"lp -d%s\"\n", arg.outFile)
os.Exit(8)
}
cmd.Start()
defer fout.Close()
}
- 根据pageType决定页的读取方式为按行读取或者按页读取(
这里由于txt文件没有换页符,所以要使用vim编辑模式下使用ctrl+v +012加入换页符),读取输出使用io.WriteCloser来实现
if !arg.pageType {
lineCount := 0
pageCount := 1
for {
line, err := buf.ReadString('\n')
if err != nil {
break
}
lineCount++
if lineCount > arg.pageLength {
pageCount++
lineCount = 1
}
if (pageCount >= arg.startPage) && (pageCount <= arg.endPage) {
_, err := fout.Write([]byte(line))
if err != nil {
fmt.Fprintf(os.Stderr, "%sprint over", arg.outFile)
os.Exit(8)
}
}
}
} else {
pageCount := 1
for {
//txt don't have change page symbol
//use vim , in editer style use ctrl+v + 012 to add change page symbol
page, err := buf.ReadString('\f')
if err != nil {
break
}
if (pageCount >= arg.startPage) && (pageCount <= arg.endPage) {
_, err := fout.Write([]byte(page))
if err != nil {
os.Exit(5)
}
}
pageCount++
}
}
2.6 Main
main:主程序
func main() {
var args selpgArgs
getArgs(&args)
checkArgs(&args)
process(&args)
}
三、使用方式
3.1 下载拓展包
go get github.com/spf13/pflag
3.2 加载selpg包
1.在GOPATH/src路径下新建selpg1文件夹
2. 将selpg1.go文件加入到文件夹中
3. 执行go install selpg1
3.3 执行selpg
selpg1 -s[startPage] -e[endPage] [-l[lineLength]] [-d[outFile]] [inputFile] [>outputFile] [2>errorFile]
- 参数说明:
- -s,开始读取的页号,必须
- -e,结束读取的页号,必须
- -l,接行数,非必须,默认72,可选参数
- -f,按页读取,可选参数
- -d,后接打印机名称(这里由于没有打印机,所以采用cat命令取代,通过获取输出文件,使用Pipe将数据存储到目标文件中)
四、测试
4.1 单元测试
使用gotest进行单元测试:
- TestGetArgs:检测getArgs函数是否能正确运行,使用gotest包自带的run test进行测试
func TestGetArgs(t *testing.T) {
var args selpgArgs
getArgs(&args)
if args.startPage != -1 {
t.Errorf("-s expected -1 but got %d", args.startPage)
}
if args.endPage != -1 {
t.Errorf("-e expected -1 but got %d", args.endPage)
}
if args.pageLength != 72 {
t.Errorf("-l expected 72 but got %d", args.pageLength)
}
if args.pageType != false {
t.Errorf("-f expected false but got %v", args.pageType)
}
if args.outFile != "" {
t.Errorf("-d expected but got %s", args.outFile)
}
if args.inputFile != "" {
t.Errorf("inputFile expected but got %s", args.inputFile)
}
}
run test结果:
- TestCheckFlags:检测检测参数函数的正确性
func TestCheckFlags(t *testing.T) {
var args selpgArgs
args.pageLength = 72
args.pageType = false
args.outFile = ""
args.inputFile = ""
args.startPage = 1
args.endPage = 2
checkArgs(&args)
}
run test结果:
- TestProcess:测试Process是否能够正确读入inputFile
func TestProcess(t *testing.T) {
var args selpgArgs
args.pageLength = 72
args.pageType = false
args.outFile = ""
args.inputFile = "input.txt"
args.startPage = 1
args.endPage = 1
process(&args)
}
run test结果:
- TestOutput:测试output的正确运行
func TestOutput(t *testing.T) {
var args selpgArgs
args.pageLength = 72
args.pageType = false
args.outFile = ""
args.inputFile = "input.txt"
args.startPage = 1
args.endPage = 1
var fin *os.File
fin, _ = os.Open(args.inputFile)
output(fin, &args)
}
run test:
4.2 压力测试
使用benchmark进行压测
- BenchmarkOutput:测试从文件中读取,并输出到终端的速率
func BenchmarkOutput1(b *testing.B) {
var args selpgArgs
args.pageLength = 72
args.pageType = false
args.outFile = ""
args.inputFile = "input.txt"
args.startPage = 1
args.endPage = 1
var fin *os.File
fin, _ = os.Open(args.inputFile)
for i := 0; i < b.N; i++ {
output(fin, &args)
}
}
压测结果:运行了642072次,每次运行时间为2411ns,每次运行分配的内存4448B,每次操作申请了两次内存,总用时1.574s
2. BenchmarkOutput2:测试-d操作使用Pipe进行数据从文件到文件传输速率
func BenchmarkOutput2(b *testing.B) {
var args selpgArgs
args.pageLength = 72
args.pageType = false
args.outFile = "/home/sunhaonan/gopath/output.txt"
args.inputFile = "input.txt"
args.startPage = 1
args.endPage = 1
var fin *os.File
fin, _ = os.Open(args.inputFile)
for i := 0; i < b.N; i++ {
output(fin, &args)
}
}
压测结果:一共执行了1314次测试,每次测试运行时间为1064320ns,每次运行分配的内存为20596B,每次操作执行了67次内存分配;
4.3 功能测试:
- 输入输出均为终端:
selpg1 -s1 -e1
结果:将输入数据打印在终端
- 读取文件输入,输出到终端
selpg1 -s=1 -e=1 input.txt
- 读取终端输入,将输出输出到文件
selpg1 -s1 -e1 >output.txt
- 进行文件间的传输
selpg1 -e1 -s1 input.txt >output.txt
- 使用-l设定每页行数
selpg1 -s1 -e2 -l10 input.txt
selpg1 -s1 -e1 -l10 input.txt
- 使用-f设定为按页输出
-这里由于txt没有换页符\f,所以要使用vim在编辑模式中使用ctrl+v +012插入换页符
selpg1 -s1 -e1 -f input.txt
用vim在txt中第24行加入换页符
运行结果:输出到换页符为止
- 使用2>error.txt,将错误信息输出到指定文件
selpgq -s1 input.txt 2>error.txt
将错误信息输出到对应文件中
- -dXXX
selpg1 -s1 -e1 -doutput.txt input.txt 2>error.txt
运行一次:
运行两次:
每次运行会将input.txt 加到output.txt后面;
- 原有的-d命令应为调用打印机,打印输入文件,但是由于没有打印机,而且因为系统环境,cups-pdf虚拟打印机不能正常使用(太菜了太菜了),所以选择了使用课件中推荐的Pipe来实现,通过Pipe实现了安全的数据传输,并通过os/exec库的command命令调用cat,找到outFile作为输出,实现将输入数据传输到目标文件的功能来代替打印机
测试使用终端输入的-d命令
selpg1 -s1 -e1 -doutput.txt 2>error.txt
实现由终端到目标文件的传输