CLI 도구 개발 — cobra & flag
Go는 CLI 도구 개발에 최적화된 언어입니다. 단일 바이너리 배포, 빠른 시작 시간, 크로스 컴파일 지원으로 DevOps 도구에 널리 사용됩니다.
표준 라이브러리 flag 패키지
간단한 CLI에는 표준 flag 패키지로 충분합니다.
// main.go
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// 플래그 정의
host := flag.String("host", "localhost", "서버 호스트")
port := flag.Int("port", 8080, "서버 포트")
verbose := flag.Bool("verbose", false, "상세 출력")
timeout := flag.Duration("timeout", 30*time.Second, "연결 타임아웃")
// 커스텀 사용법 메시지
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "사용법: %s [옵션]\n\n옵션:\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
// 파싱 후 남은 인자 (비플래그 인자)
args := flag.Args()
if *verbose {
fmt.Printf("서버: %s:%d, 타임아웃: %s\n", *host, *port, *timeout)
}
fmt.Printf("남은 인자: %v\n", args)
}
go run main.go -host=example.com -port=9090 -verbose file1.txt file2.txt
# 서버: example.com:9090, 타임아웃: 30s
# 남은 인자: [file1.txt file2.txt]
cobra — 프로덕션 CLI 프레임워크
go get github.com/spf13/cobra@latest
go install github.com/spf13/cobra-cli@latest # 코드 생성기
프로젝트 구조
mycli/
├── cmd/
│ ├── root.go ← 루트 커맨드
│ ├── serve.go ← serve 서브커맨드
│ ├── user.go ← user 서브커맨드 그룹
│ └── user_list.go ← user list 서브커맨드
├── internal/
│ └── config/
│ └── config.go
└── main.go
루트 커맨드 (root.go)
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
verbose bool
)
var rootCmd = &cobra.Command{
Use: "mycli",
Short: "마이 CLI — 멋진 도구",
Long: `mycli는 서버 관리와 배포를 자동화하는
완전한 기능을 갖춘 CLI 도구입니다.`,
// 모든 서브커맨드 실행 전 호출
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initConfig()
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
// 영구 플래그: 루트 + 모든 서브커맨드에 적용
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "설정 파일 경로 (기본: $HOME/.mycli.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "상세 출력")
// 로컬 플래그: 이 커맨드에만 적용
rootCmd.Flags().BoolP("version", "V", false, "버전 출력")
}
func initConfig() error {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, _ := os.UserHomeDir()
viper.AddConfigPath(home)
viper.SetConfigName(".mycli")
}
viper.AutomaticEnv() // 환경변수 자동 바인딩
return viper.ReadInConfig()
}
서브커맨드 (serve.go)
package cmd
import (
"fmt"
"net/http"
"github.com/spf13/cobra"
)
var (
servePort int
serveTLS bool
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "HTTP 서버 시작",
Long: `지정한 포트에서 HTTP 서버를 시작합니다.`,
Example: ` mycli serve
mycli serve --port 9090
mycli serve --port 443 --tls`,
RunE: func(cmd *cobra.Command, args []string) error {
addr := fmt.Sprintf(":%d", servePort)
fmt.Printf("서버 시작: %s (TLS: %v)\n", addr, serveTLS)
return http.ListenAndServe(addr, nil)
},
}
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().IntVarP(&servePort, "port", "p", 8080, "포트 번호")
serveCmd.Flags().BoolVar(&serveTLS, "tls", false, "TLS 활성화")
// 필수 플래그
// serveCmd.MarkFlagRequired("port")
}
중첩 서브커맨드 (user.go)
package cmd
import "github.com/spf13/cobra"
// user 커맨드 그룹
var userCmd = &cobra.Command{
Use: "user",
Short: "사용자 관리",
Long: "사용자 생성, 조회, 삭제 명령어",
}
var userListCmd = &cobra.Command{
Use: "list",
Short: "사용자 목록 조회",
RunE: func(cmd *cobra.Command, args []string) error {
format, _ := cmd.Flags().GetString("format")
limit, _ := cmd.Flags().GetInt("limit")
return listUsers(format, limit)
},
}
var userCreateCmd = &cobra.Command{
Use: "create <name> <email>",
Short: "사용자 생성",
Args: cobra.ExactArgs(2), // 정확히 2개 인자 필요
RunE: func(cmd *cobra.Command, args []string) error {
name, email := args[0], args[1]
return createUser(name, email)
},
}
func init() {
rootCmd.AddCommand(userCmd)
userCmd.AddCommand(userListCmd)
userCmd.AddCommand(userCreateCmd)
userListCmd.Flags().String("format", "table", "출력 형식 (table|json|yaml)")
userListCmd.Flags().Int("limit", 20, "최대 출력 수")
}
main.go
package main
import "mycli/cmd"
func main() {
cmd.Execute()
}
출력 포맷터 구현
package cmd
import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func printUsers(users []User, format string) error {
switch format {
case "json":
return json.NewEncoder(os.Stdout).Encode(users)
case "table":
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tEMAIL")
fmt.Fprintln(w, "--\t----\t-----")
for _, u := range users {
fmt.Fprintf(w, "%d\t%s\t%s\n", u.ID, u.Name, u.Email)
}
return w.Flush()
default:
return fmt.Errorf("알 수 없는 형식: %s", format)
}
}
진행 표시기 & 컬러 출력
go get github.com/fatih/color
go get github.com/schollz/progressbar/v3
import (
"github.com/fatih/color"
"github.com/schollz/progressbar/v3"
)
func deployWithProgress(items []string) {
// 컬러 출력
green := color.New(color.FgGreen, color.Bold)
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
// 진행 바
bar := progressbar.NewOptions(len(items),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionSetDescription("[cyan]배포 중...[reset]"),
progressbar.OptionShowCount(),
)
for _, item := range items {
if err := deploy(item); err != nil {
red.Printf("✗ %s: %v\n", item, err)
} else {
green.Printf("✓ %s\n", item)
}
bar.Add(1)
}
yellow.Println("\n배포 완료!")
}
크로스 컴파일
# macOS에서 리눅스/윈도우 바이너리 빌드
GOOS=linux GOARCH=amd64 go build -o dist/mycli-linux-amd64 .
GOOS=windows GOARCH=amd64 go build -o dist/mycli-windows-amd64.exe .
GOOS=darwin GOARCH=arm64 go build -o dist/mycli-darwin-arm64 .
# ldflags로 바이너리 경량화 및 버전 정보 삽입
VERSION=$(git describe --tags --always)
go build -ldflags="-s -w -X main.version=${VERSION}" -o mycli .
// main.go
var version = "dev" // -ldflags로 주입
var versionCmd = &cobra.Command{
Use: "version",
Short: "버전 출력",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("mycli %s\n", version)
},
}
설정 파일 연동 — viper
import "github.com/spf13/viper"
// 설정 계층 (우선순위: 높은 것이 이김)
// 1. 명령줄 플래그
// 2. 환경변수 (MYCLI_PORT)
// 3. 설정 파일 (.mycli.yaml)
// 4. 기본값
viper.SetDefault("server.port", 8080)
viper.SetEnvPrefix("MYCLI") // MYCLI_SERVER_PORT
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// 플래그와 viper 연동
rootCmd.PersistentFlags().Int("port", 8080, "포트")
viper.BindPFlag("server.port", rootCmd.PersistentFlags().Lookup("port"))
// 설정값 읽기
port := viper.GetInt("server.port")
핵심 정리
| 도구 | 용도 |
|---|---|
flag | 단순 플래그, 외부 의존성 없음 |
cobra | 서브커맨드, 자동 help, 자동 완성 |
viper | 설정 파일 + 환경변수 + 플래그 통합 |
tabwriter | 정렬된 테이블 출력 |
fatih/color | 컬러 터미널 출력 |
- cobra + viper는 같은 팀이 만들어 통합이 자연스럽다
RunE를 사용하면 에러 반환이 가능해Run보다 권장PersistentPreRunE로 인증/설정 초기화를 모든 커맨드 앞에 공통 적용