Golang开发 Linux 命令行实用程序——selpg
概述
CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。
本次实验需要用golang开发一个Linux 命令行实用程序——selpg。有关selpg的内容及介绍在此:开发 Linux 命令行实用程序
原作者是使用c语言实现的,我们的目标是使用golang实现
开发实践
一、参数介绍
根据开发 Linux 命令行实用程序中的介绍,selpg可以接收如下参数:
- -sNumber
- -eNumber
- -lNumber
- -f
- -dDestination
其中,前两二个参数为强制选项,分别代表文件读取的起始页号和终止页号。第三个参数和第四个参数为可选参数,前者接收一个数字,该数字设置了一个页有多少行,后者代表一个页的结束是由换页符(’\f’)决定。因此,这两个参数选项是互斥的,若这两个参数都没有被设置,则默认一页有72行。最后一个选项表示将选定的页直接发送至打印机。
二、代码实现
参数处理
原作者用c实现时手动处理参数输入,而在golang中,我们可以使用pflag包来方便对命令行参数的解析。关于flag和pflag的介绍可以参见
flag - 命令行参数解析
Golang : pflag 包简介
简单来说,使用flag或者pflag可以将输入的参数自动进行处理,可以自动识别出跟在参数选项后的值,并将其赋给对应的变量。
为了使用pflag,需要手动引用该库。
import flag "github.com/spf13/pflag"
定义flag:flag.XxxP(),Xxx可以是 Int、String 等, 返回一个相应类型的指针。
// 定义命令行参数对应的指针
var s = flag.IntP("s", "s", 0, "1st arg should be -sstart_page")
var e = flag.IntP("e", "e",0, "2nd arg should be -eend_page")
var l = flag.IntP("l", "l",72, "-f | -llines_per_page")
var f = flag.BoolP("f", "f", false, "-f | -llines_per_page")
var d = flag.StringP("d", "d", "", "-ddest")
通过对命令行参数对应指针的定义,其指向的变量值已默认为0,0,72,false和" "。假设在命令行中输入了如下指令:
-s1 -e2
则指针s所指向的变量值被设置为1,而指针e所指向的变量值的值被设置为2.
定义完命令行参数后,需要把用户传递的命令行参数解析为对应变量的值,为了完成这一步骤,需要调用flag.Parse()函数
flag.Parse()
由于所需的所有变量都从命令行接收,不妨定义一个结构体,其包含文件的起始与结束页,输入的文件名,页长度,页类型(即是以固定行数区分页还是以换页符区分页)以及输出打印机的地址。
type selpg_args struct {
start_page int
end_page int
in_filename string
page_len int
page_type int /* 'l' for lines-delimited, 'f' for form-feed-delimited */
/* default is 'l' */
print_dest string
}
将主要的操作分为两个函数:process_args(&sa) 与process_input(&sa),process_args(&sa)负责输入参数的处理,例如对错误输入的报错和对结构体变量的赋值。
判定参数数量是否合法时可能需要用到flag的几个函数,如下:
Arg(i int) 和 Args()、NArg()、NFlag()
Arg(i int) 和 Args() 这两个方法就是获取 non-flag 参数的;NArg() 获得 non-flag 的个数;NFlag() 获得 FlagSet 中 actual 长度(即被设置了的参数个数)。
当输入参数的值不合理的时候,需要产生错误输出,因此需要调用fmt.Fprintf函数打印错误输出。需要注意的是,由于golang中没有定义INT_MAX,因此需要通过一个简单的位运算来确定有符号整数的最大值
const INT_MAX = int(^uint(0) >> 1)
func process_args(sa *selpg_args){
flag.Parse()
if flag.NFlag() < 2 {
fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", progname)
flag.Usage()
os.Exit(1)
}
if *s < 1 || *s > INT_MAX - 1{
fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname,*s)
flag.Usage()
os.Exit(2)
}
if *e < 1 || *s > INT_MAX - 1{
fmt.Fprintf(os.Stderr, "%s: invalid end page %d\n", progname,*e)
flag.Usage()
os.Exit(3)
}
if *l < 1 || *s > INT_MAX - 1{
fmt.Fprintf(os.Stderr, "%s: invalid page length %d\n", progname,*l)
flag.Usage()
os.Exit(4)
}
sa.start_page = *s
sa.end_page = *e
sa.page_type = *l
if *f{
sa.page_type = -1
}
if(flag.NArg()>0){
_, err := os.Stat(flag.Args()[0])
if os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "%s: input file \"%s\" does not exist\n", progname,flag.Args()[0])
os.Exit(5)
}
sa.in_filename = flag.Args()[0]
}
/*if(flag.NArg()>1){
sa.print_dest = flag.Args()[1]
}*/
sa.print_dest = *d
if *l != 0{
sa.page_len = *l
}
}
输入、输出与重定向
process_input(&sa)函数负责处理输入、输出和重定向。首先定义一个标准输入
file := os.Stdin
再定义一个标准输出
write := os.Stdout
若没有输入文件,则输入来自键盘,若存在输入文件,则应从输入文件中读取数据。
if sa.in_filename != "" {
file, _ = os.Open(sa.in_filename)
}
利用bufio包来完成数据的写入。
bufio 包实现了带缓存的 I/O 操作,它封装一个 io.Reader 或 io.Writer 对象 ,使其具有缓存和一些文本读写功能
scanner := bufio.NewScanner(file)
接下来就可以通过scanner.Scan()函数来对数据进行读取。由于读取数据时需要根据文件的页数来进行读取,因此读取数据时需要跳过一些无效的数据。当参数-f为true时,则需要以分页符来作为页结束的标志。最后,使用fmt.Fprintf函数来输出标准输出。
“-dXXX”的实现比较麻烦,需要在程序中调用cmd命令,还涉及到了子进程的输入输出。由于“-dDestination”选项将选定的页直接发送至打印机。这里,“Destination”应该是 lp 命令“-d”选项可接受的打印目的地名称。因此,实现的大致思路是先将"lp -d%s"和sa.print_dest拼接在一起赋给一个字符串s1,再利用io.Pipe()来管道化shell命令的输出。最后调用cmd.Run()来执行对应命令。具体代码如下:
if sa.print_dest != ""{
var s1 string
fmt.Sprintf(s1,"lp -d%s",sa.print_dest)
pr,pw := io.Pipe()
defer pw.Close()
cmd := exec.Command(s1,"w")
cmd.Stdout = pw
go func(){
defer pr.Close()
if _, err := io.Copy(os.Stdout, pr); err != nil {
log.Fatal(err)
}
}()
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
Examples For Using io.Pipe in Go中有利用io.Pipe来实现管道化shell命令的详细解释和例子。
三、功能测试
将该程序放在一个名为CLI的包里,执行命令
go install
会生成一个可执行的二进制文件。在终端输入selpg即可运行该程序。
输入文件名为temp.txt,其中数据有150行,每行只包含一个整数,由1开始递增。
-
selpg -s1 -e1 input_file
将把“input_file”的第 1 页写至标准输出(也就是屏幕),因为这里没有重定向或管道。 -
selpg -s1 -e1 < temp.txt
selpg 读取标准输入,而标准输入已被 shell/内核重定向为来自“input_file”而不是显式命名的文件名参数。输入的第 1 页被写至屏幕。输出与上例相同,每输出一页72行数据。 -
other_command | selpg -s10 -e20
“other_command”的标准输出被 shell/内核重定向至 selpg 的标准输入。将第 10 页到第 20 页写至 selpg 的标准输出(屏幕)。由于文件中数据只有三页,则输出错误信息 -
selpg -s1 -e1 temp.txt >temp2.txt
selpg 将第 1页到第 1 页写至标准输出;标准输出被 shell/内核重定向至“output_file”。打开temp2.txt,可以看到已经被写入了一页72行数据。
-
selpg -s10 -e20 input_file 2>error_file
selpg 将第 10 页到第 20 页写至标准输出(屏幕);所有的错误消息被 shell/内核重定向至“temp2.txt”。
selpg 将第 1 页到第 2 页写至标准输出,标准输出被重定向至“temp2.txt”;selpg 写至标准错误的所有内容都被重定向至“error_file”。可以看到temp2.txt中被写入了2页共144行数据,而error_file中没有看到错误输出。 -
selpg -s1 -e1 temp.txt >temp2.txt 2>error_file
-
selpg -s1 -e20 temp.txt 2>error_file | wc
selpg 的标准输出透明地被 shell/内核重定向,成为“wc”的标准输入,第 1 页到第 20 页被写至该标准输入。可以看到屏幕上有相应命令的输出而error_file中有错误输出 -
selpg -s1 -e2 -l66 temp.txt
该命令将页长设置为 66 行,这样 selpg 就可以把输入当作被定界为该长度的页那样处理。第 1 页到第 2 页被写至 selpg 的标准输出(屏幕)。 -
selpg -s1 -e1 -f temp.txt
假定页由换页符定界。第 1 页到第 1 页被写至 selpg 的标准输出(屏幕)。由于txt文件中没有换页符,因此屏幕中会显示所有数据。 -
selpg -s1 -e1 -dlp1 temp.txt
第 1 页到第 1 页由管道输送至命令“lp -dlp1”,该命令将使输出在打印机 lp1 上打印。由于没有打印机,因此会输出错误信息 -
selpg -s10 -e20 temp.txt > temp2.txt 2>error_file &
执行完该命令后,“进程标识”(pid)46678会在屏幕上显示,而shell提示符立即出现。
四、单元测试
在CLI包里创建一个CLI_test.go测试文件,并且将开发 Linux 命令行实用程序中C语言开发的selpg程序编译成a.out可执行文件,在测试文件中对二者的输出进行比较,若输出不相同则打印一个错误信息。执行go test
package CLI
import (
"testing"
"os/exec"
)
func TestSelpg2(t *testing.T) {
stdout, err := exec.Command("bash", "-c", "selpg -s1 -e1 temp.txt").Output()
stdout2, err2 := exec.Command("bash", "-c", "./a.out -s1 -e1 temp.txt").Output()
if err != err2 {
t.Error(err)
}
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e1 <temp.txt").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e1 <temp.txt").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "ls | selpg -s10 -e20").Output()
stdout2, err2 = exec.Command("bash", "-c", "ls | ./a.out -s10 -e20").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e1 temp.txt >temp2.txt").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e1 temp.txt >temp2.txt").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s10 -e20 temp.txt 2>error_file.txt").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s10 -e20 temp.txt 2>error_file.txt").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e1 temp.txt >temp2.txt 2>error_file.txt").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e1 temp.txt >temp2.txt 2>error_file.txt").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e20 temp.txt >temp2.txt 2>/dev/null").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e20 temp.txt >temp2.txt 2>/dev/null").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e2 temp.txt >/dev/null").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e2 temp.txt >/dev/null").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e20 temp.txt 2>error_file | wc").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e20 temp.txt 2>error_file | wc").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e2 -l66 temp.txt").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e2 -l66 temp.txt").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e1 -f temp.txt").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e1 -f temp.txt").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s1 -e1 -dlp1 temp.txt").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s1 -e1 -dlp1 temp.txt").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
stdout, err = exec.Command("bash", "-c", "selpg -s10 -e20 temp.txt >temp2.txt 2>error.txt &").Output()
stdout2, err2 = exec.Command("bash", "-c", "./a.out -s10 -e20 temp.txt >temp2.txt 2>error.txt &").Output()
for i:=0;i<len(stdout);i++{
if stdout[i] != stdout2[i]{
t.Error("error")
break
}
}
if err != nil {
t.Error(err)
}
}
发现错误的输出都不是“error”,表示执行用原作者用C语言编写的selpg和本人用golang编写的selpg程序输出相同,而显示的错误输出是程序执行中遇到的错误情况而出现的,例如结束页大于文件总页数,没有打印机等。
实验总结
本次实验学会了用golang编写CLI程序,熟悉了命令行参数以及IO:stdin、stdout、stderr、管道、重定向