一. CLI 命令行程序概述
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:
Linux提供了cat、ls、copy等命令与操作系统交互;
go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
git、npm等也是大家比较熟悉的工具。
尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
1.系统环境
操作系统:CentOS7
硬件信息:使用virtual box配置虚拟机(内存3G、磁盘30G)
编程语言:GO 1.15.2
2.项目介绍
本项目的开发主要基于IBM Developer社区的C语言程序(https://www.ibm.com/developerworks/cn/linux/shell/clutil/index.html),出于熟悉golang语言的目的,笔者主要的工作只是将其翻译为golang格式,其中还使用了部分库,如os和pflag,再次感谢原作者及开源代码工作者。
项目完成后的运行效果与CLI 命令行程序一致,一个简单的输出文本第一页20行的内容的例子如下:
3.开发准备
①首先下载上文的C语言源码(点击下载)
②安装并使用 pflag 替代 goflag 以满足 Unix 命令行规范,此处出于篇幅考虑,只在后面的函数介绍时给出部分使用教程,详细的pflag 使用教程可见【六. References. 1. Golang之使用Flag和Pflag】
③将C语言源码翻译为golang语言
selpg的程序结构非常简单,主要有以下组成:
①sp_args结构
②main函数
③process_args函数
④process_input函数
⑤usage函数
主要要导入的库有:
①bufio:用于文件的读写
②io:用于文件读写、读环境变量
③pflag:用于解释命令行参数,替代 goflag 以满足 Unix 命令行规范
/*================================= includes ======================*/
package main
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"github.com/spf13/pflag"
)
3.sp_args结构体
sp_args结构体是用于记录数据的结构体,分别记录着开始页码,结束页码,文件名,每页大小,页的类型和打印输出位置等信息。
/*================================= types =========================*/
type sp_args struct {
start_page int
end_page int
in_filename string
page_len int /* default value, can be overriden by "-l number" on command line */
page_type bool /* 'l' for lines-delimited, 'f' for form-feed-delimited */
/* default is 'l' */
print_dest string
}
4.全局变量
全局变量共有两个:
①progname是程序名,在输出错误信息时有用;
②用 INT_MAX 检查一个数是否为有效整数,由于golang没有预定义的INT_MAX,此处用别的方式来手动实现
/*================================= globals =======================*/
var progname string /* program name, for error messages */
const INT_MAX = int(^uint(0) >> 1) //golang需要手动声明INT_MAX
4.main函数
main函数作为程序的入口,给出了整个程序的大概运行过程。
①首先进行sp_args变量和progname的初始化,其中主要的默认属性为开始页码和结束页码均为1,每页长度为20行,不可用用换页符换页
②然后调用process_args函数来处理输入时的各种参数错误
③最后才调用process_input函数来执行输入的参数。
/*================================= main()=== =====================*/
func main() {
var sa sp_args
sa.start_page = 1
sa.end_page = 1
sa.in_filename = ""
sa.page_len = 20 //默认20行一页
sa.page_type = false
sa.print_dest = ""
/* save name by which program is invoked, for error messages */
progname = os.Args[0]
process_args(len(os.Args), &sa)
process_input(sa)
}
5.process_args函数
process_args函数用于处理输入时的各种参数错误。
①首先通过pflag绑定各参数和usage函数
②然后判断各种参数的错误即可,比如起始页码是负数,终止页码小于起始页码等情况,具体的错误情况在代码中已给出注释
③当发生错误,首先通过pflag.usage函数输出正确的指令参数格式来提醒用户,并通过os.Exit函数退出程序
/*================================= process_args() ================*/
func process_args(ac int, psa *sp_args) {
//指令格式:selpg -sstart_page -eend_page [-lline | -f ] [-d dstFile] filename
//使用pflag绑定各参数, psa初始化
pflag.Usage = usage
pflag.IntVarP(&psa.start_page, "start_page", "s", 1, "Start page")
pflag.IntVarP(&psa.end_page, "end_page", "e", 1, "End page")
pflag.IntVarP(&psa.page_len, "page_len", "l", 20, "Lines per page")
pflag.BoolVarP(&psa.page_type, "page_type", "f", false, "Page type")
pflag.StringVarP(&psa.print_dest, "dest", "d", "", "Destination")
pflag.Parse()
/* check the command-line arguments for validity */
if ac < 3 { /* Not enough args, minimum command is "selpg -sstartpage -eend_page" */
fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", progname)
pflag.Usage()
os.Exit(1)
}
/* handle 1st arg - start page */
temp := os.Args[1]
if temp[0:2] != "-s" {
fmt.Fprintf(os.Stderr, "%s: 1st arg should be -sstart_page\n", progname)
pflag.Usage()
os.Exit(2)
}
if psa.start_page < 1 || psa.start_page > (INT_MAX-1) {
fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname, psa.start_page)
pflag.Usage()
os.Exit(3)
}
/* handle 2nd arg - end page */
temp = os.Args[2]
if temp[0:2] != "-e" {
fmt.Fprintf(os.Stderr, "%s: 2nd arg should be -eend_page\n", progname)
pflag.Usage()
os.Exit(4)
}
if psa.end_page < 1 || psa.end_page > (INT_MAX-1) || psa.end_page < psa.start_page {
fmt.Fprintf(os.Stderr, "%s: invalid end page %d\n", progname, psa.end_page)
pflag.Usage()
os.Exit(5)
}
/* now handle optional args */
//使用pflag,selpg.c的while+switch可去掉
if psa.page_len != 5 {
if psa.page_len < 1 {
fmt.Fprintf(os.Stderr, "%s: invalid page length %d\n", progname, psa.page_len)
pflag.Usage()
os.Exit(6)
}
}
if pflag.NArg() > 0 { /* there is one more arg */
psa.in_filename = pflag.Arg(0)
/* check if file exists */
file, err := os.Open(psa.in_filename)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: input file \"%s\" does not exist\n", progname, psa.in_filename)
os.Exit(7)
}
/* check if file is readable */
file, err = os.OpenFile(psa.in_filename, os.O_RDONLY, 0666)
if err != nil {
if os.IsPermission(err) {
fmt.Fprintf(os.Stderr, "%s: input file \"%s\" exists but cannot be read\n", progname, psa.in_filename)
os.Exit(8)
}
}
file.Close()
}
}
6.process_args函数
process_input函数用于执行输入的参数,执行文件读写和输出到屏幕等操作。其中由于没有打印机,转而使用cat命令测试。
/*================================= process_input() ===============*/
func process_input(sa sp_args) {
var fin *os.File /* input stream */
var fout io.WriteCloser /* output stream */
var c byte /* to read 1 char */
var line string
var line_ctr int /* line counter */
var page_ctr int /* page counter */
var err error
cmd := &exec.Cmd{}
/* set the input source */
if sa.in_filename == "" {
fin = os.Stdin
} else {
fin, err = os.Open(sa.in_filename)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: could not open input file \"%s\"\n", progname, sa.in_filename)
os.Exit(9)
}
}
/* set the output destination */
if sa.print_dest == "" {
fout = os.Stdout
} else {
cmd = exec.Command("cat") //由于没有打印机,使用cat命令测试
cmd.Stdout, err = os.OpenFile(sa.print_dest, os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: could not open output file \"%s\"\n", progname, sa.print_dest)
os.Exit(10)
}
fout, err = cmd.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "%s: could not open pipe to \"%s\"\n", progname, sa.print_dest)
os.Exit(11)
}
cmd.Start()
}
/* begin one of two main loops based on page type */
rd := bufio.NewReader(fin)
if sa.page_type == false {
line_ctr = 0
page_ctr = 1
for true {
line, err = rd.ReadString('\n')
if err != nil { /* error or EOF */
break
}
line_ctr++
if line_ctr > sa.page_len {
page_ctr++
line_ctr = 1
}
if page_ctr >= sa.start_page && page_ctr <= sa.end_page {
fmt.Fprintf(fout, "%s", line)
}
}
} else {
page_ctr = 1
for true {
c, err = rd.ReadByte()
if err != nil { /* error or EOF */
break
}
if c == '\f' {
page_ctr++
}
if page_ctr >= sa.start_page && page_ctr <= sa.end_page {
fmt.Fprintf(fout, "%c", c)
}
}
fmt.Print("\n")
}
/* end main loop */
if page_ctr < sa.start_page {
fmt.Fprintf(os.Stderr, "%s: start_page (%d) greater than total pages (%d), no output written\n", progname, sa.start_page, page_ctr)
} else if page_ctr < sa.end_page {
fmt.Fprintf(os.Stderr, "%s: end_page (%d) greater than total pages (%d), less output than expected\n", progname, sa.end_page, page_ctr)
}
fin.Close()
fout.Close()
fmt.Fprintf(os.Stderr, "%s: done\n", progname)
}
7.usage函数
usage函数用于输出正确的指令参数格式。
/*================================= usage() =======================*/
func usage() {
fmt.Fprintf(os.Stderr, "\nUSAGE: %s -sstart_page -eend_page [ -f | -llines_per_page ] [ -ddest ] [ in_filename ]\n", progname)
}
1.功能测试
此处按照IBM的c语言程序的使用实例来进行功能测试。
首先在selpg目录下建立三个txt文件,分别为:
①in.txt, 用于输入的文本,内容如下(为方便演示,只有20行):
②out.txt, 保存输出的文本,内容初始为空
③error.txt,保存错误信息,内容初始为空
selpg -s1 -e1 in.txt
该命令将把“in.txt”的第 1 页写至标准输出(也就是屏幕),因为这里没有重定向或管道。
[henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt
Hello world!
I am HenryHZY.
line 1
iine 2
line 3
line 4
line 5
line 6
iine 7
line 8
line 9
line 10
line 11
iine 12
line 13
line 14
line 15
line 16
iine 17
line 18
selpg: done
selpg -s1 -e1 < in.txt
该命令与示例 1 所做的工作相同,但在本例中,selpg 读取标准输入,而标准输入已被 shell/内核重定向为来自“in.txt”而不是显式命名的文件名参数。输入的第 1 页被写至屏幕。
[henryhzy@localhost selpg]$ selpg -s1 -e1 < in.txt
Hello world!
I am HenryHZY.
line 1
iine 2
line 3
line 4
line 5
line 6
iine 7
line 8
line 9
line 10
line 11
iine 12
line 13
line 14
line 15
line 16
iine 17
line 18
selpg: done
other_command | selpg -s1 -e1
“other_command”的标准输出被 shell/内核重定向至 selpg 的标准输入。将第 1页写至 selpg 的标准输出(屏幕)。
[henryhzy@localhost selpg]$ ls | selpg -s1 -e1
error.txt
in.txt
out.txt
selpg.go
selpg: done
selpg -s1 -e1 in.txt >out.txt
selpg 将第 1 页写至标准输出;标准输出被 shell/内核重定向至out.txt“”。
selpg -s20 -e20 in.txt 2>error.txt
selpg 将第 20 页至标准输出(屏幕);所有的错误消息被 shell/内核重定向至“error.txt”。请注意:在“2”和“>”之间不能有空格;这是 shell 语法的一部分(请参阅“man bash”或“man sh”)。
selpg -s1 -e1 in.txt >out.txt 2>error.txt
selpg 将第 1页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至“error_file”。当“input_file”很大时可使用这种调用;您不会想坐在那里等着 selpg 完成工作,并且您希望对输出和错误都进行保存。
selpg -s20 -e20 in.txt >out.txt 2>/dev/null
selpg 将第 20 页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至 /dev/null(空设备),这意味着错误消息被丢弃了。设备文件 /dev/null 废弃所有写至它的输出,当从该设备文件读取时,会立即返回 EOF。
此处本应有的的error信息被丢弃了。
selpg -s10 -e20 in.txt >/dev/null
selpg 将第 10 页到第 20 页写至标准输出,标准输出被丢弃;错误消息在屏幕出现。这可作为测试 selpg 的用途,此时您也许只想(对一些测试情况)检查错误消息,而不想看到正常输出。
[henryhzy@localhost selpg]$ selpg -s10 -e20 in.txt >/dev/null
selpg: start_page (10) greater than total pages (1), no output written
selpg: done
selpg -s10 -e20 input_file 2>error_file | other_command
selpg 的标准输出透明地被 shell/内核重定向,成为“other_command”的标准输入,第 1页被写至该标准输入。“other_command”的示例可以是 lp,它使输出在系统缺省打印机上打印。“other_command”的示例也可以 wc,它会显示选定范围的页中包含的行数、字数和字符数。“other_command”可以是任何其它能从其标准输入读取的命令。错误消息仍在屏幕显示。
[henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt | ps
selpg: done
PID TTY TIME CMD
10209 pts/0 00:00:00 bash
10528 pts/0 00:00:00 ps
selpg -s10 -e20 input_file 2>error_file | other_command
与上面的示例 9 相似,只有一点不同:错误消息被写至“error_file”。
selpg -s1 -e1 -l10 in.txt
该命令将页长设置为 10 行,这样 selpg 就可以把输入当作被定界为该长度的页那样处理。文本的前10行被写至 selpg 的标准输出(屏幕)。
[henryhzy@localhost selpg]$ selpg -s1 -e1 -l10 in.txt
Hello world!
I am HenryHZY.
line 1
iine 2
line 3
line 4
line 5
line 6
iine 7
line 8
selpg: done
selpg -s1 -e1 -f in.txt
假定页由换页符定界。第 10页被写至 selpg 的标准输出(屏幕)。
[henryhzy@localhost selpg]$ selpg -s1 -e1 -f in.txt
Hello world!
I am HenryHZY.
line 1
iine 2
line 3
line 4
line 5
line 6
iine 7
line 8
line 9
line 10
line 11
iine 12
line 13
line 14
line 15
line 16
iine 17
line 18
selpg: done
selpg -s1 -e1 in.txt | cat -n
由于没有打印机,原测试的打印机输出改为cat输出。
[henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt | cat -n
selpg: done
1 Hello world!
2 I am HenryHZY.
3 line 1
4 iine 2
5 line 3
6 line 4
7 line 5
8 line 6
9 iine 7
10 line 8
11 line 9
12 line 10
13 line 11
14 iine 12
15 line 13
16 line 14
17 line 15
18 line 16
19 iine 17
20 line 18
selpg -s10 -e20 in.txt > out.txt 2>error.txt &
该命令利用了 Linux 的一个强大特性,即:在“后台”运行进程的能力。在这个例子中发生的情况是:“进程标识”(pid)如 1234 将被显示,然后 shell 提示符几乎立刻会出现,使得您能向 shell 输入更多命令。同时,selpg 进程在后台运行,并且标准输出和标准错误都被重定向至文件。这样做的好处是您可以在 selpg 运行时继续做其它工作。
2.单元测试
根据查找的单元测试与功能测试的区别:
功能测试是站在用户的角度从外部测试应用查看效果是否达到
单元测试是站在程序员的角度从内部测试应用
既然已经测试过功能测试,那么在进行时单元测试,简单地以函数为单元测试调用情况即可。若是在结合输入输出正确与否,感觉有些多余了。:)
测试代码:
package main
import "testing"
func Test_usage(t *testing.T) {
tes := []struct {
name string
}{
{name: "Test_usage1"},
{name: "Test_usage2"},
}
for _, tt := range tes {
t.Run(tt.name, func(t *testing.T) {
usage()
})
}
}
func Test_process_args(t *testing.T) {
tests := []struct {
name string
len int
sa sp_args
}{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
process_args(tt.len, &tt.sa)
})
}
}
func Test_process_input(t *testing.T) {
tests := []struct {
name string
sa sp_args
}{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
process_input(tt.sa)
})
}
}
func Test_main(t *testing.T) {
tests := []struct {
name string
}{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
main()
})
}
}
为了方便进行单元测试,此处使用vscode进行测试,直接点击相应函数上面的run test即可,非常方便~~
各个函数的测试截图如下:
具体代码可见gitee仓库:gitee仓库