服务计算--CLI 命令行实用程序开发基础

一.概述

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

二.开发实践

使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg。
selpg 程序逻辑
selpg 是从文本输入选择页范围的实用程序。该输入可以来自作为最后一个命令行参数指定的文件,在没有给出文件名参数时也可以来自标准输入。
selpg 首先处理所有的命令行参数。在扫描了所有的选项参数后,如果 selpg 发现还有一个参数,则它会接受该参数为输入文件的名称并尝试打开它以进行读取。如果没有其它参数,则 selpg 假定输入来自标准输入。
参数处理
“-sNumber”和“-eNumber”强制选项:
selpg 要求用户用两个命令行参数“-sNumber”(例如,“-s10”表示从第 10 页开始)和“-eNumber”(例如,“-e20”表示在第 20 页结束)指定要抽取的页面范围的起始页和结束页。selpg 对所给的页号进行合理性检查;换句话说,它会检查两个数字是否为有效的正整数以及结束页是否不小于起始页。这两个选项,“-sNumber”和“-eNumber”是强制性的,而且必须是命令行上在命令名 selpg 之后的头两个参数:

$ selpg -s10 -e20 …

“-lNumber”和“-f”可选选项:
selpg 可以处理两种输入文本:
类型 1:该类文本的页行数固定。这是缺省类型,因此不必给出选项进行说明。也就是说,如果既没有给出“-lNumber”也没有给出“-f”选项,则 selpg 会理解为页有固定的长度(每页 72 行)。
选择 72 作为缺省值是因为在行打印机上这是很常见的页长度。这样做的意图是将最常见的命令用法作为缺省值,这样用户就不必输入多余的选项。该缺省值可以用“-lNumber”选项覆盖,如下所示:

$ selpg -s10 -e20 -l66 …

“-dDestination”可选选项:
selpg 还允许用户使用“-dDestination”选项将选定的页直接发送至打印机。这里,“Destination”应该是 lp 命令“-d”选项(请参阅“man lp”)可接受的打印目的地名称。该目的地应该存在 ― selpg 不检查这一点。在运行了带“-d”选项的 selpg 命令后,若要验证该选项是否已生效,请运行命令“lpstat -t”。该命令应该显示添加到“Destination”打印队列的一项打印作业。如果当前有打印机连接至该目的地并且是启用的,则打印机应打印该输出。这一特性是用 popen() 系统调用实现的,该系统调用允许一个进程打开到另一个进程的管道,将管道用于输出或输入。

selpg -s10 -e20 -dlp1

三.程序实现

流程分析
在这里插入图片描述
程序按照读取参数、判断参数是否合规、读取文件、确定输出位置并输出顺序执行。当发现错误时抛出错误并终止流程。

代码实现
我根据给出的c语言源码selpg.c来实现自己的go语言CLI。
首先是包的引用:

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

参考Golang之使用Flag和Pflag导入Pflag包以及学习Pflag包的引用。

设置程序的参数结构体

type selpg_args struct{
	start_page int
	end_page int
	in_filename string
	page_len int
	page_type bool
	print_dest string
}

使用pflag提取参数将值赋值给该结构体。 pflag包于flag用法类似,但pflag相对于flag能够更好地满足命令行规范。

flag.IntVarP(&sa.start_page,"start","s",-1,"start page(>1)")
	flag.IntVarP(&sa.end_page,"end","e",-1,"end page(>=start_page)")
	flag.IntVarP(&sa.page_len,"len","l",72,"page len")
	flag.StringVarP(&sa.print_dest,"dest","d","","print dest")
	flag.BoolVarP(&sa.page_type, "type", "f", false, "page type")
	flag.Usage = func(){
		fmt.Fprintf(os.Stderr,"USAGE:: \n-s start_page -e end_page [ -f | -l lines_per_page ]" + " [ -d dest ][ in_filename ]\n")
	}
	flag.Parse()

此处满足要求:

请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag

pflag包中的函数XXXVarP(XXX为Int、String、Bool等可选类型)可以取出命令行参数名称shorthand的参数的值,value指定*p的默认值,name为自定的名称,usage为自定的该参数的描述。该函数无返回值。获得flag参数后,要用pflag.Parse()函数才能把参数解析出来。

接下来是判断每个参数的格式是否正确,参数个数是否正确以及是否将参数的值提取出来赋值给结构体。

if len(os.Args)<3 {
		fmt.Fprintf(os.Stderr,"\nnot enough arguments\n")
		flag.Usage()
		os.Exit(0)
	}
	if (sa.start_page == -1) || (sa.end_page == -1) {
		fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be empty! Please check your command!\n")
		flag.Usage()
		os.Exit(0)
	} 
	if (sa.start_page <= 0) || (sa.end_page <= 0) {
		fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be negative! Please check your command!\n")
		flag.Usage()
		os.Exit(0)
	} 
	if sa.start_page > sa.end_page {
		fmt.Fprintf(os.Stderr, "\n[Error]The startPage can't be bigger than the endPage! Please check your command!\n")
		flag.Usage()
		os.Exit(0)
	}
	if len(flag.Args()) == 1{
		_, err:=os.Stat(flag.Args()[0])
		if err!=nil && os.IsNotExist(err) {
			fmt.Fprintf(os.Stderr,"\ninput file \"%s\" does not exist\n",flag.Args()[0])
			os.Exit(0)
		}
		sa.in_filename = flag.Args()[0]
	}
	if (sa.page_type == true) && (sa.page_len != 72) {
		fmt.Fprintf(os.Stderr, "\n[Error]The command -l and -f are exclusive, you can't use them together!\n")
		flag.Usage()
		os.Exit(0)
	} 
	if sa.page_len <= 0 {
		fmt.Fprintf(os.Stderr, "\n[Error]The pageLen can't be less than 1 ! Please check your command!\n")
		flag.Usage()
		os.Exit(0)
	}

首先检查了开始页和结束页是否正确,然后判断是否正确读入文件,接着检查自定页长-l和遇换页符换页-f是否同时出现,最后判断当自定页长-l出现时args.pageLen是否小于1。

参数检查process_args函数结束之后,程序开始调用process_input函数执行命令。

var fin *os.File
	if args.in_filename == "" {
		fin = os.Stdin
	} else {
		var err error
		fin, err = os.Open(args.in_filename )
		if err != nil {
			fmt.Fprintf(os.Stderr, "\n[Error]%s:", args.in_filename)
			os.Exit(0)
		}
	}
	line_count := 0
	page_count := 1
	buf := bufio.NewReader(fin)

	cmd = &exec.Vmd{}
	var fout io.WriteCloser
	if args.print_dest=""{
		fout = os.Stdout
	}else {
		cmd = exec.Command("cat")
		var err error
		cmd.Stdout,err = os.OpenFile(args.print_dest,os.O_WROUNLY|os.O_TRUNC,0600)
		if err != nil {
			fmt.Fprintf(os.Stderr, "\n[Error]%s:", "Input pipe open\n")
			os.Exit(0)
		}
		fout, _ = cmd.StdinPipe()
		cmd.Start()
	}
	

首先检查输入。如果没有给定文件名,则从标准输入中获取;如果给出读取的文件名,则检查文件是否存在。然后判断是否有-d参数。如果没有-d参数,选择的页直接从os.Stdout标准输出中输出。如果-d存在,则使用os/exec包,可以执行外部命令,将输出的数据作为外部命令的输入。使用exec.Command()设定要执行的外部命令,cmd.StdinPipe()返回连接到command标准输入的管道pipe。最后使用cmd.Start()命令开始非阻塞执行子进程。由于没有连接打印机,所以使用cat命令测试。不过目标文件(pinput.txt)需要先创建。
最后是需要输出的内容。

	for true {
		var line string
		var err error
		if  args.page_type {
			line, err = buf.ReadString('\f')
			page_count++
		} else {
			line, err = buf.ReadString('\n')
			line_count++
			if line_count > args.page_len {
				page_count++
				line_count = 1
			}
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Fprintf(os.Stderr, "\n[Error]%s:", "Input pipe open\n","file read in\n")
			os.Exit(0)
		}
		if (page_count >= args.start_page) && (page_count <= args.end_page) {
			var outputErr error
			_, outputErr = fout.Write([]byte(line))
			if outputErr != nil {
				fmt.Fprintf(os.Stderr, "\n[Error]%s:", "pipe input")
				os.Exit(0)
			}
			if outputErr != nil {
				fmt.Fprintf(os.Stderr, "\n[Error]%s:", "Error happend when output the pages.")
				os.Exit(0)
			}
		}
	}

主函数调用上述函数完成。

func main(){
	var args selpg_args
	process_args(&args)
	process_input(&args)
}

process_args函数解析用户的输入,将对应的参数放入参数结构体中。
process_input函数负责根据参数结构体的属性设定输入输出源,设置每页的行数并开始打印。
usage函数用于打印帮助信息。
这样就实现了通过GO语言的CLI命令行开发。

四.程序测试

按文档 使用 selpg 章节要求测试该程序。
首先是测试文件test.txt
在这里插入图片描述

selpg -s1 -e1 -l10 test.txt
将test.txt第1页的前10行打印到屏幕上

在这里插入图片描述

selpg -s1 -e1 < test.txt
selpg读取标准输入,而标准输入被shell/内核重定向为来自test.txt而不是显示命名的文件名参数。输入的第1页被写至屏幕。

在这里插入图片描述

selpg -s2 -e2 test.txt > output_file
将第2页写入out.txt中

在这里插入图片描述
output_file:
在这里插入图片描述

selpg -s1 -e2 -l5 test.txt
将两页打印在屏幕上,一页5行

在这里插入图片描述

selpg -s3 -e5 -l4 test.txt >out.txt 2>error.txt
将标准错误写入error_file中

在这里插入图片描述
error_file:
在这里插入图片描述

selpg -s2 -e3 -l10 test.txt >output_file 2>error_file
将第2到3页写入output_file中,标准错误将被写入error_file中

在这里插入图片描述
在这里插入图片描述

selpg -s1 -e1 test.txt | other_command

在这里插入图片描述

selpg -s1-e1 input_file 2>error_file | other_command

在这里插入图片描述

selpg -s1 -e1 -f test.txt
假定页由换页符界定,第1页被打印到屏幕

在这里插入图片描述
在这里插入图片描述

selpg -s1 -e1 -l5 -dlp1 test.txt
将第一页输出到打印机,因为1并没有打印机,所以通过cat来进行测试,将结果输出到文件中。

在这里插入图片描述

selpg -s1 -e2 test.txt > output_file 2>error_file &

在这里插入图片描述

五.总结

本次服务计算作业通过Go语言实现了selpg,学会了Go的简单使用,对于CLI的开发有了一些了解。同时在编程中也学习了Pflag包的运用,对于exec 包以及bufio包的函数都有部分涉及。本次作业在难度上比前两次要大很多,虽然有c语言源代码供我们参考,但是对于go语言之中一些特定的我并不了解,需要在作业中从头学习。这一次对管道,重定向有了更深的了解,也简单使用了Pflag包,exec 包以及bufio包。

GitHub地址selpg

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值