Tutoriels gratuits et ouverts à toutes et à tous · conçus au Lycée Chevrollier — AngersÀ propos

Série « Parcours Rust » · partie 7 / 8

Rust niveau 6 — Un vrai projet en ligne de commande

Structurer un projet, utiliser des bibliothèques, lire des arguments et des fichiers, livrer un outil : niveau 6 du parcours Rust, avec exercices corrigés et QCM.

Développement✎ B. Jousseaume📅 17 octobre 2026⏱ 8 h📶 Avancé

1. L’organisation d’un projet

analyse-journal/
├── Cargo.toml
├── Cargo.lock
├── src/
│   ├── main.rs        ← court : lit les arguments, appelle la bibliothèque, affiche
│   ├── lib.rs         ← déclare les modules et ce qui est public
│   ├── modele.rs      ← les types de données
│   ├── analyse.rs     ← la logique, sans affichage ni fichier
│   └── sortie.rs      ← la mise en forme des résultats
├── tests/
│   └── integration.rs ← tests qui utilisent la bibliothèque comme un client
└── exemples/
    └── auth.log       ← des données de test
// src/lib.rs
pub mod modele;
pub mod analyse;
pub mod sortie;
// src/main.rs
use analyse_journal::analyse;     // le nom du paquet, tirets remplacés par _

fn main() {
    // …
}

2. Les dépendances

Les bibliothèques Rust, appelées crates, sont publiées sur crates.io. On les ajoute avec Cargo :

$ cargo add clap --features derive
$ cargo add serde --features derive
$ cargo add serde_json csv anyhow
# Cargo.toml
[package]
name = "analyse-journal"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
csv = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ÉcritureVersions acceptées
"1" ou "^1.0"Toute version 1.x.y (mises à jour compatibles)
"~1.4"1.4.x seulement
"=1.4.2"Exactement 1.4.2

Le fichier Cargo.lock enregistre la version exacte de chaque dépendance, y compris les dépendances des dépendances. Pour un programme, on le conserve dans Git : tout le monde compile alors exactement le même code.

CrateRôle
clapAnalyse des arguments de la ligne de commande
serde, serde_json, csvConversion entre structures Rust et JSON ou CSV
anyhowGestion d’erreurs simple pour une application
thiserrorDéfinition d’énumérations d’erreurs pour une bibliothèque
regexExpressions régulières
chronoDates et heures
randNombres aléatoires, y compris cryptographiques
tokio, axum, reqwestAsynchrone, serveur web, client HTTP (niveau 7)

3. Lire les arguments

La bibliothèque standard fournit les arguments bruts :

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    // args[0] est le nom du programme
    if args.len() < 2 {
        eprintln!("usage : {} <fichier>", args[0]);
        std::process::exit(2);
    }
    println!("fichier demandé : {}", args[1]);
}

Dès qu’il y a des options, clap fait tout le travail : analyse, validation, messages d’erreur et aide générée.

use clap::{Parser, ValueEnum};
use std::path::PathBuf;

#[derive(Clone, Copy, ValueEnum)]
enum Format {
    Texte,
    Json,
    Csv,
}

/// Analyse un journal d'authentification et repère les attaques par force brute.
#[derive(Parser)]
#[command(version, about)]
struct Args {
    /// Fichier journal à analyser
    fichier: PathBuf,

    /// Nombre d'échecs à partir duquel une adresse est suspecte
    #[arg(short, long, default_value_t = 5)]
    seuil: u32,

    /// Format de sortie
    #[arg(short, long, value_enum, default_value_t = Format::Texte)]
    format: Format,

    /// Affiche le détail de chaque ligne ignorée
    #[arg(short, long)]
    verbeux: bool,
}

fn main() {
    let args = Args::parse();
    println!("{:?} seuil={} verbeux={}", args.fichier, args.seuil, args.verbeux);
}
$ cargo run -- --help
Analyse un journal d'authentification et repère les attaques par force brute

Usage: analyse-journal [OPTIONS] <FICHIER>

Arguments:
  <FICHIER>  Fichier journal à analyser

Options:
  -s, --seuil <SEUIL>    Nombre d'échecs à partir duquel une adresse est suspecte [default: 5]
  -f, --format <FORMAT>  Format de sortie [default: texte] [possible values: texte, json, csv]
  -v, --verbeux          Affiche le détail de chaque ligne ignorée
  -h, --help             Print help
  -V, --version          Print version

$ cargo run -- exemples/auth.log --seuil 3 --format json

Le -- sépare les options de Cargo de celles du programme. Les commentaires à trois barres /// deviennent le texte de l’aide.

4. Sérialiser avec serde

Sérialiser, c’est transformer une structure en texte (JSON, CSV…) ; désérialiser, l’inverse. Avec serde, il suffit d’un derive :

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Poste {
    nom: String,
    salle: u16,
    ram_go: u32,
    #[serde(default)]                 // champ absent du JSON → valeur par défaut
    logiciels: Vec<String>,
}

fn main() -> Result<(), serde_json::Error> {
    let json = r#"[
        {"nom": "PC-207-01", "salle": 207, "ram_go": 16, "logiciels": ["VS Code", "Wireshark"]},
        {"nom": "PC-112-04", "salle": 112, "ram_go": 8}
    ]"#;

    let postes: Vec<Poste> = serde_json::from_str(json)?;
    for p in &postes {
        println!("{} : {} logiciel(s)", p.nom, p.logiciels.len());
    }

    let petits: Vec<&Poste> = postes.iter().filter(|p| p.ram_go < 16).collect();
    println!("{}", serde_json::to_string_pretty(&petits)?);
    Ok(())
}
// Lire et écrire du CSV
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
struct Ligne {
    nom: String,
    salle: u16,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut lecteur = csv::ReaderBuilder::new().delimiter(b';').from_path("postes.csv")?;
    let mut ecrivain = csv::WriterBuilder::new().delimiter(b';').from_path("salle207.csv")?;
    for resultat in lecteur.deserialize() {
        let ligne: Ligne = resultat?;          // une ligne mal formée donne une erreur précise
        if ligne.salle == 207 {
            ecrivain.serialize(&ligne)?;
        }
    }
    ecrivain.flush()?;
    Ok(())
}

La désérialisation vérifie les données : un champ manquant, une salle qui n’est pas un nombre ou qui dépasse u16 produisent une erreur, avec la ligne et la colonne en cause. Les types choisis au niveau 1 deviennent ainsi un contrôle automatique des fichiers reçus.

5. Les erreurs d’une application

Au niveau 4, on a défini ses propres énumérations d’erreurs : c’est la bonne pratique pour une bibliothèque, dont l’appelant doit pouvoir distinguer les cas. Pour une application, on veut surtout un message clair avec son contexte : c’est le rôle d’anyhow.

use anyhow::{bail, Context, Result};
use std::fs;

fn lire_config(chemin: &str) -> Result<u32> {
    let texte = fs::read_to_string(chemin)
        .with_context(|| format!("impossible de lire la configuration {chemin}"))?;
    let seuil: u32 = texte
        .trim()
        .parse()
        .context("la configuration doit contenir un nombre entier")?;
    if seuil == 0 {
        bail!("le seuil ne peut pas valoir 0");
    }
    Ok(seuil)
}

fn main() {
    if let Err(e) = lire_config("seuil.conf") {
        eprintln!("Erreur : {e:#}");
        std::process::exit(1);
    }
}
Erreur : impossible de lire la configuration seuil.conf: No such file or directory (os error 2)
Code de sortieSignification usuelle
0Succès
1Erreur générale
2Mauvaise utilisation (arguments invalides) — c’est ce que renvoie clap
autreÀ documenter : par exemple 3 pour « anomalie détectée »

Le code de sortie permet à un script, une tâche planifiée ou un outil de supervision de savoir automatiquement si le programme a réussi, sans lire son affichage.

6. La qualité avant de livrer

CommandeRôle
cargo fmtMet en forme selon le style officiel : plus de débats de présentation
cargo clippy -- -D warningsDes centaines de vérifications de style et d’erreurs probables ; les avertissements deviennent bloquants
cargo testTests unitaires, tests d’intégration du dossier tests/, exemples de la documentation
cargo doc --openGénère la documentation HTML à partir des commentaires ///
cargo auditRecherche de vulnérabilités connues dans les dépendances
cargo build --releaseL’exécutable final, optimisé
// tests/integration.rs : on teste la bibliothèque comme un utilisateur externe
use analyse_journal::analyse::analyser;

#[test]
fn detecte_force_brute() {
    let journal = "2026-09-24T08:00:01 FAIL user=admin ip=203.0.113.9\n".repeat(6);
    let rapport = analyser(journal.lines(), 5);
    assert_eq!(rapport.suspectes.len(), 1);
}

7. Projet : l’analyseur de journal d’authentification

Ce projet relie développement et administration : on écrit l’outil qu’un administrateur système utiliserait pour repérer une attaque par force brute dans un journal de connexions.

2026-09-24T08:12:03 OK   user=lina  ip=10.0.0.12
2026-09-24T08:12:40 FAIL user=admin ip=203.0.113.9
2026-09-24T08:12:41 FAIL user=admin ip=203.0.113.9
2026-09-24T08:12:41 FAIL user=root  ip=203.0.113.9
ligne corrompue ####
2026-09-24T08:13:02 FAIL user=sam   ip=10.0.0.15
2026-09-24T08:13:05 OK   user=sam   ip=10.0.0.15

8. Bilan

Exercices corrigés

Ces corrigés sont écrits comme de vrais projets Cargo. Crée le projet, recopie les fichiers indiqués et lance cargo run -- --help. Les versions des crates évoluent : si une API a changé, le message du compilateur et la documentation sur docs.rs t’indiqueront la nouvelle écriture.

Exercice 6.1 — Un clone de wc

Voir le corrigé
// cargo new compte && cargo add clap --features derive
use clap::Parser;
use std::fs;
use std::path::PathBuf;
use std::process::ExitCode;

/// Compte les lignes, mots et caractères de fichiers texte.
#[derive(Parser)]
#[command(version, about)]
struct Args {
    /// Fichiers à analyser
    #[arg(required = true)]
    fichiers: Vec<PathBuf>,
    /// N'afficher que les lignes
    #[arg(short = 'l')]
    lignes: bool,
    /// N'afficher que les mots
    #[arg(short = 'w')]
    mots: bool,
    /// N'afficher que les caractères
    #[arg(short = 'c')]
    caracteres: bool,
}

#[derive(Default, Clone, Copy)]
struct Compte {
    lignes: usize,
    mots: usize,
    caracteres: usize,
}

fn compter(texte: &str) -> Compte {
    Compte {
        lignes: texte.lines().count(),
        mots: texte.split_whitespace().count(),
        caracteres: texte.chars().count(),
    }
}

fn afficher(c: Compte, nom: &str, args: &Args) {
    let tout = !(args.lignes || args.mots || args.caracteres);
    let mut colonnes = Vec::new();
    if tout || args.lignes { colonnes.push(format!("{:>8}", c.lignes)); }
    if tout || args.mots { colonnes.push(format!("{:>8}", c.mots)); }
    if tout || args.caracteres { colonnes.push(format!("{:>8}", c.caracteres)); }
    println!("{} {nom}", colonnes.join(""));
}

fn main() -> ExitCode {
    let args = Args::parse();
    let mut total = Compte::default();
    let mut echec = false;

    for chemin in &args.fichiers {
        match fs::read_to_string(chemin) {
            Ok(texte) => {
                let c = compter(&texte);
                total.lignes += c.lignes;
                total.mots += c.mots;
                total.caracteres += c.caracteres;
                afficher(c, &chemin.display().to_string(), &args);
            }
            Err(e) => {
                eprintln!("compte: {}: {e}", chemin.display());
                echec = true;
            }
        }
    }
    if args.fichiers.len() > 1 {
        afficher(total, "total", &args);
    }
    if echec { ExitCode::FAILURE } else { ExitCode::SUCCESS }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn compte_simple() {
        let c = compter("un deux\ntrois\n");
        assert_eq!((c.lignes, c.mots, c.caracteres), (2, 3, 14));
    }
    #[test]
    fn accents_en_caracteres() {
        assert_eq!(compter("été").caracteres, 3);
    }
}

Explication

main renvoie un ExitCode : c’est la façon propre de choisir le code de sortie sans appeler process::exit, qui interromprait le programme sans libérer les ressources. La fonction compter ne dépend ni des fichiers ni de l’affichage : elle se teste directement.

Exercice 6.2 — Inventaire JSON vers CSV

Voir le corrigé
// cargo add clap --features derive ; cargo add serde --features derive ; cargo add serde_json csv anyhow
use anyhow::{Context, Result};
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Parser)]
struct Args {
    /// Inventaire au format JSON
    inventaire: PathBuf,
    /// Seuil de mémoire en Go
    #[arg(long, default_value_t = 8)]
    ram_min: u32,
    /// Fichier CSV produit
    #[arg(short, long, default_value = "a_renouveler.csv")]
    sortie: PathBuf,
}

#[derive(Debug, Deserialize, Serialize)]
struct Poste {
    nom: String,
    salle: u16,
    ram_go: u32,
}

fn main() -> Result<()> {
    let args = Args::parse();
    let texte = std::fs::read_to_string(&args.inventaire)
        .with_context(|| format!("lecture de {}", args.inventaire.display()))?;
    let mut postes: Vec<Poste> = serde_json::from_str(&texte)
        .with_context(|| format!("{} n'est pas un inventaire JSON valide", args.inventaire.display()))?;

    postes.retain(|p| p.ram_go < args.ram_min);
    postes.sort_by(|a, b| a.salle.cmp(&b.salle).then_with(|| a.nom.cmp(&b.nom)));

    let mut w = csv::WriterBuilder::new().delimiter(b';').from_path(&args.sortie)?;
    for p in &postes {
        w.serialize(p)?;
    }
    w.flush()?;
    println!("{} poste(s) sous {} Go → {}", postes.len(), args.ram_min, args.sortie.display());
    Ok(())
}

Explication

Quand main renvoie un anyhow::Result, une erreur est affichée avec toute sa chaîne de contexte, par exemple : inventaire.json n’est pas un inventaire JSON valide, puis la cause : EOF while parsing a value at line 2 column 0. Le tri combine deux critères avec then_with.

Projet : l’analyseur de journal d’authentification

Voir le corrigé
// src/modele.rs
use serde::Serialize;
use std::collections::BTreeSet;
use std::net::IpAddr;

#[derive(Debug, PartialEq)]
pub struct Evenement {
    pub horodatage: String,
    pub succes: bool,
    pub utilisateur: String,
    pub ip: IpAddr,
}

#[derive(Debug, Default, Serialize)]
pub struct StatIp {
    pub ip: String,
    pub echecs: u32,
    pub succes: u32,
    pub comptes: BTreeSet<String>,
    pub critique: bool,
}

#[derive(Debug, Default, Serialize)]
pub struct Rapport {
    pub lignes_lues: usize,
    pub lignes_invalides: Vec<usize>,
    pub suspectes: Vec<StatIp>,
}
// src/analyse.rs
use crate::modele::{Evenement, Rapport, StatIp};
use std::collections::HashMap;
use std::net::IpAddr;

const LONGUEUR_MAX: usize = 1024;

/// Remplace les caractères de contrôle, pour un affichage sans danger.
pub fn nettoyer(texte: &str) -> String {
    texte.chars().map(|c| if c.is_control() { '?' } else { c }).collect()
}

/// Analyse une ligne : « horodatage OK|FAIL user=nom ip=adresse ».
pub fn analyser_ligne(ligne: &str) -> Option<Evenement> {
    if ligne.len() > LONGUEUR_MAX {
        return None;
    }
    let mut champs = ligne.split_whitespace();
    let horodatage = champs.next()?.to_string();
    let succes = match champs.next()? {
        "OK" => true,
        "FAIL" => false,
        _ => return None,
    };
    let utilisateur = nettoyer(champs.next()?.strip_prefix("user=")?);
    let ip: IpAddr = champs.next()?.strip_prefix("ip=")?.parse().ok()?;
    if champs.next().is_some() || utilisateur.is_empty() {
        return None;
    }
    Some(Evenement { horodatage, succes, utilisateur, ip })
}

pub fn analyser<'a>(lignes: impl Iterator<Item = &'a str>, seuil: u32) -> Rapport {
    let mut rapport = Rapport::default();
    let mut par_ip: HashMap<IpAddr, StatIp> = HashMap::new();

    for (i, ligne) in lignes.enumerate() {
        rapport.lignes_lues += 1;
        let Some(ev) = analyser_ligne(ligne) else {
            rapport.lignes_invalides.push(i + 1);
            continue;
        };
        let stat = par_ip.entry(ev.ip).or_insert_with(|| StatIp {
            ip: ev.ip.to_string(),
            ..Default::default()
        });
        if ev.succes {
            stat.succes += 1;
        } else {
            stat.echecs += 1;
            stat.comptes.insert(ev.utilisateur);
        }
    }

    let mut suspectes: Vec<StatIp> = par_ip
        .into_values()
        .filter(|s| s.echecs >= seuil)
        .map(|mut s| {
            s.critique = s.succes > 0;
            s
        })
        .collect();
    suspectes.sort_by(|a, b| b.echecs.cmp(&a.echecs).then_with(|| a.ip.cmp(&b.ip)));
    rapport.suspectes = suspectes;
    rapport
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ligne_valide() {
        let e = analyser_ligne("2026-09-24T08:12:40 FAIL user=admin ip=203.0.113.9").unwrap();
        assert!(!e.succes);
        assert_eq!(e.utilisateur, "admin");
    }
    #[test]
    fn ip_invalide_refusee() {
        assert!(analyser_ligne("2026-09-24T08:12:40 FAIL user=admin ip=999.1.1.1").is_none());
    }
    #[test]
    fn statut_inconnu_refuse() {
        assert!(analyser_ligne("2026-09-24T08:12:40 MAYBE user=a ip=10.0.0.1").is_none());
    }
    #[test]
    fn champ_en_trop_refuse() {
        assert!(analyser_ligne("t OK user=a ip=10.0.0.1 bonus").is_none());
    }
    #[test]
    fn caracteres_de_controle_neutralises() {
        let e = analyser_ligne("t FAIL user=ad\u{1b}[31mmin ip=10.0.0.1").unwrap();
        assert!(!e.utilisateur.contains('\u{1b}'));
    }
    #[test]
    fn ligne_trop_longue() {
        assert!(analyser_ligne(&"x".repeat(5000)).is_none());
    }
    #[test]
    fn critique_si_succes_apres_echecs() {
        let j = "t FAIL user=a ip=10.0.0.9\nt FAIL user=a ip=10.0.0.9\nt OK user=a ip=10.0.0.9";
        let r = analyser(j.lines(), 2);
        assert_eq!(r.suspectes.len(), 1);
        assert!(r.suspectes[0].critique);
    }
}
// src/sortie.rs
use crate::modele::Rapport;
use anyhow::Result;

pub fn texte(r: &Rapport) {
    println!("{} ligne(s) lue(s), {} invalide(s)", r.lignes_lues, r.lignes_invalides.len());
    if r.suspectes.is_empty() {
        println!("Aucune adresse suspecte.");
        return;
    }
    println!("{:<18} {:>7} {:>7}  comptes visés", "Adresse", "Échecs", "Succès");
    for s in &r.suspectes {
        let alerte = if s.critique { "  ← CRITIQUE" } else { "" };
        let comptes: Vec<&str> = s.comptes.iter().map(String::as_str).collect();
        println!("{:<18} {:>7} {:>7}  {}{alerte}", s.ip, s.echecs, s.succes, comptes.join(", "));
    }
}

pub fn json(r: &Rapport) -> Result<()> {
    println!("{}", serde_json::to_string_pretty(r)?);
    Ok(())
}

pub fn csv(r: &Rapport) -> Result<()> {
    let mut w = csv::WriterBuilder::new().delimiter(b';').from_writer(std::io::stdout());
    w.write_record(["ip", "echecs", "succes", "comptes", "critique"])?;
    for s in &r.suspectes {
        let comptes: Vec<&str> = s.comptes.iter().map(String::as_str).collect();
        w.write_record([
            s.ip.clone(),
            s.echecs.to_string(),
            s.succes.to_string(),
            comptes.join(","),
            s.critique.to_string(),
        ])?;
    }
    w.flush()?;
    Ok(())
}
// src/lib.rs
pub mod analyse;
pub mod modele;
pub mod sortie;
// src/main.rs
use analyse_journal::{analyse, sortie};
use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::ExitCode;

#[derive(Clone, Copy, ValueEnum)]
enum Format { Texte, Json, Csv }

/// Analyse un journal d'authentification et repère les attaques par force brute.
#[derive(Parser)]
#[command(version, about)]
struct Args {
    /// Fichier journal à analyser
    fichier: PathBuf,
    /// Nombre d'échecs à partir duquel une adresse est suspecte
    #[arg(short, long, default_value_t = 5)]
    seuil: u32,
    /// Format de sortie
    #[arg(short, long, value_enum, default_value_t = Format::Texte)]
    format: Format,
    /// Affiche les numéros des lignes ignorées
    #[arg(short, long)]
    verbeux: bool,
}

fn executer(args: &Args) -> Result<bool> {
    let fichier = File::open(&args.fichier)
        .with_context(|| format!("impossible d'ouvrir {}", args.fichier.display()))?;
    let lignes: Vec<String> = BufReader::new(fichier)
        .lines()
        .collect::<Result<_, _>>()
        .context("lecture du journal interrompue")?;
    let rapport = analyse::analyser(lignes.iter().map(String::as_str), args.seuil);

    if args.verbeux {
        for n in &rapport.lignes_invalides {
            eprintln!("ligne {n} ignorée : format non reconnu");
        }
    }
    match args.format {
        Format::Texte => sortie::texte(&rapport),
        Format::Json => sortie::json(&rapport)?,
        Format::Csv => sortie::csv(&rapport)?,
    }
    Ok(!rapport.suspectes.is_empty())
}

fn main() -> ExitCode {
    let args = Args::parse();
    match executer(&args) {
        Ok(false) => ExitCode::SUCCESS,
        Ok(true) => ExitCode::from(3),
        Err(e) => {
            eprintln!("Erreur : {e:#}");
            ExitCode::FAILURE
        }
    }
}
$ cargo run -- exemples/auth.log --seuil 3
7 ligne(s) lue(s), 1 invalide(s)
Adresse             Échecs  Succès  comptes visés
203.0.113.9              3       0  admin, root

Explication

L’analyse d’une ligne s’appuie sur ? appliqué à des Option : la moindre anomalie (champ manquant, préfixe absent, adresse invalide) fait renvoyer None, en une ligne. IpAddr valide l’adresse, IPv4 comme IPv6. BTreeSet range les comptes visés sans doublon et dans l’ordre alphabétique. La bibliothèque ne fait aucun affichage : la même fonction analyser pourra servir dans une API web au niveau 7.

Pour aller plus loin

Étape suivante naturelle : une option --depuis pour n’analyser que les dernières heures, avec la crate chrono, et un fonctionnement en continu qui lit le journal au fur et à mesure qu’il grandit, comme tail -f.

🧠 Testez vos connaissances

5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.

1. Pourquoi placer la logique dans lib.rs plutôt que dans main.rs ?

💬 main.rs relie l’utilisateur à la bibliothèque ; la bibliothèque peut servir à un autre outil ou à un serveur.

2. À quoi sert Cargo.lock ?

💬 Pour un programme, on le conserve dans Git : tout le monde compile exactement le même code.

3. Que fait cargo audit ?

💬 Il s’appuie sur la base RustSec. cargo clippy, lui, vérifie le code.

4. Quel est l’intérêt d’un code de sortie 3 pour « anomalie détectée » ?

💬 0 pour le succès, 1 pour une erreur, 2 pour une mauvaise utilisation ; les autres codes sont à documenter.

5. Pourquoi nettoyer les caractères de contrôle avant d’afficher un nom d’utilisateur lu dans un journal ?

💬 Les séquences d’échappement ANSI peuvent modifier l’affichage ou masquer des informations à l’administrateur.

© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.

Pour aller plus loin

Python niveau 1 — Premiers pas

Afficher, calculer, mémoriser dans des variables et dialoguer avec l’utilisateur : niveau 1 du parcours Python, avec exercices corrigés et QCM.

Débutant⏱ 4 à 5 h · 17 octobre 2026