Passer au contenu principal
eLearner.app
Module 10 · Leçon 4 sur 549/50 dans le cours~18 min
Leçons du module (4/5)

Mini-projet : un CLI avec drapeaux

Rassemblons les éléments de base des modules précédents pour créer une petite CLI idiomatique : analyse des indicateurs avec le package flag, gestion des arguments de position, des erreurs sur os.Stderr et du bon code de sortie.

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

Points clés :

  • flag.Int, flag.String, flag.Bool, flag.Duration (etc.) renvoient un pointeur : vous utilisez *n, *upper.
  • flag.Parse() doit être appelé UNE FOIS, avant la lecture des valeurs et avant flag.Args().
  • flag.Args() renvoie les arguments positionnels (ceux après -- ou après le dernier flag).
  • Formes acceptées : flag.String0, flag.String1, flag.String2. Booléens : flag.String3 (vrai), flag.String4.

Codes de sortie conventionnels

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

Les shells et les scripts attendent ces codes : utilisez-les de manière cohérente. Ne quittez jamais une goroutine non principale avec os.Exit : elle ignore chaque defer, y compris le vidage de log.

Stderr contre Stdout

Règle Unix ancienne et sacrée :

  • os.Stdout → sortie utile, pipe-able (cli | grep ...).
  • os.Stderr → diagnostics, erreurs, barres de progression, utilisation.
Go
fmt.Println("result: 42")                   // stdout
fmt.Fprintln(os.Stderr, "error: ...")       // stderr

De cette façon, cli 2>/dev/null affiche uniquement les résultats, cli >out.txt redirige uniquement le résultat et les erreurs restent visibles.

log contre fmt

  • fmt pour la sortie utilisateur.
  • log pour les diagnostics : ajoute un horodatage, écrit dans os.Stderr par défaut, possède log.Fatal (logs + os.Exit(1)) et log.Panic.
Go
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1

Pour les applications modernes (Go 1.21+), utilisez log/slog pour la journalisation structurée (JSON ou texte, niveaux, champs saisis).

Architecture : séparer main de la logique

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
}

Avantages :

  • Testable : vous transmettez bytes.Buffer comme stdout/stderr et vérifiez la sortie.
  • Pas d'état global : chaque FlagSet est isolé.
  • main reste minuscule : il prépare les IO + délégués.

Sous-commandes

Pour les CLI avec sous-commandes (mycli serve, mycli migrate), un schéma sans dépendances externes :

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

Chaque sous-commande possède son propre flag.NewFlagSet. Pour les projets plus importants, des bibliothèques comme cobra ou urfave/cli structurent ce schéma avec une aide générée automatiquement.

Exercices

Exercice#go.m10.l4.e1
Tentatives : 0Chargement…

Définissez le flag -n du type int avec la valeur par défaut 1 et la description des « répétitions », puis j'appelle flag.Parse() et indiquez la valeur.

Chargement de l'éditeur…

Solution disponible après 3 tentatives

Exercice#go.m10.l4.e2
Tentatives : 0Chargement…

Il n'y a pas toujours un argument positionnel derrière flag.Parse, appuyez sur 'manque argument' sur Stderr et terminez avec os.Exit(1).

Chargement de l'éditeur…

Solution disponible après 3 tentatives

Quiz#go.m10.l4.e3
Prêt

Avez-vous des messages d'erreur et d'utilisation dans une CLI compatible Unix ?

Go
fmt.Fprintln(???, "uso: prog ...")
Options de réponse