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

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

一、简介

命令行界面(英语:command-line interface,缩写:CLI)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。也有人称之为字符用户界面(CUI)。
通常认为,命令行界面(CLI)没有图形用户界面(GUI)那么方便用户操作。因为,命令行界面的软件通常需要用户记忆操作的命令,但是,由于其本身的特点,命令行界面要较图形用户界面节约计算机系统的资源。在熟记命令的前提下,使用命令行界面往往要较使用图形用户界面的操作速度要快。所以,图形用户界面的操作系统中,都保留着可选的命令行界面。
——百度百科

本篇博客的内容为使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg,要求能够用使用 selpg这一章节的要求测试程序。

:务必先看一下以上链接中的内容,这是我们接下来要实现的东西。

二、开发过程

首先要明确需求。这个任务是将一个已有的c语言程序改写为go语言程序,使得原来程序的输入输出关系不变,所以任务的难点主要在于找到go语言和c语言相对应的表达。

1. selpg_args结构

结构变量与c语言版本大致相同,不过page_type的类型由整型变为布尔值,这样是为了设计的方便,后面会讲到。

type selpg_args struct {
	start_page int
	end_page int
	page_len int
	page_type bool
	in_filename string
	print_dest string
}
2. main函数

注:因为程序中包含main函数,所以包必须是main

package main
var progname string
func main() {
    progname = os.Args[0]
    var sa selpg_args
    flag.IntVarP(&sa.start_page, "start_page", "s", -1, "start page")
    flag.IntVarP(&sa.end_page, "end_page", "e", -1, "end page")
    flag.IntVarP(&sa.page_len, "page_len", "l", 15, "page len")
    flag.BoolVarP(&sa.page_type, "page_type", "f", false, "page_type")
    flag.StringVarP(&sa.print_dest, "print_dest", "d", "", "print destination")

    flag.Parse()
    if flag.NArg() > 0 {
        sa.in_filename = flag.Arg(0)
    }
    check_args(sa)
    process_input(sa)

}

main函数主要做了以下几个工作:

  • 获取参数
  • 检查参数
  • 执行命令

获取参数时用到了两个库,一个是os,另一个是pflag。

os.Args是一个参数的数组,它相当于c语言中的args。

flag介绍
flag库简而言之是一个自动获取参数的库,它比os.Args聪明一点,不只是简单的将选项和参数存入一个字符串数组并等待进一步处理,而是能识别跟在选项后面的参数(包括类型和值)并保存在变量中,可以被直接使用。
举个简单的例子,输入$ selpg -s1 -e2时(selpg是我们要实现的命令行程序,如果不知道这是什么,请先点击简介中的链接查看文档),os.Args会简单地存储‘selpg’、‘-s1’、‘-e2’这三个字符串,但并不明白这三个参数的含义,而flag可以将跟在选项-s和-e后面的参数识别出来并存在变量start_page和end_page中,程序可以在接下来使用它们。

flag的简单使用如下:

 var port int
 flag.IntVar(&port, "port",  8000, "specify port to use.  defaults to 8000.")
 flag.Parse()

 fmt.Printf("port = %d\n", port)
 fmt.Printf("other args: %+v\n", flag.Args())

在这个命令中(不管是什么命令)我们要获取一个选项port后跟着的参数,并将其存在port变量中。
flag.IntVar的四个参数分别为参数存放的地址、选项名、默认值、参数提示。
所以在命令行输入$ command -port 80 时flag会自动将80这个值赋给port。

flag.Parse运用当前flag已经定义的规则,遍历整条命令,找到某些选项对应的参数并给对应的变量赋值,如果一些已定义的选项没有出现在命令中,则对应的变量赋默认值。

顺带一提,flag.Args()是一个参数数组,但只存储了那些没有跟在选项后的参数,比如命令$command -s 1 -e 2 input_file,flag.Args()中只有input_file。

但是在我们的程序中用的不是flag库,而是和它有点区别的pflag库,后者的一个优点是跟在选项后的参数可以与选项有空格隔开,也可以不用隔开。
$command -s 1 -e 2 和
$command -s1 -e2是等价的,这为我们提供了很大的方便,因为c语言版本实现中默认输入是第二种。

安装和使用pflag
pflag并不是go的默认库,所以需要手动安装
go get github.com/spf13/pflag
安装后可以在程序中这样引用

import  (
	flag "github.com/spf13/pflag"
)

flag是程序中使用的库名。(如果同时引用了原flag库最好将这个名字改为pflag以区分)。

3. 检查参数

好了,现在已经获取了参数,下一步需要检查参数是否合法,主要做了以下几个检查:

  • 参数是否大于等于三个(至少包含程序名、起始页、结束页)
  • 起始页和结束页是否是一个合法的整数(大于0小于INT_MAX)
  • 起始页是否不大于结束页
  • 如果有输入文件,该输入文件是否存在

在不满足以上任一条件时,程序会打印错误信息,并退出。

func check_args(sa selpg_args) {
    if len(os.Args) < 3 {
        fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", progname)
        usage()
        os.Exit(1)
    }
    if sa.start_page < 1 || sa.start_page > INT_MAX - 1 {
        fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname, sa.start_page)
        usage()
        os.Exit(2)
    }
    if sa.end_page < 1 || sa.end_page > INT_MAX - 1 {
        fmt.Fprintf(os.Stderr, "%s: invalid end page %d\n", progname, sa.end_page)
        usage()
        os.Exit(3)
    }
    if sa.end_page < sa.start_page {
        fmt.Fprintf(os.Stderr, "%s: end page should not be less than start pag\n", progname)
        usage()
        os.Exit(3)
    }
    if sa.page_len < 1 || sa.page_len > INT_MAX - 1 {
        fmt.Fprintf(os.Stderr, "%s: invalid page length %d\n", progname, sa.page_len)
        usage()
        os.Exit(4)
    }
    if sa.in_filename != "" {
        if _, err := os.Stat(sa.in_filename); os.IsNotExist(err) {
            fmt.Fprintf(os.Stderr, "%s: input file \"%s\" does not exist\n", progname, sa.in_filename)
			os.Exit(5);
        }
    }

}
4. 执行命令

首先需要解决的问题是输入流与输出流。
在c语言版本中输入是带缓冲的,输出是通过popen创建一个子进程并获得一条到达它的管道,使得程序的输出作为该子进程的输入。

在我们的go语言版本中处理是类似的(因为恰好有相对应的机制)。

输入流用bufio.Reader,如果输入命令中不包含输入文件,则输入默认来自标准输入,否则尝试打开文件,将文件流作为输入。

var reader *bufio.Reader
if sa.in_filename == "" {
    reader = bufio.NewReader(os.Stdin)
} else {
    fin, err := os.Open(sa.in_filename)
    if err != nil {
        fmt.Fprintf(os.Stderr, "%s: could not open input file \"%s\"\n", progname, sa.in_filename)
        os.Exit(6)
    }
    reader = bufio.NewReader(fin)
    defer fin.Close()
}

输出稍微复杂一点,我们先看一个通过管道连接两个命令行进程的方法。
在这个例子中首先分别创建了生产者子进程和消费者子进程,然后将生产者的输出流设为消费者的输入管道,这样就实现了生产者产生的输出通过管道作为消费者的输入。

//通过管道连接两个命令行进程的方法
func main() {
    generator := exec.Command("cmd1")
    consumer := exec.Command("cmd2")
    pipe, err := consumer.StdinPipe()
    generator.Stdout = pipe
}

我们的程序也应用了这种方法,(如果输入命令中没有目的地选项,则输出默认为标准输出;)首先通过exec.Command创建了一个子进程,执行打印,打印的目的地为输入命令中的目的地,然后我们将程序的输出流writer设为打印进程的输入管道,这样就实现了将读取的内容通过管道写到目的文件中。

var writer io.WriteCloser 
if sa.print_dest == "" {
    writer = os.Stdout
} else {
    cmd := exec.Command("lp","-d"+ sa.print_dest)
    var err error
    if writer, err = cmd.StdinPipe(); err != nil {
        fmt.Fprintf(os.Stderr, "%s: could not open pipe to \"%s\"\n", progname, sa.print_dest)
        fmt.Println(err)
        os.Exit(7)
    }
    if err = cmd.Start(); err != nil {
        fmt.Fprintf(os.Stderr, "%s: cmd start error\n", progname)
        fmt.Println(err)
        os.Exit(8)
    }
}

输入和输出流都搞定,开始读和写
这里基本套了c语言版本的思路,不过将两种不同页类型(定长和不定长)的读取统一在一个循环内。因为不定长页是以‘\f’为结束符的,所以可以看成一个页只有一行,所以用reader.ReadString每读“一行”,不定长页就增加了一页。

这里可以解释一下为什么page_type要用布尔类型了。因为pflag是要识别选项后面跟着的参数的,但-f后面不跟参数,如果用整数类型会报错,而布尔类型的一个特点是不能直接在选项后面跟参数,如果要跟参数,必须用一个等号连接,比如
selpg -s1 -e2 -f=true,如果不跟参数,单独只出现-f也相当于-f=true,所以我们恰好利用这个特点,如果没有出现-f选项,则page_type为默认值false(对应定长页),如果出现-f选项,则page_type设为true(对应不定长页)。

line_ctr, page_ctr, page_len := 1, 1, sa.page_len
ptFlag := '\n'
if sa.page_type {
    ptFlag = '\f'
    page_len = 1
}

//使用reader读取所有页的数据,并将要求范围内的页写入writer
for {
    line, crc := reader.ReadString(byte(ptFlag));
    if crc != nil && len(line) == 0 {
        break
    }
    if line_ctr > page_len {
        page_ctr++
        line_ctr = 1
    }
    if page_ctr >= sa.start_page && page_ctr <= sa.end_page {
        _, err := writer.Write([]byte(line))
        if err != nil {
            fmt.Println(err)
            os.Exit(9)
        }
    }
    line_ctr++
}

最后检查一下读取是否成功以及是否完成

if page_ctr < sa.start_page {
    fmt.Fprintf(os.Stderr,
        "\n%s: start_page (%d) greater than total pages (%d),"+
            " no output written\n", progname, sa.start_page, page_ctr)
} else if page_ctr < sa.end_page {
    fmt.Fprintf(os.Stderr, "\n%s: end_page (%d) greater than total pages (%d),"+
        " less output than expected\n", progname, sa.end_page, page_ctr)
}
三、程序测试

首先编译和安装selpg。
go install practice/CLI/selpg

根据要求,用c语言版本文档中的使用selpg章节进行测试。

1.selpg -s1 -e1 selpg.go

在这里插入图片描述
正好打印了第一页(一页15行)。

2.selpg -s1 -e1 < input_file 重定向标准输入

输出与上一条相同。

3.ls -l | selpg -s1 -e2 将ls -l输出作为selpg的输入
在这里插入图片描述
4.selpg -s1 -e20 selpg.go > temp 标准输出重定向到temp
在这里插入图片描述
5.selpg -s1 -e3 selpg.go -l5 修改页长度
在这里插入图片描述
6. selpg -s10 -e20 -f selpg.go 不定长页
在这里插入图片描述

四、项目地址

gitee地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值