基于Golang的CLI 命令行程序开发 【阅读时间:约15分钟】

  • 一. CLI 命令行程序概述
  • 二. 系统环境&项目介绍&开发准备
    • 1.系统环境
    • 2.项目介绍
    • 3.开发准备
  • 三.具体程序设计及Golang代码实现
  • 1.selpg的程序结构
  • 2.导入的库
  • 3.sp_args结构体
  • 4.全局变量
  • 4.main函数
  • 5.process_args函数
  • 6.process_args函数
  • 7.usage函数
  • 四.程序测试
    • 1.功能测试
      • (1)`selpg -s1 -e1 in.txt`
      • (2)`selpg -s1 -e1 < in.txt`
      • (3)`other_command | selpg -s1 -e1`
      • (4)`selpg -s1 -e1 in.txt >out.txt`
      • (5)`selpg -s20 -e20 in.txt 2>error.txt`
      • (6)`selpg -s1 -e1 in.txt >out.txt 2>error.txt`
      • (7)`selpg -s20 -e20 in.txt >out.txt 2>/dev/null`
      • (8)`selpg -s10 -e20 in.txt >/dev/null`
      • (9)`selpg -s10 -e20 input_file 2>error_file | other_command`
      • (10)`selpg -s10 -e20 input_file 2>error_file | other_command`
      • (11)`selpg -s1 -e1 -l10 in.txt`
      • (12)`selpg -s1 -e1 -f in.txt`
      • (13)`selpg -s1 -e1 in.txt | cat -n`
      • (14)`selpg -s10 -e20 in.txt > out.txt 2>error.txt &`
    • 2.单元测试
  • 五.完整代码
  • 六. References

一. CLI 命令行程序概述

CLI(Command Line Interface)实用程序是Linux下应用开发的基础。正确的编写命令行程序让应用与操作系统融为一体,通过shell或script使得应用获得最大的灵活性与开发效率。例如:

Linux提供了cat、ls、copy等命令与操作系统交互;
go语言提供一组实用程序完成从编码、编译、库管理、产品发布全过程支持;
容器服务如docker、k8s提供了大量实用程序支撑云服务的开发、部署、监控、访问等管理任务;
git、npm等也是大家比较熟悉的工具。

尽管操作系统与应用系统服务可视化、图形化,但在开发领域,CLI在编程、调试、运维、管理中提供了图形化程序不可替代的灵活性与效率。

二. 系统环境&项目介绍&开发准备

1.系统环境

操作系统:CentOS7
硬件信息:使用virtual box配置虚拟机(内存3G、磁盘30G)
编程语言:GO 1.15.2

2.项目介绍

本项目的开发主要基于IBM Developer社区的C语言程序(https://www.ibm.com/developerworks/cn/linux/shell/clutil/index.html),出于熟悉golang语言的目的,笔者主要的工作只是将其翻译为golang格式,其中还使用了部分库,如os和pflag,再次感谢原作者及开源代码工作者。

项目完成后的运行效果与CLI 命令行程序一致,一个简单的输出文本第一页20行的内容的例子如下:

3.开发准备

①首先下载上文的C语言源码(点击下载)
②安装并使用 pflag 替代 goflag 以满足 Unix 命令行规范,此处出于篇幅考虑,只在后面的函数介绍时给出部分使用教程,详细的pflag 使用教程可见【六. References. 1. Golang之使用Flag和Pflag】
③将C语言源码翻译为golang语言

三.具体程序设计及Golang代码实现

1.selpg的程序结构

selpg的程序结构非常简单,主要有以下组成:
①sp_args结构
②main函数
③process_args函数
④process_input函数
⑤usage函数

2.导入的库

主要要导入的库有:
①bufio:用于文件的读写
②io:用于文件读写、读环境变量
③pflag:用于解释命令行参数,替代 goflag 以满足 Unix 命令行规范

/*================================= includes ======================*/package mainimport ("bufio""fmt""io""os""os/exec""github.com/spf13/pflag"
)

3.sp_args结构体

sp_args结构体是用于记录数据的结构体,分别记录着开始页码,结束页码,文件名,每页大小,页的类型和打印输出位置等信息。

/*================================= types =========================*/type sp_args struct {start_page  intend_page    intin_filename stringpage_len    int  /* default value, can be overriden by "-l number" on command line */page_type   bool /* 'l' for lines-delimited, 'f' for form-feed-delimited *//* default is 'l' */print_dest string
}

4.全局变量

全局变量共有两个:
①progname是程序名,在输出错误信息时有用;
②用 INT_MAX 检查一个数是否为有效整数,由于golang没有预定义的INT_MAX,此处用别的方式来手动实现

/*================================= globals =======================*/var progname string                /* program name, for error messages */
const INT_MAX = int(^uint(0) >> 1) //golang需要手动声明INT_MAX

4.main函数

main函数作为程序的入口,给出了整个程序的大概运行过程。
①首先进行sp_args变量和progname的初始化,其中主要的默认属性为开始页码和结束页码均为1,每页长度为20行,不可用用换页符换页
②然后调用process_args函数来处理输入时的各种参数错误
③最后才调用process_input函数来执行输入的参数。

/*================================= main()=== =====================*/func main() {var sa sp_argssa.start_page = 1sa.end_page = 1sa.in_filename = ""sa.page_len = 20 //默认20行一页sa.page_type = falsesa.print_dest = ""/* save name by which program is invoked, for error messages */progname = os.Args[0]process_args(len(os.Args), &sa)process_input(sa)
}

5.process_args函数

process_args函数用于处理输入时的各种参数错误。
①首先通过pflag绑定各参数和usage函数
②然后判断各种参数的错误即可,比如起始页码是负数,终止页码小于起始页码等情况,具体的错误情况在代码中已给出注释
③当发生错误,首先通过pflag.usage函数输出正确的指令参数格式来提醒用户,并通过os.Exit函数退出程序

/*================================= process_args() ================*/func process_args(ac int, psa *sp_args) {//指令格式:selpg -sstart_page -eend_page [-lline | -f ] [-d dstFile] filename//使用pflag绑定各参数, psa初始化pflag.Usage = usagepflag.IntVarP(&psa.start_page, "start_page", "s", 1, "Start page")pflag.IntVarP(&psa.end_page, "end_page", "e", 1, "End page")pflag.IntVarP(&psa.page_len, "page_len", "l", 20, "Lines per page")pflag.BoolVarP(&psa.page_type, "page_type", "f", false, "Page type")pflag.StringVarP(&psa.print_dest, "dest", "d", "", "Destination")pflag.Parse()/* check the command-line arguments for validity */if ac < 3 { /* Not enough args, minimum command is "selpg -sstartpage -eend_page"  */fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", progname)pflag.Usage()os.Exit(1)}/* handle 1st arg - start page */temp := os.Args[1]if temp[0:2] != "-s" { fmt.Fprintf(os.Stderr, "%s: 1st arg should be -sstart_page\n", progname)pflag.Usage()os.Exit(2)}if psa.start_page < 1 || psa.start_page > (INT_MAX-1) {fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname, psa.start_page)pflag.Usage()os.Exit(3)}/* handle 2nd arg - end page */temp = os.Args[2]if temp[0:2] != "-e" {fmt.Fprintf(os.Stderr, "%s: 2nd arg should be -eend_page\n", progname)pflag.Usage()os.Exit(4)}if psa.end_page < 1 || psa.end_page > (INT_MAX-1) || psa.end_page < psa.start_page {fmt.Fprintf(os.Stderr, "%s: invalid end page %d\n", progname, psa.end_page)pflag.Usage()os.Exit(5)}/* now handle optional args *///使用pflag,selpg.c的while+switch可去掉if psa.page_len != 5 {if psa.page_len < 1 {fmt.Fprintf(os.Stderr, "%s: invalid page length %d\n", progname, psa.page_len)pflag.Usage()os.Exit(6)}}if pflag.NArg() > 0 { /* there is one more arg */psa.in_filename = pflag.Arg(0)/* check if file exists */file, err := os.Open(psa.in_filename)if err != nil {fmt.Fprintf(os.Stderr, "%s: input file \"%s\" does not exist\n", progname, psa.in_filename)os.Exit(7)}/* check if file is readable */file, err = os.OpenFile(psa.in_filename, os.O_RDONLY, 0666)if err != nil {if os.IsPermission(err) {fmt.Fprintf(os.Stderr, "%s: input file \"%s\" exists but cannot be read\n", progname, psa.in_filename)os.Exit(8)}}file.Close()}}

6.process_args函数

process_input函数用于执行输入的参数,执行文件读写和输出到屏幕等操作。其中由于没有打印机,转而使用cat命令测试。

/*================================= process_input() ===============*/func process_input(sa sp_args) {var fin *os.File        /* input stream */var fout io.WriteCloser /* output stream */var c byte              /* to read 1 char */var line stringvar line_ctr int /* line counter */var page_ctr int /* page counter */var err errorcmd := &exec.Cmd{}/* set the input source */if sa.in_filename == "" {fin = os.Stdin} else {fin, err = os.Open(sa.in_filename)if err != nil {fmt.Fprintf(os.Stderr, "%s: could not open input file \"%s\"\n", progname, sa.in_filename)os.Exit(9)}}/* set the output destination */if sa.print_dest == "" {fout = os.Stdout} else {cmd = exec.Command("cat") //由于没有打印机,使用cat命令测试cmd.Stdout, err = os.OpenFile(sa.print_dest, os.O_WRONLY|os.O_TRUNC, 0600)if err != nil {fmt.Fprintf(os.Stderr, "%s: could not open output file \"%s\"\n", progname, sa.print_dest)os.Exit(10)}fout, err = cmd.StdinPipe()if err != nil {fmt.Fprintf(os.Stderr, "%s: could not open pipe to \"%s\"\n", progname, sa.print_dest)os.Exit(11)}cmd.Start()}/* begin one of two main loops based on page type */rd := bufio.NewReader(fin)if sa.page_type == false {line_ctr = 0page_ctr = 1for true {line, err = rd.ReadString('\n')if err != nil { /* error or EOF */break}line_ctr++if line_ctr > sa.page_len {page_ctr++line_ctr = 1}if page_ctr >= sa.start_page && page_ctr <= sa.end_page {fmt.Fprintf(fout, "%s", line)}}} else {page_ctr = 1for true {c, err = rd.ReadByte()if err != nil { /* error or EOF */break}if c == '\f' {page_ctr++}if page_ctr >= sa.start_page && page_ctr <= sa.end_page {fmt.Fprintf(fout, "%c", c)}}fmt.Print("\n") }/* end main loop */if page_ctr < sa.start_page {fmt.Fprintf(os.Stderr, "%s: start_page (%d) greater than total pages (%d), no output written\n", progname, sa.start_page, page_ctr)} else if page_ctr < sa.end_page {fmt.Fprintf(os.Stderr, "%s: end_page (%d) greater than total pages (%d), less output than expected\n", progname, sa.end_page, page_ctr)}fin.Close()fout.Close()fmt.Fprintf(os.Stderr, "%s: done\n", progname)
}

7.usage函数

usage函数用于输出正确的指令参数格式。

/*================================= usage() =======================*/func usage() {fmt.Fprintf(os.Stderr, "\nUSAGE: %s -sstart_page -eend_page [ -f | -llines_per_page ] [ -ddest ] [ in_filename ]\n", progname)
}

四.程序测试

1.功能测试

此处按照IBM的c语言程序的使用实例来进行功能测试。
首先在selpg目录下建立三个txt文件,分别为:
①in.txt, 用于输入的文本,内容如下(为方便演示,只有20行):

②out.txt, 保存输出的文本,内容初始为空
③error.txt,保存错误信息,内容初始为空

(1)selpg -s1 -e1 in.txt

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

[henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt
Hello world!
I am HenryHZY.
line 1
iine 2
line 3
line 4
line 5
line 6
iine 7
line 8
line 9
line 10
line 11
iine 12
line 13
line 14
line 15
line 16
iine 17
line 18
selpg: done

(2)selpg -s1 -e1 < in.txt

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

[henryhzy@localhost selpg]$ selpg -s1 -e1 < in.txt
Hello world!
I am HenryHZY.
line 1
iine 2
line 3
line 4
line 5
line 6
iine 7
line 8
line 9
line 10
line 11
iine 12
line 13
line 14
line 15
line 16
iine 17
line 18
selpg: done

(3)other_command | selpg -s1 -e1

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

[henryhzy@localhost selpg]$ ls | selpg -s1 -e1
error.txt
in.txt
out.txt
selpg.go
selpg: done

(4)selpg -s1 -e1 in.txt >out.txt

selpg 将第 1 页写至标准输出;标准输出被 shell/内核重定向至out.txt“”。

(5)selpg -s20 -e20 in.txt 2>error.txt

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

(6)selpg -s1 -e1 in.txt >out.txt 2>error.txt

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

(7)selpg -s20 -e20 in.txt >out.txt 2>/dev/null

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

(8)selpg -s10 -e20 in.txt >/dev/null

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

[henryhzy@localhost selpg]$ selpg -s10 -e20 in.txt >/dev/null
selpg: start_page (10) greater than total pages (1), no output written
selpg: done

(9)selpg -s10 -e20 input_file 2>error_file | other_command

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

[henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt | ps
selpg: donePID TTY          TIME CMD
10209 pts/0    00:00:00 bash
10528 pts/0    00:00:00 ps

(10)selpg -s10 -e20 input_file 2>error_file | other_command

与上面的示例 9 相似,只有一点不同:错误消息被写至“error_file”。

(11)selpg -s1 -e1 -l10 in.txt

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

[henryhzy@localhost selpg]$ selpg -s1 -e1 -l10 in.txt
Hello world!
I am HenryHZY.
line 1
iine 2
line 3
line 4
line 5
line 6
iine 7
line 8
selpg: done

(12)selpg -s1 -e1 -f in.txt

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

[henryhzy@localhost selpg]$ selpg -s1 -e1 -f in.txt
Hello world!
I am HenryHZY.
line 1
iine 2
line 3
line 4
line 5
line 6
iine 7
line 8
line 9
line 10
line 11
iine 12
line 13
line 14
line 15
line 16
iine 17
line 18selpg: done

(13)selpg -s1 -e1 in.txt | cat -n

由于没有打印机,原测试的打印机输出改为cat输出。

[henryhzy@localhost selpg]$ selpg -s1 -e1 in.txt | cat -n
selpg: done1    Hello world!2   I am HenryHZY.3 line 14 iine 25 line 36 line 47 line 58 line 69 iine 710    line 811    line 912    line 1013   line 1114   iine 1215   line 1316   line 1417   line 1518   line 1619   iine 1720   line 18

(14)selpg -s10 -e20 in.txt > out.txt 2>error.txt &

该命令利用了 Linux 的一个强大特性,即:在“后台”运行进程的能力。在这个例子中发生的情况是:“进程标识”(pid)如 1234 将被显示,然后 shell 提示符几乎立刻会出现,使得您能向 shell 输入更多命令。同时,selpg 进程在后台运行,并且标准输出和标准错误都被重定向至文件。这样做的好处是您可以在 selpg 运行时继续做其它工作。

2.单元测试

根据查找的单元测试与功能测试的区别:

功能测试是站在用户的角度从外部测试应用查看效果是否达到
单元测试是站在程序员的角度从内部测试应用

既然已经测试过功能测试,那么在进行时单元测试,简单地以函数为单元测试调用情况即可。若是在结合输入输出正确与否,感觉有些多余了。:)

测试代码:

package mainimport "testing"func Test_usage(t *testing.T) {tes := []struct {name string}{{name: "Test_usage1"},{name: "Test_usage2"},}for _, tt := range tes {t.Run(tt.name, func(t *testing.T) {usage()})}
}func Test_process_args(t *testing.T) {tests := []struct {name stringlen  intsa   sp_args}{}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {process_args(tt.len, &tt.sa)})}
}func Test_process_input(t *testing.T) {tests := []struct {name stringsa   sp_args}{}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {process_input(tt.sa)})}
}func Test_main(t *testing.T) {tests := []struct {name string}{}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {main()})}
}

为了方便进行单元测试,此处使用vscode进行测试,直接点击相应函数上面的run test即可,非常方便~~
各个函数的测试截图如下:




五.完整代码

具体代码可见gitee仓库:gitee仓库

六. References

  1. Golang之使用Flag和Pflag
  2. 开发 Linux 命令行实用程序
  3. Package os

基于Golang的CLI 命令行程序开发相关推荐

  1. 服务计算作业三——CLI 命令行实用程序开发基础

    服务计算作业三--CLI 命令行实用程序开发基础 18342138 郑卓民 本次作业gitee仓库链接(完整代码) 概述 CLI(Command Line Interface)实用程序是Linux下应 ...

  2. 服务计算 HW3 CLI 命令行实用程序开发基础

    文章目录 一. 概述 二. Golang 的支持 ① 使用 os 包处理参数 ② 使用 flag 包处理参数 三. 开发实践 ① 项目要求 ② 设计说明 1) selpg 定义 2) 代码设计 1. ...

  3. Arduino CLI命令行ESP32开发环境搭建(Linux Ubuntu操作系统)

    陈拓2023/03/06-2023/03/11 简介 Arduino cli是一个命令行界面,您可以使用它创建草图(sketch)并将其上传到开发板中.它提供了ArduinoIDE的所有功能: 编写s ...

  4. Node交互式命令行工具开发——自动化文档工具

    转载自:小磊 https://segmentfault.com/a/1190000039749423 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网 ...

  5. 结对项目——自动生成小学四则运算题目的命令行程序(基于Python)

    这个作业属于课程 软件工程 这个作业的要求在哪里 结对项目 这个作业的目标是 实现一个自动生成小学四则运算题目的命令行程序 成员 3118005408 方俊涛 .3118005409 冯宇航 GitH ...

  6. 基于Golang的对象序列化的程序包开发——myJsonMarshal

    基于Golang的对象序列化的程序包开发--myJsonMarshal[阅读时间:约10分钟] 一.对象序列化概述 二.系统环境&项目介绍 1.系统环境 2.项目的任务要求 三.具体程序设计及 ...

  7. Cobra 快速入门 - 专为命令行程序而生

    最近一直在看 Istio(一个 Service Mesh 框架)相关的东西,当看到其源码时发现了一个新东西 Cobra,一查却发现这是个好东西,用的地方可不少,比如:Docker.Kubernetes ...

  8. 命令行工具开发:如何快速实现命令行提示?

    简介:对于稍微复杂一些的命令行工具,命令行的提示功能必不可少.那么对于不同语言的开发者,有没有一种简单快捷的实现方式呢?本文分享一种快速实现的方法,使用YAML文件定义命令行工具的使用规范,再通过工具 ...

  9. Java命令行程序构建工具airlift使用

    package com.ilucky.airlift;import java.util.Arrays;import io.airlift.airline.Cli; import io.airlift. ...

最新文章

  1. 错误:wasm streaming compile failed: CompileError: AsyncCompile: Wasm decoding failed
  2. 密码学-hash散列表
  3. javafx如何调节按钮位置_安全带高度调节器的小知识
  4. 前端学习(168)全局事件属性
  5. Node.js初探之hello world
  6. 如何在React Native中构建项目并管理静态资源
  7. 机器学习中的范数规则化之L0、L1、L2范数
  8. mpi并行 写同一文件_并行计算调度策略的笔记(001)
  9. 理解x86架构的捷径
  10. ttk.treeview鼠标悬浮文字_电竞新选择,罗技G键盘、鼠标、耳机三件套给力体验...
  11. 陆军装备环境可靠性试验|GJB150A-2009
  12. Fragstats官方教程 [汉译版] 首发预告
  13. 看完这篇“史密斯圆图”告别懵逼射频!
  14. ios应用跳转小程序Universal Links链接验证不通过
  15. 高中数学40分怎么办_高中数学40分怎么办?
  16. 自媒体文章一文多发的几种方式
  17. 记录Linux下安装elasticSearch时遇到的一些错误
  18. python 批量修改文件名_Python 批量修改文件名
  19. 2008 r2 server sql 中文版补丁_Microsoft SQL Server 2008 R2 SP1补丁 64位 官方免费版(附安装教程)...
  20. Bellman-Ford Spfa

热门文章

  1. 2022-2028年中国养老保险行业深度调研及投资前景预测报告
  2. vs2012常用快捷键
  3. python实现glove,gensim.word2vec模型训练实例
  4. Python | 安装Jupyter Notebook及其目录的更改 jupyter问题
  5. Dissecting BERT Part 1: The Encoder 解析BERT解码器(transformer)
  6. pytorch源码解析:Python层 pytorchmodule源码
  7. 如何在 GPU 上优化卷积
  8. Python分析离散心率信号(上)
  9. C/C++语言编程的隐患!
  10. 2021年大数据ZooKeeper(三):Zookeeper数据模型和节点类型