服务计算
概述
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
提示:
- 请按文档 使用 selpg 章节要求测试你的程序
- 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
- golang 文件读写、读环境变量,请自己查 os 包
- “-dXXX” 实现,请自己查
os/exec
库,例如案例 Command,管理子进程的标准输入和输出通常使用io.Pipe
,具体案例见 Pipe - 请自带测试程序,确保函数等功能正确
在做这次任务之前,我们需要先下载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 的标准输出(屏幕)。