Przejdź do głównej treści
eLearner.app
Moduł 10 · Lekcja 4 z 549/50 w kursie~18 min
Lekcje modułu (4/5)

Mini-projekt: CLI z flagami

Połączmy elementy poprzednich modułów, aby zbudować mały idiomatyczny CLI: parsowanie flag za pomocą pakietu flag, obsługa argumentów pozycyjnych, błędy w os.Stderr i właściwy kod zakończenia.

Pakiet flag

Go
package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    n := flag.Int("n", 1, "number of repetitions")
    upper := flag.Bool("u", false, "print in uppercase")
    flag.Parse()

    args := flag.Args() // remaining positional arguments
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "usage: echo [-n N] [-u] <text>")
        os.Exit(2)
    }

    text := args[0]
    if *upper {
        text = strings.ToUpper(text)
    }
    for i := 0; i < *n; i++ {
        fmt.Println(text)
    }
}

Kluczowe punkty:

  • flag.Int, flag.String, flag.Bool, flag.Duration (itp.) zwracają wskaźnik: używasz *n, *upper.
  • flag.Parse() należy wywołać RAZ, przed odczytaniem wartości i przed flag.Args().
  • flag.Args() zwraca argumenty pozycyjne (te po -- lub po ostatniej fladze).
  • Akceptowane formy: flag.String0, flag.String1, flag.String2. Wartości logiczne: flag.String3 (true), flag.String4.

Konwencjonalne kody wyjścia

Go
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)

Powłoki i skrypty oczekują tych kodów: używaj ich konsekwentnie. Nigdy nie wychodź z innej niż główna gorrutyny za pomocą os.Exit: pomija każde defer, w tym opróżnianie log.

Stderr kontra standardowe wyjście

Stara i święta zasada Uniksa:

  • os.Stdout → użyteczne wyjście, możliwość potokowania (cli | grep ...).
  • os.Stderr → diagnostyka, błędy, paski postępu, użytkowanie.
Go
fmt.Println("result: 42")                   // stdout
fmt.Fprintln(os.Stderr, "error: ...")       // stderr

W ten sposób cli 2>/dev/null pokazuje tylko wyniki, cli >out.txt przekierowuje tylko wynik, a błędy pozostają widoczne.

KODEKF0 kontra KODEKF1

  • fmt dla danych wyjściowych użytkownika.
  • log do diagnostyki: dodaje znacznik czasu, domyślnie zapisuje do os.Stderr, ma log.Fatal (logi + os.Exit(1)) i log.Panic.
Go
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1

W przypadku nowoczesnych aplikacji (Go 1.21+) użyj log/slog do rejestrowania strukturalnego (JSON lub tekst, poziomy, pola wpisane).

Architektura: oddziel main od logiki

Go
func main() {
    if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(args []string, stdout, stderr io.Writer) error {
    // here use a new flag.FlagSet instead of the global flag.CommandLine
    fs := flag.NewFlagSet("echo", flag.ContinueOnError)
    fs.SetOutput(stderr)
    n := fs.Int("n", 1, "repetitions")
    if err := fs.Parse(args); err != nil {
        return err
    }
    // ...
    return nil
}

Zalety:

  • Testowalne: przekazujesz bytes.Buffer jako stdout/stderr i sprawdzasz wyjście.
  • Brak stanu globalnego: każdy FlagSet jest izolowany.
  • main pozostaje mały: przygotowuje delegatów IO +.

Podpolecenia

W przypadku interfejsów CLI z podkomendami (mycli serve, mycli migrate) schemat bez zewnętrznych zależności:

Go
func main() {
    if len(os.Args) < 2 {
        usage()
        os.Exit(2)
    }
    switch os.Args[1] {
    case "serve":
        serveCmd(os.Args[2:])
    case "migrate":
        migrateCmd(os.Args[2:])
    default:
        usage()
        os.Exit(2)
    }
}

Każda podkomenda ma swój własny flag.NewFlagSet. W przypadku większych projektów biblioteki takie jak cobra lub urfave/cli tworzą ten schemat za pomocą automatycznie generowanej pomocy.

Ćwiczenia

Ćwiczenie#go.m10.l4.e1
Próby: 0Ładowanie...

Zdefiniuj flagę -n typ int z wartością domyślną 1 i opis „ripetizioni”, poi chiama flag.Parse() i stempel wartości.

Ładowanie edytora...

Rozwiązanie dostępne po 3 próbach

Ćwiczenie#go.m10.l4.e2
Próby: 0Ładowanie...

Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e enda con os.Exit(1).

Ładowanie edytora...

Rozwiązanie dostępne po 3 próbach

Quiz#go.m10.l4.e3
Gotowe

Jak przesyłać strumieniowo komunikaty o błędach i używać ich w interfejsie CLI przyjaznym dla Uniksa?

Go
fmt.Fprintln(???, "uso: prog ...")
Opcje odpowiedzi