GO LANG // CLI TOOLSD::03 ORTA
22m READCOMPLETION: 88%ID::GO-101

GO İLE CLI ARAÇ GELİŞTİRME: COBRA

Cobra framework ile profesyonel komut satırı araçları

Go dili, sadeliği, performansı ve güçlü standart kütüphanesiyle CLI (Command Line Interface) araçları geliştirmek için birinci tercih haline geldi. Docker, Kubernetes ve GitHub CLI gibi endüstri standardı araçların tamamı Go ile yazılmış. Bu derste Cobra framework'üyle sıfırdan profesyonel bir CLI uygulaması inşa edeceksin.

Neden Go ile CLI?

// PLAINTEXT //
Go CLI Avantajları:
✓ Tek binary — kurulum gerekmez, kopyala-çalıştır
✓ Çapraz derleme — linux/windows/mac tek komutla
✓ Düşük bellek — Python/Node'dan 10x verimli
✓ Hızlı başlangıç — 5ms vs Python'un 150ms'si
✓ Cobra + Viper — endüstri standardı CLI çatısı

Proje Kurulumu

// BASH //
mkdir codeforge-cli && cd codeforge-cli
go mod init github.com/kullanici/codeforge-cli
 
# Cobra bağımlılığı
go get github.com/spf13/cobra@latest
go get github.com/spf13/viper@latest

Cobra ile Temel Yapı

Cobra, komutları hiyerarşik olarak organize eder. Her cobra.Command birbirinden bağımsız, test edilebilir bir birimdir.

// GO //
// cmd/root.go
package cmd
 
import (
    "fmt"
    "os"
 
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)
 
var (
    cfgFile string
    verbose bool
)
 
// rootCmd — tüm alt komutların üstündeki ana komut
var rootCmd = &cobra.Command{
    Use:   "codeforge",
    Short: "CodeForge CLI — geliştirici araç seti",
    Long: `CodeForge CLI, proje iskeletleri oluşturmak,
kod şablonları üretmek ve geliştirme iş akışını
hızlandırmak için tasarlanmış bir araçtır.`,
}
 
func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
 
func init() {
    cobra.OnInitialize(initConfig)
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config dosyası (varsayılan: $HOME/.codeforge.yaml)")
    rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "ayrıntılı çıktı")
}
 
func initConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        cobra.CheckErr(err)
        viper.AddConfigPath(home)
        viper.SetConfigType("yaml")
        viper.SetConfigName(".codeforge")
    }
    viper.AutomaticEnv()
    viper.ReadInConfig()
}

Alt Komut Oluşturma: generate

// GO //
// cmd/generate.go
package cmd
 
import (
    "fmt"
    "os"
    "strings"
    "text/template"
 
    "github.com/spf13/cobra"
)
 
var (
    projectName string
    projectType string
    withDocker  bool
)
 
var generateCmd = &cobra.Command{
    Use:     "generate [proje-adı]",
    Short:   "Yeni proje iskeleti oluştur",
    Long:    "Seçilen şablona göre proje yapısını ve dosyalarını oluşturur.",
    Aliases: []string{"gen", "g"},
    Args:    cobra.ExactArgs(1), // tam olarak 1 argüman zorunlu
    RunE:    runGenerate,        // hata döndüren versiyonu kullan
}
 
func init() {
    rootCmd.AddCommand(generateCmd)
 
    generateCmd.Flags().StringVarP(&projectType, "type", "t", "api", "proje tipi: api|web|cli")
    generateCmd.Flags().BoolVar(&withDocker, "docker", false, "Dockerfile ve compose ekle")
 
    // Tamamlama fonksiyonu — tab ile öneri
    generateCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
        return []string{"api", "web", "cli"}, cobra.ShellCompDirectiveNoFileComp
    })
}
 
func runGenerate(cmd *cobra.Command, args []string) error {
    projectName = args[0]
 
    // Proje adı doğrulama
    if strings.ContainsAny(projectName, " /\\") {
        return fmt.Errorf("geçersiz proje adı: %q — boşluk ve / \\ karakterleri kullanılamaz", projectName)
    }
 
    fmt.Printf("🔨 Proje oluşturuluyor: %s (%s)\n", projectName, projectType)
 
    if err := createProjectStructure(projectName, projectType); err != nil {
        return fmt.Errorf("proje yapısı oluşturulamadı: %w", err)
    }
 
    if withDocker {
        if err := createDockerFiles(projectName); err != nil {
            return fmt.Errorf("Docker dosyaları oluşturulamadı: %w", err)
        }
        fmt.Println("🐳 Docker dosyaları eklendi")
    }
 
    fmt.Printf("\n✅ Proje hazır: ./%s\n", projectName)
    fmt.Printf("   cd %s && go run main.go\n", projectName)
    return nil
}
 
// Şablon tabanlı dosya oluşturma
var apiMainTmpl = template.Must(template.New("main").Parse(`package main
 
import (
    "log"
    "net/http"
)
 
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(` + "`" + `{"status":"ok","app":"{{.Name}}"}` + "`" + `))
    })
 
    log.Println("{{.Name}} başlatılıyor: :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
`))
 
func createProjectStructure(name, typ string) error {
    dirs := []string{name, name + "/cmd", name + "/internal"}
    for _, d := range dirs {
        if err := os.MkdirAll(d, 0755); err != nil {
            return err
        }
    }
 
    f, err := os.Create(name + "/main.go")
    if err != nil {
        return err
    }
    defer f.Close()
 
    return apiMainTmpl.Execute(f, map[string]string{"Name": name})
}
 
func createDockerFiles(name string) error {
    dockerfile := `FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o /bin/app ./...
 
FROM scratch
COPY --from=builder /bin/app /app
ENTRYPOINT ["/app"]
`
    return os.WriteFile(name+"/Dockerfile", []byte(dockerfile), 0644)
}

İnteraktif Prompt: init Komutu

// GO //
// cmd/init.go — kullanıcıdan girdi alan komut
package cmd
 
import (
    "bufio"
    "fmt"
    "os"
    "strings"
 
    "github.com/spf13/cobra"
)
 
var initCmd = &cobra.Command{
    Use:   "init",
    Short: "Mevcut dizinde proje başlat",
    RunE:  runInit,
}
 
func init() {
    rootCmd.AddCommand(initCmd)
}
 
func prompt(reader *bufio.Reader, soru, varsayilan string) string {
    if varsayilan != "" {
        fmt.Printf("%s [%s]: ", soru, varsayilan)
    } else {
        fmt.Printf("%s: ", soru)
    }
 
    cevap, _ := reader.ReadString('\n')
    cevap = strings.TrimSpace(cevap)
 
    if cevap == "" {
        return varsayilan
    }
    return cevap
}
 
func runInit(cmd *cobra.Command, args []string) error {
    reader := bufio.NewReader(os.Stdin)
 
    fmt.Println("⚡ CodeForge Proje Başlatıcı\n")
 
    ad   := prompt(reader, "Proje adı", "my-project")
    dil  := prompt(reader, "Dil (go/node/python)", "go")
    port := prompt(reader, "Port", "8080")
 
    fmt.Printf("\nProje yapılandırması:\n")
    fmt.Printf("  Ad:   %s\n", ad)
    fmt.Printf("  Dil:  %s\n", dil)
    fmt.Printf("  Port: %s\n", port)
 
    return nil
}

Flags: Zorunlu ve Kalıcı

// GO //
// Zorunlu flag — eksikse komut çalışmaz
generateCmd.MarkFlagRequired("type")
 
// Kalıcı flag — tüm alt komutlar için geçerli
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
 
// Karşılıklı dışlayan flags
generateCmd.MarkFlagsMutuallyExclusive("docker", "no-docker")
 
// Birlikte zorunlu flags
generateCmd.MarkFlagsRequiredTogether("user", "password")

Çapraz Derleme

// MAKEFILE //
# Makefile
build-all:
	GOOS=linux   GOARCH=amd64 go build -o dist/codeforge-linux-amd64 .
	GOOS=darwin  GOARCH=arm64 go build -o dist/codeforge-darwin-arm64 .
	GOOS=windows GOARCH=amd64 go build -o dist/codeforge-windows.exe .
 
# Sürüm bilgisini binary'ye göm
VERSION ?= $(shell git describe --tags --always)
build:
	go build -ldflags "-X main.Version=$(VERSION)" -o dist/codeforge .

Shell Tamamlama

// BASH //
# Bash tamamlama dosyası oluştur
codeforge completion bash > /etc/bash_completion.d/codeforge
 
# Zsh
codeforge completion zsh > ~/.zsh/completions/_codeforge
 
# Fish
codeforge completion fish > ~/.config/fish/completions/codeforge.fish

Test Yazımı

// GO //
// cmd/generate_test.go
package cmd
 
import (
    "bytes"
    "testing"
 
    "github.com/stretchr/testify/assert"
)
 
func TestGenerateCommand(t *testing.T) {
    tests := []struct {
        name    string
        args    []string
        wantErr bool
        errMsg  string
    }{
        {"geçerli proje", []string{"my-api", "--type", "api"}, false, ""},
        {"boşluk içeren ad", []string{"my api"}, true, "geçersiz proje adı"},
        {"geçersiz tip", []string{"proj", "--type", "ruby"}, true, ""},
    }
 
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            buf := new(bytes.Buffer)
            rootCmd.SetOut(buf)
            rootCmd.SetArgs(append([]string{"generate"}, tt.args...))
 
            err := rootCmd.Execute()
 
            if tt.wantErr {
                assert.Error(t, err)
                if tt.errMsg != "" {
                    assert.Contains(t, err.Error(), tt.errMsg)
                }
            } else {
                assert.NoError(t, err)
            }
        })
    }
}

Özet

Cobra ile Go CLI araçları geliştirmek hem hızlı hem de sürdürülebilir. RunE (hata döndüren), Args doğrulama, MarkFlagRequired ve RegisterFlagCompletionFunc kombinasyonu production kalitesinde CLI araçları için standart yaklaşımdır. Binary boyutu küçük, performans yüksek, dağıtım kolaydır.