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