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

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

概述

CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。Linux提供了cat、ls、copy等命令与操作系统交互;go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;git、npm等都是大家比较熟悉的工具。尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。

实现

selpg 程序逻辑

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

参数处理

“-sNumber”和“-eNumber”强制选项:
selpg 要求用户用两个命令行参数“-sNumber”(例如,“-s10”表示从第 10 页开始)和“-eNumber”(例如,“-e20”表示在第 20 页结束)指定要抽取的页面范围的起始页和结束页。这两个选项,“=sNumber”和“-eNumber”是强制性的,而且必须是命令行上在命令名 selpg 之后的头两个参数:

$ selpg -s10 -e20 ...

“-lNumber”和“-f”可选选项:
selpg 可以处理两种输入文本:

  • 类型 1:该类文本的页行数固定。这是缺省类型,因此不必给出选项进行说明。也就是说,如果既没有给出“-lNumber”也没有给出“-f”选项,则 selpg 会理解为页有固定的长度(每页 72 行)。选择 72 作为缺省值是因为在行打印机上这是很常见的页长度。这样做的意图是将最常见的命令用法作为缺省值,这样用户就不必输入多余的选项。该缺省值可以用“-lNumber”选项覆盖,如下所示:
    $ selpg -s10 -e20 -l66 ...
    
  • 类型 2:该类型文本的页由 ASCII 换页字符(十进制数值为 12,在 C 中用“\f”表示)定界。该格式与“每页行数固定”格式相比的好处在于,当每页的行数有很大不同而且文件有很多页时,该格式可以节省磁盘空间。在含有文本的行后面,类型 2 的页只需要一个字符 ― 换页 ― 就可以表示该页的结束。
    类型 2 格式由“-f”选项表示,如下所示:
    $ selpg -s10 -e20 -f ...
    

“-dDestination”可选选项:
selpg 还允许用户使用“-dDestination”选项将选定的页直接发送至打印机。这里,“Destination”应该是 lp 命令“-d”选项(请参阅“man lp”)可接受的打印目的地名称。

$ selpg -s10 -e20 -dlp1

代码实现

//定义保存参数数据的结构体
type selpgArgs struct {  
	startPage  int
	endPage    int
	inFileName string
	pageLen    int
	pageType   bool
	printDest  string
}

//输入参数使用 github.com/spf13/pflag 包提供的pflag进行处理
func getArgs(args *selpgArgs) {

	pflag.IntVarP(&(args.startPage), "startPage", "s", -1, "Define startPage")
	pflag.IntVarP(&(args.endPage), "endPage", "e", -1, "Define endPage")
	pflag.IntVarP(&(args.pageLen), "pageLength", "l", 72, "Define pageLength")
	pflag.StringVarP(&(args.printDest), "printDest", "d", "", "Define printDest")
	pflag.BoolVarP(&(args.pageType), "pageType", "f", false, "Define pageType")
	pflag.Parse()

	argLeft := pflag.Args()
	if len(argLeft) > 0 {
		args.inFileName = string(argLeft[0])
	} else {
		args.inFileName = ""
	}
}

checkArgs函数

  • 先检查开始页args.startPage和结束页args.endPage是否被赋值,然后检查开始页args.startPage和结束页args.endPage是否为正数;
  • 然后检查开始页args.startPage是否大于结束页args.endPage;检查自定页长-l和遇换页符换页-f是否同时出现。
  • 最后判断当自定页长-l出现时args.pageLen是否小于1。遇到不合规的地方正常结束程序,全部合规则输出得到的参数。
//命令行获取之参数后,先进行参数检查以避免参数错误。
//如果出现错误则将问题输出并正常结束程序。
//如果参数正确则把正确参数值输出到屏幕上。
func checkArgs(args *selpgArgs) {

	if (args.startPage == -1) || (args.endPage == -1) {
		fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be empty! Please check your command!\n")
		os.Exit(2)
	} else if (args.startPage <= 0) || (args.endPage <= 0) {
		fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be negative! Please check your command!\n")
		os.Exit(3)
	} else if args.startPage > args.endPage {
		fmt.Fprintf(os.Stderr, "\n[Error]The startPage can't be bigger than the endPage! Please check your command!\n")
		os.Exit(4)
	} else if (args.pageType == true) && (args.pageLen != 72) {
		fmt.Fprintf(os.Stderr, "\n[Error]The command -l and -f are exclusive, you can't use them together!\n")
		os.Exit(5)
	} else if args.pageLen <= 0 {
		fmt.Fprintf(os.Stderr, "\n[Error]The pageLen can't be less than 1 ! Please check your command!\n")
		os.Exit(6)
	} else {
		pageType := "page length."
		if args.pageType == true {
			pageType = "The end sign /f."
		}
		fmt.Printf("\n[ArgsStart]\n")
		fmt.Printf("startPage: %d\nendPage: %d\ninputFile: %s\npageLength: %d\npageType: %s\nprintDestation: %s\n[ArgsEnd]", args.startPage, args.endPage, args.inFileName, args.pageLen, pageType, args.printDest)
	}

}

//检查结束之后,开始调用excuteCMD函数执行命令。
func checkError(err error, object string) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "\n[Error]%s:", object)
		panic(err)
	}
}

在excuteCMD函数中:
1、检查输入。如果没有给定文件名,则从标准输入中获取;如果给出读取的文件名,则调用函数checkFileAccess检查文件是否存在。
2、打开文件,使用函数checkError检查是否出现错误。如果打开出错则输出错误并抛出恐慌。
3、判断是否有-d参数。如果没有-d参数,选择的页直接从os.Stdout标准输出中输出。如果-d存在,则从指定的打印通道中输出。

func excuteCMD(args *selpgArgs) {
	var fin *os.File
	if args.inFileName == "" {
		fin = os.Stdin
	} else {
		checkFileAccess(args.inFileName)
		var err error
		fin, err = os.Open(args.inFileName)
		checkError(err, "File input")
	}

	if len(args.printDest) == 0 {
		output2Des(os.Stdout, fin, args.startPage, args.endPage, args.pageLen, args.pageType)
	} else {
		output2Des(cmdExec(args.printDest), fin, args.startPage, args.endPage, args.pageLen, args.pageType)
	}
}

func checkFileAccess(filename string) {
	_, errFileExits := os.Stat(filename)
	if os.IsNotExist(errFileExits) {
		fmt.Fprintf(os.Stderr, "\n[Error]: input file \"%s\" does not exist\n", filename)
		os.Exit(7)
	}
}

在-d参数存在时,就需要os/exec包的使用

func cmdExec(printDest string) io.WriteCloser {
    cmd := exec.Command("lp", "-d"+printDest)
    fout, err := cmd.StdinPipe()
    checkError(err, "StdinPipe")
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    errStart := cmd.Run()
    checkError(errStart, "CMD run")
    return fout
}

最后再使用输出函数output2Des,将输入的文件,按页码要求读取并输出到fout中。

func output2Des(fout interface{}, fin *os.File, pageStart int, pageEnd int, pageLen int, pageType bool) {

	lineCount := 0
	pageCount := 1
	buf := bufio.NewReader(fin)
	for true {

		var line string
		var err error
		if pageType {
			//If the command argument is -f
			line, err = buf.ReadString('\f')
			pageCount++
		} else {
			//If the command argument is -lnumber
			line, err = buf.ReadString('\n')
			lineCount++
			if lineCount > pageLen {
				pageCount++
				lineCount = 1
			}
		}

		if err == io.EOF {
			break
		}
		checkError(err, "file read in")

		if (pageCount >= pageStart) && (pageCount <= pageEnd) {
			var outputErr error
			if stdOutput, ok := fout.(*os.File); ok {
				_, outputErr = fmt.Fprintf(stdOutput, "%s", line)
			} else if pipeOutput, ok := fout.(io.WriteCloser); ok {
				_, outputErr = pipeOutput.Write([]byte(line))
			} else {
				fmt.Fprintf(os.Stderr, "\n[Error]:fout type error. ")
				os.Exit(8)
			}
			checkError(outputErr, "Error happend when output the pages.")
		}
	}
	if pageCount < pageStart {
		fmt.Fprintf(os.Stderr, "\n[Error]: startPage (%d) greater than total pages (%d), no output written\n", pageStart, pageCount)
		os.Exit(9)
	} else if pageCount < pageEnd {
		fmt.Fprintf(os.Stderr, "\n[Error]: endPage (%d) greater than total pages (%d), less output than expected\n", pageEnd, pageCount)
		os.Exit(10)
	}
}

程序测试

按文档 使用 selpg 章节要求测试你的程序
分别使用命令:

$ selpg -s1 -e1 input_file.txt

CLI 命令行实用程序开发基础_第1张图片

$ selpg -s1 -e1 < input_file.txt

CLI 命令行实用程序开发基础_第2张图片

selpg -s1 -e2 input_file.txt > output_file.txt

在这里插入图片描述
CLI 命令行实用程序开发基础_第3张图片

selpg -s1 -e3 input_file.txt > output_file.txt 2>error_file

CLI 命令行实用程序开发基础_第4张图片

selpg -s1 -e2 -f input_file.txt 

CLI 命令行实用程序开发基础_第5张图片

你可能感兴趣的:(CLI 命令行实用程序开发基础)