基于Golang的CLI 命令行程序开发
基于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
- Golang之使用Flag和Pflag
- 開發 Linux 命令行實用程序
- Package os
總結
以上是生活随笔為你收集整理的基于Golang的CLI 命令行程序开发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中级实训第一天的自学报告
- 下一篇: Test Reprot