Vai al contenuto
eLearner.app
Modulo 10 · Lezione 4 di 549/50 nel corso~18 min
Lezioni del modulo (4/5)

Mini-progetto: una CLI con flag

Mettiamo insieme i mattoni dei moduli precedenti per costruire una piccola CLI idiomatica: parsing dei flag con il pacchetto flag, gestione di argomenti posizionali, errori su os.Stderr, exit code corretto.

Il pacchetto flag

Go
package main

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

func main() {
    n := flag.Int("n", 1, "numero di ripetizioni")
    upper := flag.Bool("u", false, "stampa in maiuscolo")
    flag.Parse()

    args := flag.Args() // argomenti posizionali rimanenti
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "uso: echo [-n N] [-u] <testo>")
        os.Exit(2)
    }

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

Punti chiave:

  • flag.Int, flag.String, flag.Bool, flag.Duration (etc.) ritornano un puntatore: usi *n, *upper.
  • flag.Parse() va chiamato UNA volta, prima di leggere i valori e prima di flag.Args().
  • flag.Args() ritorna gli argomenti posizionali (quelli dopo -- o dopo l'ultimo flag).
  • Forme accettate: -n 3, -n=3, --n=3. Booleani: -u (true), -u=false.

Exit code convenzionali

Go
os.Exit(0) // successo
os.Exit(1) // errore generico
os.Exit(2) // errore d'uso (uso flag scorretto)

Le shell e gli script si aspettano questi codici: usali consistentemente. Mai uscire da una goroutine non-main con os.Exit: salta tutti i defer, incluso log flushing.

Stderr vs Stdout

Regola Unix antica e sacra:

  • os.Stdout → output utile, pipe-abile (cli | grep ...).
  • os.Stderr → diagnostica, errori, progress bar, usage.
Go
fmt.Println("risultato: 42")                // stdout
fmt.Fprintln(os.Stderr, "errore: ...")      // stderr

Così cli 2>/dev/null mostra solo i risultati, cli >out.txt dirotta solo il risultato e gli errori restano visibili.

log vs fmt

  • fmt per output utente.
  • log per diagnostica: aggiunge timestamp, scrive di default su os.Stderr, ha log.Fatal (logga + os.Exit(1)) e log.Panic.
Go
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // stampa e esce con 1

Per app moderne (Go 1.21+) usa log/slog per logging strutturato (JSON o testo, livelli, campi tipati).

Architettura: separare main dalla logica

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 {
    // qui usa un nuovo flag.FlagSet invece del globale flag.CommandLine
    fs := flag.NewFlagSet("echo", flag.ContinueOnError)
    fs.SetOutput(stderr)
    n := fs.Int("n", 1, "ripetizioni")
    if err := fs.Parse(args); err != nil {
        return err
    }
    // ...
    return nil
}

Vantaggi:

  • Testabile: passi bytes.Buffer come stdout/stderr e verifichi l'output.
  • Niente stato globale: ogni FlagSet è isolato.
  • main resta minuscolo: prepara IO + delega.

Sotto-comandi

Per CLI con sotto-comandi (mycli serve, mycli migrate), uno schema senza dipendenze esterne:

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)
    }
}

Ogni sotto-comando ha il proprio flag.NewFlagSet. Per progetti più grandi, librerie come cobra o urfave/cli strutturano questo schema con help auto-generato.

Esercizi

Esercizio#go.m10.l4.e1
Tentativi: 0Caricamento…

Definisci il flag -n di tipo int con valore di default 1 e descrizione 'ripetizioni', poi chiama flag.Parse() e stampa il valore.

Caricamento editor…

Soluzione disponibile dopo 3 tentativi

Esercizio#go.m10.l4.e2
Tentativi: 0Caricamento…

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

Caricamento editor…

Soluzione disponibile dopo 3 tentativi

Quiz#go.m10.l4.e3
Pronto

Su quale stream stampi messaggi di errore e usage in un CLI Unix-friendly?

Go
fmt.Fprintln(???, "uso: prog ...")
Opzioni di risposta