前言
实验的详细内容请参考老师的课程网站:传送门
本次实验作业的项目在我的GitHub上:传送门
开发简单 CLI 程序——selpg
CLI基础
简介
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。Linux提供了cat、ls、copy等命令与操作系统交互;go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;git、npm等都是大家比较熟悉的工具。尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
Go的os, flag包
使用os包可以执行各种打开文件的操作,还能简单获取命令行参数
flag包可以解析各种标记参数, 形如如-h, -s, -d的参数,参考资料
为了更好满足nix 命令行规范,使用 pflag 替代 flag库,实际上两个包的使用方法差不多,只不过pflag中多了使用shorthand来代替长命令参数的选项,及类似-h 和 --help的区别
使用方法请参考:Golang之使用Flag和Pflag
以下是简单的使用例子:
package main
import (
"flag"
"fmt"
)
func main() {
var port int
flag.IntVar(&port, "p", 8000, "specify port to use. defaults to 8000.")
flag.Parse()
fmt.Printf("port = %d\n", port)
fmt.Printf("other args: %+v\n", flag.Args())
}
将标识为"-p"的参数绑定到port上,默认值是8000,usage的语句显示为"specify port…"
然后利用Parse函数进行参数解析,解析命令行中是否输入了-p,然后判断输入的类型是否与绑定的变量匹配。如果出现错误则输出usage。
selpg说明
可以参考:开发 Linux 命令行实用程序
下载C语言代码学习:网址
简介
该实用程序从标准输入或从作为命令行参数给出的文件名读取文本输入。它允许用户指定来自该输入并随后将被输出的页面范围。例如,如果输入含有 100 页,则用户可指定只打印第 35 至 65 页。这种特性有实际价值,因为在打印机上打印选定的页面避免了浪费纸张。另一个示例是,原始文件很大而且以前已打印过,但某些页面由于打印机卡住或其它原因而没有被正确打印。在这样的情况下,则可用该工具来只打印需要打印的页面。
(更多细节请到上方参考链接阅读学习,这里不再过多赘述)
Go程序设计与实现
定义Flags
此程序为命令行实用程序,需要在命令行输入参数,所以需要用到pflag的包,根据之前所讲的内容可以大致了解到,对每个可用参数都要绑定相应类型的变量以供解析。首先查看selpg需要的参数有哪些:
“-sNumber”和“-eNumber”强制选项:
- selpg 要求用户用两个命令行参数“-sNumber”(例如,“-s10”表示从第 10 页开始)和“-eNumber”(例如,“-e20”表示在第 20 页结束)指定要抽取的页面范围的起始页和结束页
$ selpg -s10 -e20 ...
“-lNumber”和“-f”可选选项:
- -l表示以固定行数作为页结束的标志,-f表示以"\f"换页符作为页结束标志,(默认为-l模式)
- -l后面接相应的参数表示多少行为一页,如:
$ selpg -s10 -e20 -l66 ...
表示以66行为一页
“-dDestination”可选选项:
- selpg 还允许用户使用“-dDestination”选项将选定的页直接发送至打印机。这里,“Destination”应该是 lp 命令“-d”选项(请参阅“man lp”)可接受的打印目的地名称。该目的地应该存在 ― selpg 不检查这一点。
实现:
type argsList struct {
s, e, lNumber int
eop bool
destnation, inputFile string
}
首先定义一个结构体,储存各个选项参数的变量。
func initFlags(args *argsList) {
flag.IntVarP(&args.s, "start", "s", -1, "The start page")
flag.IntVarP(&args.e, "end", "e", -1, "The end page")
flag.IntVarP(&args.lNumber, "lineOfPage", "l", 72, "The length of a page")
flag.StringVarP(&args.destnation, "destnation", "d", "", "The destnatioon of printing")
flag.BoolVarP(&args.eop, "endOfPage", "f", false, "Defind the end symbol of a page")
flag.Parse()
filename := flag.Args()
if len(filename) == 1 {
args.inputFile = filename[0]
} else if len(filename) == 0 {
args.inputFile = ""
} else {
fmt.Println("Too many arguments")
}
}
对五个参数进行定义绑定,然后解析。由于输入文件并不是以-XXX的形式出现,所以根据pflag包的特性,在解析完所有定义的参数之后, flag.Args()中就是非标识的参数,去除其中的第一个作为输入文件名,赋值给inputfile这个变量。如果没有则置为空。如果发现参数过多,即除了可选选项之外,还有多余的参数则直接报错。
检查选项参数
func checkFlags(args *argsList) {
if (args.s == -1) || (args.e == -1) {
fmt.Fprintf(os.Stderr, "The start page and end page can't be empty!\n")
os.Exit(1)
} else if (args.s <= 0) || (args.e <= 0) {
fmt.Fprintf(os.Stderr, "The start page and end page should be positive!\n")
os.Exit(1)
} else if args.s > args.e {
fmt.Fprintf(os.Stderr, "The start page can't be bigger than the end page!\n")
os.Exit(1)
} else if (args.eop == true) && (args.lNumber != 72) {
fmt.Fprintf(os.Stderr, "You can't use -f and -l together!\n")
os.Exit(1)
} else if args.lNumber <= 0 {
fmt.Fprintf(os.Stderr, "The line of page can't be less than 1 !\n")
os.Exit(1)
} else {
pageType := "decided by page length."
if args.eop == true {
pageType = "decided by the end sign /f."
}
dest := args.destnation
if len(dest) == 0 {
dest = "null"
}
fmt.Fprintf(os.Stderr, "startPage: %d\nendPage: %d\ninputFile: %s\npageLength: %d\npageType: %s\nprintDestation: %s\n\n",
args.s, args.e, args.inputFile, args.lNumber, pageType, dest)
}
}
由于没有办法确认用户是否输入了某个选项,所以靠默认值来判断,首先是-s和-e是必须选的,所以不能为负。其次s不能大于e(开始页不能大于结束页)。然后判断-f和-l不能同时出现。l的值也要是正数。
一切正常的话,输出各个参数的值。
打开输入文件
func readFile(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 %s\n%s", args.inputFile, err)
os.Exit(2)
}
} else {
file = os.Stdin
}
if len(args.destnation) == 0 {
output(os.Stdout, file, args)
} else {
command := exec.Command("lp", "-d"+args.destnation)
outFile, err := command.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open Pipe!\n")
os.Exit(2)
}
output(outFile, file, args)
}
}
根据之前解析到的文件名,使用os库的Open函数打开文件,然后分析是否又可选选项-d,根据这个决定输出到打印机还是屏幕上。
如果是打印机,则需要使用exec.Command解析相应的参数,也就是命令行中的lp命令,输出到相应名字的打印机中去。而这个是通过管道实现的,所以需要获取管道的数据结构,并且像输出文件一样,传输到下一个函数中去,所以输出文件中的参数需要使用空接口,因为不知道具体是什么类型的参数传入,有可能是文件有可能是管道的io.WriteCloser
读取并且输出内容
func output(out interface{}, in *os.File, args *argsList) {
var pageNum int
if args.eop {
pageNum = 0
} else {
pageNum = 1
}
lineNum := 0
buffer := bufio.NewReader(in)
for {
var pageBuf string
var err error
if args.eop {
pageBuf, err = buffer.ReadString('\f')
pageNum++
} else {
pageBuf, err = buffer.ReadString('\n')
lineNum++
if lineNum > args.lNumber {
pageNum++
lineNum = 1
}
}
if err != nil && err != io.EOF {
fmt.Fprintf(os.Stderr, "errors in reading file!\n")
}
if pageNum >= args.s && pageNum <= args.e {
if len(args.destnation) == 0 {
printOut, ok := out.(*os.File)
if ok {
fmt.Fprintf(printOut, "%s", pageBuf)
} else {
fmt.Fprintf(os.Stderr, "Wrong printing type!\n")
os.Exit(3)
}
} else {
printOut, ok := out.(io.WriteCloser)
if ok {
printOut.Write(([]byte)(pageBuf))
} else {
fmt.Fprintf(os.Stderr, "Wrong printing type!\n")
os.Exit(3)
}
}
}
if err == io.EOF {
break
}
}
if pageNum < args.s {
fmt.Fprintf(os.Stderr, "start page bigger than total pages %d, no output written\n", pageNum)
os.Exit(4)
} else if pageNum < args.e {
fmt.Fprintf(os.Stderr, "end page bigger than total pages %d\n", pageNum)
os.Exit(4)
}
}
由于存在两种模式(固定行数换页和换页符换页),所以对于页数的统计也需要分别用两种方法,如果是换页符,直接读取到’\f’页数加一,如果是固定行数,则需要每次读取一行,并且当行数到达设置的行数时,页数加一。
每次读取后,用一个缓冲器储存起来,根据输出的类型(文件输出或打印机输出),文件的话直接输出到对应文件中(当然事先要打开文件),如果是管道,则需要一个特殊的类型,并且使用自带的函数Write,将一串字符输出到里面去。
最后判断是否读到EOF,则停止。还需要判断一下读取的页数是否满足-s和-e的需求。
测试
测试的输入文件采取了数字的方法,一共是1~200,每个数字占一行,每10个数字后加一个换页符,也就是10、20、30。。。为一页。这样的好处是简单,又方便肉眼看到正确与否。
为了避免数据行数太多,采用-f方式读取
- $ selpg -s1 -e1 -f input_file
- $ selpg -s1 -e1 -f < input_file
跟第一个是一样的效果,使用 < 来进行重定向
- $ selpg -s2 -e2 -f test.txt | selpg -s1 -e1 -f
把前半部分作为后半部分的标准输入,前半部分的结果就是第二页的内容。作为输入给第二部分,所以第二部分的第一页就是11~20
- $ selpg -s1 -e1 -f input_file >output_file
结果没有在屏幕输出,因为重定向到了output的文件中了。
- $ selpg -s1 -e2 -f input_file 2>error_file
和第六个重叠,不妨截图了 - $ selpg -s1 -e2 -f input_file >output_file 2>error_file
什么都没有输出到屏幕中,都到文件中了
- $ selpg -s1 -e1 -l50 test.txt
利用-l规定行数
直接显示1~50 - $ selpg -s1 -e2 -dlp1 test.txt
显示输出到了lp1的打印机上,实际上没有这个打印机,所以并不知道输出到了哪里。
本次实验到此结束!