CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。Linux提供了cat、ls、copy等命令与操作系统交互;go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;git、npm等都是大家比较熟悉的工具。尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg。
selpg 程序逻辑
selpg 是从文本输入选择页范围的实用程序。该输入可以来自作为最后一个命令行参数指定的文件,在没有给出文件名参数时也可以来自标准输入。
selpg 首先处理所有的命令行参数。在扫描了所有的选项参数后,如果 selpg 发现还有一个参数,则它会接受该参数为输入文件的名称并尝试打开它以进行读取。如果没有其它参数,则 selpg 假定输入来自标准输入。
参数处理
“-sNumber”和“-eNumber”强制选项:
selpg 要求用户用两个命令行参数“-sNumber”(例如,“-s10”表示从第 10 页开始)和“-eNumber”(例如,“-e20”表示在第 20 页结束)指定要抽取的页面范围的起始页和结束页。selpg 对所给的页号进行合理性检查;换句话说,它会检查两个数字是否为有效的正整数以及结束页是否不小于起始页。这两个选项,“-sNumber”和“-eNumber”是强制性的,而且必须是命令行上在命令名 selpg 之后的头两个参数:
$ selpg -s10 -e20 …
“-lNumber”和“-f”可选选项:
selpg 可以处理两种输入文本:
类型 1:该类文本的页行数固定。这是缺省类型,因此不必给出选项进行说明。也就是说,如果既没有给出“-lNumber”也没有给出“-f”选项,则 selpg 会理解为页有固定的长度(每页 72 行)。
选择 72 作为缺省值是因为在行打印机上这是很常见的页长度。这样做的意图是将最常见的命令用法作为缺省值,这样用户就不必输入多余的选项。该缺省值可以用“-lNumber”选项覆盖,如下所示:
$ selpg -s10 -e20 -l66 …
“-dDestination”可选选项:
selpg 还允许用户使用“-dDestination”选项将选定的页直接发送至打印机。这里,“Destination”应该是 lp 命令“-d”选项(请参阅“man lp”)可接受的打印目的地名称。该目的地应该存在 ― selpg 不检查这一点。在运行了带“-d”选项的 selpg 命令后,若要验证该选项是否已生效,请运行命令“lpstat -t”。该命令应该显示添加到“Destination”打印队列的一项打印作业。如果当前有打印机连接至该目的地并且是启用的,则打印机应打印该输出。这一特性是用 popen() 系统调用实现的,该系统调用允许一个进程打开到另一个进程的管道,将管道用于输出或输入。
selpg -s10 -e20 -dlp1
流程分析
程序按照读取参数、判断参数是否合规、读取文件、确定输出位置并输出顺序执行。当发现错误时抛出错误并终止流程。
代码实现
我根据给出的c语言源码selpg.c来实现自己的go语言CLI。
首先是包的引用:
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
flag "github.com/spf13/pflag"
)
参考Golang之使用Flag和Pflag导入Pflag包以及学习Pflag包的引用。
设置程序的参数结构体
type selpg_args struct{
start_page int
end_page int
in_filename string
page_len int
page_type bool
print_dest string
}
使用pflag提取参数将值赋值给该结构体。 pflag包于flag用法类似,但pflag相对于flag能够更好地满足命令行规范。
flag.IntVarP(&sa.start_page,"start","s",-1,"start page(>1)")
flag.IntVarP(&sa.end_page,"end","e",-1,"end page(>=start_page)")
flag.IntVarP(&sa.page_len,"len","l",72,"page len")
flag.StringVarP(&sa.print_dest,"dest","d","","print dest")
flag.BoolVarP(&sa.page_type, "type", "f", false, "page type")
flag.Usage = func(){
fmt.Fprintf(os.Stderr,"USAGE:: \n-s start_page -e end_page [ -f | -l lines_per_page ]" + " [ -d dest ][ in_filename ]\n")
}
flag.Parse()
此处满足要求:
请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
pflag包中的函数XXXVarP(XXX为Int、String、Bool等可选类型)可以取出命令行参数名称shorthand的参数的值,value指定*p的默认值,name为自定的名称,usage为自定的该参数的描述。该函数无返回值。获得flag参数后,要用pflag.Parse()函数才能把参数解析出来。
接下来是判断每个参数的格式是否正确,参数个数是否正确以及是否将参数的值提取出来赋值给结构体。
if len(os.Args)<3 {
fmt.Fprintf(os.Stderr,"\nnot enough arguments\n")
flag.Usage()
os.Exit(0)
}
if (sa.start_page == -1) || (sa.end_page == -1) {
fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be empty! Please check your command!\n")
flag.Usage()
os.Exit(0)
}
if (sa.start_page <= 0) || (sa.end_page <= 0) {
fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be negative! Please check your command!\n")
flag.Usage()
os.Exit(0)
}
if sa.start_page > sa.end_page {
fmt.Fprintf(os.Stderr, "\n[Error]The startPage can't be bigger than the endPage! Please check your command!\n")
flag.Usage()
os.Exit(0)
}
if len(flag.Args()) == 1{
_, err:=os.Stat(flag.Args()[0])
if err!=nil && os.IsNotExist(err) {
fmt.Fprintf(os.Stderr,"\ninput file \"%s\" does not exist\n",flag.Args()[0])
os.Exit(0)
}
sa.in_filename = flag.Args()[0]
}
if (sa.page_type == true) && (sa.page_len != 72) {
fmt.Fprintf(os.Stderr, "\n[Error]The command -l and -f are exclusive, you can't use them together!\n")
flag.Usage()
os.Exit(0)
}
if sa.page_len <= 0 {
fmt.Fprintf(os.Stderr, "\n[Error]The pageLen can't be less than 1 ! Please check your command!\n")
flag.Usage()
os.Exit(0)
}
首先检查了开始页和结束页是否正确,然后判断是否正确读入文件,接着检查自定页长-l和遇换页符换页-f是否同时出现,最后判断当自定页长-l出现时args.pageLen是否小于1。
参数检查process_args函数结束之后,程序开始调用process_input函数执行命令。
var fin *os.File
if args.in_filename == "" {
fin = os.Stdin
} else {
var err error
fin, err = os.Open(args.in_filename )
if err != nil {
fmt.Fprintf(os.Stderr, "\n[Error]%s:", args.in_filename)
os.Exit(0)
}
}
line_count := 0
page_count := 1
buf := bufio.NewReader(fin)
cmd = &exec.Vmd{}
var fout io.WriteCloser
if args.print_dest=""{
fout = os.Stdout
}else {
cmd = exec.Command("cat")
var err error
cmd.Stdout,err = os.OpenFile(args.print_dest,os.O_WROUNLY|os.O_TRUNC,0600)
if err != nil {
fmt.Fprintf(os.Stderr, "\n[Error]%s:", "Input pipe open\n")
os.Exit(0)
}
fout, _ = cmd.StdinPipe()
cmd.Start()
}
首先检查输入。如果没有给定文件名,则从标准输入中获取;如果给出读取的文件名,则检查文件是否存在。然后判断是否有-d参数。如果没有-d参数,选择的页直接从os.Stdout标准输出中输出。如果-d存在,则使用os/exec包,可以执行外部命令,将输出的数据作为外部命令的输入。使用exec.Command()设定要执行的外部命令,cmd.StdinPipe()返回连接到command标准输入的管道pipe。最后使用cmd.Start()命令开始非阻塞执行子进程。由于没有连接打印机,所以使用cat命令测试。不过目标文件(pinput.txt)需要先创建。
最后是需要输出的内容。
for true {
var line string
var err error
if args.page_type {
line, err = buf.ReadString('\f')
page_count++
} else {
line, err = buf.ReadString('\n')
line_count++
if line_count > args.page_len {
page_count++
line_count = 1
}
}
if err == io.EOF {
break
}
if err != nil {
fmt.Fprintf(os.Stderr, "\n[Error]%s:", "Input pipe open\n","file read in\n")
os.Exit(0)
}
if (page_count >= args.start_page) && (page_count <= args.end_page) {
var outputErr error
_, outputErr = fout.Write([]byte(line))
if outputErr != nil {
fmt.Fprintf(os.Stderr, "\n[Error]%s:", "pipe input")
os.Exit(0)
}
if outputErr != nil {
fmt.Fprintf(os.Stderr, "\n[Error]%s:", "Error happend when output the pages.")
os.Exit(0)
}
}
}
主函数调用上述函数完成。
func main(){
var args selpg_args
process_args(&args)
process_input(&args)
}
process_args函数解析用户的输入,将对应的参数放入参数结构体中。
process_input函数负责根据参数结构体的属性设定输入输出源,设置每页的行数并开始打印。
usage函数用于打印帮助信息。
这样就实现了通过GO语言的CLI命令行开发。
按文档 使用 selpg 章节要求测试该程序。
首先是测试文件test.txt
selpg -s1 -e1 -l10 test.txt
将test.txt第1页的前10行打印到屏幕上
selpg -s1 -e1 < test.txt
selpg读取标准输入,而标准输入被shell/内核重定向为来自test.txt而不是显示命名的文件名参数。输入的第1页被写至屏幕。
selpg -s2 -e2 test.txt > output_file
将第2页写入out.txt中
selpg -s1 -e2 -l5 test.txt
将两页打印在屏幕上,一页5行
selpg -s3 -e5 -l4 test.txt >out.txt 2>error.txt
将标准错误写入error_file中
selpg -s2 -e3 -l10 test.txt >output_file 2>error_file
将第2到3页写入output_file中,标准错误将被写入error_file中
selpg -s1 -e1 test.txt | other_command
selpg -s1-e1 input_file 2>error_file | other_command
selpg -s1 -e1 -f test.txt
假定页由换页符界定,第1页被打印到屏幕
selpg -s1 -e1 -l5 -dlp1 test.txt
将第一页输出到打印机,因为1并没有打印机,所以通过cat来进行测试,将结果输出到文件中。
selpg -s1 -e2 test.txt > output_file 2>error_file &
本次服务计算作业通过Go语言实现了selpg,学会了Go的简单使用,对于CLI的开发有了一些了解。同时在编程中也学习了Pflag包的运用,对于exec 包以及bufio包的函数都有部分涉及。本次作业在难度上比前两次要大很多,虽然有c语言源代码供我们参考,但是对于go语言之中一些特定的我并不了解,需要在作业中从头学习。这一次对管道,重定向有了更深的了解,也简单使用了Pflag包,exec 包以及bufio包。
GitHub地址:selpg