CLI 命令行实用程序开发基础_18342063_刘智斌
文章目录
- CLI 命令行实用程序开发基础_18342063_刘智斌
-
- 1、概述
- 2、Golang 的支持
-
- 使用os包处理代码
- 使用flag包处理参数
- 基础知识
- 开发实践
-
- 开发要求
- 代码设计
-
- 实验准备
- 命令的参数类型及用途
- 输入参数
- 检查参数
- 处理命令
- 数据输出
- main函数
- 实验结果
-
- 文件结构
- 单元测试
- 功能测试
1、概述
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:
- Linux提供了cat、ls、copy等命令与操作系统交互;
- go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
- 容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
- git、npm等也是大家比较熟悉的工具。
尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
2、Golang 的支持
使用os包处理代码
package main
import (
"fmt"
"os"
)
func main() {
for i, a := range os.Args[1:] {
fmt.Printf("Argument %d is %s\n", i+1, a)
}
}
输出结果:在终端直接输入参数,会输出参数。
使用flag包处理参数
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())
}
输出结果:直接在终端运行并输入参数。
基础知识
selpg 允许用户指定从输入文本抽取的页的范围,这些输入文本可以来自文件或另一个进程。selpg 是以在 Linux 中创建命令的事实上的约定为模型创建的,这些约定包括:
- 独立工作
- 在命令管道中作为组件工作(通过读取标准输入或文件名参数,以及写至标准输出和标准错误)
- 接受修改其行为的命令行选项
该实用程序从标准输入或从作为命令行参数给出的文件名读取文本输入。它允许用户指定来自该输入并随后将被输出的页面范围。例如,如果输入含有 100 页,则用户可指定只打印第 35 至 65 页。这种特性有实际价值,因为在打印机上打印选定的页面避免了浪费纸张。另一个示例是,原始文件很大而且以前已打印过,但某些页面由于打印机卡住或其它原因而没有被正确打印。在这样的情况下,则可用该工具来只打印需要打印的页面。
开发实践
开发要求
使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg
提示:
- 请按文档 使用 selpg 章节要求测试你的程序
- 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
- golang 文件读写、读环境变量,请自己查 os 包
- “-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe
- 请自带测试程序,确保函数等功能正确
代码设计
实验准备
终端运行 go get github.com/spf13/pflag 安装pflag包。
在import中加入"github.com/spf13/pflag"。
import (
"github.com/spf13/pflag"
"bufio"
"fmt"
"io"
"os"
"os/exec"
)
命令的参数类型及用途
选项 | 值类型 | 用途 |
---|---|---|
-s | Num int | 指定起始页码为Num,必选项,默认为-1(不合法,强制要求重新指定) |
-e | Num int | 指定结束页码为Num,必选项,默认为-1(不合法,强制要求重新指定) |
filename | filename string | 指定输入文件,可选项,默认为空 |
-l | Num int | 指定每页的行数为Num,可选项,默认为每页72行,并且为默认解读模式 |
-f | 无附加参数 bool | 指定解读模式为,’\f’作为页分隔符,不可与-l同时使用 |
-d | destination string | 指定打印机设备地址 |
于是构建参数结构体
//参数的结构体
type selpgArgs struct {
start_page int //起始页的索引
end_page int //终止页的索引
page_len int //页的长度
page_type bool //页分隔类型
infile_name string//输入文件的名称
printf_dest string //输出的终点
}
输入参数
利用了上面所下载的pflag,获取-s -e -l -f -d 参数,-s对应起始页,-e定义终止页,-l定义页长度,-d定义输出终点,-f定义页分隔类型。
func getArgs(args *selpgArgs) {
pflag.IntVarP(&(args.start_page), "start_page", "s", -1, "Define start_page")
pflag.IntVarP(&(args.end_page), "end_page", "e", -1, "Define end_page")
pflag.IntVarP(&(args.page_len), "page_length", "l", 72, "Define page_length")
pflag.StringVarP(&(args.printf_dest), "printf_dest", "d", "", "Define printf_dest")
pflag.BoolVarP(&(args.page_type), "page_type", "f", false, "Define page_type")
pflag.Parse()
// 获取 filename 参数
filename := pflag.Args()
if len(filename) > 0 {
args.infile_name = string(filename[0])
} else {
args.infile_name = ""
}
}
检查参数
主要检测四种参数错误
- 开始页和终止页必须赋值
- 起始页不能大于终止页
- -l和-f不共存
- 页长度过小或溢出
如有参数错误则输出错误提示
func checkArgs(args *selpgArgs) {
// 判断输入参数合法性
if (args.start_page == -1) || (args.end_page == -1) {
fmt.Fprintf(os.Stderr, "[Error]The start_page and end_page can't be empty!\n")
os.Exit(1)
} else if (args.start_page <= 0) || (args.end_page <= 0) {
fmt.Fprintf(os.Stderr, "[Error]The start_page and end_page can't be less than 1!\n")
os.Exit(2)
} else if args.start_page > args.end_page {
fmt.Fprintf(os.Stderr, "[Error]The start_page can't be bigger than the end_page!\n")
os.Exit(3)
} else if (args.page_type == true) && (args.page_len != 72) {
fmt.Fprintf(os.Stderr, "[Error]The command -l and -f are exclusive!\n")
os.Exit(4)
} else if args.page_len <= 0 {
fmt.Fprintf(os.Stderr, "[Error]The page_len can't be less than 1 !\n")
os.Exit(5)
} else {
// 输入参数均合法,判断是选择了-l还是-f,并输入参数列表。
page_type := "page length."
if args.page_type == true {
page_type = "The end sign /f."
}
fmt.Printf("[ArgsStart]\n")
fmt.Printf("start_page: %d\nend_page: %d\ninputFile: %s\npage_length: %d\npage_type: %s\nprintf_destation: %s\n[ArgsEnd]\n", args.start_page, args.end_page, args.infile_name, args.page_len, page_type, args.printf_dest)
}
}
处理命令
处理命令时,首先判断是否有输入file_name参数,若有则从文件中读取输入,否则从标准输入中读取,然后判断打开是否成功。
然后判断是否输入-d参数,若有则将输出数据输出到特定的地方,否则直接在标准输出中输出。
func excuteCMD(args *selpgArgs) {
var inp *os.File
if args.infile_name == "" {
// 从标准输入中读取
inp = os.Stdin
} else {
// 检测文件是否可读取
checkFileAccess(args.infile_name)
var err error
inp, err = os.Open(args.infile_name)
// 检测文件是否打开成功
checkError(err, "input file")
}
if len(args.printf_dest) == 0 {
// 输出到标准输出
output(os.Stdout, inp, args.start_page, args.end_page, args.page_len, args.page_type)
} else {
// 输出到文件中
output(getDesio(args.printf_dest), inp, args.start_page, args.end_page, args.page_len, args.page_type)
}
}
数据输出
输出函数将已经处理过的命令根据参数的限制得到的数据输出。
首先判断-f和-l,然后判断输出页是否超出范围。
//输出函数
func output(fout interface{}, inp *os.File, pageStart int, pageEnd int, page_len int, page_type bool) {
lineCount := 0
pageCount := 1
buf := bufio.NewReader(inp)
for true {
var line string
var err error
if page_type {
// -f
line, err = buf.ReadString('\f')
pageCount++
} else {
// -l
line, err = buf.ReadString('\n')
lineCount++
if lineCount > page_len {
pageCount++
lineCount = 1
}
}
if err == io.EOF {
break
}
checkError(err, "file read in")
if (pageCount >= pageStart) && (pageCount <= pageEnd) {
var outputErr error
// 通过类型断言,判断fout的类型,知道应该调用哪个函数
if stdOutput, ok := fout.(*os.File); ok {
_, outputErr = fmt.Fprintf(stdOutput, "%s", line)
} else if pipeOutput, ok := fout.(io.WriteCloser); ok {
_, outputErr = pipeOutput.Write([]byte(line))
} else {
fmt.Fprintf(os.Stderr, "[Error]:fout type error.")
os.Exit(7)
}
checkError(outputErr, "Error happend when output the pages.")
}
}
if pageCount < pageStart {
// 起始页太大
fmt.Fprintf(os.Stderr, "[Error]: start_page (%d) greater than total pages (%d)\n", pageStart, pageCount)
os.Exit(8)
} else if pageCount < pageEnd {
// 终止页太大
fmt.Fprintf(os.Stderr, "[Error]: end_page (%d) greater than total pages (%d)\n", pageEnd, pageCount)
os.Exit(9)
}
}
main函数
func main() {
var args selpgArgs
getArgs(&args) // 读取参数
checkArgs(&args) // 判断参数是否合法
excuteCMD(&args) // 执行命令
}
实验结果
文件结构
input_file为从1到800的文本文件。
selpg运行程序通过在文件夹内运行终端并输入 go build selpg.go获得
单元测试
以 ./selpg -s1 -e2 input_file.txt为例进行单元测试,代码如下。可以以此为例设计多个单元测试。
package main
import (
"testing"
"os/exec"
"bytes"
)
func Test_selpg(t *testing.T){
cmd := exec.Command("./selpg","-s1","-e2","input_file.txt");
var output bytes.Buffer
cmd.Stdout = &output
err := cmd.Run()
if err!=nil{
t.Error(err)
}
}
测试通过:
功能测试
1. ./selpg -s1 -e1 input_file.txt
该命令将把“input_file”的第 1 页写至标准输出(也就是屏幕),因为这里没有重定向或管道。
2. ./selpg -s1 -e1 < input_file.txt
该命令与示例 1 所做的工作相同,但在本例中,selpg 读取标准输入,而标准输入已被 shell/内核重定向为来自“input_file”而不是显式命名的文件名参数。输入的第 1 页被写至屏幕。
3. more input_file.txt | ./selpg -s1 -e2
“other_command”的标准输出被 shell/内核重定向至 selpg 的标准输入。将第 1 页到第 2 页写至 selpg 的标准输出(屏幕)。一页72页,总共输出144页。
4. ./selpg -s1 -e2 input_file.txt >output_file.txt
selpg 将第 10 页到第 20 页写至标准输出;标准输出被 shell/内核重定向至“output_file”。
out_file内容如下:
5. ./selpg -s10 -e20 input_file.txt 2>error_file.txt
selpg 将第 10 页到第 20 页写至标准输出(屏幕);所有的错误消息被 shell/内核重定向至“error_file”。请注意:在“2”和“>”之间不能有空格;
6. ./selpg -s10 -e20 input_file.txt >output_file.txt 2>error_file.txt
selpg 将第 10 页到第 20 页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至“error_file”。
因输入只有12页,因此报错终止页数大于总共页数,错误信息保存在error_file中。
7. ./selpg -s10 -e20 input_file.txt >output_file.txt 2>/dev/null
selpg 将第 10 页到第 20 页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至 /dev/null(空设备),这意味着错误消息被丢弃了。设备文件 /dev/null 废弃所有写至它的输出,当从该设备文件读取时,会立即返回 EOF。
8. ./selpg -s10 -e20 input_file.txt >/dev/null
selpg 将第 10 页到第 20 页写至标准输出,标准输出被丢弃;错误消息在屏幕出现。这可作为测试 selpg 的用途,此时您也许只想(对一些测试情况)检查错误消息,而不想看到正常输出。
9. ./selpg -s10 -e20 input_file.txt | other_command
selpg 的标准输出透明地被 shell/内核重定向,成为“other_command”的标准输入,第 10 页到第 20 页被写至该标准输入。“other_command”的示例可以是 lp,它使输出在系统缺省打印机上打印。“other_command”的示例也可以 wc,它会显示选定范围的页中包含的行数、字数和字符数。“other_command”可以是任何其它能从其标准输入读取的命令,这里选用wc,它会显示选定范围的页中包含的行数、字数和字符数。。错误消息仍在屏幕显示。
10. ./selpg -s10 -e20 input_file.txt 2>error_file.txt | other_command
与上面的示例 9 相似,只有一点不同:错误消息被写至“error_file”
11. ./selpg -s1 -e2 -l66 input_file.txt
该命令将页长设置为 66 行,这样 selpg 就可以把输入当作被定界为该长度的页那样处理。第 1 页到第 2 页被写至 selpg 的标准输出(屏幕)。
12. $ ./selpg -s10 -e20 -f input_file.txt
假定页由换页符定界。第 10 页到第 20 页被写至 selpg 的标准输出(屏幕)。
13. ./selpg -s10 -e20 -dlp1 input_file.txt
第 10 页到第 20 页由管道输送至命令“lp -dlp1”,该命令将使输出在打印机 lp1 上打印。
测试无误。