Go:CLI程序开发之selpg

使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg

一、概述:

1.1 CLI背景:

CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:

  • Linux提供了cat、ls、copy等命令与操作系统交互;
  • go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
  • 容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
  • git、npm等也是大家比较熟悉的工具

1.2 任务要求:

  1. 请按文档 使用 selpg 章节要求测试你的程序
  2. 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
  3. golang 文件读写、读环境变量,请自己查 os 包
  4. “-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe
  5. 请自带测试程序,确保函数等功能正确

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设计中最主要的函数,主要进行文件输出的实现

  1. 首先使用bufio包进行输入文件fin的缓冲
	buf := bufio.NewReader(fin)
  1. 之后判断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()
	}
  1. 根据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]

  • 参数说明:
  1. -s,开始读取的页号,必须
  2. -e,结束读取的页号,必须
  3. -l,接行数,非必须,默认72,可选参数
  4. -f,按页读取,可选参数
  5. -d,后接打印机名称(这里由于没有打印机,所以采用cat命令取代,通过获取输出文件,使用Pipe将数据存储到目标文件中)

四、测试

4.1 单元测试

使用gotest进行单元测试:

  1. 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结果:
在这里插入图片描述

  1. 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结果:
在这里插入图片描述

  1. 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结果:
在这里插入图片描述

  1. 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进行压测

  1. 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 功能测试:

  1. 输入输出均为终端:

selpg1 -s1 -e1

在这里插入图片描述
结果:将输入数据打印在终端

  1. 读取文件输入,输出到终端

selpg1 -s=1 -e=1 input.txt

在这里插入图片描述

在这里插入图片描述

  1. 读取终端输入,将输出输出到文件

selpg1 -s1 -e1 >output.txt

在这里插入图片描述
在这里插入图片描述

  1. 进行文件间的传输

selpg1 -e1 -s1 input.txt >output.txt

在这里插入图片描述
在这里插入图片描述

  1. 使用-l设定每页行数

selpg1 -s1 -e2 -l10 input.txt

在这里插入图片描述

selpg1 -s1 -e1 -l10 input.txt

在这里插入图片描述

  1. 使用-f设定为按页输出
    -这里由于txt没有换页符\f,所以要使用vim在编辑模式中使用ctrl+v +012插入换页符

selpg1 -s1 -e1 -f input.txt

用vim在txt中第24行加入换页符

在这里插入图片描述
运行结果:输出到换页符为止

在这里插入图片描述

  1. 使用2>error.txt,将错误信息输出到指定文件

selpgq -s1 input.txt 2>error.txt

在这里插入图片描述
在这里插入图片描述
将错误信息输出到对应文件中

  1. -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

在这里插入图片描述
在这里插入图片描述
实现由终端到目标文件的传输

五、gitee地址

gitee仓库

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值