Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Travailler avec des variables d’environnement

Nous allons améliorer le binaire minigrep en lui ajoutant une fonctionnalité supplémentaire : une option pour rechercher sans être sensible à la casse que l’utilisateur pourra activer via une variable d’environnement. Nous pourrions appliquer cette fonctionnalité avec une option en ligne de commande et demander à l’utilisateur de la renseigner à chaque fois qu’il veut l’activer, mais à la place, en utilisant une variable d’environnement, nous permettons à nos utilisateurs de régler la variable d’environnement une seule fois et d’avoir leurs recherches insensibles à la casse dans cette session du terminal.

Écrire un test qui échoue pour la recherche insensible à la casse

Nous ajoutons d’abord une nouvelle fonction rechercher_insensible_casse à la bibliothèque minigrep qui sera appelée lorsque la variable d’environnement aura une valeur. Nous allons continuer à suivre le processus de TDD, donc la première étape est d’écrire à nouveau un test qui échoue. Nous allons ajouter un nouveau test pour la nouvelle fonction rechercher_insensible_casse et renommer notre ancien test un_resultat en sensible_casse pour clarifier les différences entre les deux tests, comme dans l’encart 12-20.

Filename: src/lib.rs
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
    let mut resultats = Vec::new();

    for ligne in contenu.lines() {
        if ligne.contains(recherche) {
            resultats.push(ligne);
        }
    }

    resultats
}

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

    #[test]
    fn sensible_casse() {
        let recherche = "duct";
        let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.
Duct tape.";

        assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
    }

    #[test]
    fn insensible_casse() {
        let recherche = "rUsT";
        let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.
C'est pas rustique.";

        assert_eq!(
            vec!["Rust:", "C'est pas rustique."],
            rechercher_insensible_casse(recherche, contenu)
        );
    }
}
Listing 12-20: Adding a new failing test for the case-insensitive function we’re about to add

Remarquez que nous avons aussi modifié le contenu de l’ancien test. Nous avons ajouté une nouvelle ligne avec le texte "Duct tape." en utilisant un D majuscule qui ne devrait pas correspondre à la recherche "duct" lorsque nous recherchons de manière à être sensible à la casse. Ce changement de l’ancien test permet de nous assurer que nous ne casserons pas accidentellement la fonction de recherche sensible à la casse que nous avons déjà implémentée. Ce test devrait toujours continuer à réussir au fur et à mesure que nous progressons sur la recherche insensible à la casse.

Le nouveau test pour la recherche insensible à la casse utilise "rUsT" comme recherche. Dans la fonction rechercher_insensible_casse que nous sommes en train d’ajouter, la recherche "rUsT" devrait correspondre à la ligne qui contient "Rust:" avec un R majuscule ainsi que la ligne C'est pas rustique. même si ces deux cas ont des casses différentes de la recherche. C’est notre test qui doit échouer, et il ne devrait pas se compiler car nous n’avons pas encore défini la fonction rechercher_insensible_casse. Ajoutez son implémentation qui retourne toujours un vecteur vide, de la même manière que nous l’avions fait pour la fonction rechercher dans l’encart 12-16 pour voir si les tests se compilent et échouent.

Implémenter la fonction rechercher_insensible_casse

La fonction rechercher_insensible_casse, présente dans l’encart 12-21, sera presque la même que la fonction rechercher. La seule différence est que nous allons transformer en minuscule le contenu de recherche et de chaque ligne pour que quelle que soit la casse des arguments d’entrée, nous aurons toujours la même casse lorsque nous vérifierons si la ligne contient la recherche.

Filename: src/lib.rs
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
    let mut resultats = Vec::new();

    for ligne in contenu.lines() {
        if ligne.contains(recherche) {
            resultats.push(ligne);
        }
    }

    resultats
}

pub fn rechercher_insensible_casse<'a>(
    recherche: &str,
    contenu: &'a str,
) -> Vec<&'a str> {
    let recherche = recherche.to_lowercase();
    let mut resultats = Vec::new();

    for ligne in contenu.lines() {
        if ligne.to_lowercase().contains(&recherche) {
            resultats.push(ligne);
        }
    }

    resultats
}

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

    #[test]
    fn sensible_casse() {
        let recherche = "duct";
        let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.
Duct tape.";

        assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
    }

    #[test]
    fn insensible_casse() {
        let recherche = "rUsT";
        let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.
C'est pas rustique.";

        assert_eq!(
            vec!["Rust:", "C'est pas rustique."],
            rechercher_insensible_casse(recherche, contenu)
        );
    }
}
Listing 12-21: Defining the search_case_insensitive function to lowercase the query and the line before comparing them

D’abord, nous convertissons la chaîne de caractères recherche en minuscules et nous l’enregistrons dans une nouvelle variable avec le même nom. L’appel à to_lowercase sur la recherche est nécessaire afin que quelle que soit la recherche de l’utilisateur, comme "rust", "RUST", "Rust", ou "rUsT", nous traitons la recherche comme si elle était "rust" et par conséquent elle est insensible à la casse. La méthode to_lowercase devrait gérer de l’Unicode de base, mais ne sera pas fiable à 100%. Si nous avions écrit une application sérieuse, nous aurions dû faire plus de choses à ce sujet, toutefois vu que la section actuelle traite des variables d’environnement et pas de la gestion de l’Unicode, nous allons conserver ce code simplifié.

Notez que recherche est désormais une String et non plus une slice de chaîne de caractères, car l’appel à to_lowercase crée des nouvelles données au lieu de modifier les données déjà existantes. Par exemple, disons que la recherche est "rUsT" : cette slice de chaîne de caractères ne contient pas de u ou de t minuscule que nous pourrions utiliser, donc nous devons allouer une nouvelle String qui contient "rust". Maintenant, lorsque nous passons recherche en argument de la méthode contains, nous devons rajouter une esperluette car la signature de contains est définie pour prendre une slice de chaîne de caractères.

Ensuite, nous ajoutons un appel à to_lowercase sur chaque ligne afin de convertir tous ses caractères en minuscules. Maintenant que nous avons ligne et recherche en minuscules, nous allons rechercher les correspondances, peu importe la casse de la recherche.

Voyons si cette implémentation passe les tests :

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 2 tests
test tests::insensible_casse ... ok
test tests::sensible_casse ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Très bien ! Elles ont réussi. Maintenant, utilisons la nouvelle fonction rechercher_insensible_casse dans la fonction run. Pour commencer, nous allons ajouter une option de configuration à la structure Config pour changer entre la recherche sensible et non sensible à la casse. L’ajout de ce champ va causer des erreurs de compilation car nous n’avons jamais initialisé ce champ pour le moment :

Fichier : src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{rechercher, rechercher_insensible_casse};

// -- partie masquée ici --


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problème rencontré lors de l'interprétation des arguments : {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Erreur applicative : {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub recherche: String,
    pub chemin_fichier: String
    pub ignore_casse: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("il n'y a pas assez d'arguments");
        }

        let recherche = args[1].clone();
        let chemin_fichier = args[2].clone();

        Ok(Config { recherche, chemin_fichier })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contenu = fs::read_to_string(config.chemin_fichier)?;

    let resultats = if config.sensible_casse {
        rechercher_insensible_casse(&config.recherche, &contenu)
    } else {
        recherche(&config.recherche, &contenu)
    };

    for ligne in resultats {
        println!("{ligne}");
    }

    Ok(())
}

Nous avons ajouté le champ ignore_casse qui contient un Booléen. Ensuite, nous devons faire en sorte que la fonction run vérifie la valeur du champ ignore_casse et l’utilise pour décider si elle doit appeler la fonction rechercher ou la fonction rechercher_insensible_casse, comme dans l’encart 12-22. Notez que cela ne se compile toujours pas.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{rechercher, rechercher_insensible_casse};

// -- partie masquée ici --


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problème rencontré lors de l'interprétation des arguments : {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Erreur applicative : {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub recherche: String,
    pub chemin_fichier: String
    pub ignore_casse: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("il n'y a pas assez d'arguments");
        }

        let recherche = args[1].clone();
        let chemin_fichier = args[2].clone();

        Ok(Config { recherche, chemin_fichier })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contenu = fs::read_to_string(config.chemin_fichier)?;

    let resultats = if config.sensible_casse {
        rechercher_insensible_casse(&config.recherche, &contenu)
    } else {
        recherche(&config.recherche, &contenu)
    };

    for ligne in resultats {
        println!("{ligne}");
    }

    Ok(())
}
Listing 12-22: Calling either search or search_case_insensitive based on the value in config.ignore_case

Enfin, nous devons vérifier la variable d’environnement. Les fonctions pour travailler avec les variables d’environnement sont dans le module env de la bibliothèque standard, donc nous allons importer ce module dans la portée avec une ligne use std::env; en haut de src/lib.rs. Ensuite, nous allons utiliser la fonction var du module env pour vérifier la présence d’une variable d’environnement IGNORE_CASSE, comme dans l’encart 12-23.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{rechercher, rechercher_insensible_casse};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problème rencontré lors de l'interprétation des arguments : {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Erreur applicative : {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub recherche: String,
    pub chemin_fichier: String
    pub ignore_casse: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("il n'y a pas assez d'arguments");
        }

        let recherche = args[1].clone();
        let chemin_fichier = args[2].clone();

        let ignore_casse = env::var("IGNORE_CASSE").is_ok();

        Ok(Config {
            recherche,
            chemin_fichier,
            ignore_casse,
        })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contenu = fs::read_to_string(config.chemin_fichier)?;

    let resultats = if config.sensible_casse {
        rechercher_insensible_casse(&config.recherche, &contenu)
    } else {
        recherche(&config.recherche, &contenu)
    };

    for ligne in resultats {
        println!("{ligne}");
    }

    Ok(())
}
Listing 12-23: Checking for any value in an environment variable named IGNORE_CASE

Ici, nous créons une nouvelle variable ignore_casse. Pour lui donner une valeur, nous appelons la fonction env::var et nous lui passons le nom de la variable d’environnement IGNORE_CASSE. La fonction env::var retourne un Result qui sera en cas de succès la variante Ok qui contiendra la valeur de la variable d’environnement si cette variable d’environnement est définie avec n’importe quelle valeur. Elle retournera la variante Err si cette variable d’environnement n’est pas définie.

Nous utilisons la méthode is_ok sur le Result pour vérifier si la variable d’environnement est définie, ce qui signifie que le programme doit effectuer une recherche insensible à la casse. Si la variable d’environnement IGNORE_CASSE n’est pas définie à quoi que ce soit, is_ok va retourner false et le programme va procéder à une recherche sensible à la casse. Nous ne nous préoccupons pas de la valeur de la variable d’environnement, mais uniquement de savoir si elle est définie ou non, donc nous utilisons is_ok plutôt que unwrap, expect ou toute autre méthode que nous avons vue avec Result.

Nous passons la valeur de la variable ignore_casse à l’instance de Config afin que la fonction run puisse lire cette valeur et décider d’appeler rechercher ou rechercher_insensible_casse, ou rechercher, comme nous l’avons implémenté dans l’encart 12-22.

Faisons un essai ! D’abord, nous allons lancer notre programme avec la variable d’environnement non définie et avec la recherche to, qui devrait trouver toutes les lignes qui contiennent le mot “to” en minuscules :

$ cargo run -- to poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!

On dirait que cela fonctionne ! Maintenant, lançons le programme avec IGNORE_CASSE définie à 1 mais avec la même recherche to.

$ IGNORE_CASE=1 cargo run -- to poem.txt

Si vous utilisez PowerShell, vous allez avoir besoin d’affecter la variable d’environnement puis exécuter le programme avec deux commandes distinctes :

PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt

Cela va faire persister la variable IGNORE_CASSE pour la durée de votre session de terminal. Elle peut être désaffectée avec la cmdlet Remove-Item :

PS> Remove-Item Env:IGNORE_CASE

Nous devrions trouver cette fois-ci également toutes les lignes qui contiennent “to” écrit avec certaines lettres en majuscules :

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

Très bien, nous avons aussi obtenu les lignes qui contiennent “To” ! Notre programme minigrep peut maintenant faire des recherches insensibles à la casse, contrôlées par une variable d’environnement. Vous savez maintenant comment gérer des options définies soit par des arguments en ligne de commande, soit par des variables d’environnement.

Certains programmes permettent d’utiliser les arguments et les variables d’environnement pour un même réglage. Dans ce cas, le programme décide si l’un ou l’autre a la priorité. Pour vous exercer à nouveau, essayez de contrôler la sensibilité à la casse via un argument de ligne de commande ou une variable d’environnement. Vous devrez choisir qui de l’argument de la ligne de commande ou de la variable d’environnement doit être prioritaire lorsque les deux sont configurés simultanément mais de manière contradictoire quand le programme est exécuté.

Le module std::env contient plein d’autres fonctionnalités utiles pour utiliser les variables d’environnement : regardez sa documentation pour voir ce qu’il est possible de faire.