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

一、程序概述

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

 

二、selpg程序简介

selpg的基本介绍来自开发 Linux 命令行实用程序

selpg 是从文本输入选择页范围的实用程序。该输入可以来自作为最后一个命令行参数指定的文件,在没有给出文件名参数时也可以来自标准输入。简单来说就是允许用户指定从输入文本抽取的页的范围,根据这些范围来输出相应的文本,可以以标准输出流输出或是输出到目的地

 

三、基本命令参数

selpg基本处理的命令形式如下

selpg -s start_page -e endpage [-l pageline | -f][-d dest] filename

从输入命令来看,-s,-e,filename三个参数是必须的

-l | -f -d三个参数是可选的

对命令进行分析:

1.selpg是实现的程序可执行文件的名称

2. -s start page -e endpage

-s后面接开始读取的页号,类型是int

-e后面接读取结束的页号,类型是int

对于这两个参数的判断,开始页号和结束页号数值都必须大于1,并且开始页号要小于等于结束页号,否则需要输出错误信息

使用示例:

selpg  -s 20 -e 25 filename

3. -l pageline 和 -f

-l和-f两个参数互斥

-l 后面需要添加行数int,代表以 pageline行为界进行分页,输入命令时一般把该参数默认为-l,pageline值需要设置一个缺省值,当程序没有接收到-f并且缺少-l时采用这个缺省值

-f 后面不接参数,该类型文本的页由 ASCII 换页字符(十进制数值为 12,在 C 中用“\f”表示)定界。该格式与“每页行数固定”格式相比的好处在于,当每页的行数有很大不同而且文件有很多页时,该格式可以节省磁盘空间。在含有文本的行后面,类型 2 的页只需要一个字符 ― 换页 ― 就可以表示该页的结束。打印机会识别换页符并自动根据在新的页开始新行所需的行数移动打印头。该参数告诉 selpg 在输入中寻找换页符,并将其作为页定界符处理。

使用示例:

selpg -s 1 -e 2 -l 6 filename
selpg -s 1 -e 2 -f filename

4.-d dest

-d命令将选定的页直接发送至打印机,这里,dest应该是 lp 命令“-d”选项(请参阅“man lp”)可接受的打印目的地名称

使用示例:

selpg -s 10 -e 20 -d lp1

该命令将选定的页作为打印作业发送至 lp1 打印目的地

 

5.filename

filename是唯一一个无标识参数,表示读取的文件名

 

四、程序逻辑及实现

必要包的导入

程序必要的包如下

import ("fmt"
		"bufio"
		"io"
		"os"
		"os/exec"
		"strconv"
		"github.com/spf13/pflag"
)

这里运用UNIX标准,用pflag包来代替flag包,该包可以通过命令导入

go get github.com/spf13/pflag

如果go get命令导入失败则可以通过git clone直接clone github上的源码使用

 

程序使用参数的结构体

type selpg_args struct{
	start_page int
	end_page int   //开始页、结束页
	length_page int 			//一页长度
	page_type bool			//-l false  -f true,输入文本类型
	file_in string
	file_out string	//输入输出文件
}

结构体中主要定义了selpg命令中的几个重要参数,后续通过pflag包来解析命令的参数。通过下面的代码进行参数值的绑定,通过 pflag.Parse()方法让pflag 对标识和参数进行解析。之后就可以直接使用绑定的值

    pflag.IntVarP(&arg.start_page, "startPage", "s",0, "Start Page:")
	pflag.IntVarP(&arg.end_page, "endPage", "e",0, "end Page:")
	pflag.IntVarP(&arg.length_page, "lenPage", "l",20, "Line in each page:")
	pflag.StringVarP(&arg.file_out,"fileout","d","","output file name:")
	pflag.BoolVarP(&arg.page_type,"page type","-f",false,"page type")
    
    pflag.Parse()
    other := pflag.Args()
	if len(other) > 0{
		arg.file_in = other[0]//设置文件名
	}else{
		arg.file_in = ""
	}

 

page_type参数的-f选项和别的参数不同如果是Bool类型,则可以不带参数。这里的做法是如果有-f,则pageType为true,表示按行读取。如果没有-f或输入为-l 20,则page_type为false,表示以’\f’为页结束符
注意绑定完所有变量之后,要调用flag.Parse()在激活变量后,需要读取最后一个参数为文件名

 

参数格式判断

参数的范围判断首先需要判断参数的数量,个数必须大于3,并且判断是否有-s -e,并且其后的数字必须大于1,最后再加上对其他参数的格式判断即可,代码实现如下

//参数少于3个
	if(len(os.Args) < 3){
		fmt.Fprintf(os.Stderr,"%s: need 3 args at least\n", progarm)
		pflag.Usage()
		os.Exit(1)
	}

	//第一个参数必须是-s
	if os.Args[1] != "-s"{
		fmt.Fprintf(os.Stderr,"%s:first arg need to be -s start_page\n", progarm)
		pflag.Usage()
		os.Exit(2)
	}

	//起始页码不能小与1
	sp,_ := strconv.Atoi(os.Args[2])
	if sp < 1 {
		fmt.Fprintf(os.Stderr, "%s: start page need to large than 0 %d\n", progarm, sp)
		pflag.Usage()
		os.Exit(3)
	}	
		//第三个参数检查
	if os.Args[3] != "-e" {
		fmt.Fprintf(os.Stderr, "%s: second arg need to be -e end_page\n", progarm)
		pflag.Usage()
		os.Exit(4)
	}
	
	ep,_ := strconv.Atoi(os.Args[4])
	if ep < 1  || ep < sp{
		fmt.Fprintf(os.Stderr, "%s: invalid end page %d\n", progarm, ep)
		pflag.Usage()
		os.Exit(5)
	}	

	//判断其他参数
	argindex := 5
	for{
		if argindex > len(os.Args)-1 || os.Args[argindex][0] != '-'{
			break
		}
		switch os.Args[argindex][1] {
		case 'l':
			pl, _ := strconv.Atoi(os.Args[argindex+1])
			fmt.Println(pl)
			if pl < 1 {
				fmt.Fprintf(os.Stderr, "%s: invaild page length %d\n", progarm, pl)
				pflag.Usage()
				os.Exit(3)
			}
			argindex += 2
		case 'f':
			if len(os.Args[argindex]) > 2 {
				fmt.Fprintf(os.Stderr, "%s: option should be -f\n", progarm)
				pflag.Usage()
				os.Exit(2)
			}
			argindex++

		case 'd':
			if len(os.Args[argindex]) <= 2 {
				fmt.Fprintf(os.Stderr, "%s: -d option requires a printer destinationination\n", progarm)
				os.Exit(1)
			}
			argindex += 2
		default:
			fmt.Fprintf(os.Stderr, "%s: unknown option", progarm)
			pflag.Usage()
			os.Exit(2)
		
		}
	}

命令处理

该函数主要实现的逻辑是从标准输入或文件中获取输入然后输出到标准输出或文件中

程序的实现逻辑首先先对输入文件名判断,如果为空则执行标准输入,否则则打开文件

通过os.Stdin或打开文件得到数据流,将其传递给bufio.Reader(),可以将缓冲区和数据流进行绑定

bufio.Writer()同样绑定了一个输出数据流

-s与-e参数的实现:使用页数计数器,用一个int类型变量实现。在满足一页的条件后页数计数器增加,判断页数是否在范围内,不是则继续读入下一行数据,否则结束读取数据。

-l参数的实现:从输入中每次读取一行,同样创建一个int变量作为行数计数器。对每一行进行计数,当行数到达-l后的数字,页数增加,判断页数是否在范围内然后输出

-f参数实现:判断page_type是否为true,如果是则从输入中每次读取一行,如果一行的字符为’\f’则页数计数增加,判断页数是否在范围内然后输出

-d参数的实现:使用exec.Command设定要执行的外部命令,cmd.StdinPipe()返回连接到command标准输入的管道pipe,cmd.Start()使某个命令开始执行

该函数代码实现过长,不在博客中展示了,详细代码见github

 

主函数逻辑

主函数主要对之前所述的函数进行调用,首先初始化参数并解析,解析完成处理输出即可


func main()  {
	sa := new(selpg_args) //参数初始化
	analy_args(sa)
	progarm = os.Args[0]
	process_input(sa)
}

 

五、程序测试

测试数据随意采用了之前写的一个代码,将其名称改为test.txt

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

 

通过go build命令生成可执行文件

测试不符合预期要求的命令输入

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

 

默认-l标准输入

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

指定-l输入

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

 

输出重定向

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

管道重定向

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

 

详细代码见我的github

goOnline项目地址selpg

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