golang的selpg命令行程序开发

本文介绍了如何使用Golang开发名为selpg的Linux命令行工具,涵盖了开发环境、开发过程、安装pflag库、结构体定义、参数检验、执行命令和测试等方面。文章详细阐述了pflag的使用,以及如何通过标准库处理输入输出,提供了代码示例和测试方法。
摘要由CSDN通过智能技术生成

CLI 命令行实用程序开发基础

实验环境

操作系统:Ubuntu18.04.5LTS-amd64
编辑器:VScode、Typora

概述

CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。

开发过程

详细代码见这

开发实践要求

使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg

提示:

  1. 请按文档 使用 selpg 章节要求测试你的程序
  2. 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
  3. golang 文件读写、读环境变量,请自己查 os 包
  4. “-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe
  5. 请自带测试程序,确保函数等功能正确

总体结构

  • 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包的文档来使用;

更多相关用法可见github文档

参考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.Runfunc (c *Cmd) Run() error

Run starts the specified command and waits for it to complete.

关于StdinPipefunc (c *Cmd) StdinPipe() (io.WriteCloser, error),StdinPipe返回一个管道,该管道将在命令启动时连接到命令的标准输入。

关于NewReaderfunc NewReader(rd io.Reader) *Reader,返回一个新的Reader;

关于ReadStringfunc (b *Reader) ReadString(delim byte) (line string, err error),ReadString读取输入到第一次终止符发生的时候,返回的string包含从当前到终止符的内容(包括终止符)。

关于ReadLine:ReadLine尝试返回单个行,不包括行尾的最后一个分隔符。

关于Scanner:Scanner类型提供了方便的读取数据的接口,如从换行符分隔的文本里读取每一行。

  • exec包的使用是为了获得外部的程序,得以调用cat命令来启用打印设备;
  • 程序中采用三种读取方式ReadStringReadLineScanner 是为了更方便的应对三种不同的读取场景,ReadString是为了方便读取到换页符\fReadLine是为了按行打印的模式运行,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的方式运行。

单元测试

运行单元测试函数

27

28

功能测试

  • 实验中的test文档在Gitee上也包含,文档中包含一系列test Line帮助检验输出行数,而输出的空行是换行符换行符以空行显示但它并不占据实际的输出行数,这点在1中可以明显看出,说明程序运行正确。

pfalg中支持多种参数输入形式,如-s1-s 1-s=1均可以,根据题目下面测试均采用unix格式,即-s1形式,但其余形式均也可以正常运行。

wc利用wc指令我们可以计算文件的Byte数、字数、或是列数

测试均参考开发 Linux 命令行实用程序使用selpg,但部分演示为了能演示功能,修改了起始终止页码等部分参数;

  1. selpg -s1 -e1 test

    输出较长,截图只截了前后两部分

    1

    ……

    2

  2. selpg -s1 -e1 < test

    输出较长,截图只截了前后两部分

    3

    ……

    4

  3. cat test | selpg -s10 -e20

    输出较长,截图只截了前后两部分

    5

    ……

    6

  4. selpg -s3 -e5 -l10 test >out

    7

    out文件:

    8

  5. selpg -s20 -e10 test 2>error

    9

    error文件:

    10

  6. selpg -s1 -e2 -l10 test >out 2>error

    11

    out文件:

    12

    error文件:为空,因为无错误

  7. selpg -s2 -e1 test >out 2>/dev/null

    13

    out文件:输入提示被输入到此处

    14

    error文件:为空,报错信息被丢弃

  8. selpg -s10 -e20 test >/dev/null

    15

    out文件:为空

  9. selpg -s1 -e1 test | wc

    16

  10. selpg -s2 -e1 test 2>error | wc

    17

    error文件:

    18

  11. selpg -s2 -e2 -l 16 test

    19

  12. selpg -s1 -e1 -f test

    输出较长,截图只截了前后两部分

    20

    ……

    21

  13. selpg -s2 -e3 -l10 -dout test

    22

    out文件:

    23

部分报错情况测试

  1. selpg -s0 -e1 test

    24

  2. selpg -s1

    25

  3. selpg -s1 -e1 tes

    26

参考

开发 Linux 命令行实用程序

Golang之使用Flag和Pflag

额外的博客

godoc与go doc相关

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值