Go flag에서 custom usage 만들기

golang에서 cli 도구를 만들 때 가장 먼저 접하는 도구는 flag입니다. 저 또한 flag로 시작하여 cobra, unfave 등 여러가지 써보다가 결국은 잘 사용하던 cobra를 버리고 flag로 다시 돌아왔습니다. go의 내장형 option parser로 심플하지만, 편의성을 위한 부분들은 많이 적어서 때때로, 직접 오버라이드와 같이 라이브러리의 원본 함수를 수정하여 사용해야하는 경우가 있습니다.

오늘은 그러한 과정에서 가장 처음 고민하게 됬던 usage에 대한 이야기를 하려고 합니다.

Flag, Usage?

아래는 간단한 flag 코드입니다.

package main

import (
  "flag"
  "fmt"
)
func main() {
    useColor := flag.Bool("color", false, "display colorized output")
    flag.Parse()

    if *useColor {
      fmt.Println("use color!")
    } else {
      fmt.Println("no color")
    }
}

빌드 후 -h 옵션을 전달해보면 아래와 같은 포맷으로 usage가 나타납니다.

go build 1.go
./1 -h
Usage of ./1:
  -color
    	display colorized output

물론 기본에 만족한다면 상관없겠지만, Usage의 message를 꾸미고 싶고 각 flag 설명 부분의 포맷도 바꿔보고 싶다면 몇가지 추가 코드 작성이 필요합니다.

익명함수로 flag.Usage를 수정하여 Usage message 수정하기

아래는 제가 만든 도구인 gee의 usage 부분 코드입니다. flag 패키지의 Usage 함수를 익명함수로 재 정의하여 새로 만들어주어 custom usage를 구현하였습니다.

  // Custom usage
	flag.Usage = func() {
		printing.Banner()
		fmt.Fprintf(os.Stderr, aurora.White("Usage: %s [flags] [file1] [file2] ...\n").String(), os.Args[0])
		fmt.Fprintf(os.Stderr, "(If you do not specify a file, only stdout is output)\n\n")
		fmt.Fprintf(os.Stderr, aurora.White("Flags:\n").String())
		flag.PrintDefaults()
	}

https://github.com/hahwul/gee/blob/main/main.go#L35 참고

여기서 flag.PrintDefaults() 함수가 각 flag의 이름과 타입, 설명 정보를 출력해주는 함수입니다.

빨간색 네모 부분

flag.PrintDefaults 부분도 바꿔보자 😎

그럼 flag.PrintDefaults() 자체를 변경하고 싶을 땐 어떻게 해야할까요? 복잡할 것 같지만 의외로 간단합니다. flag.PrintDefaults() 를 덮어쓰는건 크게 의미가 없을 것 같고, 그냥 flag.Usage 내부에서 fmt.Println , fmt.Printf 등으로 출력해주면 됩니다.

package main
import (
    "flag"
    "fmt"
)
func main() {
    flag.String("a", "", "flag 1")
    flag.String("b", "", "flag 2")
    flag.String("c", "", "flag 3")

    flag.Usage = func() {
        flagSet := flag.CommandLine
        fmt.Printf("Usage of %s:\n", "./tool")
        order := []string{"a", "b", "c"}
        for _, name := range order {
            flag := flagSet.Lookup(name)
            fmt.Printf("-%s\t%s\n", flag.Name,flag.Usage)
        }
    }
    flag.Parse()
}

output

Usage of ./tool:
-a	flag 1
-b	flag 2
-c	flag 3
*/

https://gist.github.com/hahwul/b3870d58fd038afba05099ee9be86417

코드를 보면 flag 이름을 미리 array에 넣은 후 for문을 통해 반복하며, flagSet.Lookup() 함수를 통해 해당 flag의 정보를 읽어온 후 fmt.Printf로 출력해줍니다.

flag type을 보면 아래와 같이 정의되어 있습니다.

type Flag struct {
    Name     string // name as it appears on command line
    Usage    string // help message
    Value    Value  // value as set
    DefValue string // default value (as text); for usage message
}

flag.PrintDefaults() 처럼 flag의 타입값까지 읽을 순 없지만, Name과 Usage 등을 읽을 수 있어서 이를 이용해 별도로 출력해준다면 flag 부분도 새롭게 만들어줄 수 있습니다.

References

https://golang.org/pkg/flag https://golang.org/pkg/flag/#Flag https://gist.github.com/hahwul/b3870d58fd038afba05099ee9be86417 https://github.com/hahwul/gee/blob/main/main.go#L35