源代码
selpg即SELect PaGes。允许用户指定从输入文本抽取的页的范围,这些输入文本可以来自文件或另一个进程。
func main(){
sa := new(selpgArgs)
getArgs(sa)
checkArgs(sa)
processArgs(sa)
}
整个运行流程分为3个主要部分:getArgs
checkArgs
processArgs
,其中getArgs()方法用于从输入的命令中获取各个参数的值并将它们保存在一个结构体中,checkArgs()用于检查这些参数是否合理和完整,processArgs()用于执行该命令,进行文件读写、重定向等操作。
为了方便获取及管理输入命令的参数,这里需要定义一个结构体将它们存储起来。
type selpgArgs struct {
startPage int
endPage int
length int
pageType bool
destination string
filename string
}
该结构体中包含了所有用到的参数,其中pageType对应于参数-f
,当其值为真时,文本由“\f”来确定换页,为假时则根据length值来确定每页的行数。destination对应-d
的选项。
该方法用于获取输入的参数,需要用到pflag包。
import flag "github.com/spf13/pflag"
pflag的使用方法与flag大致相同,可以将flag绑定到一个变量上来实现命令行参数和结构体中变量的绑定。
func IntVarP(flagvar, name, shorthand, defValue, usage)
其中Type是要绑定的变量的类型,flagvar就是要绑定的变量的引用,name为参数名称,shorthand为参数短名,defValue为变量的缺省值,usage为参数的提示信息,在使用-h
显示。绑定了需要的变量之后,需要调用flag.Parse()
来使其生效。
flag.IntVarP(&(sa.startPage), "start", "s", -1, "the start page")
flag.IntVarP(&(sa.endPage), "end", "e", -1, "the end page")
flag.IntVarP(&(sa.length), "length", "l", 72, "the length of a page")
flag.BoolVarP(&(sa.pageType), "type", "f", false, "change page at \\f")
flag.StringVarP(&(sa.destination), "destination", "d", "", "the destination")
flag.Parse()
对于输入文件名,则可以通过flag.Arg()
得到,该方法可以获取命令行中除了已绑定的参数外的其他输入参数,flag.NArg()
会返回这些参数的数量,当用户输入了文件名时,flag.NArg()
返回值大于0,此时flag.Arg(0)
就是我们需要的内容。
if flag.NArg() > 0 {
sa.filename = flag.Arg(0)
} else {
sa.filename = ""
}
该方法用于检查输入的参数是否包含必要内容以及是否合法。比如是否输入了开始页和结束页,开始页是否大于0,开始页数值是否不大于结束页值等。当出现参数不合理时,将错误信息输出到os.Stderr
中并调用os.Exit()
退出程序。
if sa.startPage == -1 || sa.endPage == -1 {
fmt.Fprintf(os.Stderr, "Please input start page and end page")
os.Exit(0)
}
该方法用于具体执行输入的命令。这里需要用到许多文件读写操作,因此需要引入bufio
包。
首先需要获取输入文本,定义一个bufio.Reader
用于读取,然后需要判断是从标准输入中读取,还是从外部文件中读取。当参数中的filename
值为空时,读取标准输入,即os.Stdin
,当其不为空时,调用os.Open()
打开目标文件并用reader读取。注意打开的文件要用Close()
关闭,可以用defer来延迟这一操作。
if sa.filename != "" {
fin, err := os.Open(sa.filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Open file failed")
os.Exit(0)
}
reader = bufio.NewReader(fin)
defer fin.Close()
} else {
reader = bufio.NewReader(os.Stdin)
}
接下来要处理-d
参数。这个参数指定了 lp 命令“-d”选项可接受的打印目的地名称,我们需要打开一条到目标子进程的管道来将数据输入到该进程。实现这些功能需要引入os/exec
包。
exec可以执行外部的命令,exec.Commend()
方法可以创建一个命令对象cmd并为其指定一个目标子进程。调用该对象的StdinPipe()
方法可以返回一个到目标子进程的输入管道,这是一个io.WriterClose
对象,可以用于将数据输入到目标子程序中。之后调用cmd.Run()
即可启动该子进程,打开管道。注意打开的管道同样需要调用Close()
关闭,可以用defer来延迟。
if sa.destination != "" {
cmd := exec.Command("lp", "-d", sa.destination)
inPipe, err := cmd.StdinPipe() //get the pipe
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open pipe to %s\n", sa.destination)
os.Exit(0)
}
err := cmd.Run()
if err != nil {
os.Stderr.Write([]byte("no printer\n"))
}
defer inPipe.Close()
}
然后是处理文本的输出。得到输入文本后,读出相应页数的内容并输出。这里需要考虑两种情况,分别是-f
参数和-l
参数两种。
当参数中有-f
时,采用按页读取的方式,利用reader的ReadString()
方法,每次循环一直读取到出现’\f’字符为止,用一个变量current_page
记录当前读取的页数,每当读取的页在指定的范围内时,将该页输出到管道或标准输出。
if sa.pageType {
for {
one_page, err := reader.ReadString('\f')
if err != nil && err != io.EOF {
fmt.Fprintf(os.Stderr, "Failed to read a page\n")
os.Exit(0)
}
if current_page >= sa.startPage && current_page <= sa.endPage{
if sa.destination != "" {
fmt.Fprintf(inPipe, "%s", one_page)
} else {
fmt.Fprintf(fout, "%s", one_page)
}
}
//output to stdout or pipe
if err == io.EOF || current_page > sa.endPage {
break
}
current_page ++
}
}
当参数中由-l
指定了每页的行数时,则采用逐行读取的方法。每次循环利用ReadString()
读取到’\n’为止,即获取一行。在用current_page
记录当前页数的同时,用一个变量current_line
记录当前行数,当行数到达每页最大行数时,页数增加,行数清零。然后将指定页数范围内的所有行输出到管道或标准输出。
else {
for {
one_line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
fmt.Fprintf(os.Stderr, "Failed to read a line\n")
os.Exit(0)
}
if current_page >= sa.startPage && current_page <= sa.endPage{
if sa.destination != "" {
fmt.Fprintf(inPipe, "%s", one_line)
} else {
fmt.Fprintf(fout, "%s", one_line)
}
}
//output to stdout or pipe
current_line ++
if current_line >= sa.length {
current_line = 0
current_page ++
}
if err == io.EOF || current_page > sa.endPage {
break
}
}
}
最后判断一下参数中的页数范围是否超出了文本自身包含的页数范围。
if current_page < sa.startPage {
fmt.Fprintf(os.Stderr, "The start page is greater than the total pages\n")
} else if current_page < sa.endPage {
fmt.Fprintf(os.Stderr, "The end page is greater than the total pages\n")
}
首先编写一个程序用于生成测试文本。每20行插入一个’\f’(第0行之后也有一个’\f’)。
package main
import (
"strconv"
"fmt"
"io/ioutil"
)
func main() {
name := "test.txt"
content := ""
for i := 0; i < 100; i ++ {
content = content + "Test file line " + strconv.Itoa(i) + "\n"
if i % 20 == 0 {
content = content + "\f"
}
}
data := []byte(content)
if ioutil.WriteFile(name,data,0644) == nil {
fmt.Println("写入文件成功:",content)
}
}
$ ./selpg --s 1 --e 3 --f test
使用-f参数,读取test文件,每20行一页(第一行单独一页),输出如下:
$ ./selpg --s 1 --e 3 --f
使用重定向来读取test,参数同上,输出如下:
$ ./selpg --s 1 --e 3 --l 3
输出前9行,内容如下:
$ ./selpg -s1 -e1
不指定-l和-f,默认每页72行,输出前72行,内容如下:
$ ./selpg -s1 -e3 -l3 output
将前9行导出至output文件,查看该文件,内容如下,可以看到前9行被成功导出。
$ ./selpg --s 1 --e 1 --l 3 test2 2>error
打开一个错误的文件作为非法输入,查看error文件,内容如下,可以看到导出了错误信息。
$ cat test | ./selpg --s 1 --e 2 --f
使用cat命令读取test文件,然后将输出重定向到selpg的输入,读取前两页(21行),输出如下:
$ ./selpg -s1 -e2 -f
读取test的前两页(21行),将输出重定向到cat命令的输入,输出如下:
$ ./selpg -s1 -e1
由于没有打印机,所以输出错误信息: