Travailler avec des variables d'environnement

Nous allons améliorer 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 nous allons utiliser une variable d'environnement. Ceci permet à 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.

Ecrire un test qui échoue pour la fonction rechercher insensible à la casse

Nous souhaitons ajouter une nouvelle fonction rechercher_insensible_casse que nous allons appeler lorsque la variable d'environnement est active. 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.

Fichier : src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub recherche: String,
    pub nom_fichier: String,
}

impl Config {
    pub fn new(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 nom_fichier = args[2].clone();

        Ok(Config { recherche, nom_fichier })
    }
}

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

    for ligne in rechercher(&config.recherche, &contenu) {
        println!("{}", ligne);
    }

    Ok(())
}

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.
Duck 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)
        );
    }
}

Encart 12-20 : Ajout d'un nouveau test qui échoue pour la fonction insensible à la casse que nous sommes en train d'ajouter

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é. 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.

Fichier : src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub recherche: String,
    pub nom_fichier: String,
}

impl Config {
    pub fn new(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 nom_fichier = args[2].clone();

        Ok(Config { recherche, nom_fichier })
    }
}

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

    for ligne in rechercher(&config.recherche, &contenu) {
        println!("{}", ligne);
    }

    Ok(())
}

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.
Duck 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)
        );
    }
}

Encart 12-21 : Définition de la fonction rechercher_insensible_casse pour obtenir en minuscule la recherche et la ligne avant de les comparer

D'abord, nous obtenons la chaîne de caractères recherche en minuscule et nous l'enregistrons dans une variable masquée avec le même nom. L'appel à to_lowercase sur la recherche est nécessaire afin que quel 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 avant de vérifier si elle contient recherche afin d'obtenir tous ses caractères en minuscule. 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 [unoptimized + debuginfo] target(s) in 1.33s
     Running unittests (target/debug/deps/minigrep-9cd200e5fac0fc94)

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

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

     Running unittests (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/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub recherche: String,
    pub nom_fichier: String,
    pub sensible_casse: bool,
}

impl Config {
    pub fn new(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 nom_fichier = args[2].clone();

        Ok(Config { recherche, nom_fichier })
    }
}

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

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

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

    Ok(())
}

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:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], rechercher(recherche, contenu));
    }

    #[test]
    fn case_insensitive() {
        let recherche = "rUsT";
        let contenu = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            rechercher_insensible_casse(recherche, contenu)
        );
    }
}

Remarquez que le champ sensible_casse que nous avons ajouté est un Booléen. Ensuite, nous devons faire en sorte que la fonction run vérifie la valeur du champ sensible_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 pas encore.

Fichier : src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub recherche: String,
    pub nom_fichier: String,
    pub sensible_casse: bool,
}

impl Config {
    pub fn new(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 nom_fichier = args[2].clone();

        Ok(Config { recherche, nom_fichier })
    }
}

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

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

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

    Ok(())
}

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:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], rechercher(recherche, contenu));
    }

    #[test]
    fn case_insensitive() {
        let recherche = "rUsT";
        let contenu = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            rechercher_insensible_casse(recherche, contenu)
        );
    }
}

Encart 12-22 : Appeler rechercher ou rechercher_insensible_casse en fonction de la valeur dans config.sensible_casse

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 MINIGREP_INSENSIBLE_CASSE, comme dans l'encart 12-23.

Fichier : src/lib.rs

use std::env;
// -- partie masquée ici --

use std::error::Error;
use std::fs;

pub struct Config {
    pub recherche: String,
    pub nom_fichier: String,
    pub sensible_casse: bool,
}

impl Config {
    pub fn new(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 nom_fichier = args[2].clone();

        let sensible_casse = env::var("MINIGREP_INSENSIBLE_CASSE").is_err();

        Ok(Config {
            recherche,
            nom_fichier,
            sensible_casse,
        })
    }
}

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

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

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

    Ok(())
}

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:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], rechercher(recherche, contenu));
    }

    #[test]
    fn case_insensitive() {
        let recherche = "rUsT";
        let contenu = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            rechercher_insensible_casse(recherche, contenu)
        );
    }
}

Encart 12-23 : Vérification de la présence de la variable d'environnement MINIGREP_INSENSIBLE_CASSE

Ici, nous créons une nouvelle variable sensible_casse. Pour lui donner une valeur, nous appelons la fonction env::var et nous lui passons le nom de la variable d'environnement MINIGREP_INSENSIBLE_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. Elle retournera la variante Err si cette variable d'environnement n'est pas définie.

Nous utilisons la méthode is_err sur le Result pour vérifier si nous obtenons une erreur, signalant par conséquent que la variable d'environnement n'est pas définie et donc que nous devons effectuer une recherche sensible à la casse. Si la variable d'environnement MINIGREP_INSENSIBLE_CASSE a une valeur qui lui a été assignée, is_err va retourner false et le programme va procéder à une recherche non 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_err plutôt que unwrap, expect ou toute autre méthode que nous avons vue avec Result.

Nous passons la valeur de la variable sensible_casse à l'instance de Config afin que la fonction run puisse lire cette valeur et décider d'appeler rechercher ou rechercher_insensible_casse, 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 [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 MINIGREP_INSENSIBLE_CASSE définie à 1 mais avec la même recherche to.

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:MINIGREP_INSENSIBLE_CASSE=1; cargo run to poem.txt

Cela va faire persister la variable MINIGREP_INSENSIBLE_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:MINIGREP_INSENSIBLE_CASSE

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

$ CASE_INSENSITIVE=1 cargo run to poem.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/minigrep to poem.txt`
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.