15 minute read

개요

커맨드 라인 인자(Command Line Arguments)는 프로그램 실행 시 전달되는 파라미터입니다.

주요 특징:

  • os.Args: 가장 기본적인 방법
  • flag 패키지: 표준 라이브러리의 플래그 파싱
  • cobra: 강력한 CLI 프레임워크 (kubectl, hugo 등에서 사용)
  • urfave/cli: 간결한 CLI 도구
  • 서브커맨드: git-style 명령어 구조
  • 자동 도움말: -h, –help 지원
  • 타입 안전: 문자열, 정수, 불린 등

os.Args 사용

1. 기본 사용법

package main

import (
    "fmt"
    "os"
)

func main() {
    // os.Args[0]: 프로그램 이름
    // os.Args[1:]: 인자들
    
    fmt.Println("Program:", os.Args[0])
    fmt.Println("Arguments:", os.Args[1:])
    fmt.Println("Count:", len(os.Args)-1)
    
    if len(os.Args) > 1 {
        fmt.Println("First argument:", os.Args[1])
    }
}

실행:

$ go run main.go hello world
Program: /tmp/go-build.../exe/main
Arguments: [hello world]
Count: 2
First argument: hello

2. 인자 파싱

func main() {
    if len(os.Args) < 3 {
        fmt.Println("Usage: program <name> <age>")
        os.Exit(1)
    }
    
    name := os.Args[1]
    age := os.Args[2]
    
    fmt.Printf("Name: %s, Age: %s\n", name, age)
}

실행:

$ go run main.go John 30
Name: John, Age: 30

3. 타입 변환

import "strconv"

func main() {
    if len(os.Args) < 3 {
        fmt.Println("Usage: program <x> <y>")
        os.Exit(1)
    }
    
    x, err := strconv.Atoi(os.Args[1])
    if err != nil {
        fmt.Println("Error: x must be an integer")
        os.Exit(1)
    }
    
    y, err := strconv.Atoi(os.Args[2])
    if err != nil {
        fmt.Println("Error: y must be an integer")
        os.Exit(1)
    }
    
    fmt.Printf("%d + %d = %d\n", x, y, x+y)
}

4. 플래그와 위치 인자 혼합

func main() {
    var verbose bool
    args := []string{}
    
    // 간단한 플래그 파싱
    for _, arg := range os.Args[1:] {
        if arg == "-v" || arg == "--verbose" {
            verbose = true
        } else {
            args = append(args, arg)
        }
    }
    
    if verbose {
        fmt.Println("Verbose mode enabled")
    }
    
    fmt.Println("Arguments:", args)
}

flag 패키지

1. 기본 사용법

import "flag"

func main() {
    // 플래그 정의
    var name string
    var age int
    var verbose bool
    
    flag.StringVar(&name, "name", "Guest", "Your name")
    flag.IntVar(&age, "age", 0, "Your age")
    flag.BoolVar(&verbose, "verbose", false, "Enable verbose output")
    
    // 짧은 형식
    flag.StringVar(&name, "n", "Guest", "Your name (shorthand)")
    
    // 플래그 파싱
    flag.Parse()
    
    fmt.Printf("Name: %s, Age: %d, Verbose: %v\n", name, age, verbose)
    
    // 남은 인자들
    fmt.Println("Remaining args:", flag.Args())
}

실행:

$ go run main.go -name John -age 30 -verbose file1 file2
Name: John, Age: 30, Verbose: true
Remaining args: [file1 file2]

$ go run main.go -name=John -age=30
Name: John, Age: 30, Verbose: false
Remaining args: []

$ go run main.go --help
Usage of main:
  -age int
        Your age
  -name string
        Your name (default "Guest")
  -verbose
        Enable verbose output

2. 다양한 플래그 타입

func main() {
    // 기본 타입
    stringFlag := flag.String("string", "default", "a string")
    intFlag := flag.Int("int", 0, "an int")
    boolFlag := flag.Bool("bool", false, "a bool")
    float64Flag := flag.Float64("float", 0.0, "a float64")
    durationFlag := flag.Duration("duration", 0, "a duration")
    
    flag.Parse()
    
    fmt.Println("String:", *stringFlag)
    fmt.Println("Int:", *intFlag)
    fmt.Println("Bool:", *boolFlag)
    fmt.Println("Float:", *float64Flag)
    fmt.Println("Duration:", *durationFlag)
}

실행:

$ go run main.go -string=hello -int=42 -bool -float=3.14 -duration=5s
String: hello
Int: 42
Bool: true
Float: 3.14
Duration: 5s

3. 커스텀 플래그 타입

type StringSlice []string

func (s *StringSlice) String() string {
    return strings.Join(*s, ",")
}

func (s *StringSlice) Set(value string) error {
    *s = append(*s, value)
    return nil
}

func main() {
    var tags StringSlice
    flag.Var(&tags, "tag", "Tag to add (can be repeated)")
    
    flag.Parse()
    
    fmt.Println("Tags:", tags)
}

실행:

$ go run main.go -tag=go -tag=cli -tag=tutorial
Tags: [go cli tutorial]

4. FlagSet (서브커맨드)

func main() {
    // 서브커맨드별 FlagSet
    addCmd := flag.NewFlagSet("add", flag.ExitOnError)
    addName := addCmd.String("name", "", "Name to add")
    
    listCmd := flag.NewFlagSet("list", flag.ExitOnError)
    listVerbose := listCmd.Bool("verbose", false, "Verbose listing")
    
    if len(os.Args) < 2 {
        fmt.Println("expected 'add' or 'list' subcommands")
        os.Exit(1)
    }
    
    switch os.Args[1] {
    case "add":
        addCmd.Parse(os.Args[2:])
        fmt.Println("Adding:", *addName)
        
    case "list":
        listCmd.Parse(os.Args[2:])
        fmt.Println("Listing, verbose:", *listVerbose)
        
    default:
        fmt.Println("expected 'add' or 'list' subcommands")
        os.Exit(1)
    }
}

실행:

$ go run main.go add -name=item1
Adding: item1

$ go run main.go list -verbose
Listing, verbose: true

5. 커스텀 Usage

func main() {
    var name string
    var age int
    
    flag.StringVar(&name, "name", "", "Your name")
    flag.IntVar(&age, "age", 0, "Your age")
    
    // 커스텀 사용법 메시지
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n", os.Args[0])
        fmt.Fprintf(os.Stderr, "\nA simple greeting program\n\n")
        fmt.Fprintf(os.Stderr, "OPTIONS:\n")
        flag.PrintDefaults()
        fmt.Fprintf(os.Stderr, "\nEXAMPLES:\n")
        fmt.Fprintf(os.Stderr, "  %s -name John -age 30\n", os.Args[0])
    }
    
    flag.Parse()
    
    if name == "" {
        flag.Usage()
        os.Exit(1)
    }
    
    fmt.Printf("Hello, %s (%d years old)!\n", name, age)
}

Cobra 프레임워크

go get -u github.com/spf13/cobra@latest

1. 기본 구조

package main

import (
    "fmt"
    "github.com/spf13/cobra"
    "os"
)

func main() {
    var rootCmd = &cobra.Command{
        Use:   "myapp",
        Short: "A brief description of your application",
        Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application.`,
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("Hello from myapp!")
        },
    }
    
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

2. 플래그 추가

func main() {
    var verbose bool
    var output string
    var count int
    
    var rootCmd = &cobra.Command{
        Use:   "myapp",
        Short: "My application",
        Run: func(cmd *cobra.Command, args []string) {
            if verbose {
                fmt.Println("Verbose mode enabled")
            }
            fmt.Printf("Output: %s, Count: %d\n", output, count)
            fmt.Println("Args:", args)
        },
    }
    
    // Persistent flags (모든 서브커맨드에 적용)
    rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
    
    // Local flags (현재 커맨드만)
    rootCmd.Flags().StringVarP(&output, "output", "o", "stdout", "output destination")
    rootCmd.Flags().IntVarP(&count, "count", "c", 1, "repeat count")
    
    rootCmd.Execute()
}

실행:

$ go run main.go -v -o file.txt -c 3 arg1 arg2
Verbose mode enabled
Output: file.txt, Count: 3
Args: [arg1 arg2]

3. 서브커맨드

func main() {
    var rootCmd = &cobra.Command{
        Use:   "myapp",
        Short: "My application",
    }
    
    // 'add' 서브커맨드
    var addCmd = &cobra.Command{
        Use:   "add [name]",
        Short: "Add a new item",
        Args:  cobra.ExactArgs(1),
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("Adding:", args[0])
        },
    }
    
    // 'list' 서브커맨드
    var listCmd = &cobra.Command{
        Use:   "list",
        Short: "List all items",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("Listing all items...")
        },
    }
    
    // 'delete' 서브커맨드
    var force bool
    var deleteCmd = &cobra.Command{
        Use:   "delete [name]",
        Short: "Delete an item",
        Args:  cobra.ExactArgs(1),
        Run: func(cmd *cobra.Command, args []string) {
            if force {
                fmt.Println("Force deleting:", args[0])
            } else {
                fmt.Println("Deleting:", args[0])
            }
        },
    }
    deleteCmd.Flags().BoolVarP(&force, "force", "f", false, "force deletion")
    
    rootCmd.AddCommand(addCmd, listCmd, deleteCmd)
    rootCmd.Execute()
}

실행:

$ go run main.go add item1
Adding: item1

$ go run main.go list
Listing all items...

$ go run main.go delete -f item1
Force deleting: item1

4. PreRun/PostRun Hooks

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "My application",
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
        fmt.Println("PersistentPreRun: Before any command")
    },
    PreRun: func(cmd *cobra.Command, args []string) {
        fmt.Println("PreRun: Before root command")
    },
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Run: Executing root command")
    },
    PostRun: func(cmd *cobra.Command, args []string) {
        fmt.Println("PostRun: After root command")
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
        fmt.Println("PersistentPostRun: After any command")
    },
}

5. 인자 검증

var cmd = &cobra.Command{
    Use:   "greet [name]",
    Short: "Greet someone",
    Args:  cobra.ExactArgs(1), // 정확히 1개 인자
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Hello,", args[0])
    },
}

// 다른 검증 옵션들:
// cobra.NoArgs           - 인자 없음
// cobra.MinimumNArgs(n)  - 최소 n개
// cobra.MaximumNArgs(n)  - 최대 n개
// cobra.RangeArgs(min, max) - min~max개
// cobra.ArbitraryArgs    - 임의 개수

// 커스텀 검증
var customCmd = &cobra.Command{
    Use: "custom",
    Args: func(cmd *cobra.Command, args []string) error {
        if len(args) < 1 {
            return errors.New("requires at least one arg")
        }
        if !isValidName(args[0]) {
            return fmt.Errorf("invalid name: %s", args[0])
        }
        return nil
    },
    Run: func(cmd *cobra.Command, args []string) {
        // ...
    },
}

6. 필수 플래그

var cmd = &cobra.Command{
    Use: "login",
    Run: func(cmd *cobra.Command, args []string) {
        username, _ := cmd.Flags().GetString("username")
        password, _ := cmd.Flags().GetString("password")
        fmt.Printf("Logging in as %s\n", username)
    },
}

cmd.Flags().String("username", "", "username (required)")
cmd.Flags().String("password", "", "password (required)")

// 필수로 지정
cmd.MarkFlagRequired("username")
cmd.MarkFlagRequired("password")

7. Cobra Generator

# Cobra CLI 설치
go install github.com/spf13/cobra-cli@latest

# 새 프로젝트 초기화
cobra-cli init

# 커맨드 추가
cobra-cli add serve
cobra-cli add config
cobra-cli add create -p configCmd

생성된 구조:

.
├── cmd/
│   ├── root.go
│   ├── serve.go
│   ├── config.go
│   └── create.go
└── main.go

urfave/cli

go get github.com/urfave/cli/v2

1. 기본 사용법

import (
    "github.com/urfave/cli/v2"
)

func main() {
    app := &cli.App{
        Name:  "myapp",
        Usage: "A simple CLI application",
        Action: func(c *cli.Context) error {
            fmt.Println("Hello from myapp!")
            return nil
        },
    }
    
    if err := app.Run(os.Args); err != nil {
        log.Fatal(err)
    }
}

2. 플래그

func main() {
    app := &cli.App{
        Name: "myapp",
        Flags: []cli.Flag{
            &cli.StringFlag{
                Name:    "name",
                Aliases: []string{"n"},
                Value:   "Guest",
                Usage:   "your name",
            },
            &cli.IntFlag{
                Name:    "age",
                Aliases: []string{"a"},
                Value:   0,
                Usage:   "your age",
            },
            &cli.BoolFlag{
                Name:    "verbose",
                Aliases: []string{"v"},
                Usage:   "enable verbose output",
            },
        },
        Action: func(c *cli.Context) error {
            name := c.String("name")
            age := c.Int("age")
            verbose := c.Bool("verbose")
            
            if verbose {
                fmt.Println("Verbose mode enabled")
            }
            
            fmt.Printf("Hello, %s (%d years old)!\n", name, age)
            return nil
        },
    }
    
    app.Run(os.Args)
}

3. 서브커맨드

func main() {
    app := &cli.App{
        Name: "myapp",
        Commands: []*cli.Command{
            {
                Name:    "add",
                Aliases: []string{"a"},
                Usage:   "add a new item",
                Action: func(c *cli.Context) error {
                    if c.NArg() < 1 {
                        return cli.Exit("requires item name", 1)
                    }
                    fmt.Println("Adding:", c.Args().First())
                    return nil
                },
            },
            {
                Name:    "list",
                Aliases: []string{"l"},
                Usage:   "list all items",
                Flags: []cli.Flag{
                    &cli.BoolFlag{
                        Name:  "verbose",
                        Usage: "show detailed information",
                    },
                },
                Action: func(c *cli.Context) error {
                    verbose := c.Bool("verbose")
                    if verbose {
                        fmt.Println("Listing items (verbose)...")
                    } else {
                        fmt.Println("Listing items...")
                    }
                    return nil
                },
            },
            {
                Name:  "delete",
                Usage: "delete an item",
                Flags: []cli.Flag{
                    &cli.BoolFlag{
                        Name:    "force",
                        Aliases: []string{"f"},
                        Usage:   "force deletion without confirmation",
                    },
                },
                Action: func(c *cli.Context) error {
                    if c.NArg() < 1 {
                        return cli.Exit("requires item name", 1)
                    }
                    
                    force := c.Bool("force")
                    item := c.Args().First()
                    
                    if force {
                        fmt.Printf("Force deleting: %s\n", item)
                    } else {
                        fmt.Printf("Deleting: %s\n", item)
                    }
                    return nil
                },
            },
        },
    }
    
    app.Run(os.Args)
}

4. Before/After Hooks

app := &cli.App{
    Name: "myapp",
    Before: func(c *cli.Context) error {
        fmt.Println("Before: Setting up...")
        return nil
    },
    After: func(c *cli.Context) error {
        fmt.Println("After: Cleaning up...")
        return nil
    },
    Action: func(c *cli.Context) error {
        fmt.Println("Action: Running command...")
        return nil
    },
}

5. 환경 변수 통합

app := &cli.App{
    Flags: []cli.Flag{
        &cli.StringFlag{
            Name:    "config",
            Aliases: []string{"c"},
            Value:   "config.yaml",
            Usage:   "config file path",
            EnvVars: []string{"MYAPP_CONFIG"}, // 환경 변수
        },
        &cli.IntFlag{
            Name:    "port",
            Value:   8080,
            Usage:   "server port",
            EnvVars: []string{"MYAPP_PORT", "PORT"},
        },
    },
    Action: func(c *cli.Context) error {
        config := c.String("config")
        port := c.Int("port")
        fmt.Printf("Config: %s, Port: %d\n", config, port)
        return nil
    },
}

실행:

$ export MYAPP_PORT=9000
$ go run main.go
Config: config.yaml, Port: 9000

$ go run main.go --port 3000
Config: config.yaml, Port: 3000

실전 예제

1. 파일 처리 도구

package main

import (
    "fmt"
    "io"
    "os"
    "github.com/spf13/cobra"
)

func main() {
    var inputFile string
    var outputFile string
    var verbose bool
    
    var rootCmd = &cobra.Command{
        Use:   "fileutil",
        Short: "File utility tool",
    }
    
    var copyCmd = &cobra.Command{
        Use:   "copy",
        Short: "Copy file",
        Run: func(cmd *cobra.Command, args []string) {
            if verbose {
                fmt.Printf("Copying %s to %s\n", inputFile, outputFile)
            }
            
            if err := copyFile(inputFile, outputFile); err != nil {
                fmt.Println("Error:", err)
                os.Exit(1)
            }
            
            fmt.Println("File copied successfully")
        },
    }
    
    copyCmd.Flags().StringVarP(&inputFile, "input", "i", "", "input file (required)")
    copyCmd.Flags().StringVarP(&outputFile, "output", "o", "", "output file (required)")
    copyCmd.MarkFlagRequired("input")
    copyCmd.MarkFlagRequired("output")
    
    rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
    rootCmd.AddCommand(copyCmd)
    
    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

func copyFile(src, dst string) error {
    sourceFile, err := os.Open(src)
    if err != nil {
        return err
    }
    defer sourceFile.Close()
    
    destFile, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer destFile.Close()
    
    _, err = io.Copy(destFile, sourceFile)
    return err
}

2. HTTP 서버 CLI

func main() {
    var port int
    var host string
    var debug bool
    
    var rootCmd = &cobra.Command{
        Use:   "server",
        Short: "HTTP server",
    }
    
    var serveCmd = &cobra.Command{
        Use:   "serve [directory]",
        Short: "Start HTTP server",
        Args:  cobra.MaximumNArgs(1),
        Run: func(cmd *cobra.Command, args []string) {
            dir := "."
            if len(args) > 0 {
                dir = args[0]
            }
            
            addr := fmt.Sprintf("%s:%d", host, port)
            
            if debug {
                fmt.Printf("Debug mode enabled\n")
            }
            
            fmt.Printf("Serving %s on http://%s\n", dir, addr)
            
            http.Handle("/", http.FileServer(http.Dir(dir)))
            if err := http.ListenAndServe(addr, nil); err != nil {
                fmt.Println("Error:", err)
                os.Exit(1)
            }
        },
    }
    
    serveCmd.Flags().IntVarP(&port, "port", "p", 8080, "server port")
    serveCmd.Flags().StringVarP(&host, "host", "H", "0.0.0.0", "server host")
    rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "debug mode")
    
    rootCmd.AddCommand(serveCmd)
    rootCmd.Execute()
}

실행:

$ go run main.go serve -p 3000 ./public
Serving ./public on http://0.0.0.0:3000

3. 데이터베이스 마이그레이션 도구

func main() {
    var dbURL string
    var migrationsPath string
    
    app := &cli.App{
        Name: "migrate",
        Flags: []cli.Flag{
            &cli.StringFlag{
                Name:     "database-url",
                Aliases:  []string{"d"},
                Required: true,
                EnvVars:  []string{"DATABASE_URL"},
                Usage:    "database connection URL",
            },
            &cli.StringFlag{
                Name:    "migrations",
                Aliases: []string{"m"},
                Value:   "./migrations",
                Usage:   "migrations directory",
            },
        },
        Commands: []*cli.Command{
            {
                Name:  "up",
                Usage: "run all up migrations",
                Action: func(c *cli.Context) error {
                    dbURL = c.String("database-url")
                    migrationsPath = c.String("migrations")
                    
                    fmt.Printf("Running migrations from %s\n", migrationsPath)
                    fmt.Printf("Database: %s\n", maskDBURL(dbURL))
                    
                    // 실제 마이그레이션 로직
                    return runMigrations(dbURL, migrationsPath, "up")
                },
            },
            {
                Name:  "down",
                Usage: "rollback one migration",
                Action: func(c *cli.Context) error {
                    dbURL = c.String("database-url")
                    migrationsPath = c.String("migrations")
                    
                    fmt.Println("Rolling back last migration...")
                    return runMigrations(dbURL, migrationsPath, "down")
                },
            },
            {
                Name:  "status",
                Usage: "show migration status",
                Action: func(c *cli.Context) error {
                    dbURL = c.String("database-url")
                    fmt.Println("Checking migration status...")
                    return checkMigrationStatus(dbURL)
                },
            },
        },
    }
    
    app.Run(os.Args)
}

func maskDBURL(url string) string {
    // postgres://user:password@host/db -> postgres://user:***@host/db
    re := regexp.MustCompile(`://([^:]+):([^@]+)@`)
    return re.ReplaceAllString(url, "://$1:***@")
}

4. Git-style CLI

func main() {
    var rootCmd = &cobra.Command{
        Use:   "git",
        Short: "Git-like version control",
    }
    
    // git init
    var initCmd = &cobra.Command{
        Use:   "init [directory]",
        Short: "Initialize a repository",
        Args:  cobra.MaximumNArgs(1),
        Run: func(cmd *cobra.Command, args []string) {
            dir := "."
            if len(args) > 0 {
                dir = args[0]
            }
            fmt.Printf("Initialized repository in %s\n", dir)
        },
    }
    
    // git add
    var addCmd = &cobra.Command{
        Use:   "add [files...]",
        Short: "Add files to staging",
        Args:  cobra.MinimumNArgs(1),
        Run: func(cmd *cobra.Command, args []string) {
            for _, file := range args {
                fmt.Printf("Adding %s\n", file)
            }
        },
    }
    
    // git commit
    var message string
    var commitCmd = &cobra.Command{
        Use:   "commit",
        Short: "Commit changes",
        Run: func(cmd *cobra.Command, args []string) {
            if message == "" {
                fmt.Println("Error: commit message required")
                os.Exit(1)
            }
            fmt.Printf("Committed: %s\n", message)
        },
    }
    commitCmd.Flags().StringVarP(&message, "message", "m", "", "commit message")
    commitCmd.MarkFlagRequired("message")
    
    // git log
    var limit int
    var logCmd = &cobra.Command{
        Use:   "log",
        Short: "Show commit logs",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Printf("Showing last %d commits\n", limit)
        },
    }
    logCmd.Flags().IntVarP(&limit, "limit", "n", 10, "number of commits")
    
    rootCmd.AddCommand(initCmd, addCmd, commitCmd, logCmd)
    rootCmd.Execute()
}

실행:

$ go run main.go init myrepo
Initialized repository in myrepo

$ go run main.go add file1.go file2.go
Adding file1.go
Adding file2.go

$ go run main.go commit -m "Initial commit"
Committed: Initial commit

$ go run main.go log -n 5
Showing last 5 commits

설정 우선순위

1. 우선순위 체계

type Config struct {
    Port    int
    Host    string
    Verbose bool
}

func LoadConfig(cmd *cobra.Command) *Config {
    cfg := &Config{}
    
    // 1. 기본값
    cfg.Port = 8080
    cfg.Host = "localhost"
    cfg.Verbose = false
    
    // 2. 설정 파일
    if configFile := viper.GetString("config"); configFile != "" {
        viper.SetConfigFile(configFile)
        if err := viper.ReadInConfig(); err == nil {
            cfg.Port = viper.GetInt("port")
            cfg.Host = viper.GetString("host")
            cfg.Verbose = viper.GetBool("verbose")
        }
    }
    
    // 3. 환경 변수
    if port := os.Getenv("APP_PORT"); port != "" {
        cfg.Port, _ = strconv.Atoi(port)
    }
    if host := os.Getenv("APP_HOST"); host != "" {
        cfg.Host = host
    }
    
    // 4. 커맨드 라인 플래그 (최우선)
    if cmd.Flags().Changed("port") {
        cfg.Port, _ = cmd.Flags().GetInt("port")
    }
    if cmd.Flags().Changed("host") {
        cfg.Host, _ = cmd.Flags().GetString("host")
    }
    if cmd.Flags().Changed("verbose") {
        cfg.Verbose, _ = cmd.Flags().GetBool("verbose")
    }
    
    return cfg
}

우선순위:

  1. 커맨드 라인 플래그 (최우선)
  2. 환경 변수
  3. 설정 파일
  4. 기본값

2. Viper 통합

import (
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

func main() {
    var cfgFile string
    
    var rootCmd = &cobra.Command{
        Use: "myapp",
        PersistentPreRun: func(cmd *cobra.Command, args []string) {
            // Viper 초기화
            if cfgFile != "" {
                viper.SetConfigFile(cfgFile)
            } else {
                viper.AddConfigPath(".")
                viper.SetConfigName("config")
            }
            
            viper.AutomaticEnv()
            viper.SetEnvPrefix("MYAPP")
            
            if err := viper.ReadInConfig(); err == nil {
                fmt.Println("Using config file:", viper.ConfigFileUsed())
            }
            
            // 플래그를 Viper에 바인딩
            viper.BindPFlags(cmd.Flags())
        },
        Run: func(cmd *cobra.Command, args []string) {
            port := viper.GetInt("port")
            host := viper.GetString("host")
            fmt.Printf("Host: %s, Port: %d\n", host, port)
        },
    }
    
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
    rootCmd.Flags().Int("port", 8080, "server port")
    rootCmd.Flags().String("host", "localhost", "server host")
    
    rootCmd.Execute()
}

일반적인 실수

1. 인자 검증 누락

// ❌ 나쁜 예
func main() {
    name := os.Args[1] // 패닉 가능!
    fmt.Println(name)
}

// ✅ 좋은 예
func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: program <name>")
        os.Exit(1)
    }
    name := os.Args[1]
    fmt.Println(name)
}

2. 타입 변환 에러 무시

// ❌ 나쁜 예
age, _ := strconv.Atoi(os.Args[1])

// ✅ 좋은 예
age, err := strconv.Atoi(os.Args[1])
if err != nil {
    fmt.Println("Error: age must be an integer")
    os.Exit(1)
}

3. 플래그 파싱 전 접근

// ❌ 나쁜 예
var name string
flag.StringVar(&name, "name", "Guest", "name")
fmt.Println(name) // 항상 "Guest"
flag.Parse()

// ✅ 좋은 예
var name string
flag.StringVar(&name, "name", "Guest", "name")
flag.Parse() // 먼저 파싱
fmt.Println(name)

4. 도움말 메시지 부족

// ❌ 나쁜 예
flag.String("config", "", "")

// ✅ 좋은 예
flag.String("config", "config.yaml", "path to configuration file")

5. 서브커맨드 없는 복잡한 도구

// ❌ 나쁜 예: 플래그만으로 모든 기능
// program --add --delete --list

// ✅ 좋은 예: 서브커맨드 사용
// program add
// program delete
// program list

6. 에러 처리 누락

// ❌ 나쁜 예
app.Run(os.Args)

// ✅ 좋은 예
if err := app.Run(os.Args); err != nil {
    log.Fatal(err)
}

7. 불명확한 플래그 이름

// ❌ 나쁜 예
flag.Bool("v", false, "")
flag.Bool("d", false, "")

// ✅ 좋은 예
flag.BoolVar(&verbose, "verbose", false, "enable verbose output")
flag.BoolVar(&verbose, "v", false, "enable verbose output (shorthand)")
flag.BoolVar(&debug, "debug", false, "enable debug mode")

베스트 프랙티스

1. 명확한 명명

// ✅ 명확한 커맨드/플래그 이름
cobra.Command{
    Use:   "user create",
    Short: "Create a new user",
    Flags: []cli.Flag{
        &cli.StringFlag{
            Name:  "username",
            Usage: "username for the new user",
        },
        &cli.StringFlag{
            Name:  "email",
            Usage: "email address",
        },
    },
}

2. 버전 정보 제공

var version = "1.0.0"
var buildDate = "2024-01-01"

var rootCmd = &cobra.Command{
    Use:     "myapp",
    Short:   "My application",
    Version: version,
}

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Print version information",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Version: %s\n", version)
        fmt.Printf("Build Date: %s\n", buildDate)
    },
}

3. 풍부한 도움말

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "A brief description",
    Long: `A longer description that explains what the application does,
how to use it, and provides examples.

Examples:
  myapp serve --port 8080
  myapp migrate up
  myapp user create --username john`,
}

4. 환경 변수 통합

app := &cli.App{
    Flags: []cli.Flag{
        &cli.StringFlag{
            Name:    "api-key",
            Usage:   "API key for authentication",
            EnvVars: []string{"API_KEY", "MYAPP_API_KEY"},
        },
    },
}

5. 설정 파일 지원

var rootCmd = &cobra.Command{
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
        if cfgFile != "" {
            viper.SetConfigFile(cfgFile)
            viper.ReadInConfig()
        }
    },
}

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")

6. 진행 상황 표시

import "github.com/cheggaaa/pb/v3"

func processFiles(files []string) {
    bar := pb.StartNew(len(files))
    
    for _, file := range files {
        processFile(file)
        bar.Increment()
    }
    
    bar.Finish()
}

7. 컬러 출력

import "github.com/fatih/color"

func main() {
    color.Green("Success: Operation completed")
    color.Red("Error: Something went wrong")
    color.Yellow("Warning: This is deprecated")
    
    // 조건부 컬러 (터미널 감지)
    if isTerminal() {
        color.Blue("Info: Processing...")
    } else {
        fmt.Println("Info: Processing...")
    }
}

8. 대화형 프롬프트

import "github.com/manifoldco/promptui"

func confirmDelete() bool {
    prompt := promptui.Prompt{
        Label:     "Are you sure you want to delete",
        IsConfirm: true,
    }
    
    _, err := prompt.Run()
    return err == nil
}

func selectOption() string {
    prompt := promptui.Select{
        Label: "Select environment",
        Items: []string{"development", "staging", "production"},
    }
    
    _, result, _ := prompt.Run()
    return result
}

정리

  • os.Args: 가장 기본적인 방법, 간단한 도구에 적합
  • flag 패키지: 표준 라이브러리, 플래그 파싱, POSIX 스타일
  • cobra: 강력한 CLI 프레임워크, 서브커맨드, kubectl/hugo 스타일
  • urfave/cli: 간결한 API, 빠른 개발
  • 서브커맨드: git-style 명령어 구조
  • 플래그 타입: string, int, bool, duration 등
  • 환경 변수: 플래그와 통합 가능
  • 우선순위: CLI > 환경 변수 > 설정 파일 > 기본값
  • 실수: 검증 누락, 에러 무시, 파싱 순서, 도움말 부족
  • 베스트: 명확한 명명, 버전 정보, 도움말, 환경 변수 통합, 진행 표시
  • 원칙: 사용자 친화적, 명확한 에러 메시지, UNIX 철학