#1、概述
主要内容为使用 golang 开发 Linux 命令行实用程序 中的 selpg。参照开发 Linux 命令行实用程序
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:
Linux提供了cat、ls、copy等命令与操作系统交互;
go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
git、npm等也是大家比较熟悉的工具
尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
#2、基础知识
selpg是一个unix系统下命令。
该命令本质上就是将一个文件,通过自己设定的分页方式,输出到屏幕或者重定向到其他文件上,或者利用打印机打印出来。
命令行程序主要涉及内容:
命令
命令行参数
选项:长格式、短格式
IO:stdin、stdout、stderr、管道、重定向
环境变量
格式是:
-s start_page -e end_page [ -f | -l lines_per_page ][ -d dest ] [ in_filena
参数
-s,后面接开始读取的页号 int
-e,后面接结束读取的页号 int s和e都要大于1,并且s <= e,否则提示错误s和e都要大于1,并且s <= e,否则提示错误
可选参数:
-l,后面跟行数 int,代表多少行分为一页,不指定 -l 又缺少 -f -则默认按照72行分一页
-f,该标志无参数,代表按照分页符’\f’ 分页
-d,后面接打印机名称,用于打印filename,唯一一个无标识参数,代表选择读取的文件名
#3、代码设计
1.设计selpg结构体
给selpg设计一个结构体,分别保存各个参数的值,使其易于处理。
type Selpg struct {
Start int //起始页
End int //结束页
PageType bool //是否为-f类型命令,是则为真
Length int //页长度
Destination string //打印机名称
Infile string//输入文件
data []string//储存文件数据的字符串
}
2.利用flag包解析参数,要求用pflag包来替代golang的flag包来获取命令行参数。这里需要 import "github.com/spf13/pflag"
//解析获取参数
func getArgs(args *Selpg) {
flag.IntVarP(&(args.Start), "startPage", "s", -1, "start page")
flag.IntVarP(&(args.End), "endPage", "e", -1, "end page")
flag.IntVarP(&(args.Length), "Length", "l", 72, "the length of page")
flag.BoolVarP(&(args.PageType), "PageType", "f", false, "page type")
flag.StringVarP(&(args.Destination), "Destination", "d", "", "print destination")
flag.Parse()
other := flag.Args() // 其余参数
if len(other) > 0 {
args.Infile = other[0]
} else {
args.Infile = ""
}
}
3.检查命令各参数是否合法,主要是检查起始结束页码是否有输入,是否在合理数值范围内,是否合法,起始页码是否比结束页码小等:
//检查参数合法性
func checkArgs(args *Selpg) {
if args.Start == -1 || args.End == -1 {
os.Stderr.Write([]byte("You shouid input like selpg -sNumber -eNumber ... \n"))
os.Exit(0)
}
if args.Start < 1 || args.Start > math.MaxInt32 {
os.Stderr.Write([]byte("You should input valid start page\n"))
os.Exit(0)
}
if args.End < 1 || args.End > math.MaxInt32 || args.End < args.Start {
os.Stderr.Write([]byte("You should input valid end page\n"))
os.Exit(0)
}
if (!args.PageType) && (args.Length < 1 || args.Length > math.MaxInt32) {
os.Stderr.Write([]byte("You should input valid page length\n"))
os.Exit(0)
}
}
4.执行命令的过程:
输入: 一旦处理了所有的命令行参数,就使用这些指定的选项以及输入、输出源和目标来开始输入的实际处理。selpg 通过以下方法记住当前页号:如果输入是每页行数固定的,则 selpg 统计新行数,直到达到页长度后增加页计数器。如果输入是换页定界的,则 selpg 改为统计换页符。这两种情况下,只要页计数器的值在起始页和结束页之间这一条件保持为真,selpg 就会输出文本(逐行或逐字)。当那个条件为假(也就是说,页计数器的值小于起始页或大于结束页)时,则 selpg 不再写任何输出。
输出:首先就要区分 -f 类型和 -l 类型,一个搜寻 \f 符号,将文本分隔开,每个作为一页。一个是读取 -l 时计算行数,到指定行数进行分页。在ReadBytes的时候可以根据情况来决定相应的终止符号,读到相应的页(从startPage到endPage)就输出便可。
//执行命令
func processInput(args *Selpg) {
// read the file
var reader *bufio.Reader
if args.Infile == "" {
reader = bufio.NewReader(os.Stdin)
} else {
fileIn, err := os.Open(args.Infile)
defer fileIn.Close()
if err != nil {
os.Stderr.Write([]byte("Open file error\n"))
os.Exit(0)
}
reader = bufio.NewReader(fileIn)
}
// output the file
if args.Destination == "" {
// 输出到当前命令行
outputCurrent(reader, args)
} else {
// 输出到目的地
outputToDest(reader, args)
}
}
func outputCurrent(reader *bufio.Reader, args *Selpg) {
writer := bufio.NewWriter(os.Stdout)
lineCtr := 0
pageCtr := 1
endSign := '\n'
if args.PageType == true {
endSign = '\f'
}
for {
strLine, errR := reader.ReadBytes(byte(endSign))
if errR != nil {
if errR == io.EOF {
writer.Flush()
break
} else {
os.Stderr.Write([]byte("Read bytes from reader fail\n"))
os.Exit(0)
}
}
if pageCtr >= args.Start && pageCtr <= args.End {
_, errW := writer.Write(strLine)
if errW != nil {
os.Stderr.Write([]byte("Write bytes to out fail\n"))
os.Exit(0)
}
}
if args.PageType == true {
pageCtr++
} else {
lineCtr++
}
if args.PageType != true && lineCtr == args.Length {
lineCtr = 0
pageCtr++
if pageCtr > args.End {
writer.Flush()
break
}
}
}
checkPageNum(args, pageCtr)
}
5.最后是完善其他代码,还要处理-d参数,表示送到相应的打印机打印
func outputToDest(reader *bufio.Reader, args *Selpg) {
cmd := exec.Command("./" + args.Destination)
stdin, err := cmd.StdinPipe()
if err != nil {
fmt.Println(err)
os.Exit(0)
}
startErr := cmd.Start()
if startErr != nil {
fmt.Println(startErr)
os.Exit(0)
}
lineCtr := 0
pageCtr := 1
endSign := '\n'
if args.PageType == true {
endSign = '\f'
}
for {
strLine, errR := reader.ReadBytes(byte(endSign))
if errR != nil {
if errR == io.EOF {
break
} else {
os.Stderr.Write([]byte("Read bytes from reader fail\n"))
os.Exit(0)
}
}
if args.PageType == true {
pageCtr++
} else {
lineCtr++
}
if pageCtr >= args.Start && pageCtr <= args.End {
_, errW := stdin.Write(strLine)
if errW != nil {
fmt.Println(errW)
os.Stderr.Write([]byte("Write bytes to out fail\n"))
os.Exit(0)
}
}
if args.PageType != true && lineCtr == args.Length {
lineCtr = 0
pageCtr++
stdin.Write([]byte("\f"))
if pageCtr > args.End {
break
}
}
}
stdin.Close()
if err := cmd.Wait(); err != nil {
fmt.Println(err)
os.Exit(0)
}
checkPageNum(args, pageCtr)
}
测试
按文档使用 selpg 章节要求测试你的程序:
in.txt里面是1到100行的内容分别为1到100的输入文件
- 首先是 go build selpg.go 编译文件
- go run selpg.go
- $ selpg -s1 -e1 input_file
- $ selpg -s1 -e1 < input_file
- $ other_command | selpg -s1 -e1
- $ selpg -s1 -e1 input_file >output_file
- $ selpg -s1 -e10 input_file 2>error_file
- $ selpg -s1 -e10 input_file >output_file 2>error_file
- $ selpg -s1 -e1 input_file | other_command
- $ selpg -s1 -e11 input_file 2>error_file | other_command
- $ selpg -s1 -e3 -l4 input_file’’
- $ selpg -s1 -e3 -f input_file
- 测试-dXXX