Go 와 Kingpin 으로 CLI 툴 빌드하기

잘 구성된 커맨드라인 인터페이스를 구성하는 것은 매우 어렵습니다. --help 입력없이 사용자가 도움말을 잘 얻고 관련 문서도 확인하도록 하는 것은 쉽지 않은 일입니다. 아마도 명령어가 기억나지 않아 --help 를 입력한 경험이 많을 것입니다.

Atlassian에서는 CLI 툴을 이용하여 내부 서비스를 이용하는데 사용합니다. 이것은 Go 로 제작되었으며 CLI 라이브러리인 Kingpin 에서 제공하는 모든 기능을 이용합니다.

이 글에서는 Shell completion 힌트를 제공하는 CLI를 어떻게 제작하는지 기술할 것입니다. 툴에 대한 Bash 자동완성은 툴의 사용을 더욱 빠르게 도와줍니다. 하지만, Kingpin은 shell 자동완성을 지원하지는 않습니다. 그렇지만 여기 기능을 구현하였고 Kingpin 에 해당 기능을  pull request 를 통해 기여하였습니다.

이 글에서는 어떻게 CLI 가 Shell 자동완성 힌트를 제공하게 만드는지 설명합니다.
Go 프로젝트 생성에 익숙하다고 가정하겠습니다. 이제 Go를 시작하셨다면, Golang Getting Started 문서를 확인하십시요.


시작하기


CLI를 위한 새로운 go 패키지를 생성합니다. 
package main
import (
    "os"
    "gopkg.in/alecthomas/kingpin.v2"
)
func main() {
    app := kingpin.New("my-app""My Example CLI Application With Bash Completion")
    kingpin.MustParse(app.Parse(os.Args[1:]))
}
이것은 런타임시에 전달되는 명령행을 파싱하는 kingpin CLI 앱을 구성합니다. 아래와 같이 실행합니다.

$> go build && ./my-app --help
usage: my-app []
My Example CLI Application With Bash Completion
Flags:
  --help  Show context-sensitive help (also try --help-long and --help-man).

기능 추가하기


이제 sub 명령을 추가합니다. main() 함수를 확장하여 helper factory addSubCommand 를 추가합니다:
package main
import (
    "os"
    "fmt"
    "gopkg.in/alecthomas/kingpin.v2"
)
func addSubCommand(app *kingpin.Application, name string, description string) {
    app.Command(name, description).Action(func(c *kingpin.ParseContext) error {
        fmt.Printf("Would have run command %s.\n", name)
        return nil
    })
}
func main() {
    app := kingpin.New("my-app""My Sample Kingpin App!")
    app.Flag("flag-1""").String()
    app.Flag("flag-2""").HintOptions("opt1""opt2").String()
    // Add some additional top level commands
    addSubCommand(app, "ls""Additional top level command to show command completion")
    addSubCommand(app, "ping""Additional top level command to show command completion")
    addSubCommand(app, "nmap""Additional top level command to show command completion")
    kingpin.MustParse(app.Parse(os.Args[1:]))
}

Bash Completion


아직 완전한 예제는 아니지만, 이제 Bash completion을 바로 시도해 볼 수 있습니다. 우리의 쉘에서 Bash completion을 사용설정하기 위해, completion 스크립트를 생성할 필요가 있습니다. ./my-app --completion-script-bash 과 ./my-app --completion-script-zsh  으로 해보십시요. 바이너리 이름은 kingpin.New()  로 전달되는 이름과 같아야 합니다. ((정보) Kingpin 에 --completion-script-bash 혹은 --completion-script-zsh 을 첫번째 인자로 넘겨주면 자동으로 스크립트가 생성됨)

이상적으로는, 바이너리 툴을 패키징할때, 이 스크립트도 포함하여 적절한 위치에 설치되도록 해야 할 것입니다.
지금은 현재 세션의 로컬 소스에 작업합니다. (bash_profile 혹은 .bashrc 에 추가)

# If you're using Bash
eval "$(./my-app --completion-script-bash)"
# If you're using Zsh
eval "$(./my-app --completion-script-zsh)"

이제 아래와 같이 실행합니다:
$> ./my-app 
help     ls    nmap    ping
모든것이 잘 되었다면, 사용가능한 subcommand 목록을 볼 수 있을 것입니다.

추가 기능


subcommand 에 대한 힌트를 줄 수 있는 것은 매우 유용하지만, 사용가능한 플래그에 대한 옵션도 주게되면 더욱 유용할 것입니다. 새로운 nc 명령어를 추가해 명령행 플래그를 사용하도록 합니다.

main 함수를 업데이트 하여 새로운 NetcatCommand, 와 이를 위한 configureNetcatCommand 를 추가합니다:

func main() {
    app := kingpin.New("my-app""My Sample Kingpin App!")
    configureNetcatCommand(app)
    // Add some additional top level commands
    addSubCommand(app, "ls""Additional top level command to show command completion")
    addSubCommand(app, "ping""Additional top level command to show command completion")
    addSubCommand(app, "nmap""Additional top level command to show command completion")
    kingpin.MustParse(app.Parse(os.Args[1:]))
}
type NetcatCommand struct {
    hostName string
    port     int
    format   string
}
func (n *NetcatCommand) run(c *kingpin.ParseContext) error {
    fmt.Printf("Would have run netcat to hostname %v, port %d, and output format %v\n", n.hostName, n.port, n.format)
    return nil
}
func configureNetcatCommand(app *kingpin.Application) {
    c := &NetcatCommand{}
    nc := app.Command("nc""Connect to a Host").Action(c.run)
    nc.Flag("nop-flag""Example of a flag with no options").Bool()
}

빌드 후 실행하면 netcat 명령에 대한 플래그 힌트를 볼 수 있습니다. 플래그 제안을 보려면 처음에 "--"를 입력후 탭키를 누릅니다.

$> ./my-app nc --
--help         --nop-flag

마지막으로, NetcatCommand 에 더 많은 플래그를 추가합니다. configureNetcatCommand 를 수정하고 listHosts() 함수를 추가합니다:

func configureNetcatCommand(app *kingpin.Application) {
    c := &NetcatCommand{}
    nc := app.Command("nc""Connect to a Host").Action(c.run)
    nc.Flag("nop-flag""Example of a flag with no options").Bool()
    // You can provide hint options statically
    nc.Flag("port""Provide a port to connect to").
        Required().
        HintOptions("80""443""8080").
        IntVar(&c.port)
    // Enum/EnumVar options will be turned into completion options automatically
    nc.Flag("format""Define the output format").
        Default("raw").
        EnumVar(&c.format, "raw""json")
    // You can provide hint options using a function to generate them
    nc.Flag("host""Provide a hostname to nc").
        Required().
        HintAction(listHosts).
        StringVar(&c.hostName)
}
func listHosts() []string {
    // Provide a dynamic list of hosts from a hosts file or otherwise
    // for Bash completion. In this example we simply return static slice.
    // You could use this functionality to reach into a hosts file to provide
    // completion for a list of known hosts.
    return []string{"sshhost.example""webhost.example""ftphost.example"}
}

실행하면 다음과 같습니다:
$> ./my-app nc --
--format     --help     --host     --nop-flag     --port
$> ./my-app nc --format 
json     raw
$> ./my-app nc --host
ftphost.example     sshhost.example     webhost.example
$> ./my-app nc --port
443         80        8080

정리하기

이제 CLI 툴을 이용할 수 있게 되었습니다. 이제 전체 Go CLI code 예제를 확인해 보십시요. 이것은 pull request에 포함된 Kingpin completion 예를 기반으로 한 것입니다. 실제 예는 여기에서 확인할 수 있습니다.

댓글

이 블로그의 인기 게시물

Confluence 내의 스프레드 시트 기능이 필요하시다면 애드온을 활용해 보십시요

시스템에 숨어있는 "윤초" 버그에 대해 준비하십시요

Confluence 페이지의 분류와 관련된 잘 몰랐던 기능 3가지를 확인해 보십시요