CLI 指令行實用程式開發基礎_18342063_劉智斌
文章目錄
- CLI 指令行實用程式開發基礎_18342063_劉智斌
-
- 1、概述
- 2、Golang 的支援
-
- 使用os包處理代碼
- 使用flag包處理參數
- 基礎知識
- 開發實踐
-
- 開發要求
- 代碼設計
-
- 實驗準備
- 指令的參數類型及用途
- 輸入參數
- 檢查參數
- 處理指令
- 資料輸出
- main函數
- 實驗結果
-
- 檔案結構
- 單元測試
- 功能測試
1、概述
CLI(Command Line Interface)實用程式是Linux下應用開發的基礎。正确的編寫指令行程式讓應用與作業系統融為一體,通過shell或script使得應用獲得最大的靈活性與開發效率。例如:
- Linux提供了cat、ls、copy等指令與作業系統互動;
- go語言提供一組實用程式完成從編碼、編譯、庫管理、産品釋出全過程支援;
- 容器服務如docker、k8s提供了大量實用程式支撐雲服務的開發、部署、監控、通路等管理任務;
- git、npm等也是大家比較熟悉的工具。
盡管作業系統與應用系統服務可視化、圖形化,但在開發領域,CLI在程式設計、調試、運維、管理中提供了圖形化程式不可替代的靈活性與效率。
2、Golang 的支援
使用os包處理代碼
package main
import (
"fmt"
"os"
)
func main() {
for i, a := range os.Args[1:] {
fmt.Printf("Argument %d is %s\n", i+1, a)
}
}
輸出結果:在終端直接輸入參數,會輸出參數。
使用flag包處理參數
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())
}
輸出結果:直接在終端運作并輸入參數。
基礎知識
selpg 允許使用者指定從輸入文本抽取的頁的範圍,這些輸入文本可以來自檔案或另一個程序。selpg 是以在 Linux 中建立指令的事實上的約定為模型建立的,這些約定包括:
- 獨立工作
- 在指令管道中作為元件工作(通過讀取标準輸入或檔案名參數,以及寫至标準輸出和标準錯誤)
- 接受修改其行為的指令行選項
該實用程式從标準輸入或從作為指令行參數給出的檔案名讀取文本輸入。它允許使用者指定來自該輸入并随後将被輸出的頁面範圍。例如,如果輸入含有 100 頁,則使用者可指定隻列印第 35 至 65 頁。這種特性有實際價值,因為在列印機上列印標明的頁面避免了浪費紙張。另一個示例是,原始檔案很大而且以前已列印過,但某些頁面由于列印機卡住或其它原因而沒有被正确列印。在這樣的情況下,則可用該工具來隻列印需要列印的頁面。
開發實踐
開發要求
使用 golang 開發 開發 Linux 指令行實用程式 中的 selpg
提示:
- 請按文檔 使用 selpg 章節要求測試你的程式
- 請使用 pflag 替代 goflag 以滿足 Unix 指令行規範, 參考:Golang之使用Flag和Pflag
- golang 檔案讀寫、讀環境變量,請自己查 os 包
- “-dXXX” 實作,請自己查 os/exec 庫,例如案例 Command,管理子程序的标準輸入和輸出通常使用 io.Pipe,具體案例見 Pipe
- 請自帶測試程式,確定函數等功能正确
代碼設計
實驗準備
終端運作 go get github.com/spf13/pflag 安裝pflag包。
在import中加入"github.com/spf13/pflag"。
import (
"github.com/spf13/pflag"
"bufio"
"fmt"
"io"
"os"
"os/exec"
)
指令的參數類型及用途
選項 | 值類型 | 用途 |
---|---|---|
-s | Num int | 指定起始頁碼為Num,必選項,預設為-1(不合法,強制要求重新指定) |
-e | Num int | 指定結束頁碼為Num,必選項,預設為-1(不合法,強制要求重新指定) |
filename | filename string | 指定輸入檔案,可選項,預設為空 |
-l | Num int | 指定每頁的行數為Num,可選項,預設為每頁72行,并且為預設解讀模式 |
-f | 無附加參數 bool | 指定解讀模式為,’\f’作為頁分隔符,不可與-l同時使用 |
-d | destination string | 指定列印機裝置位址 |
于是建構參數結構體
//參數的結構體
type selpgArgs struct {
start_page int //起始頁的索引
end_page int //終止頁的索引
page_len int //頁的長度
page_type bool //頁分隔類型
infile_name string//輸入檔案的名稱
printf_dest string //輸出的終點
}
輸入參數
利用了上面所下載下傳的pflag,擷取-s -e -l -f -d 參數,-s對應起始頁,-e定義終止頁,-l定義頁長度,-d定義輸出終點,-f定義頁分隔類型。
func getArgs(args *selpgArgs) {
pflag.IntVarP(&(args.start_page), "start_page", "s", -1, "Define start_page")
pflag.IntVarP(&(args.end_page), "end_page", "e", -1, "Define end_page")
pflag.IntVarP(&(args.page_len), "page_length", "l", 72, "Define page_length")
pflag.StringVarP(&(args.printf_dest), "printf_dest", "d", "", "Define printf_dest")
pflag.BoolVarP(&(args.page_type), "page_type", "f", false, "Define page_type")
pflag.Parse()
// 擷取 filename 參數
filename := pflag.Args()
if len(filename) > 0 {
args.infile_name = string(filename[0])
} else {
args.infile_name = ""
}
}
檢查參數
主要檢測四種參數錯誤
- 開始頁和終止頁必須指派
- 起始頁不能大于終止頁
- -l和-f不共存
- 頁長度過小或溢出
如有參數錯誤則輸出錯誤提示
func checkArgs(args *selpgArgs) {
// 判斷輸入參數合法性
if (args.start_page == -1) || (args.end_page == -1) {
fmt.Fprintf(os.Stderr, "[Error]The start_page and end_page can't be empty!\n")
os.Exit(1)
} else if (args.start_page <= 0) || (args.end_page <= 0) {
fmt.Fprintf(os.Stderr, "[Error]The start_page and end_page can't be less than 1!\n")
os.Exit(2)
} else if args.start_page > args.end_page {
fmt.Fprintf(os.Stderr, "[Error]The start_page can't be bigger than the end_page!\n")
os.Exit(3)
} else if (args.page_type == true) && (args.page_len != 72) {
fmt.Fprintf(os.Stderr, "[Error]The command -l and -f are exclusive!\n")
os.Exit(4)
} else if args.page_len <= 0 {
fmt.Fprintf(os.Stderr, "[Error]The page_len can't be less than 1 !\n")
os.Exit(5)
} else {
// 輸入參數均合法,判斷是選擇了-l還是-f,并輸入參數清單。
page_type := "page length."
if args.page_type == true {
page_type = "The end sign /f."
}
fmt.Printf("[ArgsStart]\n")
fmt.Printf("start_page: %d\nend_page: %d\ninputFile: %s\npage_length: %d\npage_type: %s\nprintf_destation: %s\n[ArgsEnd]\n", args.start_page, args.end_page, args.infile_name, args.page_len, page_type, args.printf_dest)
}
}
處理指令
處理指令時,首先判斷是否有輸入file_name參數,若有則從檔案中讀取輸入,否則從标準輸入中讀取,然後判斷打開是否成功。
然後判斷是否輸入-d參數,若有則将輸出資料輸出到特定的地方,否則直接在标準輸出中輸出。
func excuteCMD(args *selpgArgs) {
var inp *os.File
if args.infile_name == "" {
// 從标準輸入中讀取
inp = os.Stdin
} else {
// 檢測檔案是否可讀取
checkFileAccess(args.infile_name)
var err error
inp, err = os.Open(args.infile_name)
// 檢測檔案是否打開成功
checkError(err, "input file")
}
if len(args.printf_dest) == 0 {
// 輸出到标準輸出
output(os.Stdout, inp, args.start_page, args.end_page, args.page_len, args.page_type)
} else {
// 輸出到檔案中
output(getDesio(args.printf_dest), inp, args.start_page, args.end_page, args.page_len, args.page_type)
}
}
資料輸出
輸出函數将已經處理過的指令根據參數的限制得到的資料輸出。
首先判斷-f和-l,然後判斷輸出頁是否超出範圍。
//輸出函數
func output(fout interface{}, inp *os.File, pageStart int, pageEnd int, page_len int, page_type bool) {
lineCount := 0
pageCount := 1
buf := bufio.NewReader(inp)
for true {
var line string
var err error
if page_type {
// -f
line, err = buf.ReadString('\f')
pageCount++
} else {
// -l
line, err = buf.ReadString('\n')
lineCount++
if lineCount > page_len {
pageCount++
lineCount = 1
}
}
if err == io.EOF {
break
}
checkError(err, "file read in")
if (pageCount >= pageStart) && (pageCount <= pageEnd) {
var outputErr error
// 通過類型斷言,判斷fout的類型,知道應該調用哪個函數
if stdOutput, ok := fout.(*os.File); ok {
_, outputErr = fmt.Fprintf(stdOutput, "%s", line)
} else if pipeOutput, ok := fout.(io.WriteCloser); ok {
_, outputErr = pipeOutput.Write([]byte(line))
} else {
fmt.Fprintf(os.Stderr, "[Error]:fout type error.")
os.Exit(7)
}
checkError(outputErr, "Error happend when output the pages.")
}
}
if pageCount < pageStart {
// 起始頁太大
fmt.Fprintf(os.Stderr, "[Error]: start_page (%d) greater than total pages (%d)\n", pageStart, pageCount)
os.Exit(8)
} else if pageCount < pageEnd {
// 終止頁太大
fmt.Fprintf(os.Stderr, "[Error]: end_page (%d) greater than total pages (%d)\n", pageEnd, pageCount)
os.Exit(9)
}
}
main函數
func main() {
var args selpgArgs
getArgs(&args) // 讀取參數
checkArgs(&args) // 判斷參數是否合法
excuteCMD(&args) // 執行指令
}
實驗結果
檔案結構
input_file為從1到800的文本檔案。
selpg運作程式通過在檔案夾内運作終端并輸入 go build selpg.go獲得
單元測試
以 ./selpg -s1 -e2 input_file.txt為例進行單元測試,代碼如下。可以以此為例設計多個單元測試。
package main
import (
"testing"
"os/exec"
"bytes"
)
func Test_selpg(t *testing.T){
cmd := exec.Command("./selpg","-s1","-e2","input_file.txt");
var output bytes.Buffer
cmd.Stdout = &output
err := cmd.Run()
if err!=nil{
t.Error(err)
}
}
測試通過:
功能測試
1. ./selpg -s1 -e1 input_file.txt
該指令将把“input_file”的第 1 頁寫至标準輸出(也就是螢幕),因為這裡沒有重定向或管道。
2. ./selpg -s1 -e1 < input_file.txt
該指令與示例 1 所做的工作相同,但在本例中,selpg 讀取标準輸入,而标準輸入已被 shell/核心重定向為來自“input_file”而不是顯式命名的檔案名參數。輸入的第 1 頁被寫至螢幕。
3. more input_file.txt | ./selpg -s1 -e2
“other_command”的标準輸出被 shell/核心重定向至 selpg 的标準輸入。将第 1 頁到第 2 頁寫至 selpg 的标準輸出(螢幕)。一頁72頁,總共輸出144頁。
4. ./selpg -s1 -e2 input_file.txt >output_file.txt
selpg 将第 10 頁到第 20 頁寫至标準輸出;标準輸出被 shell/核心重定向至“output_file”。
out_file内容如下:
5. ./selpg -s10 -e20 input_file.txt 2>error_file.txt
selpg 将第 10 頁到第 20 頁寫至标準輸出(螢幕);所有的錯誤消息被 shell/核心重定向至“error_file”。請注意:在“2”和“>”之間不能有空格;
6. ./selpg -s10 -e20 input_file.txt >output_file.txt 2>error_file.txt
selpg 将第 10 頁到第 20 頁寫至标準輸出,标準輸出被重定向至“output_file”;selpg 寫至标準錯誤的所有内容都被重定向至“error_file”。
因輸入隻有12頁,是以報錯終止頁數大于總共頁數,錯誤資訊儲存在error_file中。
7. ./selpg -s10 -e20 input_file.txt >output_file.txt 2>/dev/null
selpg 将第 10 頁到第 20 頁寫至标準輸出,标準輸出被重定向至“output_file”;selpg 寫至标準錯誤的所有内容都被重定向至 /dev/null(空裝置),這意味着錯誤消息被丢棄了。裝置檔案 /dev/null 廢棄所有寫至它的輸出,當從該裝置檔案讀取時,會立即傳回 EOF。
8. ./selpg -s10 -e20 input_file.txt >/dev/null
selpg 将第 10 頁到第 20 頁寫至标準輸出,标準輸出被丢棄;錯誤消息在螢幕出現。這可作為測試 selpg 的用途,此時您也許隻想(對一些測試情況)檢查錯誤消息,而不想看到正常輸出。
9. ./selpg -s10 -e20 input_file.txt | other_command
selpg 的标準輸出透明地被 shell/核心重定向,成為“other_command”的标準輸入,第 10 頁到第 20 頁被寫至該标準輸入。“other_command”的示例可以是 lp,它使輸出在系統預設列印機上列印。“other_command”的示例也可以 wc,它會顯示標明範圍的頁中包含的行數、字數和字元數。“other_command”可以是任何其它能從其标準輸入讀取的指令,這裡選用wc,它會顯示標明範圍的頁中包含的行數、字數和字元數。。錯誤消息仍在螢幕顯示。
10. ./selpg -s10 -e20 input_file.txt 2>error_file.txt | other_command
與上面的示例 9 相似,隻有一點不同:錯誤消息被寫至“error_file”
11. ./selpg -s1 -e2 -l66 input_file.txt
該指令将頁長設定為 66 行,這樣 selpg 就可以把輸入當作被定界為該長度的頁那樣處理。第 1 頁到第 2 頁被寫至 selpg 的标準輸出(螢幕)。
12. $ ./selpg -s10 -e20 -f input_file.txt
假定頁由換頁符定界。第 10 頁到第 20 頁被寫至 selpg 的标準輸出(螢幕)。
13. ./selpg -s10 -e20 -dlp1 input_file.txt
第 10 頁到第 20 頁由管道輸送至指令“lp -dlp1”,該指令将使輸出在列印機 lp1 上列印。
測試無誤。