服务计算 - 3 | CLI 命令行实用程序开发基础
服务计算 - 3 | CLI 命令行实用程序开发基础
概述
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。Linux提供了cat、ls、copy等命令与操作系统交互;go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;git、npm等都是大家比较熟悉的工具。尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
基本要求
参阅 Selpg命令行程序设计逻辑,实现一个selpg页选择程序,满足selpg设计要求。
程序实现
流程分析
程序按照读取参数、判断参数是否合规、读取文件、确定输出位置并输出顺序执行。当发现错误时抛出错误并终止流程。
代码实现
selpg所需参数有必须的开始页码-s以及结束页码-e,可选的输入文件名、自定页长-l、遇换页符换页-f和输出地址。其中自定页长和遇换页符换页两个选项是互斥的,不能同时使用。
定义保存参数数据的结构体
type selpgArgs struct { startPage int endPage int inFileName string pageLen int pageType bool printDest string }
输入参数使用 github.com/spf13/pflag 包提供的pflag进行处理,pflag包于flag用法类似,但pflag相对于flag能够更好地满足 Unix 命令行规范。参考:Golang pflag
func getArgs(args *selpgArgs) { pflag.IntVarP(&(args.startPage), "startPage", "s", -1, "Define startPage") pflag.IntVarP(&(args.endPage), "endPage", "e", -1, "Define endPage") pflag.IntVarP(&(args.pageLen), "pageLength", "l", 72, "Define pageLength") pflag.StringVarP(&(args.printDest), "printDest", "d", "", "Define printDest") pflag.BoolVarP(&(args.pageType), "pageType", "f", false, "Define pageType") pflag.Parse() argLeft := pflag.Args() if len(argLeft) > 0 { args.inFileName = string(argLeft[0]) } else { args.inFileName = "" } }
- pflag包中的函数XXXVarP(XXX为Int、String、Bool等可选类型)可以取出命令行参数名称shorthand的参数的值,value指定*p的默认值,name为自定的名称,usage为自定的该参数的描述。该函数无返回值。
func XXXVarP(p *XXX, name, shorthand string, value XXX, usage string)
- 获得flag参数后,要用pflag.Parse()函数才能把参数解析出来。这里还有一个东西,解析完指定的参数后,可以通过调用argLeft := pflag.Args()来获得未定义但输入了的参数如文件名。
命令行参数获取之后,首先要进行参数检查以尽量避免参数谬误。出现错误时输出问题并正常结束程序。参数正确则将各个参数值输出到屏幕上。
func checkArgs(args *selpgArgs) { if (args.startPage == -1) || (args.endPage == -1) { fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be empty! Please check your command!\n") os.Exit(2) } else if (args.startPage <= 0) || (args.endPage <= 0) { fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be negative! Please check your command!\n") os.Exit(3) } else if args.startPage > args.endPage { fmt.Fprintf(os.Stderr, "\n[Error]The startPage can't be bigger than the endPage! Please check your command!\n") os.Exit(4) } else if (args.pageType == true) && (args.pageLen != 72) { fmt.Fprintf(os.Stderr, "\n[Error]The command -l and -f are exclusive, you can't use them together!\n") os.Exit(5) } else if args.pageLen <= 0 { fmt.Fprintf(os.Stderr, "\n[Error]The pageLen can't be less than 1 !\n") os.Exit(6) } else { pageType := "page length." if args.pageType == true { pageType = "The end sign /f." } fmt.Printf("\n[ArgsStart]\n") fmt.Printf("startPage: %d\nendPage: %d\ninputFile: %s\npageLength: %d\npageType: %s\nprintDestation: %s\n[ArgsEnd]", args.startPage, args.endPage, args.inFileName, args.pageLen, pageType, args.printDest) } }
- 在这个函数中,首先检查了开始页args.startPage和结束页args.endPage是否被赋值,然后检查开始页args.startPage和结束页args.endPage是否为正数,接下来检查开始页args.startPage是否大于结束页args.endPage,然后检查自定页长-l和遇换页符换页-f是否同时出现,最后判断当自定页长-l出现时args.pageLen是否小于1。遇到不合规的地方正常结束程序,全部合规则输出得到的参数。
参数检查结束之后,程序开始调用excuteCMD函数执行命令。
func checkError(err error, object string) { if err != nil { fmt.Fprintf(os.Stderr, "\n[Error]%s:", object) panic(err) } } func excuteCMD(args *selpgArgs) { var fin *os.File if args.inFileName == "" { fin = os.Stdin } else { checkFileAccess(args.inFileName) var err error fin, err = os.Open(args.inFileName) checkError(err, "File input") } if len(args.printDest) == 0 { output2Des(os.Stdout, fin, args.startPage, args.endPage, args.pageLen, args.pageType) } else { output2Des(cmdExec(args.printDest), fin, args.startPage, args.endPage, args.pageLen, args.pageType) } } func checkFileAccess(filename string) { _, errFileExits := os.Stat(filename) if os.IsNotExist(errFileExits) { fmt.Fprintf(os.Stderr, "\n[Error]: input file \"%s\" does not exist\n", filename) os.Exit(7) } }
- 第一步检查输入。如果没有给定文件名,则从标准输入中获取;如果给出读取的文件名,则调用函数checkFileAccess检查文件是否存在。
- 第二步是打开文件,使用函数checkError检查是否出现错误。如果打开出错则输出错误并抛出恐慌。
- 第三步判断是否有-d参数。如果没有-d参数,选择的页直接从os.Stdout标准输出中输出。如果-d存在,则从指定的打印通道中输出。
在-d参数存在时,涉及到了os/exec包的使用,这里可以参考golang中os/exec包用法。
func cmdExec(printDest string) io.WriteCloser { cmd := exec.Command("lp", "-d"+printDest) fout, err := cmd.StdinPipe() checkError(err, "StdinPipe") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr errStart := cmd.Run() checkError(errStart, "CMD run") return fout }
- 其中printDest就是获取的打印地址,将命令行的输入管道cmd.StdinPipe()获取的指针赋值给fout,然后再将fout返回给output2Des函数中的作为输出位置参数的输入,最后在output2Des中将需要的页输出到fout。
输出函数output2Des将输入的文件,按页码要求读取并输出到fout中。
func output2Des(fout interface{}, fin *os.File, pageStart int, pageEnd int, pageLen int, pageType bool) { lineCount := 0 pageCount := 1 buf := bufio.NewReader(fin) for true { var line string var err error if pageType { //If the command argument is -f line, err = buf.ReadString('\f') pageCount++ } else { //If the command argument is -lnumber line, err = buf.ReadString('\n') lineCount++ if lineCount > pageLen { pageCount++ lineCount = 1 } } if err == io.EOF { break } checkError(err, "file read in") if (pageCount >= pageStart) && (pageCount <= pageEnd) { var outputErr error if stdOutput, ok := fout.(*os.File); ok { _, outputErr = fmt.Fprintf(stdOutput, "%s", line) } else if pipeOutput, ok := fout.(io.WriteCloser); ok { _, outputErr = pipeOutput.Write([]byte(line)) } else { fmt.Fprintf(os.Stderr, "\n[Error]:fout type error. ") os.Exit(8) } checkError(outputErr, "Error happend when output the pages.") } } if pageCount < pageStart { fmt.Fprintf(os.Stderr, "\n[Error]: startPage (%d) greater than total pages (%d), no output written\n", pageStart, pageCount) os.Exit(9) } else if pageCount < pageEnd { fmt.Fprintf(os.Stderr, "\n[Error]: endPage (%d) greater than total pages (%d), less output than expected\n", pageEnd, pageCount) os.Exit(10) } }
- bufio包实现了带缓存的 I/O 操作,在文件读取中十分方便。具体使用参见Golang学习 - bufio 包。这里使用buf.ReadString(symbol),每次读取字符串直到遇到字符symbol为止。
- 由于fout存在两种输入-io.Stdout标准输出作为输入、cmd.StdinPipe()管道作为输入。所以使用空接口interface{}作为fout的类型,借助类型断言stdOutput, ok := fout.(*os.File)和pipeOutput, ok := fout.(io.WriteCloser)来判断fout具体类型并调用相应函数。
程序测试
- 按文档 使用 selpg 章节要求测试该程序
- 测试文档input_file.txt包含两个换页符
./selpg -s1 -e1 input_file.txt
./selpg -s1 -e1 input_file.txt
./selpg -s1 -e2 input_file.txt >output_file
./selpg -s1 -e4 input_file.txt 2>error_file
./selpg -s1 -e3 input_file.txt >output_file 2>error_file
./selpg -s1 -e2 -f input_file.txt
./selpg -s1 -e1 -dCups-PDF selpg.go
- 借助虚拟打印机插件Cups-PDF,打印输出结果是代码的前72行,所以结果正确。