服务计算第五周作业——开发简单 CLI 程序

作业内容

Go Online 传送门

Github 传送门

实验内容

使用 golang 开发 Linux 命令行实用程序中的 selpg

  1. 请按文档 使用 selpg 章节要求测试你的程序
  2. 请使用 pflag 替代 goflag 以满足 Unix 命令行规范, 参考:Golang之使用Flag和Pflag
  3. golang 文件读写、读环境变量,请自己查 os 包
  4. “-dXXX” 实现,请自己查 os/exec 库,例如案例 Command,管理子进程的标准输入和输出通常使用 io.Pipe,具体案例见 Pipe

实验过程

首先selpg提供了c语言版的源代码:源代码传送门
所有我只需要根据c语言源代码改写成go语言版本即可。
首先需要导入pflag,在终端上执行如下命令:
go get github.com/spf13/pflag
然后我们go代码需要的包如下:

import (
	"bufio"
	"github.com/spf13/pflag"
	"fmt"
	"io"
	"math"
	"os"
	"os/exec"
)

在c语言版本中,定义了一个处理参数用的结构体:

struct selpg_args
{
	int start_page;
	int end_page;
	char in_filename[BUFSIZ];
	int page_len; /* default value, can be overriden by "-l number" on command line */
	int page_type; /* 'l' for lines-delimited, 'f' for form-feed-delimited */
					/* default is 'l' */
	char print_dest[BUFSIZ];
};

所以我们也需要在go语言版本中定义一个这样的结构体,类型基本一样,但由于go没有提供char类型的变量,所以改成使用是string类型的变量

type selpg_args struct{
	start_page int
	end_page int
	in_filename string
	page_len int	/* default value, can be overriden by "-l number" on command line */
	page_type string	/* 'l' for lines-delimited, 'f' for form-feed-delimited */
					/* default is 'l' */
	print_dest string	
}

接着是定义帮助函数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 ] [ -ddest ] [ in_filename ]\n")
    pflag.PrintDefaults()
}

对于读取参数部分,pflag包提供了简单命令的接口,我们直接使用这些函数,比c语言版本简化了许多:
对于pflag中的XXXVarP函数:

  • 第一个参数是需要与参数名绑定的变量
  • 第二个参数是参数名
  • 第三个参数是参数名的简写
  • 第四个参数是默认值
  • 第五个参数是help输出的信息
func Init(args *selpg_args){
	pflag.Usage = usage
    pflag.IntVarP(&(args.start_page), "start", "s", -1, "start page")
    pflag.IntVarP(&(args.end_page), "end", "e", -1, "end page")
    pflag.IntVarP(&(args.page_len), "line", "l", 72, "page len")
    pflag.StringVarP(&(args.print_dest), "destionation", "d", "", "print destionation")
	pflag.StringVarP(&(args.page_type), "type", "f", "l", "type of print")
}

然后是check函数,对于c语言版本的process_args函数,主要用于检查参数的合法性:参数的检查包括起始页和结束页是否合法;每一页的长度是否合法;页的格式是否合法等,这里只提供部分代码,详细参考文末的GitHub链接:

//Not enough args
	if len(os.Args) < 3 || args.start_page == -1 || args.end_page == -1{
		fmt.Fprintf(os.Stderr, "ERROR: %s not enough parameters!\n", os.Args[0])
		fmt.Fprintf(os.Stderr, "You should input the parameters start page and end page!\n")
		usage()
		os.Exit(0)
	}

	//Page's index error
	if args.start_page <= 0{
		fmt.Fprintf(os.Stderr, "ERROR: Invalid start page number! Can not less than or equal to 0!\n")
		os.Exit(1)
	}
	if args.end_page <= 0{
		fmt.Fprintf(os.Stderr, "ERROR: Invalid end page number! Can not less than or equal to 0!\n")
		os.Exit(2)
	}

注意在上述代码中,输出错误信息的时候要使用fmt.Fprintf(os.Stderr, "xxx")函数,只有这样,在后续重定向输出错误信息到文件中的操作才能完成。

接着是处理输入的函数process_input,这个函数主要负责根据参数的输入判断输入是从文件读取,还是标准输入,输出是标准输出还是输出到文件中。首先是输入的处理:

var fin *os.File
	if args.in_filename != ""{
		var opErr error
		fin, opErr = os.Open(args.in_filename)
		if opErr != nil{
			fmt.Fprintf(os.Stderr, "\nERROR! Can not open the input file: %s\n", args.in_filename)
			os.Exit(9)
		}
	}else{
		fin = os.Stdin
	}
	finBuffer := bufio.NewReader(fin)

根据in_filename来决定是标准输入还是文件输入,如果是文件输入,则需要打开用户所给的文件,打开失败则显示错误信息,成功后,调用NewReader函数读入到buffer中。输出与输入类似,这里就不再细讲了。

接着是对输入转换到输出的处理,对于是‘l’类型的,很简单,基本跟c语言版本的一致,只需把语法改成go语法的就好了:
line_ctr是当前的行数,page_ctr是页数,根据用户给定的参数page_len和start_page、end_page来输出

if args.page_type == "l" {
		line_ctr := 0
		page_ctr := 1
		for {
			line,  crc := finBuffer.ReadString('\n')
			if crc != nil {
				break
			}
			line_ctr ++
			if line_ctr > args.page_len {
				page_ctr ++
				line_ctr = 1
			}
	
			if (page_ctr >= args.start_page) && (page_ctr <= args.end_page) {
				_, err := fout.Write([]byte(line))
				if err != nil {
					fmt.Fprintf(os.Stderr, "ERROR!", err)
					os.Exit(11)
				}
			 }
		}  
	}

如果是‘f’类型的就更简单了,与上述代码类似,只是少了line_ctr,以及写的方式是page:

page_ctr := 1
	for{
		page,  crc := finBuffer.ReadString('\f')
		if crc != nil {
			break
		}
		page_ctr ++
		if ( (page_ctr >= args.start_page) && (page_ctr <= args.end_page) ){
			_, err := fout.Write([]byte(page))
			if err != nil {
				fmt.Fprintf(os.Stderr, "ERROR!", err)
				os.Exit(12)
			}
		}
	}

最后记得把文件给关闭:

fin.Close()
fout.Close()

最后是主函数,只需新建一个类型为selpg_args*的变量,依次执行上述函数即可,值得注意的是还得获取输入文件名这一个操作:

func main() {
	args := new(selpg_args)
	//Get args
	Init(args)
    pflag.Parse()
    
    //Get other args
    othersArg := pflag.Args()
    if pflag.NArg() > 0 {
        args.in_filename = othersArg[0]
    } else {
        args.in_filename = ""
	}

	//Check the grammar
	check(args)
	process_input(args)
}

实验结果

  • 首先测试了一下没传参数的结果:
    在这里插入图片描述

  • $ selpg -s1 -e1 input_file

    该命令将把“input_file”的第 1 页写至标准输出(也就是屏幕),因为这里没有重定向或管道。

    在这里插入图片描述
    可以对比一下test.txt文件,与原文件前72行一模一样:
    在这里插入图片描述

  • $ selpg -s1 -e1 < input_file

    该命令与示例 1 所做的工作相同,但在本例中,selpg 读取标准输入,而标准输入已被 shell/内核重定向为来自“input_file”而不是显式命名的文件名参数。输入的第 1 页被写至屏幕。

    与上面一条命令输出一致:
    在这里插入图片描述

  • $ other_command | selpg -s10 -e20

    “other_command”的标准输出被 shell/内核重定向至 selpg 的标准输入。将第 10 页到第 20 页写至 selpg 的标准输出(屏幕)。

    首先在终端输入了ls命令,输出当前路径的所有文件,然后再执行ls | selpg -s10 -e20,可以看到按行输出了每一个文件名:
    在这里插入图片描述

  • $ selpg -s10 -e20 input_file >output_file

    selpg 将第 10 页到第 20 页写至标准输出;标准输出被 shell/内核重定向至“output_file”。

    由于我的test.txt长度没有这么长,所以起始页都设为第一页,终止页为第二页:
    在这里插入图片描述
    打开当前路径下新出现的output_file.txt文件:
    在这里插入图片描述
    与test.txt文件进行对比,发现一致。

  • $ selpg -s10 -e20 input_file 2>error_file

    selpg 将第 10 页到第 20 页写至标准输出(屏幕);所有的错误消息被 shell/内核重定向至“error_file”。请注意:在“2”和“>”之间不能有空格;这是 shell 语法的一部分(请参阅“man bash”或“man sh”)。

    在这里插入图片描述
    因为只是我输入的是没有错误的命令,所以error_file没有任何文字,接着尝试故意输入一条错误的命令,来观察error_file有没有改变:
    在这里插入图片描述
    我将输入文件名做了修改,可以看到error_file中出现了错误信息,无法打开输入文件。

  • $ selpg -s10 -e20 input_file >output_file 2>error_file

    selpg 将第 10 页到第 20 页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至“error_file”。当“input_file”很大时可使用这种调用;您不会想坐在那里等着 selpg 完成工作,并且您希望对输出和错误都进行保存。

    在这里插入图片描述
    命令前没加./所以出错了,也可以看到,错误信息输出到error_file中了。
    在这里插入图片描述
    执行了正确的命令,output_file中有了输出。

  • $ selpg -s10 -e20 input_file >output_file 2>/dev/null

    selpg 将第 10 页到第 20 页写至标准输出,标准输出被重定向至“output_file”;selpg 写至标准错误的所有内容都被重定向至 /dev/null(空设备),这意味着错误消息被丢弃了。设备文件 /dev/null 废弃所有写至它的输出,当从该设备文件读取时,会立即返回 EOF。

    在这里插入图片描述
    可以看到,即使input_file是不存在的,但是也没有输出错误信息,因为错误的所有内容都重定向到空设备上了。

  • $ selpg -s10 -e20 input_file >/dev/null

    selpg 将第 10 页到第 20 页写至标准输出,标准输出被丢弃;错误消息在屏幕出现。这可作为测试 selpg 的用途,此时您也许只想(对一些测试情况)检查错误消息,而不想看到正常输出。

    在这里插入图片描述
    只显示了错误信息,而当没有错误的时候,输出没有显示。

  • $ selpg -s10 -e20 input_file | other_command

    selpg 的标准输出透明地被 shell/内核重定向,成为“other_command”的标准输入,第 10 页到第 20 页被写至该标准输入。“other_command”的示例可以是 lp,它使输出在系统缺省打印机上打印。“other_command”的示例也可以 wc,它会显示选定范围的页中包含的行数、字数和字符数。“other_command”可以是任何其它能从其标准输入读取的命令。错误消息仍在屏幕显示。

    在这里插入图片描述
    wc命令输出了输入中的行数,字数,字符数,本来想测试一下cd命令的,新建了一个文件夹,命名为1,新建一个文本文件test2,只有一行,内容为“1”,但是执行命令selpg -s1 -e1 test2 | cd并没有任何反应,不知道哪里出错了… …

  • $ selpg -s10 -e20 input_file 2>error_file | other_command

    与上一条命令相似,只有一点不同:错误消息被写至“error_file”。

    在这里插入图片描述

  • $ selpg -s10 -e20 -l66 input_file

    该命令将页长设置为 66 行,这样 selpg 就可以把输入当作被定界为该长度的页那样处理。第 10 页到第 20 页被写至 selpg 的标准输出(屏幕)。

    在这里插入图片描述
    在这里插入图片描述
    由于起始页是1,终止页是2,所以一共输出了2 * 66 = 132行。

  • $ selpg -s10 -e20 -f input_file

    假定页由换页符定界。第 10 页到第 20 页被写至 selpg 的标准输出(屏幕)。

    这一条指令不知道如何创建换页符的文件,所以没做测试。

  • $ selpg -s10 -e20 -dlp1 input_file

    第 10 页到第 20 页由管道输送至命令“lp -dlp1”,该命令将使输出在打印机 lp1 上打印。

    在这里插入图片描述
    运行这条指令的时候出错了,不是很理解为什么说文件已经关闭了?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值