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 les variables d’environnement

Nous allons améliorer le binaire minigrep en ajoutant une fonctionnalité supplémentaire : une option de recherche insensible à la casse que l’utilisateur peut activer via une variable d’environnement. Nous pourrions faire de cette fonctionnalité une option de ligne de commande et exiger que les utilisateurs la saisissent à chaque fois qu’ils veulent l’appliquer, mais en en faisant plutôt une variable d’environnement, nous permettons à nos utilisateurs de définir la variable d’environnement une seule fois et d’avoir toutes leurs recherches insensibles à la casse dans cette session de terminal.

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

Nous ajoutons d’abord une nouvelle fonction search_case_insensitive à la bibliothèque minigrep qui sera appelée lorsque la variable d’environnement à une valeur. Nous continuerons à suivre le processus TDD, donc la première étape est à nouveau d’écrire un test qui échoue. Nous ajouterons un nouveau test pour la nouvelle fonction search_case_insensitive et renommerons notre ancien test de one_result en case_sensitive pour clarifier les différences entre les deux tests, comme illustré dans l’encart 12-20.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\nRust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

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

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 12-20: Adding a new failing test for the case-insensitive function we’re about to add

Notez que nous avons aussi modifié le contents de l’ancien test. Nous avons ajouté une nouvelle ligne avec le texte "Duct tape." utilisant un D majuscule qui ne devrait pas correspondre à la requête "duct" lorsque nous cherchons de manière sensible à la casse. Modifier l’ancien test de cette façon permet de s’assurer que nous ne cassons pas accidentellement la fonctionnalité de recherche sensible à la casse que nous avons déjà implémentée. Ce test devrait passer maintenant et continuer à passer pendant que nous travaillons sur la recherche insensible à la casse.

Le nouveau test pour la recherche insensible à la casse utilise "rUsT" comme requête. Dans la fonction search_case_insensitive que nous sommes sur le point d’ajouter, la requête "rUsT" devrait correspondre à la ligne contenant "Rust:" avec un R majuscule et correspondre à la ligne "Trust me." même si les deux ont une casse différente de la requête. C’est notre test qui échoue, et il ne compilera pas car nous n’avons pas encore défini la fonction search_case_insensitive. N’hésitez pas à ajouter une implémentation squelette qui renvoie toujours un vecteur vide, de la même manière que nous l’avons fait pour la fonction search dans l’encart 12-16, pour voir le test compiler et échouer.

Implémenter la fonction search_case_insensitive

La fonction search_case_insensitive, illustrée dans l’encart 12-21, sera presque identique à la fonction search. La seule différence est que nous mettrons en minuscules la query et chaque line de sorte que, quelle que soit la casse des arguments d’entrée, ils seront dans la même casse lorsque nous vérifierons si la ligne contient la requête.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\n# Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

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

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 12-21: Defining the search_case_insensitive function to lowercase the query and the line before comparing them

Tout d’abord, nous mettons en minuscules la chaîne query et la stockons dans une nouvelle variable du même nom, masquant la query originale. Appeler to_lowercase sur la requête est nécessaire pour que, quelle que soit la requête de l’utilisateur — "rust", "RUST", "Rust" ou "rUsT" — nous traitions la requête comme si elle était "rust" et soyons insensibles à la casse. Bien que to_lowercase gère l’Unicode de base, ce ne sera pas précis à 100 %. Si nous écrivions une vraie application, nous voudrions faire un peu plus de travail ici, mais cette section porte sur les variables d’environnement, pas sur l’Unicode, donc nous en resterons là.

Notez que query est maintenant un String plutôt qu’une tranche de chaîne, car l’appel à to_lowercase crée de nouvelles données plutôt que de référencer des données existantes. Prenons l’exemple de la requête "rUsT" : cette tranche de chaîne ne contient pas de u ou de t minuscule que nous pourrions utiliser, nous devons donc allouer un nouveau String contenant "rust". Lorsque nous passons query comme argument à la méthode contains maintenant, nous devons ajouter une esperluette car la signature de contains est définie pour prendre une tranche de chaîne.

Ensuite, nous ajoutons un appel à to_lowercase sur chaque line pour mettre tous les caractères en minuscules. Maintenant que nous avons converti line et query en minuscules, nous trouverons des correspondances quelle que soit la casse de la requête.

Voyons si cette implémentation passe les tests : console {{#include ../listings/ch12-an-io-project/listing-12-21/output.txt}}

$ 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::case_insensitive ... ok
test tests::case_sensitive ... 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

Parfait ! Ils sont passés. Maintenant, appelons la nouvelle fonction search_case_insensitive depuis la fonction run. Tout d’abord, nous ajouterons une option de configuration à la structure Config pour basculer entre la recherche sensible et insensible à la casse. L’ajout de ce champ provoquera des erreurs de compilation car nous n’initialisons ce champ nulle part pour le moment :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

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

use minigrep::{search, search_case_insensitive};

// --snip--


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

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

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

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

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

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

Nous avons ajouté le champ ignore_case qui contient un booléen. Ensuite, nous avons besoin que la fonction run vérifie la valeur du champ ignore_case et l’utilise pour décider s’il faut appeler la fonction search ou la fonction search_case_insensitive, comme illustré dans l’encart 12-22. Cela ne compilera toujours pas encore.

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

use minigrep::{search, search_case_insensitive};

// --snip--


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

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

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

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

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

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    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, qui est déjà dans la portée en haut de src/main.rs. Nous utiliserons la fonction var du module env pour vérifier si une valeur a été définie pour une variable d’environnement nommée IGNORE_CASE, comme illustré dans l’encart 12-23.

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

use minigrep::{search, search_case_insensitive};

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

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

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

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

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

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

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

Ici, nous créons une nouvelle variable, ignore_case. Pour définir sa valeur, nous appelons la fonction env::var et lui passons le nom de la variable d’environnement IGNORE_CASE. La fonction env::var renvoie un Result qui sera le variant Ok de succès contenant la valeur de la variable d’environnement si celle-ci est définie à une quelconque valeur. Elle renverra le variant Err si la 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 devrait effectuer une recherche insensible à la casse. Si la variable d’environnement IGNORE_CASE n’est définie à rien, is_ok renverra false et le programme effectuera une recherche sensible à la casse. Nous ne nous soucions pas de la valeur de la variable d’environnement, juste de savoir si elle est définie ou non, donc nous vérifions is_ok plutôt que d’utiliser unwrap, expect ou l’une des autres méthodes que nous avons vues sur Result.

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

Essayons ! D’abord, nous exécuterons notre programme sans la variable d’environnement définie et avec la requête to, qui devrait correspondre à toute ligne contenant le mot to entièrement en minuscules : console {{#include ../listings/ch12-an-io-project/listing-12-23/output.txt}}

$ 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!

Il semblerait que ça fonctionne toujours ! Maintenant, exécutons le programme avec IGNORE_CASE défini à 1 mais avec la même requête to :

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

Si vous utilisez PowerShell, vous devrez définir la variable d’environnement et exécuter le programme comme des commandes séparées :

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

Cela fera persister IGNORE_CASE pour le reste de votre session shell. Elle peut être supprimée avec le cmdlet Remove-Item :

PS> Remove-Item Env:IGNORE_CASE

Nous devrions obtenir des lignes contenant to qui pourraient avoir des lettres majuscules :

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

Excellent, nous avons aussi obtenu des lignes contenant To ! Notre programme minigrep peut maintenant effectuer une recherche insensible à la casse contrôlée par une variable d’environnement. Vous savez maintenant comment gérer des options définies en utilisant soit des arguments de ligne de commande, soit des variables d’environnement.

Certains programmes permettent des arguments et des variables d’environnement pour la même configuration. Dans ces cas, les programmes décident que l’un où l’autre à la priorité. Pour un autre exercice de votre côté, essayez de contrôler la sensibilité à la casse via un argument de ligne de commande ou une variable d’environnement. Décidez si l’argument de ligne de commande ou la variable d’environnement devrait avoir la priorité si le programme est exécuté avec l’un défini sur sensible à la casse et l’autre sur insensible à la casse.

Le module std::env contient de nombreuses autres fonctionnalités utiles pour travailler avec les variables d’environnement : consultez sa documentation pour voir ce qui est disponible.