开发简单CLI程序
CLI 命令行实用程序开发基础
github项目链接:https://github.com/lurui7/service-computing/tree/master/hw3
gitee项目链接:https://gitee.com/richard_lrlrlr/service-computing/tree/master/hw3
概述
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:
- Linux提供了cat、ls、copy等命令与操作系统交互;
- go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
- 容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
- git、npm等也是大家比较熟悉的工具。
尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
基础知识的学习
根据作业给出的资料的学习,我总结了以下要点。
命令
命令是比较好理解的,我们在使用cmd或者linux的terminal时,使用的指令都是命令。例如:cd path, go run file.go , go test -v…等等
命令的一般格式为:
$ command [option] [paraments]
可以看到一些命令还在后面接了 -?,这便是参数。文档中给出了POSIX中关于程序名、参数的约定,如下:
选项类型有两种
1)短选项(short option):由一个连字符和一个字母构成,例如:-a, -s等;
2)长选项(long options):由两个连字符和一些大小写字母组合的单词构成,例如:–size,–help等。
通常,一个程序会提供short option和long options两种形式,例如:ls -a,–all。
另外,短选项(short option)是可以合并的,例如:-sh表示-s和-h的组合,如果要表示为一个选项需要用长选项–sh。
IO
-
输入
应该允许输入来自以下两种方式:- 在命令行上指定的文件名。例如:
$ command input_file
- 标准输入(stdin),缺省情况下为终端(也就是用户的键盘)。例如:
$ command
重定向:
使用 shell 操作符“<”(重定向标准输入),也可将标准输入重定向为来自文件,如下所示:command < input_file
管道:
使用 shell 操作符“|”(pipe)也可以使标准输入来自另一个程序的标准输出,如下所示:other_command | command
这里,other_command 的标准输出(stdout)被 shell/内核透明地传递至 command 的标准输入。 - 在命令行上指定的文件名。例如:
-
输出
输出应该被写至标准输出,缺省情况下标准输出同样也是终端(也就是用户的屏幕):$ command
类似地,使用 shell 操作符“>”(重定向标准输出)可以将标准输出重定向至文件
command > output_file
还是使用“|”操作符,command 的输出可以成为另一个程序的标准输入,如下所示:
$ command | other_command
更多有关IO部分的内容移步开发 Linux 命令行实用程序
Golang 的支持
使用flag/pflag包来绑定参数(二者大同小异,这里主要关注flag的使用)
-
安装pflag:
go get github.com/spf13/pflag
-
flag的基本使用
定义flags// 返回的是 指针 var ip = flag.Int("flagname", 1234, "help message for flagname")
将flag绑定到一个变量
var flagvar int func init() { flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname") }
绑定自定义的类型
// 自定义类型需要实现value接口 flag.Var(&flagVal, "name", "help message for flagname")
flag解析
// 解析函数将会在碰到第一个非flag命令行参数时停止 flag.Parse()
而pflag比flag多了shorthand参数和非必须选项的默认值
// func IntP(name, shorthand string, value int, usage string) *int // IntP is like Int, but accepts a shorthand letter that can be used after a single dash. var ip= flag.IntP("flagname", "f", 1234, "help message")
var ip = flag.IntP("flagname", "f", 1234, "help message") flag.Lookup("flagname").NoOptDefVal = "4321"
更多关于falg和pflag的使用的内容移步Golang之使用Flag和Pflag
项目要求
使用 golang开发开发 Linux 命令行实用程序 中的selpg
提示:
- 请按文档 使用 selpg 章节要求测试你的程序
- 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
- golang 文件读写、读环境变量,请自己查 os 包
- “-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe
- 请自带测试程序,确保函数等功能正确
项目实现
selpg 是从文本输入选择页范围的实用程序,主要步骤就是读取用户参数、处理用户参数、读取输入、将选中页数据送入输出地址。
在开发 Linux 命令行实用程序 中提供了slepg.c的源代码,因此对于这个selpg的实现的结构和思路都不用担心(仅是一个翻译任务),但是要特别关注go语言与C语言的不同,要学会使用go中对应的各个包。
1.根据selpg的命令格式,创建结构体,并使用pflag绑定参数。
selpg的命令格式:
- -s:startPage,后面接开始读取的页号
- -e:endPage,后面接结束读取的页号
- -l:后面跟行数,代表多少行分为一页
- -f:该标志无参数,代表按照分页符’\f’ 分页,一般默认72行为一页
- -d:“-dDestination”选项将选定的页直接发送至打印机,“Destination”应该是 lp 命令“-d”选项可接受的打印目的地名称
- input_file,output_file 2,error_file:输入文件、输出文件、错误信息文件的名字
创建结构体:
//a sruct for selpg_args
type selpgArgs struct {
startPage int
endPage int
inFilename string
pageLen int
pageType bool
printDest string
}
使用pflag绑定参数:
//Usage is used for telling the use of selpg
func Usage() {
fmt.Fprintf(os.Stderr, "\nUSAGE: %s -sstartPage -eendPage [ -f | -llinesPerPage ] [ -ddest ] [ inFilename ]\n", progname)
}
//pflag 绑定参数
pflag.IntVarP(&sa.startPage, "startPage", "s", 0, "Start page number")
pflag.IntVarP(&sa.endPage, "endPage", "e", 0, "End page number")
pflag.IntVarP(&sa.pageLen, "pageLen", "l", 7, "Lines per page")//在C源码中为72,此处为了方便测试改为7
pflag.BoolVarP(&sa.pageType, "pageType", "f", false, "Page type")
pflag.StringVarP(&sa.printDest, "dest", "d", "", "Destination")
pflag.Usage = func() {
Usage()
pflag.PrintDefaults()
}
pflag.Parse()
2.判断参数是否合法
sa.inFilename = ""
if remain := pflag.Args(); len(remain) > 0 {
sa.inFilename = remain[0]
}
//check the args are valid
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", progname)
pflag.Usage()
os.Exit(1)
}
if sa.startPage < 1 || sa.startPage > (INTMAX-1) {
fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname, sa.startPage)
pflag.Usage()
os.Exit(2)
}
if sa.endPage < 1 || sa.endPage > (INTMAX-1) || sa.endPage < sa.startPage {
fmt.Fprintf(os.Stderr, "%s: invalid end page %d\n", progname, sa.endPage)
pflag.Usage()
os.Exit(3)
}
if sa.pageLen < 1 || sa.pageLen > (INTMAX-1) {
fmt.Fprintf(os.Stderr, "%s: invalid page length %d\n", progname, sa.pageLen)
pflag.Usage()
os.Exit(4)
}
if sa.inFilename != "" {
if _, err := os.Stat(sa.inFilename); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "%s: input file \"%s\" does not exist\n", progname, sa.inFilename)
pflag.Usage()
os.Exit(5)
}
}
3.实现文件的读写(指令运行逻辑的实现)
主要使用os.Stdin与os.Stdout实现读写。
//ProcessInput complete the program running method
func ProcessInput(sa *selpgArgs) {
filein := os.Stdin
fileout := os.Stdout
lineCount := 0
pageCount := 1
//读取文件并判断读取是否出错
if sa.inFilename != "" {
err := errors.New("")
filein, err = os.Open(sa.inFilename)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: could not open input file \"%s\"\n", progname, sa.inFilename)
os.Exit(6)
}
defer filein.Close()
}
//开始根据是否以换页符分页进行分页
readLine := bufio.NewReader(filein)
if sa.pageType == false {
for {
line, err := readLine.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
fmt.Fprintf(os.Stderr, "%s: Read file error!\n", progname)
os.Exit(7)
}
lineCount++
if lineCount > sa.pageLen {
pageCount++
lineCount = 1
}
if pageCount >= sa.startPage && pageCount <= sa.endPage {
fmt.Fprintf(fileout, "%s", line)
}
}
} else {
for {
page, err := readLine.ReadString('\f')
if err == io.EOF {
break
}
if err != nil {
fmt.Fprintf(os.Stderr, "%s: Read file error!\n", progname)
os.Exit(8)
}
pageCount++
if pageCount >= sa.startPage && pageCount <= sa.endPage {
fmt.Fprintf(fileout, "%s", page)
}
}
}
cmd := exec.Command("cat", "-n")
_, err := cmd.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "%s: Create pipe error\n", progname)
os.Exit(9)
}
if sa.printDest != "" {
cmd.Stdout = fileout
cmd.Run()
}
filein.Close()
fileout.Close()
}
4.main函数
func main() {
sa := selpgArgs{0, 0, "", 7, false, ""}
progname = os.Args[0]
ProcessArgs(&sa)
ProcessInput(&sa)
}
测试
1.功能测试
测试用的文件为input.txt,输出文件为output.txt
- 测试go run selpg.go -s1 -e1 input.txt
此时输出第1到7行,正确 - 测试go run selpg.go -s2 -e1 input.txt
此时startPage > endPage,报错 - 测试go run selpg.go -s0 -e1 input.txt
此时startPage == 0,不合法,报错 - 测试go run selpg.go -s1 -e2 -l4 input.txt
此时修改pageLen参数为4,可以看到输出前两页到line 8,正确 - 测试 go run selpg.go -s1 -e2 input.txt > output.txt
此时成功写入 - 测试go run selpg.go -s3 -e2 input.txt > output.txt 2>error.txt
此时显然startPage > endPage, output.txt中没有写入内容,而error.txt中写入错误信息 - 测试more input.txt | go run selpg.go -s1 -e1
此时重定向成功 - 测试go run selpg.go -s1 -e2 input.txt | cat -n
- 测试go run selpg.go -s1 -e2 -dcat input.txt
2.单元测试
测试文件为selpg_test.go:
package main
import "testing"
func TestUsuage(t *testing.T) {
tests := []struct {
name string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Usage()
})
}
}
func TestProcessArgs(t *testing.T) {
tests := []struct {
name string
args selpgArgs
}{
// TODO: Add test cases.
{
name: "Nil options",
args: selpgArgs{2, 2, "test.txt", 7, false, ""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ProcessArgs(&(tt.args))
})
}
}
func TestProcessInput(t *testing.T) {
tests := []struct {
name string
args selpgArgs
}{
// TODO: Add test cases.
{
name: "Nil options",
args: selpgArgs{2, 2, "test.txt", 7, false, ""},
},
{},//由于未知原因,只有在加入一个空测试对象才能跑起来...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ProcessInput(&(tt.args))
})
}
}
测试结果为:
单元测试通过。