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

服务计算

概述

CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:

  • Linux提供了cat、ls、copy等命令与操作系统交互;
  • go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
  • 容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
  • git、npm等也是大家比较熟悉的工具。
  • 尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。

基础知识

几乎所有语言都提供了完善的 CLI 实用程序支持工具。以下是一些入门文档(c 语言):

  • 开发 Linux 命令行实用程序。
  • Linux命令行程序设计

如果你熟悉 python :

  • Using Python to create UNIX command line tools

阅读以后你应该知道 POSIX/GNU 命令行接口的一些概念与规范。命令行程序主要涉及内容:

  • 命令
  • 命令行参数
  • 选项:长格式、短格式
  • IO:stdin、stdout、stderr、管道、重定向
  • 环境变量

Golang的支持

使用os,flag包,最简单处理参数的代码:

package main

import (
    "fmt"
    "os"
)

func main() {
    for i, a := range os.Args[1:] {
        fmt.Printf("Argument %d is %s\n", i+1, a)
    }

}

我们先运行一下:



使用flag包的代码:

package main

import (
    "flag" 
    "fmt"
)

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

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

运行结果:


中文参考:

  • 标准库—命令行参数解析FLAG
  • Go学习笔记:flag库的使用

更多代码实践:

  • cat demo
  • goimports 实现

4、开发实践

使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg

提示:

  1. 请按文档 使用 selpg 章节要求测试你的程序
  2. 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
  3. golang 文件读写、读环境变量,请自己查 os 包
  4. “-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe
  5. 请自带测试程序,确保函数等功能正确

在做这次任务之前,我们需要先下载plfag的包



本次任务的实质是把selpg.c文件中的代码用golang实现,了解完内容我们就开始实现吧
首先定义数据类型:

type selpgArgs struct {
    startPage  int        //开始页
    endPage    int        //结束页
    inFilename string     //输入文件的名字
    pageLen    int        //页长
    pageType   bool       //是否按页结束符计算
    printDest  string     //打印地址
}

基本的数据类型和C中的并没有什么区别,唯一改变的是pageType的类型,这里选择使用bool型。
我们先来看一下main函数:
main函数主要做了以下几个工作:
获取参数、检查参数、执行命令


func main() {
  
    progname = os.Args[0]  //progname为程序名
    var a selpgArgs  
    pflag.IntVarP(&a.startPage, "startPage", "s", -1, "Start Page")
    pflag.IntVarP(&a.endPage, "endPage", "e", -1, "End page")
    pflag.BoolVarP(&a.pageType, "pageType", "f", false, "Page type")
    pflag.IntVarP(&a.pageLen, "pageLen", "l", 20, "Lines per page")
    pflag.StringVarP(&a.printDest, "printDest", "d", "", "Destination")
    pflag.Parse()  //将命令行参数加入到绑定的变量
    a.inFilename = ""
    if b := pflag.Args(); len(b) > 0 {
        a.inFilename = b[0]
    }

    processArgs(a)  //对参数进行判断
    processInput(a) //执行指令
}

检查参数

需要对输入的参数进行判断是否满足输入要求

func processArgs(a selpgArgs) {
    //参数小于3个报错
    if len(os.Args) < 3 {      
        fmt.Fprintf(os.Stderr, "%s:please input enough arguments\n", progname)
        usage()
        os.Exit(1)
    }
    //对开始页进行判断是否满足要求
    if a.startPage < 1 || a.startPage > intMax {      
        fmt.Fprintf(os.Stderr, "%s:please input right integer for startPage\n", progname)
        usage()
        os.Exit(2)

    }
    //对结束页判断是否满足要求
    if a.endPage < 1 || a.endPage > (intMax-1) || a.endPage < a.startPage {
        fmt.Fprintf(os.Stderr, "%s:please input right integer for endPage\n", progname)
        usage()
        os.Exit(3)
    }
    //对页长进行判断是否满足要求
    if a.pageLen < 1 || a.pageLen > (intMax-1) {
        fmt.Fprintf(os.Stderr, "%s:please input right integer for pageLen\n", progname)
        usage()
        os.Exit(4)
    }
    //对文件名进行判断是否满足要求
    if a.inFilename != "" {
        if _, err := os.Stat(a.inFilename); os.IsNotExist(err) {
            fmt.Fprintf(os.Stderr, "%s:  input file \"%s\" does not exist\n", progname, a.inFilename)
            usage()
            os.Exit(5)
        }
    }
}

执行指令

下面是定义的两个参数

    var read *bufio.Reader    //读取输入
    var write io.WriteCloser  //写入输出

使用 os 包 进行 golang 文件读写、读环境变量
如果用户输入了inFilename,就将文件作为输入,否则将命令行作为输入

    if a.inFilename == "" {
        read = bufio.NewReader(os.Stdin)
    } else {
        file, err := os.Open(a.inFilename)
        if err != nil {
            fmt.Fprintf(os.Stderr, "%s:could not open input file %s\n", progname, a.inFilename)
            os.Exit(6)
        }
        read = bufio.NewReader(file)
        defer file.Close()
    }

紧接着是输出,如果输入命令中没有目的地选项,则输出默认为标准输出。
首先通过exec.Command创建了一个子进程,执行打印,打印的目的地为输入命令中的目的地,然后我们将程序的输出流writer设为打印进程的输入管道

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

下面将指定页范围的数据送入输出地址

lineNumber, pageNumber, pageL := 1, 1, a.pageLen
    flag := '\n'
    if a.pageType == true {
        flag = '\f'
        pageL = 1
    }
    //使用reade读取所有页的数据,并将要求范围内的页写入write
    for {
        line, err := read.ReadString(byte(flag))
        if err != nil && len(line) == 0 {
            break
        }
        if lineNumber > pageL {
            pageNumber++
            lineNumber = 1
        }
        if pageNumber >= a.startPage && pageNumber <= a.endPage {
            _, err := write.Write([]byte(line))
            if err != nil {
                fmt.Println(err)
                os.Exit(9)
            }
        }
        lineNumber++
    }

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

if pageNumber < a.startPage {
        fmt.Fprintf(os.Stderr, "\n%s: start_page (%d) greater than total pages (%d),"+" no output written\n", progname, a.startPage, pageNumber)
    } else if pageNumber < a.endPage {
        fmt.Fprintf(os.Stderr, "\n%s: end_page (%d) greater than total pages (%d),"+" less output than expected\n", progname, a.endPage, pageNumber)
    }

usage 函数会输出 selpg 命令的格式,当用户输入有误时的提示使用。

func usage() {
    fmt.Fprintf(os.Stderr, "\nUSAGE: %s -sstartPage -eendPage [ -f | -llinesPerPage ] [ -dprintDest ] [ inFilename ]\n", progname)
}

测试程序

先安装程序

go install server-computing/selpg
selpg -s1 -e1 input.txt

该命令将把“input.txt”的第 1 页写至标准输出(也就是屏幕),因为这里没有重定向或管道。

selpg -s1 -e1 < input.txt

该命令与示例 1 所做的工作相同,但在本例中,selpg 读取标准输入,而标准输入已被 shell/内核重定向为来自“input.txt”而不是显式命名的文件名参数。输入的第 1 页被写至屏幕。

 more input.txt | selpg -s1 -e2

“other_command”的标准输出被 shell/内核重定向至 selpg 的标准输入。将第 1 页到第 2 页写至 selpg 的标准输出(屏幕)。

selpg -s1 -e2 input.txt > output.txt

selpg 将第 1 页到第 2 页写至标准输出;标准输出被 shell/内核重定向至“output.txt”。

selpg -s10 -e20 input.txt 2>error.txt

selpg 将第 10 页到第 20 页写至标准输出(屏幕);所有的错误消息被 shell/内核重定向至“error_file”。请注意:在“2”和“>”之间不能有空格;这是 shell 语法的一部分(请参阅“man bash”或“man sh”)。

 selpg -s10 -e20 input.txt >output.txt 2>error.txt

selpg 将第 10 页到第 20 页写至标准输出,标准输出被重定向至“output.txt”;selpg 写至标准错误的所有内容都被重定向至“error.txt”。当“input.txt”很大时可使用这种调用;您不会想坐在那里等着 selpg 完成工作,并且您希望对输出和错误都进行保存。


selpg -s5 -e7 input.txt >output.txt 2>/dev/null

selpg 将第 5 页到第 7 页写至标准输出,标准输出被重定向至“output.txt”;selpg 写至标准错误的所有内容都被重定向至 /dev/null(空设备),这意味着错误消息被丢弃了。设备文件 /dev/null 废弃所有写至它的输出,当从该设备文件读取时,会立即返回 EOF。

selpg -s10 -e20 input.txt >/dev/null

selpg 将第 10 页到第 20 页写至标准输出,标准输出被丢弃;错误消息在屏幕出现。这可作为测试 selpg 的用途,此时您也许只想(对一些测试情况)检查错误消息,而不想看到正常输出。

selpg -s1 -e2 input.txt | wc

selpg 的标准输出透明地被 shell/内核重定向,成为“other_command”的标准输入,第 10 页到第 20 页被写至该标准输入。“other_command”的示例可以是 lp,它使输出在系统缺省打印机上打印。“other_command”的示例也可以 wc,它会显示选定范围的页中包含的行数、字数和字符数。“other_command”可以是任何其它能从其标准输入读取的命令。错误消息仍在屏幕显示。

selpg -s10 -e20 input.txt 2>error.txt | wc

与上面的示例 9 相似,只有一点不同:错误消息被写至“error.txt”。



selpg -s1 -e2 -l33 input.txt

该命令将页长设置为 33 行,这样 selpg 就可以把输入当作被定界为该长度的页那样处理。第 1 页到第 2 页被写至 selpg 的标准输出(屏幕)。


selpg -s1 -e2 -f input_file

假定页由换页符定界。第 1 页到第 2 页被写至 selpg 的标准输出(屏幕)。


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