CLI 命令行实用程序开发基础
文章目录
实验环境
操作系统:Ubuntu18.04.5LTS-amd64
编辑器:VScode、Typora
概述
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。
开发过程
开发实践要求
使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg
提示:
- 请按文档 使用 selpg 章节要求测试你的程序
- 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
- golang 文件读写、读环境变量,请自己查 os 包
- “-dXXX” 实现,请自己查
os/exec
库,例如案例 Command,管理子进程的标准输入和输出通常使用io.Pipe
,具体案例见 Pipe - 请自带测试程序,确保函数等功能正确
总体结构
- selpg.go包含selpg的相关程序
- selpg_test.go包含相关测试函数
- selpg为可执行二进制文件
- test、out、error为测试输入输出所用的文件
安装pflag和使用
安装
go get github.com/spf13/pflag
import
import flag "github.com/spf13/pflag"
代码可以使用flag.xxx
来调用函数了;
使用
pflag包在此处的优势是它对参数的读取与unix标准相同;
pflag包基本用法和flag包类似,由于flag包文档多,基本是参照flag包的文档来使用;
参考C源码
开发 Linux 命令行实用程序 中提供了 selpg.c 可进行参考;
事实上,go与c之间的良好的移植性可以简化代码开发;
而且在许多方面,go的实现能更简单快捷,使得整体程序不像c那样繁琐,就是可能效率慢点;
下面的代码中有许多是移植C源码的,但在pflag、输入输出相关的方面需要做大的改动。
以下代码如无特别说明,均位于selpg.go中
定义结构体以存储参数
type selpgArgs struct {
startPage int
endPage int
inFilename string
pageLen int
pageType bool
printDest string
}
- startPage:开始页码
- endPage:结束页码
- inFilename:输入的文件名
- printDest:输出的文件名
- pageLen:每页的行数
- pageType:打印的模式,"-l"按行打印,"-f"按换页符打印
测试型开发
程序需要进行参数的初始化,在Initflag
函数中完成,对其的测试函数:
函数位于selpg_test.go
func TestInitflag(t *testing.T) {
want := selpgArgs{0, 0, "", 72, false, ""}
var got selpgArgs
Initflag(&got)
if got != want {
t.Errorf("Initflag got %v, want %v\n", got, want)
}
}
程序需要检查参数格式,在ProcessArgs
函数中完成,对其的测试函数:
函数位于selpg_test.go
func TestProcessArgs(t *testing.T) {
cases := []struct {
insel selpgArgs
inos []string
want string
}{
{selpgArgs{1, 1, "test", 72, false, ""}, []string{"./selpg", "-s1", "-e1", "test"}, ""},
{selpgArgs{0, 1, "test", 72, false, ""}, []string{"./selpg", "-s0", "-e1", "test"}, "./selpg: invalid start page 0\n"},
{selpgArgs{2, 1, "test", 72, false, ""}, []string{"./selpg", "-s2", "-e1", "test"}, "./selpg: invalid end page 1\n"},
{selpgArgs{1, 0, "", 72, false, ""}, []string{"./selpg", "-s1"}, "./selpg: not enough arguments\n"},
{selpgArgs{0, 1, "test", 72, false, ""}, []string{"./selpg", "-e1", "-e1", "test"}, "./selpg: 1st arg should be -sstartPage\n"},
{selpgArgs{1, 0, "test", 72, false, ""}, []string{"./selpg", "-s1", "-s1", "test"}, "./selpg: 2nd arg should be -eendPage\n"},
}
for _, c := range cases {
got := ProcessArgs(&c.insel, c.inos)
if got != c.want {
t.Errorf("ProcessArgs(%v, %v) == %s, want %s\n", c.insel, c.inos, got, c.want)
}
}
}
绑定flag到变量上并初始化
pflag包中处理参数的关键;
func Initflag(sa *selpgArgs) {
flag.IntVarP(&sa.startPage, "startPage", "s", 0, "Start page number")
flag.IntVarP(&sa.endPage, "endPage", "e", 0, "End page number")
flag.IntVarP(&sa.pageLen, "pageLen", "l", 72, "Line number for a page")
flag.BoolVarP(&sa.pageType, "pageType", "f", false, "Determine form-feed-delimited")
flag.StringVarP(&sa.printDest, "dest", "d", "", "Set printer")
flag.Usage = usage
flag.Parse()
}
提供使用说明
func usage() {
fmt.Printf("\nUSAGE: %s -sstartPage -eendPage [ -f | -llines_per_page ] [ -ddest ] [ inFilename ]\n", progname)
}
检验参数
检验必要的必要参数是否有、参数形式是否正确、参数内容是否合理(如页码不能少于1、起始页码不能大于终止页码等);
// ProcessArgs check args format
func ProcessArgs(sa *selpgArgs, args []string) string {
progname = args[0]
/* check the command-line arguments for validity */
if len(args) < 3 {
return fmt.Sprintf("%s: not enough arguments\n", progname)
}
/* handle 1st arg - start page */
if args[1][1] != 's' {
return fmt.Sprintf("%s: 1st arg should be -sstartPage\n", progname)
}
if sa.startPage < 1 || sa.startPage > (math.MaxInt32-1) {
return fmt.Sprintf("%s: invalid start page %d\n", progname, sa.startPage)
}
/* handle 2nd arg - end page */
index := 2
if len(args[1]) == 2 {
index = 3
}
if args[index][1] != 'e' {
return fmt.Sprintf("%s: 2nd arg should be -eendPage\n", progname)
}
if sa.endPage < 1 || sa.endPage > (math.MaxInt32-1) || sa.startPage > sa.endPage {
return fmt.Sprintf("%s: invalid end page %d\n", progname, sa.endPage)
}
var noerr string
return noerr
}
执行命令
根据参数执行相应的命令;
func processInput(sa *selpg_args) {...}
使用的部分标准库中的函数
关于io.Writecloser :WriteCloser 接口组合了基本的 Write 和 Close 方法。
Write 将 len§ 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数 n(0 <= n <= len§)以及任何遇到的引起写入提前停止的错误。
关于exec.Cmd :Cmd表示正在准备或运行的外部命令。
关于Cmd.Run:func (c *Cmd) Run() error
Run starts the specified command and waits for it to complete.
关于StdinPipe:func (c *Cmd) StdinPipe() (io.WriteCloser, error)
,StdinPipe返回一个管道,该管道将在命令启动时连接到命令的标准输入。
关于NewReader:func NewReader(rd io.Reader) *Reader
,返回一个新的Reader;
关于ReadString:func (b *Reader) ReadString(delim byte) (line string, err error)
,ReadString读取输入到第一次终止符发生的时候,返回的string包含从当前到终止符的内容(包括终止符)。
关于ReadLine:ReadLine尝试返回单个行,不包括行尾的最后一个分隔符。
关于Scanner:Scanner类型提供了方便的读取数据的接口,如从换行符分隔的文本里读取每一行。
- exec包的使用是为了获得外部的程序,得以调用cat命令来启用打印设备;
- 程序中采用三种读取方式
ReadString
、ReadLine
、Scanner
是为了更方便的应对三种不同的读取场景,ReadString
是为了方便读取到换页符\f
,ReadLine
是为了按行打印的模式运行,Scanner
则是适应最参数缺省下的场景;
部分代码展示与解释
利用os/exec包和io包管理进程的输入与输出,将cat命令放在管道中,在相应时间且需要打印时调用Run函数运行。
cat(英文全拼:concatenate)命令用于连接文件并打印到标准输出设备上。
var stdin io.WriteCloser
var cmd *exec.Cmd
...
if sa.printDest != "" {
var err1 error
var err2 error
cmd = exec.Command("cat")
cmd.Stdout, err1 = os.OpenFile(sa.printDest, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err1 != nil {
fmt.Fprintf(os.Stderr, "\n%s: fail to open file %s\n", progname, sa.printDest)
os.Exit(4)
}
stdin, err2 = cmd.StdinPipe()
if err2 != nil {
fmt.Fprintf(os.Stderr, "\n%s: fail to open pipe to %s\n", progname, sa.printDest)
os.Exit(5)
}
} else {
stdin = nil
}
...
if sa.printDest != "" {
stdin.Close()
err := cmd.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "\n%s: fail to connect to device\n", progname)
os.Exit(9)
}
}
按行打印模式下的程序读写,通过ReadLine函数来计算读取的行,并以此确定页数;
count := 0
for {
line, _, err := reader.ReadLine()
if err != io.EOF && err != nil {
return "", fmt.Sprintf("\n%s: fail to read line\n", progname)
}
if err == io.EOF {
break
}
if count/sa.pageLen+1 >= sa.startPage {
if count/sa.pageLen+1 <= sa.endPage {
// printAns(sa, string(line), stdin)
ref += string(line)
ref += "\n"
} else {
break
}
}
count++
}
打印输出
打印输出时需要考虑是输出到打印设备还是直接输出到终端;
- 注意:这里输出到打印设备只是通过
Write函数
将信息放在管道中,直到main
函数中cmd.Run()
时才输出到打印设备。
func printAns(sa *selpg_args, line string, stdin io.WriteCloser) {
if sa.print_dest != "" {
stdin.Write([]byte(line + "\n"))
} else {
fmt.Println(line)
}
}
main函数
调用各函数,并处理错误的输出、打印设备的输出;
func main() {
var sa selpgArgs
Initflag(&sa)
err := ProcessArgs(&sa, os.Args)
if err != "" {
fmt.Fprintf(os.Stderr, err)
flag.Usage()
os.Exit(1)
}
ans, err := ProcessInput(&sa)
if err != "" {
fmt.Fprintf(os.Stderr, err)
os.Exit(1)
} else {
printAns(&sa, ans, stdin)
}
if sa.printDest != "" {
stdin.Close()
err := cmd.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "\n%s: fail to connect to device\n", progname)
}
}
}
测试
- 关于运行程序,文件夹中已有selpg二进制文件,可以通过
./selpg xxxx
的方式运行,也可以使用go install
命令,之后用selpg xxxx
的方式运行。
单元测试
运行单元测试函数
功能测试
- 实验中的test文档在Gitee上也包含,文档中包含一系列test Line帮助检验输出行数,而输出的空行是
换行符
,换行符
以空行显示但它并不占据实际的输出行数,这点在1中可以明显看出,说明程序运行正确。
pfalg中支持多种参数输入形式,如
-s1
、-s 1
、-s=1
均可以,根据题目下面测试均采用unix格式,即-s1
形式,但其余形式均也可以正常运行。
wc利用wc指令我们可以计算文件的Byte数、字数、或是列数
测试均参考开发 Linux 命令行实用程序的使用selpg
,但部分演示为了能演示功能,修改了起始终止页码等部分参数;
-
selpg -s1 -e1 test
输出较长,截图只截了前后两部分
……
-
selpg -s1 -e1 < test
输出较长,截图只截了前后两部分
……
-
cat test | selpg -s10 -e20
输出较长,截图只截了前后两部分
……
-
selpg -s3 -e5 -l10 test >out
out文件:
-
selpg -s20 -e10 test 2>error
error文件:
-
selpg -s1 -e2 -l10 test >out 2>error
out文件:
error文件:为空,因为无错误
-
selpg -s2 -e1 test >out 2>/dev/null
out文件:输入提示被输入到此处
error文件:为空,报错信息被丢弃
-
selpg -s10 -e20 test >/dev/null
out文件:为空
-
selpg -s1 -e1 test | wc
-
selpg -s2 -e1 test 2>error | wc
error文件:
-
selpg -s2 -e2 -l 16 test
-
selpg -s1 -e1 -f test
输出较长,截图只截了前后两部分
……
-
selpg -s2 -e3 -l10 -dout test
out文件:
部分报错情况测试
-
selpg -s0 -e1 test
-
selpg -s1
-
selpg -s1 -e1 tes