服务计算 作业三 开发简单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

  1. 输入
    应该允许输入来自以下两种方式:

    • 在命令行上指定的文件名。例如:

      $ command input_file

    • 标准输入(stdin),缺省情况下为终端(也就是用户的键盘)。例如:

      $ command

    重定向:
    使用 shell 操作符“<”(重定向标准输入),也可将标准输入重定向为来自文件,如下所示:

    command < input_file

    管道:
    使用 shell 操作符“|”(pipe)也可以使标准输入来自另一个程序的标准输出,如下所示:

    other_command | command
    这里,other_command 的标准输出(stdout)被 shell/内核透明地传递至 command 的标准输入。

  2. 输出
    输出应该被写至标准输出,缺省情况下标准输出同样也是终端(也就是用户的屏幕):

    $ command

    类似地,使用 shell 操作符“>”(重定向标准输出)可以将标准输出重定向至文件

    command > output_file

    还是使用“|”操作符,command 的输出可以成为另一个程序的标准输入,如下所示:

    $ command | other_command

更多有关IO部分的内容移步开发 Linux 命令行实用程序

Golang 的支持
使用flag/pflag包来绑定参数(二者大同小异,这里主要关注flag的使用)

  1. 安装pflag:

    go get github.com/spf13/pflag
    
  2. 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
在这里插入图片描述

  1. 测试go run selpg.go -s1 -e1 input.txt
    在这里插入图片描述
    此时输出第1到7行,正确
  2. 测试go run selpg.go -s2 -e1 input.txt
    在这里插入图片描述
    此时startPage > endPage,报错
  3. 测试go run selpg.go -s0 -e1 input.txt
    在这里插入图片描述
    此时startPage == 0,不合法,报错
  4. 测试go run selpg.go -s1 -e2 -l4 input.txt
    在这里插入图片描述
    此时修改pageLen参数为4,可以看到输出前两页到line 8,正确
  5. 测试 go run selpg.go -s1 -e2 input.txt > output.txt
    在这里插入图片描述
    此时成功写入
  6. 测试go run selpg.go -s3 -e2 input.txt > output.txt 2>error.txt
    在这里插入图片描述
    此时显然startPage > endPage, output.txt中没有写入内容,而error.txt中写入错误信息
  7. 测试more input.txt | go run selpg.go -s1 -e1
    在这里插入图片描述
    此时重定向成功
  8. 测试go run selpg.go -s1 -e2 input.txt | cat -n
    在这里插入图片描述
  9. 测试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))
		})
	}
}

测试结果为:
在这里插入图片描述
单元测试通过。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值