Go-CLI 命令行实用程序

来源:互联网 发布:中银国际期货软件 编辑:程序博客网 时间:2024/05/02 00:41

Go语言实现CLI命令行

一. 概述

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

二. 具体要求

实现指令的要求请点击这里

三. 实现

1. 引用到的包

import (    "fmt"    "flag"    "os"    "io/ioutil"    "io"    "bufio"    "os/exec")

2. 数据结构(指令结构)

type selpg_args struct{    start_page int      //开始操作的页编号    end_page int        //结束操作的页编号    in_filename string  //读取的文件名    page_len int        //读取的文本页的数量    page_type bool      //操作的类型,-lNumber指这表明页有固定长度,每页为Nnumber行,-f在输入中寻找换页符,并将其作为页定界符处理。    print_dest string   //输出文件名}

3. main函数

func main(){    var args selpg_args //命令行参数    get_args(&args)     //读取命令行参数    check_args(&args)   //检查参数是否符合要求    run(&args)          //运行该命令}

4. 获取参数

func get_args(parg *selpg_args) {
​ flag.IntVar(&(parg.start_page), “s”, -1, “startPage”)
​ flag.IntVar(&(parg.end_page), “e”, -1, “endPage”)
​ flag.IntVar(&(parg.page_len), “l”, 72, “pageLength”)
​ flag.StringVar(&(parg.print_dest), “d”, “”, “printDest”)
​ flag.BoolVar(&(parg.page_type), “f”, false, “pageType”)

​ flag.Parse()

​ args_left := flag.Args()
​ if(len(args_left) > 0){
​ parg.in_filename = string(args_left[0])
​ } else {
​ parg.in_filename = “”
​ }
}

flag是go语言用来解析命令行参数的包,具有很强的功能,定义flags有两种方式:

1)flag.Xxx(),其中Xxx可以是Int、String等;返回一个相应类型的指针,如:

var ip = flag.Int("flagname", 1234, "help message for flagname")

2)flag.XxxVar(),将flag绑定到一个变量上,如:

var flagvar intflag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")

另外,还可以创建自定义flag,只要实现flag.Value接口即可(要求receiver是指针),这时候可以通过如下方式定义该flag:

flag.Var(&flagVal, "name", "help message for flagname")

获得flag参数后,要用flag.Parse()函数才能把参数解析出来,这里还有一个神奇的东西就是,解析完指定的参数后,还可以通过调用args_left := flag.Args()来获得未定义但输入了的参数

5. 检查参数是否符合要求

func check_args(parg *selpg_args) {    if parg == nil{        fmt.Fprintf(os.Stderr, "\n[Error]The args is nil!Please check your program!\n\n")        os.Exit(1)    }else if(parg.start_page == -1) || (parg.end_page == -1){        fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage is not allowed empty!Please check your command!\n\n")        os.Exit(2)    }else if (parg.start_page < 0) || (parg.end_page < 0){        fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage is not negative!Please check your command!\n\n")        os.Exit(3)    }else if parg.start_page > parg.end_page{        fmt.Fprintf(os.Stderr, "\n[Error]The startPage can not be bigger than the endPage!Please check your command!\n\n")        os.Exit(4)    }else{        pt := 'f'        if parg.page_type == false {            pt = 'l'        }        fmt.Printf("\n[ArgsStart]\n")        fmt.Printf("startPage: %d\nendPage: %d\ninputFile: %s\npageLength: %d\npageType: %c\nprintDestation: %s\n[ArgsEnd]\n\n", parg.start_page, parg.end_page, parg.in_filename, parg.page_len, pt, parg.print_dest)    }}

这里主要是比较起始页和终止页之间的逻辑关系,没有什么难度。这里要特别说明的是这个pt := 'f'pt = 'l'的问题,因为这里用了一个Bool值来判断是两种类型中的哪一个。

6. 运行指令

func run(parg *selpg_args) {    var fin *os.File    var fout *os.File    var fout_d io.WriteCloser    if parg.in_filename == ""{        fin = os.Stdin    } else {        check_file_access(parg.in_filename)        var err_fin error        fin, err_fin = os.Open(parg.in_filename)        check_err(err_fin, "fileInput")    }    fout, fout_d = check_fout(parg.print_dest)    if(fout != nil){        output_to_file(fout, fin, parg.start_page, parg.end_page, parg.page_len)    } else {        output_to_exc(fout_d, fin, parg.start_page, parg.end_page, parg.page_len)    }}

这个模块是这次的核心内容,主要涉及文件的读写问题,下面分模块介绍

(1)输入文件检查
func check_file_access(filename string) {    _, err_o := os.Stat(filename)    if os.IsNotExist(err_o){        fmt.Fprintf(os.Stderr, "\n[Error]: input file \"%s\" does not exist\n\n",filename);        os.Exit(5);    }    _, err_r := ioutil.ReadFile(filename)    check_err(err_r, filename)}

文件的读写使用到了go语言的os包,这个包将文件检测,读写和输入输出流封装起来。

os.Stat()这个函数是获取文件的信息,函数函数的原型func Stat(name string) (fi FileInfo, err error)输出是文件的名称返回一个FileInfo的接口和err信息

os.IsNotExist(err_o)检测文件是否存在

os.Exit(5)强制终止命令的执行

(2)错误检查
func check_err(err error, object string) {    if err != nil{        fmt.Fprintf(os.Stderr, "\n[Error]%s:",object);        panic(err)    }}

这里主要介绍panic函数,当出现error(即error != nil)时,panic(error)会停止命令的执行,并将错误原因转成字符串打印出来,是个检测错误的实用函数,要掌握

(3)输出文件检查
unc check_fout(printDest string) (*os.File, io.WriteCloser) {    var fout *os.File    var fout_d io.WriteCloser    if len(printDest) == 0 {        fout = os.Stdout;        fout_d = nil    } else {        fout = nil        var err_dest error        cmd := exec.Command("./" + printDest)        fout_d, err_dest = cmd.StdinPipe()        check_err(err_dest, "fout_dest")        cmd.Stdout = os.Stdout        cmd.Stderr = os.Stderr        err_start := cmd.Start()         check_err(err_start,"command-start")    }    return fout, fout_d}

这里的逻辑是,检查是否存在输出文件名,存在,则将输入的内容由管道送至其它命令(如将输出在打印机上打印),如果不存在,则将数据打印到屏幕(即标准输出os.Stdout

这里有个难点,就是exec这个东西,跟go语言的os/exec包有关,exec包执行外部命令,它将os.StartProcess进行包装使得它更容易映射到stdin和stdout,并且利用pipe连接i/o

type Cmd struct {      Path         string   //运行命令的路径,绝对路径或者相对路径      Args         []string   // 命令参数      Env          []string         //进程环境,如果环境为空,则使用当前进程的环境      Dir          string   //指定command的工作目录,如果dir为空,则comman在调用进程所在当前目录中运行      Stdin        io.Reader  //标准输入,如果stdinnil的话,进程从null device中读取(os.DevNull),stdin也可以时一个文件,否则的话则在运行过程中再开一个goroutine去               //读取标准输入      Stdout       io.Writer       //标准输出      Stderr       io.Writer  //错误输出,如果这两个(StdoutStderr)为空的话,则command运行时将响应的文件描述符连接到os.DevNull      ExtraFiles   []*os.File         SysProcAttr  *syscall.SysProcAttr      Process      *os.Process    //Process是底层进程,只启动一次      ProcessState *os.ProcessState  //ProcessState包含一个退出进程的信息,当进程调用Wait或者Run时便会产生该信息.  }  
(4)将输入内容打印到屏幕
func output_to_file(fout *os.File, fin *os.File, pageStart int, pageEnd int, pageLen int) {    line_ctr := 0    page_ctr := 1    buf := bufio.NewReader(fin)    for true {        line,err := buf.ReadString('\n')        if err == io.EOF{            break        }        check_err(err, "file_in_out")        line_ctr++        if line_ctr > pageLen{            page_ctr++            line_ctr = 1        }        if (page_ctr >= pageStart) && (page_ctr <= pageEnd){            fmt.Fprintf(fout, "%s", line)        }    }    if page_ctr < pageStart {        fmt.Fprintf(os.Stderr, "\n[Error]: start_page (%d) greater than total pages (%d), no output written\n\n", pageStart, page_ctr)        os.Exit(6)    } else if page_ctr < pageEnd {        fmt.Fprintf(os.Stderr,"\n[Error]: end_page (%d) greater than total pages (%d), less output than expected\n\n", pageEnd, page_ctr);        os.Exit(7)    }}

这里用buf := bufio.NewReader(fin)进行文件的读写

(5)将输出内容通过管道调用外部指令
func output_to_exc(fout io.WriteCloser, fin *os.File, pageStart int, pageEnd int, pageLen int) {    line_ctr := 0    page_ctr := 1    buf := bufio.NewReader(fin)    for true {        bytes, err := buf.ReadByte()        if err == io.EOF{            break        }        if line_ctr > pageLen{            page_ctr++            line_ctr = 1        }        if (page_ctr >= pageStart) && (page_ctr <= pageEnd){            fout.Write([]byte{bytes})        }    }    if page_ctr < pageStart {        fmt.Fprintf(os.Stderr, "\n[Error]: start_page (%d) greater than total pages (%d), no output written\n\n", pageStart, page_ctr)        os.Exit(8)    } else if page_ctr < pageEnd {        fmt.Fprintf(os.Stderr,"\n[Error]: end_page (%d) greater than total pages (%d), less output than expected\n\n", pageEnd, page_ctr);        os.Exit(9)    }}

两个函数的区别在于输出流是不一样的

四. 测试

在github上有指令的测试,请点击这里

五. 完整代码

放在github上,请点击这里

原创粉丝点击