잘 구성된 커맨드라인 인터페이스를 구성하는 것은 매우 어렵습니다. --help 입력없이 사용자가 도움말을 잘 얻고 관련 문서도 확인하도록 하는 것은 쉽지 않은 일입니다. 아마도 명령어가 기억나지 않아 --help 를 입력한 경험이 많을 것입니다.
Atlassian에서는 CLI 툴을 이용하여 내부 서비스를 이용하는데 사용합니다. 이것은 Go 로 제작되었으며 CLI 라이브러리인
Kingpin 에서 제공하는 모든 기능을 이용합니다.
이 글에서는 Shell completion 힌트를 제공하는 CLI를 어떻게 제작하는지 기술할 것입니다. 툴에 대한 Bash 자동완성은 툴의 사용을 더욱 빠르게 도와줍니다. 하지만, Kingpin은 shell 자동완성을 지원하지는 않습니다. 그렇지만 여기 기능을 구현하였고 Kingpin 에 해당 기능을
pull request 를 통해 기여하였습니다.
이 글에서는 어떻게 CLI 가 Shell 자동완성 힌트를 제공하게 만드는지 설명합니다.
시작하기
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()
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 에 추가)
eval "$(./my-app --completion-script-bash)"
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)
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()
nc.Flag( "port" , "Provide a port to connect to" ).
Required().
HintOptions( "80" , "443" , "8080" ).
IntVar(&c.port)
nc.Flag( "format" , "Define the output format" ).
Default( "raw" ).
EnumVar(&c.format, "raw" , "json" )
nc.Flag( "host" , "Provide a hostname to nc" ).
Required().
HintAction(listHosts).
StringVar(&c.hostName)
}
func listHosts() [] string {
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 예를 기반으로 한 것입니다. 실제 예는
여기에서 확인할 수 있습니다.
댓글