18342138 郑卓民
本次作业gitee仓库链接(完整代码)
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:
Linux提供了cat、ls、copy等命令与操作系统交互;go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;git、npm等也是大家比较熟悉的工具。尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg
提示:
请按文档 使用 selpg 章节要求测试你的程序请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflaggolang 文件读写、读环境变量,请自己查 os 包“-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe请自带测试程序,确保函数等功能正确参考开发 Linux 命令行实用程序
selpg 允许用户指定从输入文本抽取的页的范围,这些输入文本可以来自文件或另一个进程。selpg 是以在 Linux 中创建命令的事实上的约定为模型创建的,这些约定包括:
独立工作在命令管道中作为组件工作(通过读取标准输入或文件名参数,以及写至标准输出和标准错误)接受修改其行为的命令行选项该实用程序从标准输入或从作为命令行参数给出的文件名读取文本输入。它允许用户指定来自该输入并随后将被输出的页面范围。例如,如果输入含有 100 页,则用户可指定只打印第 35 至 65 页。这种特性有实际价值,因为在打印机上打印选定的页面避免了浪费纸张。另一个示例是,原始文件很大而且以前已打印过,但某些页面由于打印机卡住或其它原因而没有被正确打印。在这样的情况下,则可用该工具来只打印需要打印的页面。
根据上面基础知识的程序运行逻辑描述,可以大概知道实现该程序需要三个主要的函数:
main:解析命令参数的入口函数processArgs:处理参数,错误检测processInput:根据命令进行操作Selpg的使用命令为Usage: selpg [-s startPage] [-e endPage] [-l linesPerPage | -f] [-d printDest] [filename]
参数分别是:开始页码-s、结束页码-e、(自定页长-l 或 遇换页符换页-f)、输出地址-d、输入文件名。
其中开始页码和结束页码是必须的。
将上述命令的参数列表转化为一个数据结构来存储。
type selpgArgs struct { startPage int endPage int pageLen int pageType bool inFileName string printDest string }需要安装pflag包:
使用命令:go get github.com/spf13/pflag
此部分主要利用了pflag包,在上面的实验准备中,已经安装了该包。
主要利用了pflag中的IntVarP、StringVarP、BoolVarP、Parse、Args函数。
通过调用pflag.Args()来获得未定义但输入了的参数如文件名。
因此获取输入参数的函数可实现如下:
func getArgs(args *selpgArgs) { // 获取-s -e -l -f -d 参数 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() // 获取 filename 参数 filename := pflag.Args() if len(filename) > 0 { args.inFileName = string(filename[0]) } else { args.inFileName = "" } }主要的规则如下:
开始页和结束页是必须要有的参数。开始页和结束页不得小于等于零,且开始页不能大于结束页-l和-f参数不能同时输入自定页长不得小于等于0 func checkArgs(args *selpgArgs) { // 判断输入参数合法性 if (args.startPage == -1) || (args.endPage == -1) { fmt.Fprintf(os.Stderr, "[Error]The startPage and endPage can't be empty!\n") os.Exit(1) } else if (args.startPage <= 0) || (args.endPage <= 0) { fmt.Fprintf(os.Stderr, "[Error]The startPage and endPage can't be less than 1!\n") os.Exit(2) } else if args.startPage > args.endPage { fmt.Fprintf(os.Stderr, "[Error]The startPage can't be bigger than the endPage!\n") os.Exit(3) } else if (args.pageType == true) && (args.pageLen != 72) { fmt.Fprintf(os.Stderr, "[Error]The command -l and -f are exclusive!\n") os.Exit(4) } else if args.pageLen <= 0 { fmt.Fprintf(os.Stderr, "[Error]The pageLen can't be less than 1 !\n") os.Exit(5) } else { // 输入参数均合法,判断是选择了-l还是-f,并输入参数列表。 pageType := "page length." if args.pageType == true { pageType = "The end sign /f." } fmt.Printf("[ArgsStart]\n") fmt.Printf("startPage: %d\nendPage: %d\ninputFile: %s\npageLength: %d\npageType: %s\nprintDestation: %s\n[ArgsEnd]\n", args.startPage, args.endPage, args.inFileName, args.pageLen, pageType, args.printDest) } }完成处理参数任务后,可以根据输入参数执行任务。
首先根据有无输入的filename参数,判断是从标准输入中获取还是从对应文件名的文件中读取。
打开文件的过程要判断是否打开成功。
最后判断是否有-d参数,以此决定是直接在标准输出中输出还是在特定的位置输出。
如果包含了-d参数,则需要利用到了os/exec包。exec包执行外部命令,它将os.StartProcess进行包装使得它更容易映射到stdin和stdout,并且利用pipe连接i/o。
func excuteCMD(args *selpgArgs) { var inp *os.File if args.inFileName == "" { // 从标准输入中读取 inp = os.Stdin } else { // 从文件中读取 // 检测文件是否可读取 checkFileAccess(args.inFileName) var err error inp, err = os.Open(args.inFileName) // 检测文件是否打开成功 checkError(err, "input file") } if len(args.printDest) == 0 { // 输出到标准输出 output(os.Stdout, inp, args.startPage, args.endPage, args.pageLen, args.pageType) } else { // 输出到文件中 output(getDesio(args.printDest), inp, args.startPage, args.endPage, args.pageLen, args.pageType) } } func checkError(err error, object string) { if err != nil { fmt.Fprintf(os.Stderr, "[Error]%s:", object) panic(err) } } func checkFileAccess(filename string) { _, errFileExits := os.Stat(filename) // 检查是否不存在此文件 if os.IsNotExist(errFileExits) { fmt.Fprintf(os.Stderr, "[Error]: input file \"%s\" does not exist\n", filename) os.Exit(6) } } func getDesio(printDest string) io.WriteCloser { cmd := exec.Command("lp", "-d"+printDest) // 将命令行的输入管道stdinpipe获取到的指针赋给fout fout, err := cmd.StdinPipe() checkError(err, "StdinPipe") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr errStart := cmd.Run() checkError(errStart, "CMD run") // 将fout返回给output函数作为输出地址参数 return fout }output函数按参数的要求读取特定内容并输出到指定地方中。
此处利用了bufio包,该包实现了带缓存的IO操作。
func output(fout interface{}, inp *os.File, pageStart int, pageEnd int, pageLen int, pageType bool) { lineCount := 0 pageCount := 1 buf := bufio.NewReader(inp) for true { var line string var err error if pageType { // -f的情况 line, err = buf.ReadString('\f') pageCount++ } else { // -l的情况 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 // 通过类型断言,判断fout的类型,知道应该调用哪个函数 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, "[Error]:fout type error.") os.Exit(7) } // 检测输出有无出错 checkError(outputErr, "Error happend when output the pages.") } } if pageCount < pageStart { // 开始页太大 fmt.Fprintf(os.Stderr, "[Error]: startPage (%d) greater than total pages (%d)\n", pageStart, pageCount) os.Exit(8) } else if pageCount < pageEnd { // 结束页太大 fmt.Fprintf(os.Stderr, "[Error]: endPage (%d) greater than total pages (%d)\n", pageEnd, pageCount) os.Exit(9) } }在工作区中创建myselpg文件夹并在此文件夹下创建go文件与输入txt文件
其中input_file.txt中的内容为:
注意:^L 是ascii 0x0C ‘\f’, 换页控制符。使用vim编辑器ctrl+L输入。
可以编写一些简单的测试来查看是否达到预期的效果,下面展示一个简单的testing,可创建更多的testing来验证功能。
由于没有打印机,该命令正常执行直到发现没有打印机为止。