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
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 avantflag.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
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.
fmt.Println("result: 42") // stdout
fmt.Fprintln(os.Stderr, "error: ...") // stderrDe 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
fmtpour la sortie utilisateur.logpour les diagnostics : ajoute un horodatage, écrit dansos.Stderrpar défaut, possèdelog.Fatal(logs +os.Exit(1)) etlog.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1Pour 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
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.Buffercomme stdout/stderr et vérifiez la sortie. - Pas d'état global : chaque
FlagSetest isolé. mainreste 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 :
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
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.
Solution disponible après 3 tentatives
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).
Solution disponible après 3 tentatives
Avez-vous des messages d'erreur et d'utilisation dans une CLI compatible Unix ?
fmt.Fprintln(???, "uso: prog ...")