# selpg-cli **Repository Path**: nonli/selpg-cli ## Basic Information - **Project Name**: selpg-cli - **Description**: selpg命令行实用程序开发 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-10-10 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # CLI 命令行实用程序开发基础 - [实验环境](#实验环境) - [概述](#概述) - [开发过程](#开发过程) - [开发实践要求](#开发实践要求) - [总体结构](#总体结构) - [安装pflag和使用](#安装pflag和使用) - [安装](#安装) - [import](#import) - [使用](#使用) - [参考C源码](#参考c源码) - [定义结构体以存储参数](#定义结构体以存储参数) - [测试型开发](#测试型开发) - [绑定flag到变量上并初始化](#绑定flag到变量上并初始化) - [提供使用说明](#提供使用说明) - [检验参数](#检验参数) - [执行命令](#执行命令) - [使用的部分标准库中的函数](#使用的部分标准库中的函数) - [部分代码展示与解释](#部分代码展示与解释) - [打印输出](#打印输出) - [main函数](#main函数) - [测试](#测试) - [单元测试](#单元测试) - [功能测试](#功能测试) - [部分报错情况测试](#部分报错情况测试) - [参考](#参考) - [额外的博客](#额外的博客) ## 实验环境 操作系统:Ubuntu18.04.5LTS-amd64 编辑器:VScode、Typora ## 概述 CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。 ## 开发过程 详细代码见gitee ### 开发实践要求 使用 golang 开发 [开发 Linux 命令行实用程序](https://www.ibm.com/developerworks/cn/linux/shell/clutil/index.html) 中的 **selpg** 提示: 1. 请按文档 **使用 selpg** 章节要求测试你的程序 2. 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:[Golang之使用Flag和Pflag](https://o-my-chenjian.com/2017/09/20/Using-Flag-And-Pflag-With-Golang/) 3. golang 文件读写、读环境变量,请自己查 os 包 4. “-dXXX” 实现,请自己查 `os/exec` 库,例如案例 [Command](https://godoc.org/os/exec#example-Command),管理子进程的标准输入和输出通常使用 `io.Pipe`,具体案例见 [Pipe](https://godoc.org/io#Pipe) 5. 请自带测试程序,确保函数等功能正确 ### 总体结构 - selpg.go包含selpg的相关程序 - selpg_test.go包含相关测试函数 - selpg为可执行二进制文件 - test、out、error为测试输入输出所用的文件 ### 安装pflag和使用 #### 安装 ```shell go get github.com/spf13/pflag ``` #### import ```go import flag "github.com/spf13/pflag" ``` 代码可以使用`flag.xxx`来调用函数了; #### 使用 pflag包在此处的优势是它对参数的读取与unix标准相同; pflag包基本用法和flag包类似,由于flag包文档多,基本是参照flag包的文档来使用; [更多相关用法可见github文档](https://github.com/spf13/pflag) ### 参考C源码 [开发 Linux 命令行实用程序](https://www.ibm.com/developerworks/cn/linux/shell/clutil/index.html) 中提供了 [selpg.c](https://www.ibm.com/developerworks/cn/linux/shell/clutil/selpg.c) 可进行参考; 事实上,go与c之间的良好的移植性可以简化代码开发; 而且在许多方面,go的实现能更简单快捷,使得整体程序不像c那样繁琐,就是可能效率慢点; > 下面的代码中有许多是移植C源码的,但在pflag、输入输出相关的方面需要做大的改动。 > > 以下代码如无特别说明,均位于selpg.go中 ### 定义结构体以存储参数 ```go type selpgArgs struct { startPage int endPage int inFilename string pageLen int pageType bool printDest string } ``` - startPage:开始页码 - endPage:结束页码 - inFilename:输入的文件名 - printDest:输出的文件名 - pageLen:每页的行数 - pageType:打印的模式,"-l"按行打印,"-f"按换页符打印 ### 测试型开发 程序需要进行参数的初始化,在`Initflag`函数中完成,对其的测试函数: 函数位于selpg_test.go ```go func TestInitflag(t *testing.T) { want := selpgArgs{0, 0, "", 72, false, ""} var got selpgArgs Initflag(&got) if got != want { t.Errorf("Initflag got %v, want %v\n", got, want) } } ``` 程序需要检查参数格式,在`ProcessArgs`函数中完成,对其的测试函数: 函数位于selpg_test.go ```go func TestProcessArgs(t *testing.T) { cases := []struct { insel selpgArgs inos []string want string }{ {selpgArgs{1, 1, "test", 72, false, ""}, []string{"./selpg", "-s1", "-e1", "test"}, ""}, {selpgArgs{0, 1, "test", 72, false, ""}, []string{"./selpg", "-s0", "-e1", "test"}, "./selpg: invalid start page 0\n"}, {selpgArgs{2, 1, "test", 72, false, ""}, []string{"./selpg", "-s2", "-e1", "test"}, "./selpg: invalid end page 1\n"}, {selpgArgs{1, 0, "", 72, false, ""}, []string{"./selpg", "-s1"}, "./selpg: not enough arguments\n"}, {selpgArgs{0, 1, "test", 72, false, ""}, []string{"./selpg", "-e1", "-e1", "test"}, "./selpg: 1st arg should be -sstartPage\n"}, {selpgArgs{1, 0, "test", 72, false, ""}, []string{"./selpg", "-s1", "-s1", "test"}, "./selpg: 2nd arg should be -eendPage\n"}, } for _, c := range cases { got := ProcessArgs(&c.insel, c.inos) if got != c.want { t.Errorf("ProcessArgs(%v, %v) == %s, want %s\n", c.insel, c.inos, got, c.want) } } } ``` ### 绑定flag到变量上并初始化 pflag包中处理参数的关键; ```go func Initflag(sa *selpgArgs) { flag.IntVarP(&sa.startPage, "startPage", "s", 0, "Start page number") flag.IntVarP(&sa.endPage, "endPage", "e", 0, "End page number") flag.IntVarP(&sa.pageLen, "pageLen", "l", 72, "Line number for a page") flag.BoolVarP(&sa.pageType, "pageType", "f", false, "Determine form-feed-delimited") flag.StringVarP(&sa.printDest, "dest", "d", "", "Set printer") flag.Usage = usage flag.Parse() } ``` ### 提供使用说明 ```go func usage() { fmt.Printf("\nUSAGE: %s -sstartPage -eendPage [ -f | -llines_per_page ] [ -ddest ] [ inFilename ]\n", progname) } ``` ### 检验参数 检验必要的必要参数是否有、参数形式是否正确、参数内容是否合理(如页码不能少于1、起始页码不能大于终止页码等); ```go // ProcessArgs check args format func ProcessArgs(sa *selpgArgs, args []string) string { progname = args[0] /* check the command-line arguments for validity */ if len(args) < 3 { return fmt.Sprintf("%s: not enough arguments\n", progname) } /* handle 1st arg - start page */ if args[1][1] != 's' { return fmt.Sprintf("%s: 1st arg should be -sstartPage\n", progname) } if sa.startPage < 1 || sa.startPage > (math.MaxInt32-1) { return fmt.Sprintf("%s: invalid start page %d\n", progname, sa.startPage) } /* handle 2nd arg - end page */ index := 2 if len(args[1]) == 2 { index = 3 } if args[index][1] != 'e' { return fmt.Sprintf("%s: 2nd arg should be -eendPage\n", progname) } if sa.endPage < 1 || sa.endPage > (math.MaxInt32-1) || sa.startPage > sa.endPage { return fmt.Sprintf("%s: invalid end page %d\n", progname, sa.endPage) } var noerr string return noerr } ``` ### 执行命令 根据参数执行相应的命令; ```go func processInput(sa *selpg_args) {...} ``` #### 使用的部分标准库中的函数 [关于io.Writecloser](https://go-zh.org/pkg/io/) :WriteCloser 接口组合了基本的 Write 和 Close 方法。 > Write 将 len(p) 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数 n(0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误。 [关于exec.Cmd](https://go-zh.org/pkg/os/exec/#Cmd) :Cmd表示正在准备或运行的外部命令。 [关于Cmd.Run](https://go-zh.org/pkg/os/exec/#Cmd.Run):`func (c *Cmd) Run() error` > Run starts the specified command and waits for it to complete. [关于StdinPipe](https://go-zh.org/pkg/os/exec/#Cmd):`func (c *Cmd) StdinPipe() (io.WriteCloser, error)`,StdinPipe返回一个管道,该管道将在命令启动时连接到命令的标准输入。 [关于NewReader](https://go-zh.org/pkg/bufio/#NewReader):`func NewReader(rd io.Reader) *Reader`,返回一个新的Reader; [关于ReadString](https://go-zh.org/pkg/bufio/#Reader.ReadString):`func (b *Reader) ReadString(delim byte) (line string, err error)`,ReadString读取输入到第一次终止符发生的时候,返回的string包含从当前到终止符的内容(包括终止符)。 [关于ReadLine](https://go-zh.org/pkg/bufio/#Reader.ReadLine):ReadLine尝试返回单个行,不包括行尾的最后一个分隔符。 [关于Scanner](https://go-zh.org/pkg/bufio/#Scanner):Scanner类型提供了方便的读取数据的接口,如从换行符分隔的文本里读取每一行。 - exec包的使用是为了获得外部的程序,得以调用cat命令来启用打印设备; - 程序中采用三种读取方式`ReadString`、`ReadLine`、`Scanner` 是为了更方便的应对三种不同的读取场景,`ReadString`是为了方便读取到换页符`\f`,`ReadLine`是为了按行打印的模式运行,`Scanner`则是适应最参数缺省下的场景; #### 部分代码展示与解释 利用os/exec包和io包管理进程的输入与输出,将cat命令放在管道中,在相应时间且需要打印时调用Run函数运行。 > cat(英文全拼:concatenate)命令用于连接文件并打印到标准输出设备上。 ```go var stdin io.WriteCloser var cmd *exec.Cmd ... if sa.printDest != "" { var err1 error var err2 error cmd = exec.Command("cat") cmd.Stdout, err1 = os.OpenFile(sa.printDest, os.O_APPEND|os.O_WRONLY, os.ModeAppend) if err1 != nil { fmt.Fprintf(os.Stderr, "\n%s: fail to open file %s\n", progname, sa.printDest) os.Exit(4) } stdin, err2 = cmd.StdinPipe() if err2 != nil { fmt.Fprintf(os.Stderr, "\n%s: fail to open pipe to %s\n", progname, sa.printDest) os.Exit(5) } } else { stdin = nil } ... if sa.printDest != "" { stdin.Close() err := cmd.Run() if err != nil { fmt.Fprintf(os.Stderr, "\n%s: fail to connect to device\n", progname) os.Exit(9) } } ``` 按行打印模式下的程序读写,通过ReadLine函数来计算读取的行,并以此确定页数; ```go count := 0 for { line, _, err := reader.ReadLine() if err != io.EOF && err != nil { return "", fmt.Sprintf("\n%s: fail to read line\n", progname) } if err == io.EOF { break } if count/sa.pageLen+1 >= sa.startPage { if count/sa.pageLen+1 <= sa.endPage { // printAns(sa, string(line), stdin) ref += string(line) ref += "\n" } else { break } } count++ } ``` ### 打印输出 打印输出时需要考虑是输出到打印设备还是直接输出到终端; - 注意:这里输出到打印设备只是通过`Write函数`将信息放在管道中,直到`main`函数中`cmd.Run()`时才输出到打印设备。 ```go func printAns(sa *selpg_args, line string, stdin io.WriteCloser) { if sa.print_dest != "" { stdin.Write([]byte(line + "\n")) } else { fmt.Println(line) } } ``` ### main函数 调用各函数,并处理错误的输出、打印设备的输出; ```go func main() { var sa selpgArgs Initflag(&sa) err := ProcessArgs(&sa, os.Args) if err != "" { fmt.Fprintf(os.Stderr, err) flag.Usage() os.Exit(1) } ans, err := ProcessInput(&sa) if err != "" { fmt.Fprintf(os.Stderr, err) os.Exit(1) } else { printAns(&sa, ans, stdin) } if sa.printDest != "" { stdin.Close() err := cmd.Run() if err != nil { fmt.Fprintf(os.Stderr, "\n%s: fail to connect to device\n", progname) } } } ``` ## 测试 - **关于运行程序,文件夹中已有selpg二进制文件,可以通过`./selpg xxxx`的方式运行,也可以使用`go install`命令,之后用`selpg xxxx`的方式运行。** ### 单元测试 运行单元测试函数 ![27](http://nonli.gitee.io/picture-ol/service-computing/3/27.png) ![28](http://nonli.gitee.io/picture-ol/service-computing/3/28.png) ### 功能测试 - 实验中的test文档在Gitee上也包含,文档中包含一系列test Line帮助检验输出行数,而输出的空行是`换行符`,`换行符`以空行显示但它并不占据实际的输出行数,这点在**1**中可以明显看出,说明程序运行正确。 > **pfalg**中支持多种参数输入形式,如`-s1`、`-s 1`、`-s=1`均可以,根据题目下面测试均采用unix格式,即`-s1`形式,但其余形式均也可以正常运行。 > wc利用wc指令我们可以计算文件的Byte数、字数、或是列数 测试均参考[开发 Linux 命令行实用程序](https://www.ibm.com/developerworks/cn/linux/shell/clutil/index.html)的`使用selpg`,但部分演示为了能演示功能,修改了起始终止页码等部分参数; 1. `selpg -s1 -e1 test` 输出较长,截图只截了前后两部分 ![1](http://nonli.gitee.io/picture-ol/service-computing/3/1.png) …… ![2](http://nonli.gitee.io/picture-ol/service-computing/3/2.png) 2. `selpg -s1 -e1 < test` 输出较长,截图只截了前后两部分 ![3](http://nonli.gitee.io/picture-ol/service-computing/3/3.png) …… ![4](http://nonli.gitee.io/picture-ol/service-computing/3/4.png) 3. `cat test | selpg -s10 -e20` 输出较长,截图只截了前后两部分 ![5](http://nonli.gitee.io/picture-ol/service-computing/3/5.png) …… ![6](http://nonli.gitee.io/picture-ol/service-computing/3/6.png) 4. `selpg -s3 -e5 -l10 test >out` ![7](http://nonli.gitee.io/picture-ol/service-computing/3/7.png) out文件: ![8](http://nonli.gitee.io/picture-ol/service-computing/3/8.png) 5. `selpg -s20 -e10 test 2>error` ![9](http://nonli.gitee.io/picture-ol/service-computing/3/9.png) error文件: ![10](http://nonli.gitee.io/picture-ol/service-computing/3/10.png) 6. `selpg -s1 -e2 -l10 test >out 2>error` ![11](http://nonli.gitee.io/picture-ol/service-computing/3/11.png) out文件: ![12](http://nonli.gitee.io/picture-ol/service-computing/3/12.png) error文件:为空,因为无错误 7. `selpg -s2 -e1 test >out 2>/dev/null` ![13](http://nonli.gitee.io/picture-ol/service-computing/3/13.png) out文件:输入提示被输入到此处 ![14](http://nonli.gitee.io/picture-ol/service-computing/3/14.png) error文件:为空,报错信息被丢弃 8. `selpg -s10 -e20 test >/dev/null` ![15](http://nonli.gitee.io/picture-ol/service-computing/3/15.png) out文件:为空 9. `selpg -s1 -e1 test | wc` ![16](http://nonli.gitee.io/picture-ol/service-computing/3/16.png) 10. `selpg -s2 -e1 test 2>error | wc` ![17](http://nonli.gitee.io/picture-ol/service-computing/3/17.png) error文件: ![18](http://nonli.gitee.io/picture-ol/service-computing/3/10.png) 11. `selpg -s2 -e2 -l 16 test` ![19](http://nonli.gitee.io/picture-ol/service-computing/3/19.png) 12. `selpg -s1 -e1 -f test` 输出较长,截图只截了前后两部分 ![20](http://nonli.gitee.io/picture-ol/service-computing/3/20.png) …… ![21](http://nonli.gitee.io/picture-ol/service-computing/3/21.png) 13. `selpg -s2 -e3 -l10 -dout test` ![22](http://nonli.gitee.io/picture-ol/service-computing/3/22.png) out文件: ![23](http://nonli.gitee.io/picture-ol/service-computing/3/23.png) ### 部分报错情况测试 1. `selpg -s0 -e1 test` ![24](http://nonli.gitee.io/picture-ol/service-computing/3/24.png) 2. `selpg -s1` ![25](http://nonli.gitee.io/picture-ol/service-computing/3/25.png) 3. `selpg -s1 -e1 tes` ![26](http://nonli.gitee.io/picture-ol/service-computing/3/26.png) ## 参考 [开发 Linux 命令行实用程序](https://www.ibm.com/developerworks/cn/linux/shell/clutil/index.html) [Golang之使用Flag和Pflag](https://o-my-chenjian.com/2017/09/20/Using-Flag-And-Pflag-With-Golang/) ## 额外的博客 [godoc与go doc相关](https://blog.csdn.net/nonoli287/article/details/108965929)