selpg介绍
本文介绍开发selpg(SELect PaGes),一个类似于cat
,ls
,pr
等标准命令的Linux命令行实用程序。selpg允许用户指定从输入文本抽取的页的范围,这些输入文本可以来自文件或者另一个进程,然后将输入进行打印操作。由于运行环境为CentOS7系统的虚拟机,并且并没有实际的打印机,所以我们需要在虚拟机中安装虚拟打印机。具体方法参见传送门。
命令行准则
在实现selpg的过程中,我们需要掌握命令行的标准,从而可以开发出更加对用户更加有好的,确保用户以更加灵活的方式使用该程序。
主要准则包括输入、输出、错误输出、执行以及命令行参数。简要介绍:
输入
$ command input_file # 从input_file获取标准输入(stdin)
$ command # 从终端获取标准输入
$ command < input_file # 利用 < 重定向标准输入,从input_file获取输入
$ other_command | command # 利用 | (pipe)使得标准输入来自另一个程序的标准输出
输出
$ command # 标准输出(stdout)输出到终端
$ command > output_file # 利用 > 重定向标准输出到output_file
$ command | other_command # 利用 | (pipe)将标准输出成为另一命令的标准输入
错误输出
$ command # 标准错误(stderr)被输出到终端
$ command 2>error_file # 标准错误重定向到error_file
$ command >output_file 2>error_file # 标准输出和标准错误重都定向到文件
$ command 2>&1 # 将标准错误发送至标准输出被重定向的任何位置,此命令中标准错输出为终端,所以标准错误也为终端
$ command >output_file 2>&1 # 将标准错误和标准输出都被定向到output_file
执行
不管程序的输入源(文件、管道或终端)和输出目的地是什么,程序都应该以同样的方式工作。这使得在如何使用它方面有最大的灵活性。
命令行参数
作为选项的命令行参数右前缀“-”或者“–”标识;另一类参数是那些不是选项的参数,并不改变程序的行为,这类参数代表要处理的文件名等等。
Linux 实用程序语法图看起来如下:
$ command mandatory_opts [ optional_opts ] [ other_args ]
其中:
command
是命令本身的名称。mandatory_opts
是为使命令正常工作必须出现的选项列表。optional_opts
是可指定也可不指定的选项列表,这由用户来选择;但是,其中一些参数可能是互斥的。other_args
是命令要处理的其它参数的列表;这可以是任何东西,而不仅仅是文件名。
以上内容整理来自开发Linux命令行实用程序。
selpg参数介绍
参数(值) | 是否必须 | 介绍 |
---|---|---|
s(start_page_num) | 必须 | 表示从输入数据的第start_page_num开始 |
e(end_page_num) | 必须 | 表示从输入数据的第end_page_num结束 |
l(page_len) | 非必须 | 表示输入数据每page_len行为1页 |
f | 非必须 | 表示输入数据以\f为换页符 |
d(Destination) | 非必须 | Destination是lp命令的“-d”选项可接受的打印目的地名称;即selpg命令加上-d参数之后,表示Destination打印机打印输入数据。若要验证该选项是否已经生效,可以运行命令“lpstat -t”,查看添加到“Destination”打印队列是否有此打印作业。如果当前打印机可用,则会打印该输出,这一特性是用popen()系统调用来实现的,该系统调用允许一个进程打开到另一个进程的管道,将管道用于输出或输入。 |
Golang实现
数据结构
type Selpg_Args struct {
start_page *int // -s参数值(开始页面数)
end_page *int // -e参数值(结束页面数量)
page_len *int // -l参数值(以page_len行为1页)
page_type_f *bool // -f参数值(以\f为分页符)
output *string // -o参数值(输出文件名)(可忽略)
input string // 输入文件名
dest_print *string // -d参数值(打印目的地)
}
参数解析
利用pflag第三方库可以方便处理参数情况,有关flag和pflag库介绍详见传送门
func initial() {
// selpg_args.page_len = 72
// 添加shorthand参数(去掉方法后面的字母P即取消shorthand参数)
selpg_args.start_page = pflag.IntP("start_page", "s", -1, "start page")
selpg_args.end_page = pflag.IntP("end_page", "e", -1, "end page")
selpg_args.page_type_f = pflag.BoolP("form_feed", "f", false, "delimited by form feeds")
selpg_args.page_len = pflag.IntP("limit", "l", 72, "delimited by fixed page length")
selpg_args.output = pflag.StringP("output", "o", "", "output filename")
selpg_args.dest_print = pflag.StringP("dest", "d", "", "target printer")
// 另外一种写法
// pflag.IntVarP(selpg_args.start_page, "start_page", "s", -1, "start page")
// pflag.IntVarP(selpg_args.end_page, "end_page", "e", -1, "end page")
// pflag.BoolVarP(selpg_args.page_type_f, "form_feed", "f", false, "delimited by form feeds")
// pflag.IntVarP(selpg_args.page_len, "limit", "l", 72, "delimited by fixed page length")
// pflag.StringVarP(selpg_args.output, "output", "o", "", "output filename")
// pflag.StringVarP(selpg_args.dest_print, "dest", "d", "", "target printer")
selpg_args.input = ""
pflag.Usage = usage
pflag.Parse()
}
// usage 函数定义如下:
func usage() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, os.Args[0]+" -sstart_page_num -eend_page_num [ -f | -llines_per_page ] [ -doutput ] [ input ]\n")
fmt.Fprintln(os.Stderr, "[OPTIONS]:")
fmt.Fprintln(os.Stderr, " filename string input filename")
pflag.PrintDefaults() // PrintDefaults()表示输出默认的提示信息
}
校验参数
func handle_args() {
/* if len(os.Args) < 3, then we can know that -s or -e is not entered. */
/* if *selpg_args_.start_page == -1, then arg -s isn't entered, because -s-1 is not allowed(pflag can't analysis). */
if len(os.Args) < 3 || *selpg_args.start_page == -1 || *selpg_args.end_page == -1 {
fmt.Println("You are expected to input the start_page_num and end_page_num which will be greater than zero")
pflag.Usage()
os.Exit(1)
}
/* judge the start_page_num and end_page_num */
/* INT_MAX definition: const INT_MAX = int(^uint(0) >> 1) */
if *selpg_args.start_page <= 0 {
checkErr(errors.New("The start_page_num can't be less than or equal to zero"))
}
if *selpg_args.start_page > INT_MAX {
checkErr(errors.New("The start_page_num can't be greater than INT_MAX: " + strconv.Itoa(INT_MAX)))
}
if *selpg_args.end_page <= 0 {
checkErr(errors.New("The end_page_num can't be less than or equal to zero"))
}
if *selpg_args.end_page > INT_MAX {
checkErr(errors.New("The end_page_num can't be greater than INT_MAX: " + strconv.Itoa(INT_MAX)))
}
if *selpg_args.start_page > *selpg_args.end_page {
checkErr(errors.New("The end_page_num can't be greater than start_page_num"))
}
/* judge the page_len */
if *selpg_args.page_len <= 0 {
checkErr(errors.New("The -l limit can't be less than or equal to zero"))
}
for _, arg := range os.Args[1:] {
/* in the following judgements we can also use method: strings.HasPrefix() */
/* judge the option -f */
if matched, _ := regexp.MatchString(`^-f`, arg); matched {
if len(arg) > 2 {
checkErr(errors.New(os.Args[0] + ": option should be only \"-f\""))
}
}
/* judge the dest printer */
if matched, _ := regexp.MatchString(`^-d`, arg); *selpg_args.dest_print == "" && matched {
checkErr(errors.New(os.Args[0] + ": option -d requires a printer destination\n"))
}
/* judge the output file */
if matched, _ := regexp.MatchString(`^-o`, arg); *selpg_args.output == "" && matched {
checkErr(errors.New(os.Args[0] + ": option -o requires a output filename\n"))
}
/* store the input filename */
if arg[0] != '-' {
selpg_args.input = arg
break
}
}
}
处理命令输入
func handle_input() {
/* define InputFile = os.Stdin or *os.File */
var InputFile *os.File
if selpg_args.input != "" {
var err error
InputFile, err = os.Open(selpg_args.input)
if err != nil {
fmt.Fprintf(os.Stderr, "An error occurred on opening the inputfile: %s\nDoes the file exist?\n", selpg_args.input)
os.Exit(1)
}
} else {
InputFile = os.Stdin
}
defer InputFile.Close() // close the file before the end of the function
var OutputFile *os.File
var cmd *exec.Cmd
if *selpg_args.output != "" {
var err error
OutputFile, err = os.Create(*selpg_args.output)
checkErr(err)
} else if *selpg_args.dest_print != "" {
fmt.Printf("Printer: %s\n", *selpg_args.dest_print)
cmd = exec.Command("lp", "-d", *selpg_args.dest_print)
/* the command below can also work */
// cmd = exec.Command("lpr", "-P", *selpg_args.dest_print)
} else {
OutputFile = os.Stdout
}
defer OutputFile.Close()
/* use io.Pipe to create the pipe between input(including reading from file and stdin) and output(including writing to file, stdout and printing the data) */
r, w := io.Pipe()
/* create the go routine to read from stdin or file and put the data to PipeWriter*/
go readFile(w, InputFile)
/* define buffer to read from the PipeReader */
buf := new(bytes.Buffer)
buf.ReadFrom(r)
/* cmd != nil denotes that *selpg_args.dest_print != "" and we need to print the input data */
if cmd != nil {
/* define the reader and assign it to cmd.Stdin which denotes the input of the command(lp -ddest_print) */
cmd.Stdin = strings.NewReader(buf.String())
var outErr bytes.Buffer
cmd.Stderr = &outErr
/* execute the command(lp -ddest_print) */
err := cmd.Run()
if err != nil {
checkErr(errors.New(fmt.Sprint(err) + ": " + outErr.String()))
}
}
/* if we use argument -o, then we can write the input data to a file */
if OutputFile != nil {
OutputFile.WriteString(buf.String())
}
}
readFile函数
func readFile(w *io.PipeWriter, InputFile *os.File) {
/* define the reader */
inputReader := bufio.NewReader(InputFile)
/* default delimit = '\n' */
delimit := '\n'
pages := 0
lines := 0
inputString := ""
if *selpg_args.page_type_f {
delimit = '\f'
}
for {
/* we use the delimit as the delimiter to read from the file */
inputSubString, readerError := inputReader.ReadString(byte(delimit))
/* handle the input, replace all form feeds '\f' by '', then our print will not be influenced */
inputSubString = strings.Replace(inputSubString, "\f", "", -1)
/* inputString will store one page's content */
inputString += inputSubString
/* when delimiter is '\n', then we need to count the lines until lines are equal to *selpg_args.page_len, then we can print the page. */
if delimit == '\n' {
lines++
if lines == *selpg_args.page_len {
pages++
lines = 0
/* if pages is within the expected scope, we write it to PipeWriter. */
if pages >= *selpg_args.start_page && pages < *selpg_args.end_page {
print_pages++
fmt.Fprint(w, inputString+"\f")
inputString = ""
}
}
} else { /* the delimiter is '\f' */
pages++
if pages >= *selpg_args.start_page && pages < *selpg_args.end_page {
print_pages++
fmt.Fprint(w, inputString+"\f")
inputString = ""
}
}
/* the final page */
if pages >= *selpg_args.end_page {
print_pages++
fmt.Fprint(w, inputString)
break
}
/* the end of the file */
if readerError == io.EOF {
if inputString != "" && inputString != "\n" && inputString != "\f" {
print_pages++
fmt.Fprint(w, inputString)
}
break
}
}
/* close the PipeWriter */
w.Close()
}
程序测试
输入文件input.txt
如下:(按照换行符一共14行,按照换页符一共7页)
测试文件输入,控制台输出
$ ./selpg -s1 -e1 input_file
$ ./selpg -s1 -e1 < input_file
$ other_command | selpg -s1 -e1
$ ./selpg -s1 -e2 input_file >output_file 2>error_file
将第1页第二页写到标准输出,所有的错误信息被shell /内核重定向到“error_file”,2和>之间不能有空格。
$ ./selpg -s1 -e2 input_file >output_file 2>/dev/null
selpg 将第 1 页到第 2 页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至 /dev/null(空设备),这意味着错误消息被丢弃了。设备文件 /dev/null 废弃所有写至它的输出,当从该设备文件读取时,会立即返回 EOF。
$ ./selpg -s1 -e2 input_file >/dev/null
selpg 将第 10 页到第 20 页写至标准输出,标准输出被丢弃;错误消息在屏幕出现。
$ ./selpg -s1 -e2 input_file 2>error_file | other_command
$ ./selpg -s1 -e2 -l2 input_file
$ ./selpg -s1 -e2 -f input_file
由输入文件,前三行为第一页,接下来两行为第二页
$ ./selpg -s1 -e2 -dCups-PDF input_file
$ ./selpg -s1 -e2 input_file > output_file 2>error_file &
该命令利用了 Linux 的一个强大特性,即:在“后台”运行进程的能力。在这个例子中发生的情况是:“进程标识”(pid)如 1234 将被显示,然后 shell 提示符几乎立刻会出现,使得您能向 shell 输入更多命令。同时,selpg 进程在后台运行,并且标准输出和标准错误都被重定向至文件。这样做的好处是您可以在 selpg 运行时继续做其它工作。
可以使用ps命令来查看该进程的状态,由于瞬间完成,所以并没有看到该进程。
至此,selpg命令已经基本实现。