프로그래밍 언어/Go

Go: flag 패키지로 CLI 도구 만들기

flag

Go 의 flag 패키지는 CLI(Command Line Interface) 어플리케이션을 작성하기 위해 사용하는 내장 패키지이다. 주로 서브 커맨드가 없는 싱글 커맨드에 대해 옵션을 지정하여 사용할 때 사용한다. 가장 간단한 사용법은 아래와 같다. 예를 들어 사용자에게 --port, --p 옵션을 통해 포트를 지정할 수 있는 옵션을 지정하여 값을 받는다고 생각해보자.

package main

import (
	"flag"
)

func main() {
	var port int
    
	flag.IntVar(&port, "port", 3000, "")
	flag.IntVar(&port, "p", 3000, "")

	flag.Parse()
}

어플리케이션의 코드가 main.go 라고 가정해보면 다음과 같이 사용할 수 있다.

$ go run main.go --port 3000

또한 help 메시지를 자동으로 만들어주고 있어서 다음과 같이 확인할 수 있다.

$ go run main.go --help
Usage of main.exe:
  -p int
         (default 3000)
  -port int
         (default 3000)

flag 패키지를 이렇게 사용해도 큰 문제는 없긴하지만 조금 더 내부의 코드를 들여다보고 이를 사용하면 조금 더 유연하게 사용해볼 수 있는데, 한 번 알아보도록 하자.

IntVar()

우리가 int 타입의 옵션을 지정할 때 사용한 함수인 flag.IntVar() 의 구현을 보자.

// IntVar defines an int flag with specified name, default value, and usage string.
// The argument p points to an int variable in which to store the value of the flag.
func IntVar(p *int, name string, value int, usage string) {
	CommandLine.Var(newIntValue(value, p), name, usage)
}

구현을 보면, CommandLine.Var() 메서드를 호출하고 있는 모습을 볼 수 있는데, CommandLine 은 사실 FlagSet 타입을 할당한 변수이며 오류가 나면 에러를 발생시키고 프로그램을 종료하게된다. NewFlagSet() 은 외부에 노출되어 있는 함수이기 때문에 외부에서도 호출할 수 있다는 것을 기억하자.

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

FlagSet.Var()

CommandLine.Var() 메서드는 FlagSet.Var() 와 같다는 결론을 낼 수 있으며 구현은 아래와 같은데, Flag 를 생성하고 등록하는 모습을 볼 수 있다. 물론 우리가 직접 Flag 타입을 사용하지는 않을 것이고, FlagSet.Var() 메서드를 호출하기만 할 것이다. 중요한 것은 첫 번째 파라매터에 있는 Value 타입이다.

// Var defines a flag with the specified name and usage string. The type and
// value of the flag are represented by the first argument, of type Value, which
// typically holds a user-defined implementation of Value. For instance, the
// caller could create a flag that turns a comma-separated string into a slice
// of strings by giving the slice the methods of Value; in particular, Set would
// decompose the comma-separated string into the slice.
func (f *FlagSet) Var(value Value, name string, usage string) {
	// Remember the default value as a string; it won't change.
	flag := &Flag{name, usage, value, value.String()}
    
	// ...

	if f.formal == nil {
		f.formal = make(map[string]*Flag)
	}
	f.formal[name] = flag
}

Value

제목에 서술했듯이 Value 타입은 사실 인터페이스다. 따라서 인터페이스만 충족하기만 하면 FlagSet.Var() 를 통해 사용할 수 있음을 의미한다.

type Value interface {
	String() string
	Set(string) error
}

나만의 플래그 타입 만들기

이제 커스텀 플래그 타입을 만들어보고 이를 적용시켜보자. 먼저 MyFlag 타입을 만든 뒤, Value 인터페이스에 부합하도록 String(), Set() 메서드를 만들어주어야 한다.

type MyFlag string

func (f *MyFlag) String() string {
	return string(*f)
}

func (f *MyFlag) Set(s string) error {
	*f = MyFlag(s)
	return nil
}

func NewMyFlagValue(val string, p *string) *MyFlag {
	*p = val
	return (*MyFlag)(p)
}

IntVar() 에서 보았듯이, NewIntValue() 를 사용한 것처럼 NewMyFlagValue() 함수를 사용해보았다. 이는 필수는 아니지만, 비슷하게 따라해본 것이다. 중요한 것은 인터페이스를 충족하기 위한 두 메서드이며 아래와 같이 사용할 수 있다.

func main() {
	flagSet := flag.NewFlagSet("main", flag.ExitOnError)

	var message string
	flagSet.Var(NewMyFlagValue("Hello, world!", &message), "myFlag", "")

	if err := flagSet.Parse(os.Args[1:]); err != nil {
		log.Fatal(err)
	}
}

Hello, world 에 해당하는 값은 플래그의 기본 값이며, 만약 아래와 같이 입력했다면 message 변수에는 Hello, Go! 가 입력될 것이다.

go run main.go --myFlag Hello, Go!

써드파티 라이브러리

flag 패키지는 주로 싱글 커맨드를 작성할 때 사용하고, 만약 go run 처럼 서브 커맨드가 있는 경우에는 써드파티 라이브러리를 사용할 수 있다.

 

https://github.com/urfave/cli

 

urfave/cli

A simple, fast, and fun package for building command line apps in Go - urfave/cli

github.com