【Go語言學習】開發簡單 CLI 程序

前言

實驗的詳細內容請參考老師的課程網站:傳送門
本次實驗作業的項目在我的GitHub上:傳送門

開發簡單 CLI 程序——selpg

CLI基礎

簡介

CLI(Command Line Interface)實用程序是Linux下應用開發的基礎。正確的編寫命令行程序讓應用與操作系統融爲一體,通過shell或script使得應用獲得最大的靈活性與開發效率。Linux提供了cat、ls、copy等命令與操作系統交互;go語言提供一組實用程序完成從編碼、編譯、庫管理、產品發佈全過程支持;容器服務如docker、k8s提供了大量實用程序支撐雲服務的開發、部署、監控、訪問等管理任務;git、npm等都是大家比較熟悉的工具。儘管操作系統與應用系統服務可視化、圖形化,但在開發領域,CLI在編程、調試、運維、管理中提供了圖形化程序不可替代的靈活性與效率。

Go的os, flag包

使用os包可以執行各種打開文件的操作,還能簡單獲取命令行參數
flag包可以解析各種標記參數, 形如如-h, -s, -d的參數,參考資料
爲了更好滿足nix 命令行規範,使用 pflag 替代 flag庫,實際上兩個包的使用方法差不多,只不過pflag中多了使用shorthand來代替長命令參數的選項,及類似-h 和 --help的區別
使用方法請參考:Golang之使用Flag和Pflag
以下是簡單的使用例子:

package main

import (
    "flag" 
    "fmt"
)

func main() {
    var port int
    flag.IntVar(&port, "p", 8000, "specify port to use.  defaults to 8000.")
    flag.Parse()

    fmt.Printf("port = %d\n", port)
    fmt.Printf("other args: %+v\n", flag.Args())
}

將標識爲"-p"的參數綁定到port上,默認值是8000,usage的語句顯示爲"specify port…"
然後利用Parse函數進行參數解析,解析命令行中是否輸入了-p,然後判斷輸入的類型是否與綁定的變量匹配。如果出現錯誤則輸出usage。

selpg說明

可以參考:開發 Linux 命令行實用程序
下載C語言代碼學習:網址

簡介

該實用程序從標準輸入或從作爲命令行參數給出的文件名讀取文本輸入。它允許用戶指定來自該輸入並隨後將被輸出的頁面範圍。例如,如果輸入含有 100 頁,則用戶可指定只打印第 35 至 65 頁。這種特性有實際價值,因爲在打印機上打印選定的頁面避免了浪費紙張。另一個示例是,原始文件很大而且以前已打印過,但某些頁面由於打印機卡住或其它原因而沒有被正確打印。在這樣的情況下,則可用該工具來只打印需要打印的頁面。
(更多細節請到上方參考鏈接閱讀學習,這裏不再過多贅述)

Go程序設計與實現

定義Flags

此程序爲命令行實用程序,需要在命令行輸入參數,所以需要用到pflag的包,根據之前所講的內容可以大致瞭解到,對每個可用參數都要綁定相應類型的變量以供解析。首先查看selpg需要的參數有哪些:

“-sNumber”和“-eNumber”強制選項:

  • selpg 要求用戶用兩個命令行參數“-sNumber”(例如,“-s10”表示從第 10 頁開始)和“-eNumber”(例如,“-e20”表示在第 20 頁結束)指定要抽取的頁面範圍的起始頁和結束頁
$ selpg -s10 -e20 ...

“-lNumber”和“-f”可選選項:

  • -l表示以固定行數作爲頁結束的標誌,-f表示以"\f"換頁符作爲頁結束標誌,(默認爲-l模式)
  • -l後面接相應的參數表示多少行爲一頁,如:
$ selpg -s10 -e20 -l66 ...

表示以66行爲一頁

“-dDestination”可選選項:

  • selpg 還允許用戶使用“-dDestination”選項將選定的頁直接發送至打印機。這裏,“Destination”應該是 lp 命令“-d”選項(請參閱“man lp”)可接受的打印目的地名稱。該目的地應該存在 ― selpg 不檢查這一點。

實現:

type argsList struct {
	s, e, lNumber         int
	eop                   bool
	destnation, inputFile string
}

首先定義一個結構體,儲存各個選項參數的變量。

func initFlags(args *argsList) {
	flag.IntVarP(&args.s, "start", "s", -1, "The start page")
	flag.IntVarP(&args.e, "end", "e", -1, "The end page")
	flag.IntVarP(&args.lNumber, "lineOfPage", "l", 72, "The length of a page")
	flag.StringVarP(&args.destnation, "destnation", "d", "", "The destnatioon of printing")
	flag.BoolVarP(&args.eop, "endOfPage", "f", false, "Defind the end symbol of a page")
	flag.Parse()
	filename := flag.Args()
	if len(filename) == 1 {
		args.inputFile = filename[0]
	} else if len(filename) == 0 {
		args.inputFile = ""
	} else {
		fmt.Println("Too many arguments")
	}
}

對五個參數進行定義綁定,然後解析。由於輸入文件並不是以-XXX的形式出現,所以根據pflag包的特性,在解析完所有定義的參數之後, flag.Args()中就是非標識的參數,去除其中的第一個作爲輸入文件名,賦值給inputfile這個變量。如果沒有則置爲空。如果發現參數過多,即除了可選選項之外,還有多餘的參數則直接報錯。

檢查選項參數

func checkFlags(args *argsList) {
	if (args.s == -1) || (args.e == -1) {
		fmt.Fprintf(os.Stderr, "The start page and end page can't be empty!\n")
		os.Exit(1)
	} else if (args.s <= 0) || (args.e <= 0) {
		fmt.Fprintf(os.Stderr, "The start page and end page should be positive!\n")
		os.Exit(1)
	} else if args.s > args.e {
		fmt.Fprintf(os.Stderr, "The start page can't be bigger than the end page!\n")
		os.Exit(1)
	} else if (args.eop == true) && (args.lNumber != 72) {
		fmt.Fprintf(os.Stderr, "You can't use -f and -l together!\n")
		os.Exit(1)
	} else if args.lNumber <= 0 {
		fmt.Fprintf(os.Stderr, "The line of page can't be less than 1 !\n")
		os.Exit(1)
	} else {
		pageType := "decided by page length."
		if args.eop == true {
			pageType = "decided by the end sign /f."
		}
		dest := args.destnation
		if len(dest) == 0 {
			dest = "null"
		}
		fmt.Fprintf(os.Stderr, "startPage: %d\nendPage: %d\ninputFile: %s\npageLength: %d\npageType: %s\nprintDestation: %s\n\n",
			args.s, args.e, args.inputFile, args.lNumber, pageType, dest)
	}
}

由於沒有辦法確認用戶是否輸入了某個選項,所以靠默認值來判斷,首先是-s和-e是必須選的,所以不能爲負。其次s不能大於e(開始頁不能大於結束頁)。然後判斷-f和-l不能同時出現。l的值也要是正數。
一切正常的話,輸出各個參數的值。

打開輸入文件

func readFile(args *argsList) {
	var file *os.File
	var err error
	if args.inputFile != "" {
		file, err = os.Open(args.inputFile)
		defer file.Close()
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to open file %s\n%s", args.inputFile, err)
			os.Exit(2)
		}
	} else {
		file = os.Stdin
	}

	if len(args.destnation) == 0 {
		output(os.Stdout, file, args)
	} else {
		command := exec.Command("lp", "-d"+args.destnation)
		outFile, err := command.StdinPipe()
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to open Pipe!\n")
			os.Exit(2)
		}
		output(outFile, file, args)
	}
}

根據之前解析到的文件名,使用os庫的Open函數打開文件,然後分析是否又可選選項-d,根據這個決定輸出到打印機還是屏幕上。
如果是打印機,則需要使用exec.Command解析相應的參數,也就是命令行中的lp命令,輸出到相應名字的打印機中去。而這個是通過管道實現的,所以需要獲取管道的數據結構,並且像輸出文件一樣,傳輸到下一個函數中去,所以輸出文件中的參數需要使用空接口,因爲不知道具體是什麼類型的參數傳入,有可能是文件有可能是管道的io.WriteCloser

讀取並且輸出內容

func output(out interface{}, in *os.File, args *argsList) {
	var pageNum int
	if args.eop {
		pageNum = 0
	} else {
		pageNum = 1
	}
	lineNum := 0
	buffer := bufio.NewReader(in)
	for {
		var pageBuf string
		var err error

		if args.eop {
			pageBuf, err = buffer.ReadString('\f')
			pageNum++
		} else {
			pageBuf, err = buffer.ReadString('\n')
			lineNum++
			if lineNum > args.lNumber {
				pageNum++
				lineNum = 1
			}
		}

		if err != nil && err != io.EOF {
			fmt.Fprintf(os.Stderr, "errors in reading file!\n")
		}

		if pageNum >= args.s && pageNum <= args.e {
			if len(args.destnation) == 0 {
				printOut, ok := out.(*os.File)
				if ok {
					fmt.Fprintf(printOut, "%s", pageBuf)
				} else {
					fmt.Fprintf(os.Stderr, "Wrong printing type!\n")
					os.Exit(3)
				}
			} else {
				printOut, ok := out.(io.WriteCloser)
				if ok {
					printOut.Write(([]byte)(pageBuf))
				} else {
					fmt.Fprintf(os.Stderr, "Wrong printing type!\n")
					os.Exit(3)
				}
			}
		}
		if err == io.EOF {
			break
		}
	}

	if pageNum < args.s {
		fmt.Fprintf(os.Stderr, "start page bigger than total pages %d, no output written\n", pageNum)
		os.Exit(4)
	} else if pageNum < args.e {
		fmt.Fprintf(os.Stderr, "end page bigger than total pages %d\n", pageNum)
		os.Exit(4)
	}
}

由於存在兩種模式(固定行數換頁和換頁符換頁),所以對於頁數的統計也需要分別用兩種方法,如果是換頁符,直接讀取到’\f’頁數加一,如果是固定行數,則需要每次讀取一行,並且當行數到達設置的行數時,頁數加一。
每次讀取後,用一個緩衝器儲存起來,根據輸出的類型(文件輸出或打印機輸出),文件的話直接輸出到對應文件中(當然事先要打開文件),如果是管道,則需要一個特殊的類型,並且使用自帶的函數Write,將一串字符輸出到裏面去。
最後判斷是否讀到EOF,則停止。還需要判斷一下讀取的頁數是否滿足-s和-e的需求。

測試

測試的輸入文件採取了數字的方法,一共是1~200,每個數字佔一行,每10個數字後加一個換頁符,也就是10、20、30。。。爲一頁。這樣的好處是簡單,又方便肉眼看到正確與否。

爲了避免數據行數太多,採用-f方式讀取

  1. $ selpg -s1 -e1 -f input_file
    在這裏插入圖片描述
  2. $ selpg -s1 -e1 -f < input_file
    跟第一個是一樣的效果,使用 < 來進行重定向
    在這裏插入圖片描述
  3. $ selpg -s2 -e2 -f test.txt | selpg -s1 -e1 -f
    把前半部分作爲後半部分的標準輸入,前半部分的結果就是第二頁的內容。作爲輸入給第二部分,所以第二部分的第一頁就是11~20
    在這裏插入圖片描述
  4. $ selpg -s1 -e1 -f input_file >output_file
    在這裏插入圖片描述
    結果沒有在屏幕輸出,因爲重定向到了output的文件中了。
    在這裏插入圖片描述
  5. $ selpg -s1 -e2 -f input_file 2>error_file
    和第六個重疊,不妨截圖了
  6. $ selpg -s1 -e2 -f input_file >output_file 2>error_file
    在這裏插入圖片描述
    什麼都沒有輸出到屏幕中,都到文件中了
    在這裏插入圖片描述
  7. $ selpg -s1 -e1 -l50 test.txt
    利用-l規定行數
    在這裏插入圖片描述
    直接顯示1~50
  8. $ selpg -s1 -e2 -dlp1 test.txt
    在這裏插入圖片描述
    顯示輸出到了lp1的打印機上,實際上沒有這個打印機,所以並不知道輸出到了哪裏。

本次實驗到此結束!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章