服務計算——開發簡單 CLI 程序


分享鏈接

1、概述

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

2、基礎知識

selpg 程序邏輯

selpg 是從文本輸入選擇頁範圍的實用程序。該輸入可以來自作爲最後一個命令行參數指定的文件,在沒有給出文件名參數時也可以來自標準輸入。

selpg 首先處理所有的命令行參數。在掃描了所有的選項參數後,如果 selpg 發現還有一個參數,則它會接受該參數爲輸入文件的名稱並嘗試打開它以進行讀取。如果沒有其它參數,則 selpg 假定輸入來自標準輸入。

參數處理

“-sNumber”和“-eNumber”強制選項:
selpg 要求用戶用兩個命令行參數“-sNumber”(例如,“-s10”表示從第 10 頁開始)和“-eNumber”(例如,“-e20”表示在第 20 頁結束)指定要抽取的頁面範圍的起始頁和結束頁。selpg 對所給的頁號進行合理性檢查;換句話說,它會檢查兩個數字是否爲有效的正整數以及結束頁是否不小於起始頁。這兩個選項,“-sNumber”和“-eNumber”是強制性的,而且必須是命令行上在命令名 selpg 之後的頭兩個參數:

$ selpg -s10 -e20 ...

(… 是命令的餘下部分,下面對它們做了描述)。

“-lNumber”和“-f”可選選項:
selpg 可以處理兩種輸入文本:

類型 1: 該類文本的頁行數固定。這是缺省類型,因此不必給出選項進行說明。也就是說,如果既沒有給出“-lNumber”也沒有給出“-f”選項,則 selpg 會理解爲頁有固定的長度(每頁 72 行)。

選擇 72 作爲缺省值是因爲在行打印機上這是很常見的頁長度。這樣做的意圖是將最常見的命令用法作爲缺省值,這樣用戶就不必輸入多餘的選項。該缺省值可以用“-lNumber”選項覆蓋,如下所示:

$ selpg -s10 -e20 -l66 ...

這表明頁有固定長度,每頁爲 66 行。

類型 2: 該類型文本的頁由 ASCII 換頁字符(十進制數值爲 12,在 C 中用“\f”表示)定界。該格式與“每頁行數固定”格式相比的好處在於,當每頁的行數有很大不同而且文件有很多頁時,該格式可以節省磁盤空間。在含有文本的行後面,類型 2 的頁只需要一個字符 ― 換頁 ― 就可以表示該頁的結束。打印機會識別換頁符並自動根據在新的頁開始新行所需的行數移動打印頭。

類型 2 格式由“-f”選項表示,如下所示:

$ selpg -s10 -e20 -f ...

該命令告訴 selpg 在輸入中尋找換頁符,並將其作爲頁定界符處理。

注:“-lNumber”和“-f”選項是互斥的。

“-dDestination”可選選項:
selpg 還允許用戶使用“-dDestination”選項將選定的頁直接發送至打印機。這裏,“Destination”應該是 lp 命令“-d”選項可接受的打印目的地名稱。該目的地應該存在 ― selpg 不檢查這一點。在運行了帶“-d”選項的 selpg 命令後,若要驗證該選項是否已生效,請運行命令“lpstat -t”。該命令應該顯示添加到“Destination”打印隊列的一項打印作業。如果當前有打印機連接至該目的地並且是啓用的,則打印機應打印該輸出。這一特性是用 popen() 系統調用實現的,該系統調用允許一個進程打開到另一個進程的管道,將管道用於輸出或輸入。在下面的示例中,打開到命令

$ lp -dDestination

的管道以便輸出,並寫至該管道而不是標準輸出:

selpg -s10 -e20 -dlp1

該命令將選定的頁作爲打印作業發送至 lp1 打印目的地。可以看到類似“request id is lp1-6”的消息。該消息來自 lp 命令;它顯示打印作業標識。如果在運行 selpg 命令之後立即運行命令 lpstat -t | grep lp1 ,應該看見 lp1 隊列中的作業。如果在運行 lpstat 命令前耽擱了一些時間,那麼可能看不到該作業,因爲它一旦被打印就從隊列中消失了。

輸入處理

一旦處理了所有的命令行參數,就使用這些指定的選項以及輸入、輸出源和目標來開始輸入的實際處理。

selpg 通過以下方法記住當前頁號:如果輸入是每頁行數固定的,則 selpg 統計新行數,直到達到頁長度後增加頁計數器。如果輸入是換頁定界的,則 selpg 改爲統計換頁符。這兩種情況下,只要頁計數器的值在起始頁和結束頁之間這一條件保持爲真,selpg 就會輸出文本(逐行或逐字)。當那個條件爲假(也就是說,頁計數器的值小於起始頁或大於結束頁)時,則 selpg 不再寫任何輸出。

3、開發實踐

  • 引用到的包如下:
import (
	"bufio" // bufio 用來幫助處理 I/O 緩存
	"fmt"
	"io"
	"os"
	"os/exec"

	flag "github.com/spf13/pflag"
)
  • 定義保存參數數據的結構體 selpg_args 如下:
type selpgArgs struct {
	startPage int // 開始頁
	endPage   int // 結束頁

	inFilename string // 輸入文件名
	printDest  string // 輸出文件名

	pageLen  int    // 每頁的行數,默認爲72
	pageType string // 'l'按行打印,'f'按換頁符打印,默認按行
}
  • 聲明用來保存程序名的全局變量,用於顯示錯誤信息。
var progname string // 保存名稱(命令就是通過該名稱被調用)的全局變量,作爲在錯誤消息中顯示之用
  • main 函數首先聲明一個名爲 sa 的 selpgArgs,然後使用 os.Args讀取程序輸入的所有參數,初始化 selpgArgs 裏的各個參數,接着調用 processArgs 函數和 processInput 函數。具體代碼如下:
func main() {
	sa := selpgArgs{}
	progname = os.Args[0]

	processArgs(&sa) // 處理參數
	processInput(sa) // 處理輸入輸出
}
  • 函數 processArgs 主要是分析用戶輸入的命令,進行錯誤檢查,判斷每個參數的格式是否正確、參數個數是否正確,並將各種信息存儲在 sa 中。用 pflag 綁定 sa 的各個參數,命令行中的信息就會自動存入 sa。參考:Golang 之使用 Flag和 Pflag

首先將 flag 綁定到 sa 的各個參數上:

	flag.IntVarP(&sa.startPage, "start", "s", -1, "start page(>1)")
	flag.IntVarP(&sa.endPage, "end", "e", -1, "end page(>=start_page)")
	flag.IntVarP(&sa.pageLen, "len", "l", 10, "page len")
	flag.StringVarP(&sa.printDest, "dest", "d", "", "print dest")
	flag.StringVarP(&sa.pageType, "type", "f", "l", "'l' for lines-delimited, 'f' for form-feed-delimited. default is 'l'")
	flag.Lookup("type").NoOptDefVal = "f"

第一個參數爲變量,第二個參數爲命令行參數名,第三個參數爲該參數的簡寫,第四個參數爲該參數沒有在命令行出現時的默認值,第五個參數爲幫助信息。

接着調用 flag.Parse() 解析命令行參數到定義的 flag,然後檢查各個參數的合法性。

參數個數不夠:

	if len(os.Args) < 3 { // 參數個數不夠(至少爲 progname -s start_page -e end_page)
		fmt.Fprintf(os.Stderr, "\n%s: not enough arguments\n", progname)
		flag.Usage()
		os.Exit(1)
	}

處理第一個參數:

	if os.Args[1] != "-s" {
		fmt.Fprintf(os.Stderr, "\n%s: 1st arg should be -s start_page\n", progname)
		flag.Usage()
		os.Exit(2)
	}

	intMax := 1<<32 - 1

	if sa.startPage < 1 || sa.startPage > intMax {
		fmt.Fprintf(os.Stderr, "\n%s: invalid start page %s\n", progname, os.Args[2])
		flag.Usage()
		os.Exit(3)
	}

處理第二個參數:

	if os.Args[3] != "-e" {
		fmt.Fprintf(os.Stderr, "\n%s: 2nd arg should be -e end_page\n", progname)
		flag.Usage()
		os.Exit(4)
	}

	if sa.endPage < 1 || sa.endPage > intMax || sa.endPage < sa.startPage {
		fmt.Fprintf(os.Stderr, "\n%s: invalid end page %s\n", progname, sa.endPage)
		flag.Usage()
		os.Exit(5)
	}

處理每頁行數:

	if sa.pageLen < 1 || sa.pageLen > (intMax-1) {
		fmt.Fprintf(os.Stderr, "\n%s: invalid page length %s\n", progname, sa.pageLen)
		flag.Usage()
		os.Exit(5)
	}

檢查輸入文件:

	if len(flag.Args()) == 1 {
		_, inFileErr := os.Stat(flag.Args()[0])
		// 檢查文件是否存在
		if inFileErr != nil && os.IsNotExist(inFileErr) {
			fmt.Fprintf(os.Stderr, "\n%s: input file \"%s\" does not exist\n",
				progname, flag.Args()[0])
			os.Exit(6)
		}
		sa.inFilename = flag.Args()[0]
	}
  • 函數 processInput 首先設置輸入源,即選擇從哪裏進行讀取,然後設置輸出源,即選擇打印到哪裏,接下來進行打印。

讀取輸入文件,若缺省,則通過標準輸入(鍵盤或重定向)讀取輸入流。

	var fin *os.File
	if len(sa.inFilename) == 0 {
		fin = os.Stdin
	} else {
		var inputError error
		fin, inputError = os.Open(sa.inFilename)
		if inputError != nil {
			fmt.Fprintf(os.Stderr, "\n%s: could not open input file \"%s\"\n",
				progname, sa.inFilename)
			os.Exit(7)
		}
		defer fin.Close()
	}

設置輸出流,若缺省,則通過標準輸出(屏幕或重定向)讀取輸入流。並通過 StdinPipe 建立連接到 cmd 標準輸入的管道,將當前的輸出作爲 cmd 的輸入。

	var fout io.WriteCloser
	cmd := &exec.Cmd{}

	if len(sa.printDest) == 0 {
		fout = os.Stdout
	} else {
		cmd = exec.Command("cat")
		// 用只寫的方式打開 print_dest 文件,如果文件不存在,就創建該文件。
		var outputErr error
		cmd.Stdout, outputErr = os.OpenFile(sa.printDest, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
		if err != nil {
			fmt.Fprintf(os.Stderr, "\n%s: could not open file %s\n",
				progname, sa.printDest)
			os.Exit(8)
		}

		// StdinPipe返回一個連接到command標準輸入的管道pipe
		fout, outputErr = cmd.StdinPipe()
		if outputErr != nil {
			fmt.Fprintf(os.Stderr, "\n%s: could not open pipe to \"lp -d%s\"\n",
				progname, sa.printDest)
			os.Exit(8)
		}

		cmd.Start()
		defer fout.Close()
	}

打印,根據 pageType 選擇按固定行數打印或分頁符(在這裏用換行符替代)打印。

按固定行數打印:

		line := 0
		page := 1
		for {
			line, crc := bufFin.ReadString('\n')
			if crc != nil {
				break 	// 讀完一行
			}
			line++		// 行數加一
			if line > sa.pageLen { 	//讀完一頁
				page++	// 頁數加一
				line = 1
			}
			// 到達指定頁碼,開始打印
			if (page >= sa.startPage) && (page <= sa.endPage) {
				_, err := fout.Write([]byte(line))
				if err != nil {
					fmt.Println(err)
					os.Exit(9)
				}
			}
		}

按分頁符(換行符)打印:

		page = 1
		for {
			page, err := bufFin.ReadString('\n')
			if err != nil {
				break // 讀完一行
			}
			// 到達指定頁碼,開始打印
			if (page >= sa.startPage) && (page <= sa.endPage) {
				_, err := fout.Write([]byte(page))
				if err != nil {
					os.Exit(5)
				}
			}
			// 每碰到一個換頁符都增加一頁
			page++
		}

4、使用 selpg

① 該命令將把“input_file”的第 1 頁寫至標準輸出(也就是屏幕),因爲這裏沒有重定向或管道。

$ selpg -s1 -e1 input_file

測試結果如圖:
在這裏插入圖片描述
② 該命令與示例 1 所做的工作相同,但在本例中,selpg 讀取標準輸入,而標準輸入已被 shell/內核重定向爲來自“input_file”而不是顯式命名的文件名參數。輸入的第 1 頁被寫至屏幕。

$ selpg -s1 -e1 < input_file

測試結果如圖:
在這裏插入圖片描述
③ “other_command”的標準輸出被 shell/內核重定向至 selpg 的標準輸入。將第 10 頁到第 20 頁寫至 selpg 的標準輸出(屏幕)。

$ other_command | selpg -s10 -e20

測試結果如圖:
在這裏插入圖片描述
④ “other_command”的標準輸出被 shell/內核重定向至 selpg 的標準輸入。將第 10 頁到第 20 頁寫至 selpg 的標準輸出(屏幕)。

$ selpg -s10 -e20 input_file >output_file

測試結果如圖:
在這裏插入圖片描述
⑤ selpg 將第 10 頁到第 20 頁寫至標準輸出(屏幕);所有的錯誤消息被 shell/內核重定向至“error_file”。請注意:在“2”和“>”之間不能有空格;這是 shell 語法的一部分(請參閱“man bash”或“man sh”)。

$ selpg -s10 -e20 input_file 2>error_file

測試結果如圖:
在這裏插入圖片描述
⑥ selpg 將第 10 頁到第 20 頁寫至標準輸出,標準輸出被重定向至“output_file”;selpg 寫至標準錯誤的所有內容都被重定向至“error_file”。當“input_file”很大時可使用這種調用;您不會想坐在那裏等着 selpg 完成工作,並且您希望對輸出和錯誤都進行保存。

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

測試結果如圖:
在這裏插入圖片描述
⑦ selpg 將第 10 頁到第 20 頁寫至標準輸出,標準輸出被重定向至“output_file”;selpg 寫至標準錯誤的所有內容都被重定向至 /dev/null(空設備),這意味着錯誤消息被丟棄了。設備文件 /dev/null 廢棄所有寫至它的輸出,當從該設備文件讀取時,會立即返回 EOF。

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

測試結果如圖:
在這裏插入圖片描述
⑧ selpg 將第 10 頁到第 20 頁寫至標準輸出,標準輸出被丟棄;錯誤消息在屏幕出現。這可作爲測試 selpg 的用途,此時您也許只想(對一些測試情況)檢查錯誤消息,而不想看到正常輸出。

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

測試結果如圖:
在這裏插入圖片描述
⑨ selpg 的標準輸出透明地被 shell/內核重定向,成爲“other_command”的標準輸入,第 10 頁到第 20 頁被寫至該標準輸入。“other_command”的示例可以是 lp,它使輸出在系統缺省打印機上打印。“other_command”的示例也可以 wc,它會顯示選定範圍的頁中包含的行數、字數和字符數。“other_command”可以是任何其它能從其標準輸入讀取的命令。錯誤消息仍在屏幕顯示。

$ selpg -s10 -e20 input_file | other_command

測試結果如圖:
在這裏插入圖片描述
⑩ 與上面的示例 9 相似,只有一點不同:錯誤消息被寫至“error_file”。

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

測試結果如圖:
在這裏插入圖片描述
⑾ 該命令將頁長設置爲 66 行,這樣 selpg 就可以把輸入當作被定界爲該長度的頁那樣處理。第 10 頁到第 20 頁被寫至 selpg 的標準輸出(屏幕)。

$ selpg -s10 -e20 -l66 input_file

測試結果如圖:
在這裏插入圖片描述
⑿ 該命令將頁長設置爲 66 行,這樣 selpg 就可以把輸入當作被定界爲該長度的頁那樣處理。第 10 頁到第 20 頁被寫至 selpg 的標準輸出(屏幕)。

$ selpg -s10 -e20 -f input_file

測試結果如圖:
在這裏插入圖片描述
項目地址

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