使用 golang 开发 开发 Linux 命令行实用程序 中的 selpg
1:selpg程序介绍:
1.1简介:
selpg 是类似于ls等标准Linux命令的命令程序,这个名称代表 SELect PaGes。selpg 允许用户指定从输入文本抽取的页的范围,这些输入文本可以来自文件或另一个进程。selpg 是以在 Linux 中创建命令的事实上的约定为模型创建的,这些约定包括:
独立工作
在命令管道中作为组件工作(通过读取标准输入或文件名参数,以及写至标准输出和标准错误)
接受修改其行为的命令行选项
1.2程序逻辑:
selpg 是从文本输入选择页范围的实用程序。该输入可以来自作为最后一个命令行参数指定的文件,在没有给出文件名参数时也可以来自标准输入。
selpg 首先处理所有的命令行参数。在扫描了所有的选项参数(也就是那些以连字符为前缀的参数)后,如果 selpg 发现还有一个参数,则它会接受该参数为输入文件的名称并尝试打开它以进行读取。如果没有其它参数,则 selpg 假定输入来自标准输入。
1.3 参数介绍:
1:强制选项“-sNumber”和“-eNumber”:
selpg 要求用户用两个命令行参数“-sNumber”(例如,“-s10”表示从第 10 页开始)和“-eNumber”(例如,“-e20”表示在第 20 页结束)指定要抽取的页面范围的起始页和结束页。另外需要进行合理性检查:它会检查两个数字是否为有效的正整数以及结束页是否不小于起始页。
$ selpg -s10 -e20 …
2:“-lNumber”和“-f”可选选项:
这两个选项是互斥的,分别表示selpg 可以处理的两种输入文本类型:
类型 1:该类文本的页行数固定。如果既没有给出“-lNumber”也没有给出“-f”选项,则 selpg 会理解为页有固定的长度(每页 72 行)。该缺省值可以用“-lNumber”选项覆盖。
$ selpg -s10 -e20 -l66 …
表明页有固定长度,每页为 66 行。
类型 2:该类型文本的页由 ASCII 换页字符(十进制数值为 12,在 C 中用“\f”表示)定界。
类型 2 格式由“-f”选项表示,如下所示:
$ selpg -s10 -e20 -f …
3:“-dDestination”可选选项:
允许用户使用“-dDestination”选项将选定的页直接发送至打印机。这里,“Destination”应该是 lp 命令“-d”选项(请参阅“man lp”)可接受的打印目的地名称。
在下面的示例中,我们打开到命令
$ lp -dDestination
的管道以便输出,并写至该管道而不是标准输出:
selpg -s10 -e20 -dlp1
该命令将选定的页作为打印作业发送至 lp1 打印目的地。
如果在运行 selpg 命令之后立即运行命令
lpstat -t | grep lp1 ,
您应该看见 lp1 队列中的作业。
1.4:输入处理
一旦处理了所有的命令行参数,就使用这些指定的选项以及输入、输出源和目标来开始输入的实际处理。
selpg 通过以下方法记住当前页号:如果输入是每页行数固定的,则 selpg 统计新行数,直到达到页长度后增加页计数器。如果输入是换页定界的,则 selpg 改为统计换页符。这两种情况下,只要页计数器的值在起始页和结束页之间这一条件保持为真,selpg 就会输出文本(逐行或逐字)。当那个条件为假(也就是说,页计数器的值小于起始页或大于结束页)时,则 selpg 不再写任何输出。瞧!您得到了想输出的那些页。
2.代码编写:
1.使用pflag:
为了满足unix命令行规范,使用pflag替换flag;需要到spf13/pflag下载pflag包并解压到src目录下
2:编写代码:
2.1:储存selpg命令参数值的结构体;
type SelpArgs struct {
progname string //程序名
StartPage int //起始页号
EndPage int //结束页号
InFile string //输入文件路径
PrintDest string //打印机的路径
PageLen int //每页的行数
PageType bool //页类型 false -> 按行数计算 true -》按分页符
}
2.2 处理参数函数ProcessArgs():
1:在这个函数中,我们将上面结构体的成员变量绑定在flag上。这是默认值,名字等属性。
2:然后设置Usage,在输入错误时提示正确的命令格式;
3:最后通过Parse()函数来进行解析
func ProcessArgs(args *SelpArgs) {
//将结构体args的各个成员变量绑定到pflag上
pflag.IntVarP(&args.StartPage, "StartPage", "s", -1, "Define startPage")
pflag.IntVarP(&args.EndPage, "EndPage", "e", -1, "Define endPage")
pflag.IntVarP(&args.PageLen, "PageLength", "l", 72, "Define pageLength")
pflag.StringVarP(&args.PrintDest, "PrintDest", "d", "", "Define printDest")
pflag.BoolVarP(&args.PageType, "PageType", "f", false, "Define pageType")
//提示命令的正确格式
pflag.Usage = func() {
fmt.Fprintf(os.Stderr, "USAGE: %s -sstart_page -eend_page [ -f | -l lines_per_page ][ -d dest ] [ in_filename ]\n", args.progname)
pflag.PrintDefaults()
}
//分析各个成员的值
pflag.Parse()
}
2.3 执行命令的函数:ProcessInput()
该函数主要由两部分组成:
//参数正确性检查
//命令执行
func ProcessInput(args *SelpArgs) {
//检查命令参数是否正确
checkArgs(args)
//执行命令
CmdOpera(args)
}
1:正确性检查函数checkArgs()
//-s n -e n 为必须参数,所以个数不会少于5
//第二个和第四个参数必须分别为’-s’ ‘-e’
//起始页必须大于0且小于终止页
//命令行中不能同时出现-l和-f
//页的行数不能小于0
//如果命令中有输入文件路径,判断文件是否存在。
//设置结构体输入文件成员变量
func checkArgs(arg *SelpArgs) bool {
if len(os.Args) < 5 { //-s n -e n 为必须参数,所以个数不会少于5
fmt.Fprintf(os.Stderr, "[Error] The arguments are incomplete\n")
pflag.Usage()
os.Exit(1)
}
if os.Args[1] != "-s" || os.Args[3] != "-e" { //第二个和第四个参数必须分别为'-s' '-e'
fmt.Fprintf(os.Stderr, "The start and end page argumnet can't be empty")
pflag.Usage()
os.Exit(2)
} else if arg.StartPage > arg.EndPage || arg.StartPage < 0 { //起始页必须大于0且小于终止页
fmt.Fprintf(os.Stderr, "The satrt page or end page is invalid")
pflag.Usage()
os.Exit(3)
} else if arg.PageType == true && arg.PageLen != 72 { //命令行中不能同时出现-l和-f
fmt.Fprintf(os.Stderr, "The argument 'l' and 'f' are exclude")
pflag.Usage()
os.Exit(4)
} else if arg.PageLen < 0 { //页的行数不能小于0
fmt.Fprintf(os.Stderr, "The page length is invalid")
pflag.Usage()
os.Exit(5)
}
if len(pflag.Args()) == 1 { //如果命令中有输入文件路径,判断文件是否存在。
_, err := os.Stat(pflag.Args()[0])
if os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "\n Can not find the file \n")
os.Exit(6)
}
arg.InFile = pflag.Args()[0] //设置结构体输入文件成员变量
}
return true
}
2:命令执行函数 CmdOpera()
//判断输入路径是否为空,如果为空就将输入设置为标准输入
//输入文件不为空,设置输入为该文件
//如果输出打印机为空,将输出设置为标准输出
//输出打印机存在,则执行系统命令cat创建管道,将输出传送到打印机的输入接口
func CmdOpera(arg *SelpArgs) {
var FileInput *os.File
if arg.InFile == "" { //判断输入路径是否为空,如果为空就将输入设置为标准输入
FileInput = os.Stdin
} else { //输入文件不为空,设置输入为该文件
var err error
FileInput, err = os.Open(arg.InFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Can't open the file")
os.Exit(7)
}
}
if arg.PrintDest == "" { //如果输出打印机为空,将输出设置为标准输出
fout := os.Stdout
stdPrint(fout, FileInput, arg.StartPage, arg.EndPage, arg.PageLen, arg.PageType)
} else { //输出打印机存在,则创建管道,将输出传送到打印机的输入接口
cmd := exec.Command("cat")
var err1 error
cmd.Stdout, err1 = os.OpenFile(arg.PrintDest, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err1 != nil {
fmt.Fprintf(os.Stderr, "Fail to open file\n")
os.Exit(8)
}
fout, err := cmd.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "Fail to open stdinpipe")
os.Exit(9)
}
cmd.Start()
PipePrint(fout, FileInput, arg.StartPage, arg.EndPage, arg.PageLen, arg.PageType)
}
}
3 打印函数:
//按两种分页的类型进行处理
func PipePrint(fout io.WriteCloser, fin *os.File, start int, end int, len int, Type bool) {
atLine := 1 //当前行号
atPage := 1 //当前页号
buffIn := bufio.NewReader(fin) //定义输入流
for {
if Type == false { //按固定行数分页
line, err := buffIn.ReadString('\n') //以换行符为分隔读取一行
if err != nil {
break
}
atLine++ //每读取一行,行号加一
if atLine > len {
atPage++
atLine = 1
}
if atPage >= start && atPage <= end {//判断是否处于需要输出的页
_, err2 := fout.Write([]byte(line))
if err2 != nil {
os.Exit(12)
}
}
} else { //按分页符'\f'分页
page, err3 := buffIn.ReadString('\f')
if err3 != nil {
break
}
atPage++
if atPage >= start && atPage <= end {
_, err5 := fout.Write([]byte(page))
if err5 != nil {
os.Exit(13)
}
}
}
}
}
3:编写测试代码:
编写测试观察命令的执行是否正确:
package main
import (
"fmt"
"os/exec"
"testing"
)
func TestSelpg(t *testing.T) {
var commands []string
commands = []string{"./selpg -s 2 -e 4 -l 2 clitest.txt",
"./selpg -s 2 -e 5 -l 2 clitest.txt 2>error.txt",
"more +20 clitest.txt | ./selpg -s 2 -e 5 -l 1",
}
cmd := exec.Command("./selpg", "-s", "2", "-e", " 4", " -l", " 2", " clitest.txt")
err1 := cmd.Start()
if err1 != nil {
fmt.Printf("\nCommont1 :\" %s \" fail to run \n", commands[0])
t.Error(err1)
}
cmd = exec.Command("./selpg", "-s", "2", "-e", " 5", " -l", " 2", "clitest.txt", "2>error.txt")
err1 = cmd.Start()
if err1 != nil {
fmt.Printf("\nCommont2 :\" %s \" fail to run \n", commands[1])
t.Error(err1)
}
cmd = exec.Command("more", "+20", "clitest.txt", "|./selpg", "-s", "2", "-e", " 5", " -l", " 2")
err1 = cmd.Start()
if err1 != nil {
fmt.Printf("\nCommont3 :\" %s \" fail to run \n", commands[2])
t.Error(err1)
}
}
4:按文档 使用 selpg 章节要求测试你的程序:
1:先编译程序,生成程序selpg
go build selpg.go
2:clitest.txt:默认输入文件
clitest1.txt输出重定向文件
error.txt错误重定向文件:
下面的命令将把“input_file”的第 1 页写至标准输出(也就是屏幕),因为这里没有重定向或管道。文本文件没有分页符,所以输出为整个文件内容:
$ selpg -s 1 -e 1 clitest.txt
3该命令与示例 1 所做的工作相同,但在本例中,selpg 读取标准输入,而标准输入已被 shell/内核重定向为来自“input_file”而不是显式命名的文件名参数。输入的第 1 页被写至屏幕。
$ selpg -s 1 -e 1 < clitest.txt
4:“more”的标准输出被 shell/内核重定向至 selpg 的标准输入。
$ more +20 clitest.txt | selpg -s 2 -e 5 -l 1
5:selpg 将第 2 页到第 5 页写至标准输出(屏幕);每页两行;标准输出被 shell/内核重定向至“output_file”。
$ selpg -s 2 -e 5 -l 2 clitest.txt >clitest1.txt
6::selpg 将第 2 页到第 5 页写至标准输出(屏幕);每页两行;所有的错误消息被 shell/内核重定向至“error_file”。请注意:在“2”和“>”之间不能有空格;
$ selpg -s 2 -e 5 -l 2 clitest.txt 2>error.txt
上面命令正确,查看error.txt文件里面无内容,我们输入一个错误的命令观察该文件:
$ selpg -s clitest.txt 2>error.txt
7:selpg 将第 2 页到第 5 页写至标准输出,标准输出被重定向至“clitest1.txt”;selpg 写至标准错误的所有内容都被重定向至“error.txt”
$ selpg -s 2 -e 5 -l 2 clitest.txt >clitest1.txt 2>error.txt
8:selpg 的标准输出透明地被 shell/内核重定向,成为“more”的标准输入,第 2 页到第 5 页被写至该标准输入。
$ selpg -s 2 -e 5 -l 2 clitest.txt |more
$ selpg -s 2 -e 5 -l 2 clitest.txt |more +3