Cobra로 테스트하기 쉬운 CLI 구성

41910 단어 Goprogrammingtech
드디어 포장된 v1.1.0이 나오자마자 어느새 spf13/cobra라는 사이트가 생겼다.모처럼의 기회인 만큼 실제 만들면서 기념으로 활용한 CLI(Command-Line Interface)를 소개했다.
참고로 Cobra.DevCobra에서 CLI를 구성하는 데 사용되는 프레임워크 패키지로 표준Cobra 패키지에 비해 다음과 같은 특징(주요)이 있다.
  • 다중 하위 명령을 비교적 간단하게 조합할 수 있음

  • Go 포장과 결합하여 POSIX/GNU 스타일의 로고를 실현할 수 있습니다.또한 플래그 종속 연결
  • 도 가능합니다.

  • flag 로고와 설정 파일을 패키지와 결합할 수 있는 내용
  • (사용자 정의 가능)
  • 자동 생성 및 표시 가능
  • 이번 제목.


    이번에 제작된 CLI 애플리케이션의 사양은 다음과 같습니다.
  • 명령hashencode 하위 명령
  • 이 있음
  • 하위 명령encode은 SHA 256 알고리즘을 통해 입력 텍스트를 해시 값
  • 으로 인코딩한다.

    UNIX Philosophy


    실제 CLI 앱을 제작하기 전에 디자인 포인터로 쓰이는'UNIX Philosophy'가 있기 때문에 소개해 드리겠습니다.가로되
  • Small is beautiful. (작은 것이 아름답다)
  • Make each program do one thing well. (절차별로 한 가지 일을 잘 해야 한다)
  • Build a prototype as soon as possible. (빨리 프로토타입 만들기)
  • Choose portability over efficiency. (이식이 효율보다 쉽다)
  • Store data in flat text files. (간단한 텍스트 파일에 데이터 저장)
  • Use software leverage to your advantage. (소프트웨어 효율성을 이점으로 활용)
  • Use shell scripts to increase leverage and portability. (조개 스크립트를 사용하여 효율과 이식성을 높인다)
  • Avoid captive user interfaces.
  • Make every program a Filter. (모든 프로그램은 필터로 사용 가능)
  • 의 9개(근거spf13/pflag.이러한 요소spf13/viper를 기반으로 CLI를 구성할 때는 다음 사항에 유의하십시오.
  • 셸을 통해 다른 도구와 협업할 수 있는 표준 입력과 출력을 이용한 필터 프로그램
  • 결과의 출력에서 JSON, YAML 등 형식도 유효하고 외부 도구와의 협업에 편리하다
  • 가능한 경우 UTF-8 인코딩을 통한 통합 출력
  • 플랫폼 종속성을 방지하기 위해 코드의 이동성(또는 이동성)을 고려
  • 또한 CLI는 이른바'대화 모드'에서도 이 지침을 적용하지 않으니 탓하지 마십시오.

    하위 명령 및 세 번째 모드


    하위 명령 방식은 언뜻 보면'UNIX Philosophy'와는 반대인 듯위키백과한 경우 모든 소프트웨어 패키지를 바이너리에 결합하기 위해 관련 기능을 하위 명령으로 편입하는 것이 좋다.
    하위 명령을 구성하는 경우 facade pattern을 고려할 수 있습니다.
    facade pattern
    '파슬'은'건축물의 정면'이라는 뜻으로 시스템 내의 각 서브시스템 창의 역할을 한다.제3자 자신은 서브시스템의 세부 사항을 모르고 상하문 정보만 통해 걷어찬다.비결은 서브시스템 측이father에 의존하지 않고 상하문 정보만 있으면 처리할 수 있다는 것이다.
    CLI이기 때문에 서브시스템 측면의 처리 결과는string, []byte, io입니다.Reader 또는 그것들을 출력할 수 있는 형식으로 파삼덕에 되돌려주면 된다.

    Cobra 명령을 사용하여 초기 형태 생성


    오프닝이 길어졌으니 빨리 Go로 간단한 CLI를 조립하세요.

    Go 명령 설치


    Cobra 가방에 출력 코드의 초기 도구를 준비했습니다.바이너리에서 제공되지 않았기 때문에 go get 명령으로 구축하고 설치합니다.
    $ go get -u github.com/spf13/cobra/cobra
    

    main과 cmd 패키지의 생성


    먼저 환경을 만들다.이런 느낌 어때요?
    $ mkdir hash & cd hash
    $ go mod init sample/hash
    
    편의를 위해 가방이나 모듈의 경로를 sample/hash로 설정합니다.실제로는 github.com/user/hash와 같은 경로일 것이다.
    다음은 이전 절에서 만든 cobra 명령main으로 패키지의 초기 형태를 만듭니다.이런 느낌.
    $ cobra init --pkg-name sample/hash --viper=false
    $ tree .
    .
    ├── LICENSE
    ├── cmd
    │   └── root.go
    ├── go.mod
    └── main.go
    
    이번에는 Cobra의 설정 파일을 제어하지 않기 때문에 --viper=false 옵션을 추가했습니다.LICENSE 파일은 삭제되거나 타당하게 바꿀 수 있다.main.go의 내용은 이런 느낌(평론 부분 제외)이다.
    main.go
    package main
    
    import "sample/hash/cmd"
    
    func main() {
        cmd.Execute()
    }
    
    cmd/root.go의 내용은 이런 느낌(리뷰 부분 제외)이다.
    cmd/root.go
    package cmd
    
    import (
        "fmt"
        "github.com/spf13/cobra"
        "os"
    )
    
    var rootCmd = &cobra.Command{
        Use:   "hash",
        Short: "A brief description of your application",
        Long: `A longer description that spans multiple lines and likely contains
    examples and usage of using your application. For example:
    
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.`,
    }
    
    func Execute() {
        if err := rootCmd.Execute(); err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
    }
    
    func init() {
        rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
    }
    
    일단 이 상태에서도 작동할 수 있다.
    $ go run main.go -h
    A longer description that spans multiple lines and likely contains
    examples and usage of using your application. For example:
    
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.
    
    구하고 싶지만 조금만 더 참고 계속 전진하자.

    하위 명령 추가


    하위 명령encode을 추가해 보십시오.이런 느낌.
    추가
    $ cobra add encode
    $ tree .
    .
    ├── LICENSE
    ├── cmd
    │   ├── encode.go
    │   └── root.go
    ├── go.mod
    └── main.go
    
    
    cmd/encode.go된 거 아세요?또 기타main.go 또는 cmd/root.go를 가공하지 않았다.cmd/encode.go의 내용은 이런 느낌(평론 부분 제외)이다.
    cmd/encode.go
    package cmd
    
    import (
        "fmt"
    
        "github.com/spf13/cobra"
    )
    
    var encodeCmd = &cobra.Command{
        Use:   "encode",
        Short: "A brief description of your command",
        Long: `A longer description that spans multiple lines and likely contains examples
    and usage of using your command. For example:
    
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.`,
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("encode called")
        },
    }
    
    func init() {
        rootCmd.AddCommand(encodeCmd)
    }
    
    이 상태에서 움직이면 이렇게 되는 느낌.
    $ go run main.go
    A longer description that spans multiple lines and likely contains
    examples and usage of using your application. For example:
    
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.
    
    Usage:
      hash [command]
    
    Available Commands:
      encode      A brief description of your command
      help        Help about any command
    
    Flags:
      -h, --help     help for hash
      -t, --toggle   Help message for toggle
    
    Use "hash [command] --help" for more information about a command.
    
    $ go run main.go encode -h
    A longer description that spans multiple lines and likely contains examples
    and usage of using your command. For example:
    
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.
    
    Usage:
      hash encode [flags]
    
    Flags:
      -h, --help   help for encode
    
    $ go run main.go encode
    encode called
    
    에서 하위 명령encode을 병합할 수 있다.

    포맷 코드 다시 쓰기

    cobra 명령으로 생성된 모드 코드에 기능을 직접 삽입할 수 있지만 모드 코드에는 다음과 같은 문제가 있다.
  • 편지지cmd 패키지 내부에서 표준 입력 출력과 명령행 파라미터를 직접 사용
  • cobra.Command 실례는 cmd 패키지에서 정적 변수로 정의
  • cmd.Execute() 함수 중 일부는 함수os.Exit()를 통해 강제 종료
  • 이렇게 하면cmd 포장 자체의 테스트가 어렵기 때문에 테스트에 편리하도록 해 보세요.

    spiegel-im-spiegel/gocli 패키지 가져오기


    직언을 용서하십시오. 저는 여기에 포장Cobra을 도입했습니다.
    spf13/viper 봉인은 입력과 출력을 상하문 정보로 전달할 수 있고 이렇게 사용할 수 있다.
    package main
    
    import (
        "os"
    
        "github.com/spiegel-im-spiegel/gocli/exitcode"
        "github.com/spiegel-im-spiegel/gocli/rwi"
    )
    
    func run(ui *rwi.RWI) exitcode.ExitCode {
        ui.Outputln("Hello world")
        return exitcode.Normal
    }
    
    func main() {
        run(rwi.New(
            rwi.WithReader(os.Stdin),
            rwi.WithWriter(os.Stdout),
            rwi.WithErrorWriter(os.Stderr),
        )).Exit()
    }
    
    다시 쓰는 cmd 포장의 초기 포장을 사용하세요.

    하위 명령 재정의


    먼저 하위 명령encode부터 시작합니다.이런 느낌을 해봤어요.
    cmd/encode.go
    func newEncodeCmd(ui *rwi.RWI) *cobra.Command {
        encodeCmd := &cobra.Command{
            Use:     "encode",
            Aliases: []string{"enc", "e"},
            Short:   "hash input data",
            Long:    "hash input data (detail)",
            RunE: func(cmd *cobra.Command, args []string) error {
                if err := ui.Outputln("encode called"); err != nil {
                    return err
                }
                return nil
            },
        }
        return encodeCmd
    }
    
    따라서 내부 함수를 사용하여 하위 명령encode에 사용되는 실례를 동적으로 생성할 수 있다.

    명령 재정의


    같은 내용cobra.Command도 다시 썼다.
    cmd/root.go
    func newRootCmd(ui *rwi.RWI, args []string) *cobra.Command {
        rootCmd := &cobra.Command{
            Use:   "hash",
            Short: "Hash functions",
            Long:  "Hash functions (detail)",
        }
        rootCmd.SilenceUsage = true
        rootCmd.SetArgs(args)            //arguments of command-line
        rootCmd.SetIn(ui.Reader())       //Stdin
        rootCmd.SetOut(ui.ErrorWriter()) //Stdout -> Stderr
        rootCmd.SetErr(ui.ErrorWriter()) //Stderr
        rootCmd.AddCommand(
            newEncodeCmd(ui),
        )
        return rootCmd
    }
    
    func Execute(ui *rwi.RWI, args []string) exitcode.ExitCode {
        if err := newRootCmd(ui, args).Execute(); err != nil {
            return exitcode.Abnormal
        }
        return exitcode.Normal
    }
    
    겸사겸사 root.go 함수의
    rootCmd.SetArgs(args)            //arguments of command-line
    rootCmd.SetIn(ui.Reader())       //Stdin
    rootCmd.SetOut(ui.ErrorWriter()) //Stdout -> Stderr
    rootCmd.SetErr(ui.ErrorWriter()) //Stderr
    
    섹션에서 명령행 매개변수와 입력 출력을 설정합니다.또한 newRootCmd() 함수 내부에서는 반환값을 평가하는 error 실례가 없어 보이지만 실제로cmd.Execute()는 방법 내부에서 잘못된 내용을 평가했기 때문에cobra.Command.Execute() 함수는 cmd.Execute()의 진위만 보인다.
    마지막err != nil 함수도 상술한 내용에 따라 수정해야 한다.
    main.go
    func main() {
        cmd.Execute(
            rwi.New(
                rwi.WithReader(os.Stdin),
                rwi.WithWriter(os.Stdout),
                rwi.WithErrorWriter(os.Stderr),
            ),
            os.Args[1:],
        ).Exit()
    }
    
    이 정도면 됐어.

    테스트 명령


    쓴 코드를 써야 하는 테스트입니다.이런 느낌?
    cmd/encode_test.go
    func TestEncode(t *testing.T) {
        testCases := []struct {
            inp  string
            outp string
            ext  exitcode.ExitCode
        }{
            {inp: "hello world\n", outp: "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447\n", ext: exitcode.Normal},
        }
    
        for _, tc := range testCases {
            r := strings.NewReader(tc.inp)
            wbuf := &bytes.Buffer{}
            ebuf := &bytes.Buffer{}
            ext := Execute(
                rwi.New(
                    rwi.WithReader(r),
                    rwi.WithWriter(wbuf),
                    rwi.WithErrorWriter(ebuf),
                ),
                []string{"encode"},
            )
            if ext != tc.ext {
                t.Errorf("Execute() is \"%v\", want \"%v\".", ext, tc.ext)
                fmt.Println(ebuf.String())
            }
            str := wbuf.String()
            if str != tc.outp {
                t.Errorf("Execute() -> \"%v\", want \"%v\".", str, tc.outp)
            }
        }
    }
    
    main() 함수의 매개 변수를 주의하세요.실행 결과
    $ go test ./...
    ?       sample/hash    [no test files]
    --- FAIL: TestEncode (0.00s)
        encode_test.go:36: Execute() -> "encode called
            ", want "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
            ".
    FAIL
    FAIL    sample/hash/cmd    0.002s
    FAIL
    
    덮어쓰기.중요한 거 까먹었어 (웃음) 그럼 Execute() 서류 하나 만들어
    encode/encode.go
    package encode
    
    import (
        "crypto"
        "errors"
        "io"
    )
    
    var (
        ErrNoImplement = errors.New("no implementation")
    )
    
    //Value returns hash value string from io.Reader
    func Value(r io.Reader, alg crypto.Hash) ([]byte, error) {
        if !alg.Available() {
            return nil, ErrNoImplement
        }
        h := alg.New()
        if _, err := io.Copy(h, r); err != nil {
            return nil, err
        }
        return h.Sum(nil), nil
    }
    
    이런 느낌 괜찮아요?encode/encode.go 함수에 결합
    cmd/encode.go
    func newEncodeCmd(ui *rwi.RWI) *cobra.Command {
        encodeCmd := &cobra.Command{
            Use:     "encode",
            Aliases: []string{"enc", "e"},
            Short:   "hash input data",
            Long:    "hash input data (detail)",
            RunE: func(cmd *cobra.Command, args []string) error {
                v, err := encode.Value(ui.Reader(), crypto.SHA256)
                if err != nil {
                    return err
                }
                fmt.Fprintf(ui.Writer(), "%x\n", v)
                return nil
            },
        }
        return encodeCmd
    }
    
    테스트를 다시 시작하다.
    좋아, 응, 좋아.
    마지막으로 명령을 실제로 집행하다.
    $ go test ./...
    ?       sample/hash    [no test files]
    ok      sample/hash/cmd    0.003s
    ?       sample/hash/encode    [no test files]
    
    순조롭게 집행하다.
    또 이번에 쓴 코드는 spiegel-im-spiegel/gocli 위에 놓았다.참고해주세요.

    좋은 웹페이지 즐겨찾기