Golang之CLI简单应用-selpg
具体要求与说明:参考开发 Linux 命令行实用程序,使用 Golang 语言构建 selpg 应用程序。
gitee代码
selpg命令介绍
selpg实用程序的名称selpg代表SELect PaGes。selpg允许用户指定从输入文本抽取的页的范围,这些输入文本可以来自文件或另一个进程。该实用程序从标准输入或从作为命令行参数给出的文件名读取文本输入。它允许用户指定来自该输入并随后将被输出的页面范围。例如,如果输入含有 100 页,则用户可指定只打印第 35 至 65 页。这种特性有实际价值,因为在打印机上打印选定的页面避免了浪费纸张。另一个示例是,原始文件很大而且以前已打印过,但某些页面由于打印机卡住或其它原因而没有被正确打印。在这样的情况下,则可用该工具来只打印需要打印的页面
实现说明
结构体设计
type argsList struct {
s, e, l int
f bool
d, inputFile string
}
其中参数s表示起始页,e表示结束页。参数l和参数f分别代表两种输入文本换页方式,不能共存,其中l表示固定的每页行数,即到达固定的行数后会自动换页重新计算。f表示通过ASCII 换页字符(十进制数值为 12,在 C 中用“\f”表示)定界,遇到换页符就换页。参数d表示打印的目的地,inputFile是一个non-flag参数,表示该命令的输入文本。
参数处理
初始化参数变量并调用
func initFlags(args *argsList, nArgs *[]string) {
flag.IntVarP(&args.s, "startPage", "s", -1, "The start page")
flag.IntVarP(&args.e, "endPage", "e", -1, "The end page")
flag.IntVarP(&args.l, "lineOfPage", "l", 72, "The length of a page")
flag.StringVarP(&args.d, "destination", "d", "", "The destnatioon of printing")
flag.BoolVarP(&args.f, "endOfPage", "f", false, "Defind the end symbol of a page")
flag.Parse()
*nArgs = flag.Args()
}
鉴于需要满足 Unix 命令行规范,我们使用pflag
来实现selpg
命令的参数显示和处理。设置好每个参数的描述和缺省值之后,调用 flag.Parse()
函数来解析用户参数。
检查参数是否合理
func testFlags(args *argsList, nArgs []string) {
if (args.s == -1) && (args.e == -1) && (args.l == 72) && (args.d == "") &&
(args.f == false) && (args.inputFile == "") {
fmt.Fprintf(os.Stderr, "selpg -s startPage -e endPage [-l lineOfPage | -f] [-d destination] [<input_file] [>output_file] [2>error_file]\n")
os.Exit(1)
}
//判断nArgs
if len(nArgs) == 1 {
args.inputFile = nArgs[0]
} else if len(nArgs) == 0 {
args.inputFile = ""
} else {
fmt.Fprintf(os.Stderr, "Too many non-flag arguments!\n")
os.Exit(1)
}
//判断args.s 和 args.e
if (args.s == -1) || (args.e == -1) {
fmt.Fprintf(os.Stderr, "The flag -s and -e can't be empty!\n")
os.Exit(1)
} else if (args.s < 0) || (args.e < 0) {
fmt.Fprintf(os.Stderr, "The value of flag -s and -e must be positive!\n")
os.Exit(1)
} else if args.s > args.e {
fmt.Fprintf(os.Stderr, "The value of flag -s can't be bigger than the value of flag -e!\n")
os.Exit(1)
}
//判断args.f 和 args.l
if (args.f == true) && (args.l != 72) {
fmt.Fprintf(os.Stderr, "The flag -f and -l can't be used at the same time!\n")
os.Exit(1)
} else if args.l <= 0 {
fmt.Fprintf(os.Stderr, "The value of flag -l must be positive!\n")
os.Exit(1)
}
}
我们检验了所有标识的合法性,包括:
- 没有参数的时候显示帮助文档。
- non-flag参数的个数是否大于1。
- 必需的
-s
和-e
参数是否被设置。 - 各个参数的值是否合法。
-l
和-f
的参数互斥,即通过行数分页和通过分页符分页,是否被同时设置。- 参数数量是否过多。
输入输出方式处理
在对参数标识进行检验发现没有问题,我们就需要对拿到的参数进行对应的执行处理以达到selpg
命令的效果。
func proecssArgs(args *argsList) {
var file *os.File
var err error
//读取输入文件,没有输入文件用控制台输入
if args.inputFile != "" {
file, err = os.Open(args.inputFile)
defer file.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open file!\n")
os.Exit(2)
}
} else {
file = os.Stdin
}
//判断是打印机输出还是屏幕上输出
if len(args.d) == 0 {
output(os.Stdout, file, args)
} else {
command := exec.Command("lp", "-d"+args.d)
var outFile io.WriteCloser
outFile, err = command.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open pipe!\n")
os.Exit(2)
}
output(outFile, file, args)
}
}
输入处理
根据args.inputFile
来判断是否有提前写好的输入文件。如果有,则读取文件上的内容到该命令的输入流中,否则就需要在终端上输入来写到该命令的输入流中。
//读取输入文件,没有输入文件用控制台输入
if args.inputFile != "" {
file, err = os.Open(args.inputFile)
defer file.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open file!\n")
os.Exit(2)
}
} else {
file = os.Stdin
}
输出处理
该命令有两种输出方式:打印机上输出和终端上输出(输出重定向到文件上不用处理)。通过args.d
参数来判断输出到哪里,如果没有-d参数会直接在终端中输出,否则需要通过管道输出到指定的打印机上。注意到,我们通过os/exec
下的 Command
方法,执行了lp
命令,实现与打印机的通讯。
//判断是打印机输出还是终端上输出
if len(args.d) == 0 {
output(os.Stdout, file, args)
} else {
command := exec.Command("lp", "-d"+args.d)
var outFile io.WriteCloser
outFile, err = command.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open pipe!\n")
os.Exit(2)
}
output(outFile, file, args)
}
}
输出内容处理
在对输入输出的方式进行处理之后,我们就可以根据参数值定义具体的输出内容了。
func output(out interface{}, file *os.File, args *argsList) {
//注意pageNum的初始值,-f 和 -l 两种方式不一样
var pageNum int
if args.f {
pageNum = 0
} else {
pageNum = 1
}
lineNum := 0
buffer := bufio.NewReader(file)
for {
var pageBuf string
var err error
if args.f {
pageBuf, err = buffer.ReadString('\f')
pageNum++
} else {
pageBuf, err = buffer.ReadString('\n')
lineNum++
if lineNum > args.l {
pageNum++
lineNum = 1
}
}
if (err != nil) && (err != io.EOF) {
fmt.Fprintf(os.Stderr, "Failed to read file!\n")
os.Exit(3)
}
if (pageNum >= args.s) && (pageNum <= args.e) {
if len(args.d) == 0 {
res, ok := out.(*os.File)
if ok {
fmt.Fprintf(res, "%s", pageBuf)
} else {
fmt.Fprintf(os.Stderr, "Failed to print text!\n")
os.Exit(4)
}
} else {
res, ok := out.(io.WriteCloser)
if ok {
res.Write([]byte(pageBuf))
} else {
fmt.Fprintf(os.Stderr, "Failed to print text with pipe\n")
os.Exit(4)
}
}
}
if err == io.EOF {
break
}
}
if pageNum < args.s {
fmt.Fprintf(os.Stderr, "startPage(%d) bigger than total pages(%d), no output written!\n", args.s, pageNum)
os.Exit(5)
} else if pageNum < args.e {
fmt.Fprintf(os.Stderr, "endPage(%d) bigger than total pages(%d), error!\n", args.e, pageNum)
os.Exit(5)
}
}
该函数主要围绕-f和-l的两种读取输入流的分页方式进行不同的处理。-f表示按分页符分页,-l表示按固定行数分页。由于我们输出方式有两种,所以我们需要使用接口interface
来表示输出的方式。首先我们把输入流放到一个reader
缓冲区中,再按照不同的方式持续读取缓冲区的内容,同时更新行计数器和页计数器,并将符合起始页和结束页范围之内的内容输出到打印机或终端上去。另外,用户输入的起始页号和终止页号可能超过实际页数,需要对用户输入再次进行判断。
使用说明
安装
通过执行以下命令,完成 selpg
的安装。
go get github.com/Joyo_zhou/selpg
运行
selpg
的运行参数说明如下。
selpg -sNumber -eNumber [-lNumber | -f] [-dDestination] [[<]file_name] [>output_file] [2>error_file]
- -sNumber 表示起始页号(从 1 开始),是必需参数,例如 “-s4” 表示打印将从第 4 页初开始。
- -eNumber 表明终止页号(从 1 开始),是必需参数,例如 “-e5” 表示打印将在第 5 页末结束。
- -lNumber 和 -f 为互斥的可选参数。-lNumber 表示按行数分页,是缺省设置。-f 表示按分页符分页。
- -dDestination 表明结果将输出至打印机,是可选参数。
- file_name 表明输入类型为文件输入,是可选参数,输入类型的缺省值是标准输入。
- >output_file是可选参数,可将输出重定向到输出文件(output_file)中。
- 2>error_file是可选参数,可将错误信息重定向到错误文件(error_file)中。
示例
我们在输入文件 input.txt
中写下从test1到test200总共200个字符串,每个字符串占一行。
- 示例1
运行结果为:selpg -s2 -e4 -l3 input.txt
- 示例2
运行结果为:selpg -s2 -e4 -l3 < input.txt
- 示例3
运行结果为:selpg -s2 -e4 -l3 < input.txt >output.txt
- 示例4
运行结果为:selpg -s2 -e4 -l3 < input.txt >output.txt 2>error.txt
- 示例5
运行结果为:selpg -s2 -e4 -l3 | selpg -s1 -e1 -l4
- 示例6
运行结果为:(电脑未连接打印机,所以不知道输出到哪里了)selpg -s2 -e4 -l3 -dlp1 input.txt
单元测试
对initFlags
和testFlags
函数进行单元测试编写,代码如下:
func TestInitFlags(t *testing.T) {
var args argsList
var nArgs []string
initFlags(&args, &nArgs)
if args.s != -1 {
t.Errorf("-s expected -1 but got %d", args.s)
}
if args.e != -1 {
t.Errorf("-e expected -1 but got %d", args.e)
}
if args.l != 72 {
t.Errorf("-l expected 72 but got %d", args.l)
}
if args.f != false {
t.Errorf("-f expected false but got %v", args.f)
}
if args.d != "" {
t.Errorf("-d expected but got %s", args.d)
}
if args.inputFile != "" {
t.Errorf("inputFile expected but got %s", args.inputFile)
}
}
func TestTestFlags(t *testing.T) {
var args argsList
var nArgs []string
args.l = 72
args.f = false
args.d = ""
args.inputFile = ""
args.s = 1
args.e = 3
testFlags(&args, nArgs)
}
运行go test -v
来执行单元测试,结果如下:
表示通过单元测试。