CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。Linux提供了cat、ls、copy等命令与操作系统交互;go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;git、npm等都是大家比较熟悉的工具。尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。
使用 golang 开发 开发Linux命令行实用程序中的 selpg。selpg 是从文本输入选择页范围的实用程序。该输入可以来自作为最后一个命令行参数指定的文件,在没有给出文件名参数时也可以来自标准输入。
强制选项:“-sNumber”和“-eNumber”
selpg 要求用户用两个命令行参数“-sNumber”(例如,“-s10”表示从第 10 页开始)和“-eNumber”(例如,“-e20”表示在第 20 页结束)指定要抽取的页面范围的起始页和结束页。
$ selpg -s10 -e20 ...
可选选项:“-lNumber”和“-f”
selpg 可以处理两种输入文本:
$ selpg -s10 -e20 -l66 ...
$ selpg -s10 -e20 -f ...
注:“-lNumber”和“-f”选项是互斥的。
可选选项:“-dDestination”
selpg 还允许用户使用“-dDestination”选项将选定的页直接发送至打印机。
$ selpg -s10 -e20 -dlp1
1.设置程序的参数结构体。提取参数将值赋值给该结构体
//selpg的参数
type selpg_args struct {
start_page int //开始页
end_page int //结束页
in_filename string //作为输入的文件名
dest string //输出
page_len int //一页的长度
page_type int //划分页方式
}
var sa selpg_args //当前输入的参数
var progname string //程序名
var argcount int //参数个数
2.读取各个参数。使用os.Args
读取程序输入的所有参数,得到的值是包含参数的string数组。判断每个参数的格式是否正确,参数个数是否正确以及将参数的值提取出来赋值给结构体
func process_args(args []string) {
//参数数量不够
if len(args) < 3 {
fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", progname)
Usage()
os.Exit(1)
}
//处理第一个参数
if args[1][0] != '-' || args[1][2] != 's' {
fmt.Fprintf(os.Stderr, "%s: 1st arg should be -sstart_page\n", progname)
Usage()
os.Exit(1)
}
//提取开始页数
sp, _ := strconv.Atoi(args[1][2:])
if sp < 1 {
fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname, sp)
Usage()
os.Exit(1)
}
sa.start_page = sp
//处理第二个参数
if args[2][0] != '-' || args[2][3] != 'e' {
fmt.Fprintf(os.Stderr, "%s: 2nd arg should be -eend_page\n", progname)
Usage()
os.Exit(1)
}
//提取结束页数
ep, _ := strconv.Atoi(args[2][2:])
if ep < 1 || ep < sp {
fmt.Fprintf(os.Stderr, "%s: invalid end page %d\n", progname, ep)
Usage()
os.Exit(1)
}
sa.end_page = ep
//其他参数处理
argindex := 3
for {
if argindex > argcount-1 || args[argindex][0] != '-' {
break
}
switch args[argindex][4] {
case 'l':
//获取一页的长度
pl, _ := strconv.Atoi(args[argindex][2:])
if pl < 1 {
fmt.Fprintf(os.Stderr, "%s: invalid page length %d\n", progname, pl)
Usage()
os.Exit(1)
}
sa.page_len = pl
argindex++
case 'f':
if len(args[argindex]) > 2 {
fmt.Fprintf(os.Stderr, "%s: option should be \"-f\"\n", progname)
Usage()
os.Exit(1)
}
sa.page_type = 'f'
argindex++
case 'd':
if len(args[argindex]) <= 2 {
fmt.Fprintf(os.Stderr, "%s: -d option requires a printer destination\n", progname)
Usage()
os.Exit(1)
}
sa.dest = args[argindex][2:]
argindex++
default:
fmt.Fprintf(os.Stderr, "%s: unknown option", progname)
Usage()
os.Exit(1)
}
}
if argindex <= argcount-1 {
sa.in_filename = args[argindex]
}
}
这里的参数处理可以使用flag
包,flag包能解析命令的参数,更加容易地对参数进行赋值和说明。flag包的使用可以参考:Go学习笔记:flag库的使用。这里因为要求使用Unix标准,所以使用pflag
包代替flag
,但两者用法类似。
//导入pflag包:
import (
...
"github.com/spf13/pflag"
)
然后可以通过下面的代码进行参数值的绑定,通过 pflag.Parse()
方法让pflag 对标识和参数进行解析。之后就可以直接使用绑定的值。
pflag.IntVarP(&sa.start_page,"start", "s", 0, "Start page of file")
pflag.IntVarP(&sa.end_page,"end","e", 0, "End page of file")
pflag.IntVarP(&sa.page_len,"linenum", "l", 20, "lines in one page")
pflag.StringVarP(&sa.page_type,"printdes","f", "l", "flag splits page")
pflag.StringVarP(&sa.dest, "destination","d", "", "name of printer")
pflag.Parse()
通过
pflag.NArg()
可以知道是否有要进行操作的文件。如果是pflag解析不了的类型参数。我们称这种参数为non-flag
参数,flag解析遇到non-flag参数就停止了。pflag提供了Arg(i)
,Args()
来获取non-flag参数,NArg()
来获取non-flag的个数。所以可以使用pflag.Arg(0)
来获取输入的文件路径。
2.从标准输入或文件中获取输入然后输出到标准输出或文件中,可以重定向输出作为其他命令的输入。
-s与-e参数的实现:使用页数计数器,在满足一页的条件后页数计数器增加,判断页数是否在范围内,不是则继续读入下一行数据,否则结束读取数据。
-l参数的实现:从输入中每次读取一行,然后对每一行进行计数,当行数到达-l后的数字,页数增加,判断页数是否在范围内然后输出。
-f参数的实现:当有-f参数时,将sa.page_type
赋值为’f’,从输入中每次读取一行,如果一行的字符为’\f’则页数计数增加,判断页数是否在范围内然后输出。
-d参数的实现:使用os/exec包,可以执行外部命令,将输出的数据作为外部命令的输入。使用exec.Command
设定要执行的外部命令,cmd.StdinPipe()
返回连接到command标准输入的管道pipe,cmd.Start()
使某个命令开始执行,但是并不等到他执行结束。更多包信息见:Package exec。
func process_input() {
var cmd *exec.Cmd
var cmd_in io.WriteCloser
var cmd_out io.ReadCloser
if sa.dest != "" {
cmd = exec.Command("bash", "-c", sa.dest)
cmd_in, _ = cmd.StdinPipe()
cmd_out, _ = cmd.StdoutPipe()
//执行设定的命令
cmd.Start()
}
if sa.in_filename != "" {
inf, err := os.Open(sa.in_filename)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
line_count := 1
page_count := 1
fin := bufio.NewReader(inf)
for {
//读取输入文件中的一行数据
line, _, err := fin.ReadLine()
if err != io.EOF && err != nil {
fmt.Println(err)
os.Exit(1)
}
if err == io.EOF {
break
}
if page_count >= sa.start_page && page_count <= sa.end_page {
if sa.dest == "" {
//打印到屏幕
fmt.Println(string(line))
} else {
//写入文件中
fmt.Fprintln(cmd_in, string(line))
}
}
line_count++
if sa.page_type == 'l' {
if line_count > sa.page_len {
line_count = 1
page_count++
}
} else {
if string(line) == "\f" {
page_count++
}
}
}
if sa.dest != "" {
cmd_in.Close()
cmdBytes, err := ioutil.ReadAll(cmd_out)
if err != nil {
fmt.Println(err)
}
fmt.Print(string(cmdBytes))
//等待command退出
cmd.Wait()
}
} else {
//从标准输入读取内容
ns := bufio.NewScanner(os.Stdin)
line_count := 1
page_count := 1
out := ""
for ns.Scan() {
line := ns.Text()
line += "\n"
if page_count >= sa.start_page && page_count <= sa.end_page {
out += line
}
line_count++
if sa.page_type == 'l' {
if line_count > sa.page_len {
line_count = 1
page_count++
}
} else {
if string(line) == "\f" {
page_count++
}
}
}
if sa.dest == "" {
fmt.Print(out)
} else {
fmt.Fprint(cmd_in, out)
cmd_in.Close()
cmdBytes, err := ioutil.ReadAll(cmd_out)
if err != nil {
fmt.Println(err)
}
fmt.Print(string(cmdBytes))
//等待command退出
cmd.Wait()
}
}
}
本次开发实践,实现了一个golang版的selpg,学会了golang的简单使用,对于CLI的开发有了一些了解。一开始对于处理命令行的参数只是通过简单的处理字符串的方式处理,后来发现有flag库可以进行命令行参数的绑定,它支持多种类型,比简单的进行处理方便和灵活。这里对flag包的使用也不是很完整只是使用了部分功能。exec 包以及bufio包的函数都有部分涉及。完整项目和测试文档指路:Github