Golang开发 Linux 命令行实用程序——selpg

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、管道、重定向

gitee地址

传送门

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值