服务计算(3)——开发简单CLI程序(selpg)

1、概述

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

  • Linux提供了cat、ls、copy等命令与操作系统交互;
  • go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
  • 容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
  • git、npm等也是大家比较熟悉的工具

尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。

2、基础知识

命令行程序主要涉及内容:

  • 命令
  • 命令行参数
  • 选项:长格式、短格式
  • IO:stdin、stdout、stderr、管道、重定向
  • 环境变量

3、Golang 的支持

通过pflag包来获取命令行参数。

	pflag.IntVarP(&(args.startPage),"startPage","s",-1,"start page")
	pflag.IntVarP(&(args.endPage),"endPage","e",-1,"end page")
	pflag.IntVarP(&(args.pageLen),"pageLen","l",72,"the length of page")
	pflag.BoolVarP(&(args.pageType),"pageType","f",false,"page type")
	pflag.StringVarP(&(args.outDestination),"outDestination","d","","print destination")
	pflag.Parse()
	args_left:=pflag.Args() // 其余参数
	if len(args_left) > 0 {
    
    
		args.inFile=args_left[0]
	} else {
    
    
		args.inFile=""
	}

4、开发实践

要求

使用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介绍

selpg是从文本输入选择页范围的实用程序。该输入可以来自作为最后一个命令行参数指定的文件,在没有给出文件名参数时也可以来自标准输入。

selpg首先处理所有的命令行参数。在扫描了所有的选项参数(也就是那些以连字符为前缀的参数)后,如果 selpg 发现还有一个参数,则它会接受该参数为输入文件的名称并尝试打开它以进行读取。如果没有其它参数,则 selpg 假定输入来自标准输入。

指令格式:selpg -sNumber -eNumber [ -f | -lNumber ][ -dDestination ] [ input_filename ]

详情可看文档

我的完整代码

设计与实现

首先,先要安装spf13/pflag。

go get github.com/spf13/pflag

根据指令的各个参数,可以创建一个结构体来储存。

type selpg_args struct{
    
    
	startPage      int
	endPage        int
	inFile         string
	pageLen        int
	pageType       bool // true for -f, false for -lNumber
	outDestination string
}

对于命令行中输入的参数,需要通过pflag来解析,解析完之后存储到结构体当中。

(ps:为什么不能用flag而是要用pflag呢?因为用flag来解析参数的话,参数后面的值需要用空格' '或者等号'='来隔开,那么指令就会变成selpg -s Number -e Number [ -f | -l Number ][ -d Destination ] [ input_filename ]或是selpg -s=Number -e=Number [ -f | -l=Number ][ -d=Destination ] [ input_filename ],这样并不满足Unix命令行规范。)

因而,程序的设计可以分为如下步骤:

解析参数、检查参数、处理输入、输出

解析参数

func getArgs(args *selpg_args){
    
    
	pflag.IntVarP(&(args.startPage),"startPage","s",-1,"start page")
	pflag.IntVarP(&(args.endPage),"endPage","e",-1,"end page")
	pflag.IntVarP(&(args.pageLen),"pageLen","l",72,"the length of page")
	pflag.BoolVarP(&(args.pageType),"pageType","f",false,"page type")
	pflag.StringVarP(&(args.outDestination),"outDestination","d","","print destination")
	pflag.Parse()
	args_left:=pflag.Args() // 其余参数
	if len(args_left) > 0 {
    
    
		args.inFile=args_left[0]
	} else {
    
    
		args.inFile=""
	}
	check_args(args)
}

主要考验的是对pflag包的使用。

检查参数

func check_args(args *selpg_args){
    
    
	if args==nil{
    
    
		fmt.Fprintf(os.Stderr,"\n[Error]The args is nil!Please check your program!\n\n")
		os.Exit(0)
	}else if(args.startPage==-1)||(args.endPage==-1){
    
    
		fmt.Fprintf(os.Stderr,"\n[Error]The startPage and endPage is not allowed empty!Please check your command!\n\n")
		os.Exit(0)
	}else if (args.startPage<0)||(args.endPage<0){
    
    
		fmt.Fprintf(os.Stderr,"\n[Error]The startPage and endPage is not negative!Please check your command!\n\n")
		os.Exit(0)
	}else if args.startPage>args.endPage{
    
    
		fmt.Fprintf(os.Stderr,"\n[Error]The startPage can not be bigger than the endPage!Please check your command!\n\n")
		os.Exit(0)
	}
	if args.pageType==false&&args.pageLen<1 {
    
    
		fmt.Fprintln(os.Stderr,"\n[Error]You should input valid page length!\n\n")
		os.Exit(0)
	}
}

主要是对-s-e两个强制选项做判断,还有就是-l里面定义的行数不能小于1。

处理输入

func processInput(args *selpg_args){
    
    
	var reader *bufio.Reader
	if args.inFile=="" {
    
    
		reader=bufio.NewReader(os.Stdin)
	} else{
    
    
		fileIn,err:=os.Open(args.inFile)
		defer fileIn.Close()
		if err!=nil {
    
    
			os.Stderr.Write([]byte("Open file error.\n"))
			os.Exit(0)
		}
		reader=bufio.NewReader(fileIn)
	}
	output(reader,args)
}

输入的处理主要是要判断是标准输入亦或是文件源输入,最后一个命令行参数指定源文件,但有时候缺少该参数时用的就是标准输入。

输出

	writer:=bufio.NewWriter(os.Stdout)
	lineCtr:=0
	pageCtr:=1
	endSign:='\n'
	if args.pageType==true {
    
    
		endSign='\f'
	}
	for{
    
    
		strLine,errRead:=reader.ReadBytes(byte(endSign))
		if errRead!=nil {
    
    
			if errRead==io.EOF {
    
    
				writer.Flush()
				break
			} else{
    
    
				os.Stderr.Write([]byte("Read bytes from reader failed.\n"))
				os.Exit(0)
			}
		}
		if pageCtr>=args.startPage&&pageCtr<=args.endPage {
    
    
			_,errWrite:=writer.Write(strLine)
			if errWrite!=nil {
    
    
				fmt.Println(errWrite)
				os.Stderr.Write([]byte("Write bytes to out failed.\n"))
				os.Exit(0)
			}
		}
		if args.pageType==true {
    
    
			pageCtr++
		} else{
    
    
			lineCtr++
		}
		if args.pageType==false&&lineCtr==args.pageLen {
    
    
			lineCtr=0
			pageCtr++
		}
		if pageCtr>args.endPage {
    
    
			writer.Flush()
			break
		}
	}

首先就要区分-f类型和-lNumber类型,一个读到'\f'就分页,一个是读取pageLen行再分页。在ReadBytes的时候可以根据情况来决定相应的终止符号,读到相应的页(从startPage到endPage)就输出便可。

同时,还要处理-d参数,表示送到相应的打印机打印,但是因为我这里没有连接打印机,因而只是另外写了一个程序去模拟打印机。

	var cmd *exec.Cmd=nil
	var stdin io.WriteCloser=nil
	writer:=bufio.NewWriter(os.Stdout)
	if args.outDestination!="" {
    
    
		cmd=exec.Command(args.outDestination)
		var pipeErr error;
		stdin,pipeErr=cmd.StdinPipe()
		if pipeErr!=nil {
    
    
			fmt.Println(pipeErr)
			os.Exit(0)
		}
		startErr:=cmd.Start()
		if startErr!=nil {
    
    
			fmt.Println(startErr)
			os.Exit(0)
		}
	}

基本思路就是通过io.Pipe来管理子进程的标准输入和输出,把内容传给子进程,再让子进程把内容输出到一个特定的文件里面。相应的程序在lp1.go里面实现。

ps:lp1.go的实现

首先,通过mkdir $GOPATH/src/github.com/github-user/lp1 -p来创建一个新目录,在新目录里面创建新文件lp1.go,编写程序。

package main
import(
	"bufio"
	"io"
	"os"
)
func main(){
    
    
	reader:=bufio.NewReader(os.Stdin)
	file,openErr:=os.OpenFile("./lp1.txt",os.O_RDWR|os.O_CREATE|os.O_APPEND,0644)
	if openErr!=nil {
    
    
		panic(openErr)
	}
	writer:=bufio.NewWriter(file)
	for{
    
    
		line,errRead:=reader.ReadBytes('\n')
		if errRead!=nil {
    
    
			if errRead==io.EOF {
    
    
				break
			} else {
    
    
				os.Stderr.Write([]byte("Read bytes from reader fail\n"))
				os.Exit(0)
			}
		}
		_,errWrite:=writer.Write(line)
		if errWrite!=nil {
    
    
			os.Stderr.Write([]byte("Write bytes to file fail\n"))
			os.Exit(0)
		}
		writer.Flush()
	}
}

无非就是打开目标文件,把从selpg.go传来的“标准输入”读取出来,写进目标文件里面。

编写完文件之后,需要执行go install github.com/github-user/lp1,方便后续在任何路径都能直接通过lp1来直接调用这个程序,(同样也使得这个模拟的打印机更加逼真)。

selpg.go也是如此,执行go install github.com/github-user/selpg,后续就能直接在终端直接通过selpg -sNumber -eNumber [ -f | -lNumber ][ -dDestination ] [ input_filename ]这样的指令格式来直接调用自己的程序。

原本,-d参数的处理应该是可以另外用一个函数来实现的,但是考虑到跟前面的输出函数有太多的相似之处,在这里我就直接将它们合为一起了。完整函数如下:

func output(reader *bufio.Reader,args *selpg_args){
    
    
	var cmd *exec.Cmd=nil
	var stdin io.WriteCloser=nil
	writer:=bufio.NewWriter(os.Stdout)
	if args.outDestination!="" {
    
    
		cmd=exec.Command(args.outDestination)
		var pipeErr error;
		stdin,pipeErr=cmd.StdinPipe()
		if pipeErr!=nil {
    
    
			fmt.Println(pipeErr)
			os.Exit(0)
		}
		startErr:=cmd.Start()
		if startErr!=nil {
    
    
			fmt.Println(startErr)
			os.Exit(0)
		}
	}
	lineCtr:=0
	pageCtr:=1
	endSign:='\n'
	if args.pageType==true {
    
    
		endSign='\f'
	}
	for{
    
    
		strLine,errRead:=reader.ReadBytes(byte(endSign))
		if errRead!=nil {
    
    
			if errRead==io.EOF {
    
    
				writer.Flush()
				break
			} else{
    
    
				os.Stderr.Write([]byte("Read bytes from reader failed.\n"))
				os.Exit(0)
			}
		}
		if pageCtr>=args.startPage&&pageCtr<=args.endPage {
    
    
			_,errWrite:=writer.Write(strLine)
			if errWrite!=nil {
    
    
				fmt.Println(errWrite)
				os.Stderr.Write([]byte("Write bytes to out failed.\n"))
				os.Exit(0)
			}
			if stdin!=nil {
    
    
				_,errWrite:=stdin.Write(strLine)
				if errWrite!=nil {
    
    
					fmt.Println(errWrite)
					os.Stderr.Write([]byte("Write bytes to out failed.\n"))
					os.Exit(0)
				}
			}
		}
		if args.pageType==true {
    
    
			pageCtr++
		} else{
    
    
			lineCtr++
		}
		if args.pageType==false&&lineCtr==args.pageLen {
    
    
			lineCtr=0
			pageCtr++
		}
		if pageCtr>args.endPage {
    
    
			writer.Flush()
			break
		}
	}
	if stdin!=nil {
    
    
		stdin.Close()
	}
	if cmd!=nil {
    
    
		if err:=cmd.Wait();err!=nil {
    
    
			fmt.Println(err)
			os.Exit(0)
		}
	}
}

相关指令的测试

  1. selpg -s1 -e1 input_file

在这里插入图片描述

在这里插入图片描述

2.selpg -s1 -e1 < input_file

在这里插入图片描述

这里同样也是72行为一页,但在这里,最后一个参数并不是作为selpg的源输入文件参数,当前selpg读取的是标准输入,只不过是这时候标准输入已被shell/内核重定向为来自“input_file”而不是显式命名的文件名参数。

这是shell自行实现了的,因而并不在我们程序的实现范围之内。

3.other_command | selpg -s10 -e20

在这里插入图片描述

这个也是shell自行实现好了的,“other_command”的标准输出被shell/内核重定向至 selpg 的标准输入。因为总共也不够72行(总共才1页),因而要输出第10到第20页的话就是空值。

4.selpg -s10 -e20 input_file >output_file

在这里插入图片描述

在这里插入图片描述

因为我的in.txt总共才101行,把第1页到第3页输出到out.txt的话,实际上也就输出了两页。这里同样地,shell也已经帮我们实现好了输出的重定向,标准输出被shell/内核重定向至“output_file”。

5.selpg -s10 -e20 input_file 2>error_file

在这里插入图片描述

在这里插入图片描述

输入一个错误的指令,就能看到错误信息输出到err.txt里面了。所有的错误消息被shell/内核重定向至“error_file”。请注意:在“2”和“>”之间不能有空格;这是 shell 语法的一部分(请参阅“man bash”或“man sh”)。

6.selpg -s10 -e20 input_file >output_file 2>error_file

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

out.txt里面新得到的内容跟先前的测试一样,同时err.txt里面的内容被清空了。

7.selpg -s10 -e20 input_file >output_file 2>/dev/null

在这里插入图片描述

写至标准输出的内容被重定向到了out.txt,写至标准错误的内容被丢弃。

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

在这里插入图片描述

9.selpg -s10 -e20 input_file | other_command

在这里插入图片描述

同时,这里的other_command还可以设置为先前自行写的模拟lp1程序。

在这里插入图片描述

在这里插入图片描述

10.selpg -s10 -e20 input_file 2>error_file | other_command

在这里插入图片描述

1.selpg -s10 -e20 -l66 input_file

在这里插入图片描述

在这里插入图片描述

2.selpg -s10 -e20 -f input_file

在这里插入图片描述

在这里插入图片描述

(这里,我在第72行的后面加了一个’\f’。)

3.selpg -s10 -e20 -dlp1 input_file

在这里插入图片描述

在这里插入图片描述

4.selpg -s10 -e20 input_file > output_file 2>error_file &

在这里插入图片描述

由于打印的内容太少,一下子就执行完了。

编写测试程序

为了方便,遇到了困难,这里并没有用go test,而是直接写了一个selpgTest.go程序。

同时,排除上诉语义差不多的指令,还有shell已经实现好了的输入/输出重定向,一般来说就对-l-f-d来做测试就差不多了。

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main(){
    
    
	var N,count int
	fileIn,err:=os.Open("in.txt")
	fileOut,err:=os.Open("out.txt")
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	dataIn:=make([]byte, 1000)
	dataOut:=make([]byte, 1000)
	N,err=fileIn.Read(dataIn)
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	cmd:=exec.Command("bash","-c","selpg -s1 -e1 in.txt >out.txt")
	if err = cmd.Run(); err != nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	count,err=fileOut.Read(dataOut)
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	count=0
	for i:=0;count<72;i++ {
    
    
		if dataIn[i]!=dataOut[i] {
    
    
			fmt.Println("Failed at test1.")
			os.Exit(0)
		}
		if dataIn[i]=='\n' {
    
    
			count++;
		}
	}
	cmd=exec.Command("bash","-c","selpg -s1 -e1 -f in.txt >out.txt")
	if err = cmd.Run(); err != nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	fileOut,err=os.Open("out.txt")
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	count,err=fileOut.Read(dataOut)
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	count=0
	for i:=1;count<1;i++ {
    
    
		if dataIn[i]!=dataOut[i] {
    
    
			fmt.Println("Failed at test2.")
		}
		if dataIn[i]=='\f' {
    
    
			count++;
		}
	}
	cmd=exec.Command("bash","-c","selpg -s2 -e2 -l66 in.txt >out.txt")
	if err = cmd.Run(); err != nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	fileOut,err=os.Open("out.txt")
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	count,err=fileOut.Read(dataOut)
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	count=0
	i:=0
	for ;count<66;i++ {
    
    
		if dataIn[i]=='\n' {
    
    
			count++;
		}
	}
	count=0
	for j:=0;count<66&&i<N; {
    
    
		if dataIn[i]!=dataOut[j] {
    
    
			fmt.Println("Failed at test3.")
			os.Exit(0)
		}
		if dataIn[i]=='\n' {
    
    
			count++;
		}
		i++;
		j++
	}
	cmd=exec.Command("bash","-c","selpg -s2 -e2 -l66 -dlp1 in.txt >out.txt")
	if err = cmd.Run(); err != nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	fileOut,err=os.Open("lp1.txt")
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	count,err=fileOut.Read(dataOut)
	if err!=nil {
    
    
		fmt.Print(err)
		fmt.Print('\n')
	}
	count=0
	for i=0;count<66;i++ {
    
    
		if dataIn[i]=='\n' {
    
    
			count++;
		}
	}
	count=0
	for j:=0;count<66&&i<N; {
    
    
		if dataIn[i]!=dataOut[j] {
    
    
			fmt.Printf("%d %d %q %q\n",i,j,dataIn[i],dataOut[j])
			fmt.Println("Failed at test4.")
			os.Exit(0)
		}
		if dataIn[i]=='\n' {
    
    
			count++;
		}
		i++;
		j++
	}
	fmt.Println("PASS.")
}

大致思路就是,通过Command的方式,将指令传到shell去执行,然后比对输出文件内容即可。最终执行程序,输出PASS即为通过。

在这里插入图片描述

5、代码提交

我的完整代码

猜你喜欢

转载自blog.csdn.net/qq_43278234/article/details/108942948