服務計算 - 3 | CLI 命令行實用程序開發基礎

服務計算 - 3 | CLI 命令行實用程序開發基礎


概述

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


基本要求

參閱 Selpg命令行程序設計邏輯,實現一個selpg頁選擇程序,知足selpg設計要求。linux


程序實現

流程分析

CLI流程圖

程序按照讀取參數、判斷參數是否合規、讀取文件、肯定輸出位置並輸出順序執行。當發現錯誤時拋出錯誤並終止流程。git

代碼實現

  1. selpg所需參數有必須的開始頁碼-s以及結束頁碼-e,可選的輸入文件名、自定頁長-l、遇換頁符換頁-f和輸出地址。其中自定頁長和遇換頁符換頁兩個選項是互斥的,不能同時使用。github

    • 定義保存參數數據的結構體golang

      type selpgArgs struct {
          startPage  int
          endPage    int
          inFileName string
          pageLen    int
          pageType   bool
          printDest  string
      }
    • 輸入參數使用 github.com/spf13/pflag 包提供的pflag進行處理,pflag包於flag用法相似,但pflag相對於flag可以更好地知足 Unix 命令行規範。參考:Golang pflagdocker

      func getArgs(args *selpgArgs) {
          pflag.IntVarP(&(args.startPage), "startPage", "s", -1, "Define startPage")
          pflag.IntVarP(&(args.endPage), "endPage", "e", -1, "Define endPage")
          pflag.IntVarP(&(args.pageLen), "pageLength", "l", 72, "Define pageLength")
          pflag.StringVarP(&(args.printDest), "printDest", "d", "", "Define printDest")
          pflag.BoolVarP(&(args.pageType), "pageType", "f", false, "Define pageType")
          pflag.Parse()
      
          argLeft := pflag.Args()
          if len(argLeft) > 0 {
              args.inFileName = string(argLeft[0])
          } else {
              args.inFileName = ""
          }
      }
      • pflag包中的函數XXXVarP(XXX爲Int、String、Bool等可選類型)能夠取出命令行參數名稱shorthand的參數的值,value指定*p的默認值,name爲自定的名稱,usage爲自定的該參數的描述。該函數無返回值。
      • func XXXVarP(p *XXX, name, shorthand string, value XXX, usage string)
      • 得到flag參數後,要用pflag.Parse()函數才能把參數解析出來。這裏還有一個東西,解析完指定的參數後,能夠經過調用argLeft := pflag.Args()來得到未定義但輸入了的參數如文件名。
  2. 命令行參數獲取以後,首先要進行參數檢查以儘可能避免參數謬誤。出現錯誤時輸出問題並正常結束程序。參數正確則將各個參數值輸出到屏幕上。shell

    func checkArgs(args *selpgArgs) {
        if (args.startPage == -1) || (args.endPage == -1) {
            fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be empty! Please check your command!\n")
            os.Exit(2)
        } else if (args.startPage <= 0) || (args.endPage <= 0) {
            fmt.Fprintf(os.Stderr, "\n[Error]The startPage and endPage can't be negative! Please check your command!\n")
            os.Exit(3)
        } else if args.startPage > args.endPage {
            fmt.Fprintf(os.Stderr, "\n[Error]The startPage can't be bigger than the endPage! Please check your command!\n")
            os.Exit(4)
        } else if (args.pageType == true) && (args.pageLen != 72) {
            fmt.Fprintf(os.Stderr, "\n[Error]The command -l and -f are exclusive, you can't use them together!\n")
            os.Exit(5)
        } else if args.pageLen <= 0 {
            fmt.Fprintf(os.Stderr, "\n[Error]The pageLen can't be less than 1 !\n")
            os.Exit(6)
        } else {
            pageType := "page length."
            if args.pageType == true {
                pageType = "The end sign /f."
            }
            fmt.Printf("\n[ArgsStart]\n")
            fmt.Printf("startPage: %d\nendPage: %d\ninputFile: %s\npageLength: %d\npageType: %s\nprintDestation: %s\n[ArgsEnd]", args.startPage, args.endPage, args.inFileName, args.pageLen, pageType, args.printDest)
        }
    }
    • 在這個函數中,首先檢查了開始頁args.startPage和結束頁args.endPage是否被賦值,而後檢查開始頁args.startPage和結束頁args.endPage是否爲正數,接下來檢查開始頁args.startPage是否大於結束頁args.endPage,而後檢查自定頁長-l和遇換頁符換頁-f是否同時出現,最後判斷當自定頁長-l出現時args.pageLen是否小於1。遇到不合規的地方正常結束程序,所有合規則輸出獲得的參數。
  3. 參數檢查結束以後,程序開始調用excuteCMD函數執行命令。npm

    func checkError(err error, object string) {
        if err != nil {
            fmt.Fprintf(os.Stderr, "\n[Error]%s:", object)
            panic(err)
        }
    }
    
    func excuteCMD(args *selpgArgs) {
        var fin *os.File
        if args.inFileName == "" {
            fin = os.Stdin
        } else {
            checkFileAccess(args.inFileName)
            var err error
            fin, err = os.Open(args.inFileName)
            checkError(err, "File input")
        }
    
        if len(args.printDest) == 0 {
            output2Des(os.Stdout, fin, args.startPage, args.endPage, args.pageLen, args.pageType)
        } else {
            output2Des(cmdExec(args.printDest), fin, args.startPage, args.endPage, args.pageLen, args.pageType)
        }
    }
    
    func checkFileAccess(filename string) {
        _, errFileExits := os.Stat(filename)
        if os.IsNotExist(errFileExits) {
            fmt.Fprintf(os.Stderr, "\n[Error]: input file \"%s\" does not exist\n", filename)
            os.Exit(7)
        }
    }
    • 第一步檢查輸入。若是沒有給定文件名,則從標準輸入中獲取;若是給出讀取的文件名,則調用函數checkFileAccess檢查文件是否存在。
    • 第二步是打開文件,使用函數checkError檢查是否出現錯誤。若是打開出錯則輸出錯誤並拋出恐慌。
    • 第三步判斷是否有-d參數。若是沒有-d參數,選擇的頁直接從os.Stdout標準輸出中輸出。若是-d存在,則從指定的打印通道中輸出。
  4. 在-d參數存在時,涉及到了os/exec包的使用,這裏能夠參考golang中os/exec包用法編程

    func cmdExec(printDest string) io.WriteCloser {
        cmd := exec.Command("lp", "-d"+printDest)
        fout, err := cmd.StdinPipe()
        checkError(err, "StdinPipe")
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        errStart := cmd.Run()
        checkError(errStart, "CMD run")
        return fout
    }
    • 其中printDest就是獲取的打印地址,將命令行的輸入管道cmd.StdinPipe()獲取的指針賦值給fout,而後再將fout返回給output2Des函數中的做爲輸出位置參數的輸入,最後在output2Des中將須要的頁輸出到fout。
  5. 輸出函數output2Des將輸入的文件,按頁碼要求讀取並輸出到fout中。緩存

    func output2Des(fout interface{}, fin *os.File, pageStart int, pageEnd int, pageLen int, pageType bool) {
    
        lineCount := 0
        pageCount := 1
        buf := bufio.NewReader(fin)
        for true {
    
            var line string
            var err error
            if pageType {
                //If the command argument is -f
                line, err = buf.ReadString('\f')
                pageCount++
            } else {
                //If the command argument is -lnumber
                line, err = buf.ReadString('\n')
                lineCount++
                if lineCount > pageLen {
                    pageCount++
                    lineCount = 1
                }
            }
    
            if err == io.EOF {
                break
            }
            checkError(err, "file read in")
    
            if (pageCount >= pageStart) && (pageCount <= pageEnd) {
                var outputErr error
                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, "\n[Error]:fout type error. ")
                    os.Exit(8)
                }
                checkError(outputErr, "Error happend when output the pages.")
            }
        }
        if pageCount < pageStart {
            fmt.Fprintf(os.Stderr, "\n[Error]: startPage (%d) greater than total pages (%d), no output written\n", pageStart, pageCount)
            os.Exit(9)
        } else if pageCount < pageEnd {
            fmt.Fprintf(os.Stderr, "\n[Error]: endPage (%d) greater than total pages (%d), less output than expected\n", pageEnd, pageCount)
            os.Exit(10)
        }
    }
    • bufio包實現了帶緩存的 I/O 操做,在文件讀取中十分方便。具體使用參見Golang學習 - bufio 包。這裏使用buf.ReadString(symbol),每次讀取字符串直到遇到字符symbol爲止。
    • 因爲fout存在兩種輸入-io.Stdout標準輸出做爲輸入、cmd.StdinPipe()管道做爲輸入。因此使用空接口interface{}做爲fout的類型,藉助類型斷言stdOutput, ok := fout.(*os.File)和pipeOutput, ok := fout.(io.WriteCloser)來判斷fout具體類型並調用相應函數。

程序測試

  • 按文檔 使用 selpg 章節要求測試該程序
  • 測試文檔input_file.txt包含兩個換頁符
    測試文檔
  1. ./selpg -s1 -e1 input_file.txt

    pic1

  2. ./selpg -s1 -e1 input_file.txt

    pic2

  3. ./selpg -s1 -e2 input_file.txt >output_file

    pic3
    output1

  4. ./selpg -s1 -e4 input_file.txt 2>error_file

    pic4
    output2

  5. ./selpg -s1 -e3 input_file.txt >output_file 2>error_file

    pic5
    output3

  6. ./selpg -s1 -e2 -f input_file.txt

    pic6

  7. ./selpg -s1 -e1 -dCups-PDF selpg.go

    pic8
    答應結果

    • 藉助虛擬打印機插件Cups-PDF,打印輸出結果是代碼的前72行,因此結果正確。

GITHUB項目地址

相關文章
相關標籤/搜索