diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 1208320ee6098275e12a220b21519a5257bcca8c..db3a86acc42af5cdcbcc65f9db07bf0051860d06 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -14,4 +14,5 @@ - [Part 09](./part09.md) - [Part 10](./part10.md) - [Part 11](./part11.md) +- [CLI](./cli.md) - [Part 12](./part12.md) diff --git a/book/src/cli.md b/book/src/cli.md new file mode 100644 index 0000000000000000000000000000000000000000..80d0a41f10a7b44a371bac5f71ce51066c228f19 --- /dev/null +++ b/book/src/cli.md @@ -0,0 +1,388 @@ +# Interface en ligne de commande et entrées / sorties + +## Concepts + +Les concepts abordés dans cet exemple sont: + +1. [L'interface en ligne de commande et l'utilisation de librairies externes.](#linterface-à-la-ligne-de-commande-et-lutilisation-de-librairies-externes) +2. [Les entrées / sorties.](#les-entrées--sorties) +3. Une gestion des erreurs plus ergonomique. + +## Discussion + +L'écosystème de Rust contient énormément de librairies ergonomiques et efficaces. Dans ce chapitre, nous +parlerons de [clap](https://github.com/clap-rs/clap), une librairie pour construire des interfaces en ligne de commande. +Nous en profiterons pour discuter également les entrées / sorties et en particulier comment écrire dans des +fichiers. Finalement, nous verrons également avoir une gestion d'erreur un peu plus ergonomique à +l'aide de l'opérateur `?` et de la fonction `map_err()`. + +Vous pouvez trouver plus d'informations aux liens suivants: + +- [Tutoriel pour le pattern `builder`](https://docs.rs/clap/latest/clap/_tutorial/index.html) +- [Tutoriel pour le pattern `derive`](https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html) +- [Chapitre du livre sur les entrées sorties](https://doc.rust-lang.org/book/ch12-00-an-io-project.html) +- [Command Line Applications in Rust](https://rust-cli.github.io/book/index.html) + +## L'interface à la ligne de commande et l'utilisation de librairies externes + +Dans cette section nous allons voir une façon différente +de lire la ligne de commande (par rapport à ce que nous avons fait dans la [partie 07](part07.md)). + +Cette façon de faire est trop complexe pour construire une vraie application et +rajouterait beaucoup d'efforts à chaque fois qu'on veut en reconstruire une: +elle demanderait un parsing long et fastidieux de la ligne de commande manuel. +La [librairie clap](https://github.com/clap-rs/clap), la librairie de CLI la plus populaire +pour Rust, nous permet de de construire une interface pour un programme avec arguments +nommés, optionnel, et un menu d'aide de façon élégante et une bonne gestion des erreurs. + +Afin d'utiliser une librairie externe, il faut l'ajouter comme dépendance dans le fichier `Cargo.toml` +de notre projet. Pour ce faire, il y a deux méthodes, et nous allons voir comment cela fonctionne pour `clap`: + +1. Ajouter la ligne +```toml +clap = { version = "4.4.0", features = ["derive"] } +``` +sous l'entrée `[dependecies]`. + +2. Utiliser l'outil `cargo` qui le fait pour nous +```bash +cargo add clap --features derive +``` +Il y a énormément de fonctionnalités dans l'outil [cargo](https://doc.rust-lang.org/cargo/index.html). + +Ces deux façons sont équivalentes. Lors de l'ajout manuel, on doit choisir la version manuellement qu'on veut mettre dans +le fichier `Cargo.toml` (cela permet de figer une version) ou on peut remplacer `4.4.0` par `*` pour avoir toujours +la dernière version de la `crate` à utiliser. Cependant cette façon de faire n'est pas recommandée, car +cela peut "casser" la compilation lors d'une mise à jour majeure (ou avoir des effets de sécurité +indésirables). + +On note également, qu'on a un champs `features` qui est optionnel, mais qui ici est mis à `derive`. +Le langage Rust permet d'omettre une partie des fonctionnalités d'une librairies qui sont ajoutées +à l'aide d'annotations lorsque la `feature` est activée. Nous n'entrerons pas dans les détails de ces annotations, +mais avons besoin de la feature `derive` pour compiler notre code. + +Nous pouvons à présent commencer à écrire nos fonctions pour lire la ligne de commande +à l'aide de la librairie `clap`. Nous allons voir deux façons différentes +de créer une telle interface avec la librairie: les pattern [`builder`](#le-builder-pattern) et [`derive`](#le-derive-pattern) (c'est pour +ce dernier que nous avons besoin de la feature `derive`). + +Le but de cette interface à la ligne de commande est pour l'utilisateur·trice de pouvoir +choisir les options suivantes pour notre petit programme de calcul de minimum +dans une liste. + +1. Entrer à la main une liste de nombres. +2. Créer une liste de `count` nombres aléatoires qui seront lus depuis `/dev/urandom`. +3. Écrire la liste de nombres et le minimum de la liste dans un fichier de sortie (en en fournissant le nom) ou sur l'écran. + +Il faut noter que l'option 1 et 2 son mutuellement exclusives. L'option 3 +écrira dans un fichier uniquement si un nom de fichier est fourni par l'utilisateur·trice. + +Il est fondamental que si les entrées sont mal formatées (on ne donne pas des nombres p.ex.) +ou si on essaie d'utiliser les options 1 et 2 en même temps, on ait un bon traitement de l'erreur +et un message d'erreur lisible. + +Des tentatives d'exécution typiques seraient +```bash +$ cargo run -- --numbers 1 2 3 4 5 6 +``` +où on donne une liste de nombres après l'option `--numbers` ou encore +```bash +$ cargo run -- --output fichier.txt --numbers 1 2 3 4 5 6 +``` +où `--output` permet de spécifier un nom de fichier. En revanche, on **doit** avoir une erreur +si on essaie de faire +```bash +$ cargo run -- --count 10 --numbers 1 2 3 4 5 6 +``` +car on ne veut pas pouvoir générer deux listes de nombres, mais une seule. + +Ainsi on a trois arguments possibles et tous sont optionnels, mais deux sont exclusifs. + +### Le `builder` pattern + +Nous allons voir à présent comment construire une interface en ligne de commande à proprement parler avec `clap`. Pour ce faire et comprendre le fonctionnement interne de la librairie +nous allons d'abord étudier le `builder` pattern, qui consiste à construire l'interface +à l'aide de fonctions qui vont construire petit à petit notre application. + +La fonction qui va faire tourner notre application se trouve dans `src/io.rs` +et a la signature suivante +```rust,ignore +pub fn read_command_line_builder() -> Result<(), String> +``` +On remarque qu'elle ne prend aucun argument en paramètre et qu'elle retourne un `Result<(), +String>`. En d'autres termes, si tout s'est bien passé, nous ne retournons "rien". Dans l'éventualité +où quelque chose ne s'est pas passé comme prévu, nous retournons une chaîne de caractères +qui contiendra un message d'erreur. + +Le création de la "commande" se trouve dans le code +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:matches}} +``` +Ici nous effectuons diverses opérations. Nous commençons par créer une nouvelle commande +dont le nom est `cli`[^1] +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:consts}} +{{#include ../../codes/rust_lang/cli/src/io.rs:new_command}} +``` +avec différents composants optionnels, comme le nom de l'auteur de l'application, +sa version, etc. Cela permet maintenant d'ajouter les arguments sur cette application. + +Comme discuté plus haut nous voulons *trois* arguments (`numbers`, `output`, et `count`) qui sont ajouté dans un ordre +qui n'a aucune importance. +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:new_command}} +``` +L'appel à la méthode `.arg()` nous permet d'ajouter un nouvel argument, créé avec l'appel à +```rust,ignore +Arg::new(id) +``` +où `id` est une chaîne de caractères qui permet d'identifier de façon unique l'argument. +Puis viennent toutes les propriétés de notre argument: + +- `short('n')`: l'option peut être nommée `-n`, +- `long("numbers")`: l'option peut être nommée `--numbers`, permettant d'appeler le programme avec +```bash +$ cargo run -- --numbers 1 2 3 4 5 6 +$ cargo run -- -n 1 2 3 4 5 6 +``` +- `help("A list of i32 numbers")`: le message d'aide si nous appelons +```bash +⋊> ~/g/p/r/c/r/cli on 25-cli-i-o ⨯ cargo run -- --help +``` +```console +Finished dev [unoptimized + debuginfo] target(s) in 0.01s + Running `target/debug/cli --help` +Usage: cli [OPTIONS] + +Options: + -n, --numbers <numbers>... A list of i32 numbers + -c, --count <count> How many random numbers we want? + -o, --output <output> Should we write output in a file? + -h, --help Print help + -V, --version Print version +``` +- `num_args(1..)`: qui permet d'avoir plusieurs valeurs dans l'argument et savons qu'il doit y en avoir plus d'un. Sans cet argument, l'appel +```bash +$ cargo run -- --numbers 1 2 3 4 5 6 +``` +considérerait le `2` comme la valeur d'un autre argument et ne ferait pas partie de `numbers`. +Puis viennent encore +- `allow_negative_numbers(true)`: pour autoriser les nombres négatifs (sinon `-` est parsé comme nouvel argument) +- `value_parser(value_parser!(i32))`: on ne veut que des `i32` (les nombres à virgules, les lettres, etc sont automatiquement rejetées et un message d'erreur est affiché) +- `required(false)`: est-ce que l'argument est obligatoire (ici ce n'est pas le cas)? En d'autres termes est-ce l'exécution suivante est valide? +```bash +$ cargo run -- # sans option numbers +``` +Si l'argument est `required(true)` alors il est nécessaire de spécifier l'option sinon on aura un message d'erreur. + +Dans la suite du code on crée encore deux arguments `count` et `output`. Nous avons déjà couvert +les différentes fonctions appelée, à l'exception d'une: +- `conflicts_with("numbers")`: ici nous spécifions que l'argument `count` ne peut pas être +présent **en même temps** que l'argument `numbers` (peu importe l'ordre d'appel). Ainsi si nous essayons d'exécuter +```bash +$ cargo run -- --numbers 1 2 3 4 5 6 -c 6 +``` +nous aurons le message d'erreur +```console +error: the argument '--numbers <numbers>...' cannot be used with '--count <count>' + +Usage: cli --numbers <numbers>... + +For more information, try '--help' +``` + +Après avoir construit les arguments, nous devons appeler la fonction `get_matches()` qui +termine la construction de la commande et vérifie s'il n'y a pas d'arguments qui sont contradictoires (un message d'erreur sera produit à l'exécution si cela est le cas). + +Lorsque ce code est exécuté, notre programme peut maintenant parser la ligne de commande +lorsqu'il est exécuté. A nous maintenant d'utiliser correctement les différents arguments. +Ici, nous devons traiter deux "groupes" d'arguments: +- `numbers` et `count` qui sont deux options exclusives, +- `output` qui est optionnel également. + +Le traitement de +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:numbers_matches}} +``` +crée la liste de nombre que nous voulons avoir pour calculer le minimum. + +Comme les deux arguments sont optionnels, nous voyons que pour les déstructurer, +il faut passer par une construction `if let Some() = ...`. +Dans le cas de l'argument `count` nous savons que nous voulons un `usize` dont l'identifiant +est `"count"`. Si l'argument est présent, +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:get_one_matches}} +``` +retourne `Some(&usize)` (nous obtenons une référence vers l'argument) +et nous appellerons la fonction `read_from_urandom()` (que nous discuterons plus bas). Sinon, nous devons vérifier si l'argument `numbers` est présents +et quelles valeurs lui sont assignées. Ainsi, si l'argument est présent +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:get_many_matches}} +``` +retournera un nombre arbitraire de références d'entiers, qui seront ensuite +transformés en `Vec<i32>` à l'aide de la ligne +```rust,ignore +numbers.copied().collect() +``` +qui commence par faire une copie des valeurs de la liste de références pour pouvoir +en devenir les propriétaires, puis les met dans un `Vec<i32>` (il faut noter que le type +de `numbers` est *inféré* grâce au reste du code). A la fin de ce `if let Some() = ...` nous retournons un `Vec<i32>` (qui peut être vide) +qui sera utilisé dans la suite de la fonction. + +Il nous reste à décider si nous allons écrire les sorties de notre programme (la liste de nombre et son minimum) dans un fichier ou dans la sortie standard à l'aide du code +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:output_matches}} +``` +Rien de très nouveau ici, si `"output"` est présent +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:get_one_string_matches}} +``` +nous pouvons déstructurer le retour de la fonction et obtenir le nom du fichier +dans lequel nous allons écrire dans la fonction `write_to_file()` ou le cas échant +écrire dans la sortie standard . + +#### Gestion d'erreur un peu simplifiée + +Aussi bien les appels à `read_from_urandom()` que `write_to_file()` sont suivis d'un `?`. +Ces fonctions doivent manipuler des fichiers et peuvent donc échouer à n'importe quel moment +(si le fichier n'existe pas, s'il ne peut être créé, etc). Elles retournent +donc des `Result`. L'opérateur `?` en Rust est très pratique. Il est utilisé pour répercuter +les erreurs de façon courte dans les fonctions. En gros il répond à la question: +"Est-ce que la fonction a retourné `Ok()` ou `Err()`?" Si la réponse est `Ok()` il retourne ce +qui est contenu dans le `Ok()` qui peut être assigné à une variable (ou retourné de la fonction). +En revanche si la réponse est `Err()`, on retourne l'erreur de la fonction courante. +Cet opérateur permet d'éviter d'alourdi le code avec du pattern matching à chaque appel +qui peut échouer et est très utilisé dans le code Rust. + +### Le `derive` pattern + +Nous avons vu en grand détail comment construire une commande avec un `builder` +design. Nous allons voir à présent de façon très succincte comment faire la même +chose avec le pattern `derive`. Ici, tout le code écrit plus haut sera généré +pour nous à l'aide de `macro` Rust, et nous avons uniquement besoin de spécifier +ce qui doit être généré. Afin de créer une interface en ligne de commande nous devons uniquement +créer une `struct` annotée +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:derive}} +``` +qui contient différent champs qui sont annotés (ou pas). + +La ligne +```rust,ignore +#[derive(Parser)] +``` +va dire au compilateur de Rust de générer automatiquement tout le parsing d'arguments +en fonction de ce qu'il va trouver dans la structure en dessous, ici `CliMin`. +Puis vient la commande préprocesseur +```rust,ignore +#[command(author, version, about, long_about = None)] +``` +qui va créer la nouvelle commande à proprement parler. Elle correspond à +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:new_command}} +``` +A la différence de cette ligne où on spécifie explicitement l'auteur, etc., ici, +le contenu du champs author, version, etc. est directement récupéré du `Cargo.toml`. +Ensuite la structure `CliMin` possède trois membres: `numbers`, `count`, et `output` +qui sont annotés avec un `#[arg()]`. +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:arg}} +``` +Cette annotation va dire au compilateur de générer +automatiquement le code pour toutes les paires clés valeurs se trouvant entre les parenthèses. +Si une valeur est absente alors un comportement par défaut est appliqué. +Ainsi, `long` va générer automatiquement que l'option en version longue pour la variable `numbers` est `--numbers`. De façon similaire, par défaut la version `short` de +numbers sera `-n` (la première lettre de `numbers`). Cette façon de faire par défaut permet de réduire la quantité de code. Par contre, elle est également dangereuse, car si deux +champs commencent par le même nom, seul le premier aura le `short` qui lui correspondra. + +Le reste des arguments correspondent à toutes les méthodes de la version `builder` +vues dans le chapitre précédent. Il y a deux grandes différences: + +1. On ne trouve pas +d'équivalent à `required(false)`. En fait, l'obligation ou non de spécifier un argument +est directement inféré par le type de l'argument: si c'est une `Option` alors l'argument est... +optionnel. +2. `numbers` est directement parsé en `Vec<i32>`, pas besoin de faire des conversions. + +Le reste du code est relativement trivial. Pour utiliser notre interface en ligne de commande +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:read_command_line_derive}} +``` +Il faut appeler la fonction `parse()` +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:parse}} +``` +et les champs `cli.cout`, `cli.numbers`, et `cli.output` seront automatiquement assignés +aux valeurs dans la `cli` si elles sont compatibles avec les formats spécifiés dans les +arguments correspondants. Sinon des erreurs seront affichées. + +## Les entrées / sorties + +Il y a deux fonctions qui gèrent la lecture / écriture de fichiers dans notre programme. +1. La fonction `read_from_urandom()` permet de lire le fichier `/dev/urandom` qui contient +dans les systèmes unix des "vrais" nombres aléatoires générés par le bruit du système. +2. La fonction `write_to_file()` qui permet d'écrire la liste de nombre, ainsi que le minimum +dans un fichier. Nous allons brièvement discuter ces fonctions, afin de comprendre un peu +mieux comment faire des entrées / sorties depuis le disque en Rust. + +### Lecture de fichier + +Pour la lecture de fichier, nous nous concentrons sur la fonction +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:read_from_urandom}} +``` +L'ouverture d'un fichier en lecture se fait avec la fonction `File::open()` +qui peut échouer si le fichier n'existe pas par exemple. Dans le cas d'une +erreur, nous nous empressons de convertir l'erreur dans un message d'erreur +avec la fonction `map_err()` qui prend en argument une fonction anonyme qui a pour argument +ce qui est encapsulé dans le type `Err()` et qui retourne une nouvelle valeur qui sera +encapsulée dans une nouvelle `Err()`. Cette façon de faire n'est pas très idiomatique +pour Rust, mais elle nous satisfait pour le moment, afin d'avoir des types de retour homogènes +et de pouvoir utiliser l'opérateur `?` (voir [plus haut](#gestion-derreur-un-peu-simplifiée)). + +Comme nous lisons dans le fichier `/dev/urandom` qui est un flux continu d'octets, nous définissons +une mémoire tampon sur le fichier et allons lire exactement `4 * count` fois octets, soit +exactement l'équivalent de `count` entiers 32-bits soit `i32` +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:read}} +``` +Finalement, notre mémoire tableau `numbers` (qui est rien d'autre qu'une suite d'octets) +est convertie en `Vec<i32>` grâce à la puissance des itérateurs. +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:convert_to_i32}} +``` +Dans un premier temps +le tableau est découpé en tranches de 4 éléments grâce à la méthode `.chunks(4)` (l'itérateur est maintenant une suite +d'itérateurs de 4 éléments). Puis chacun des éléments de l'itérateur (soit 4 octets) +est transformé en `i32`, grâce à la méthode `map(|i| i32::from_be_bytes(i.try_into().unwrap()))`. +Il faut noter qu'ici nous utilisons la fonction `try_into()` qui peut échouer si nous n'avons pas +4 octets à disposition quand nous faisons la conversion. Ici, par construction cela ne peut pas se produire et pouvons `unwrap()` le résultat. Finalement, à l'aide de `collect()`, +nous créons un `Vec<i32>` à partir de l'itérateur obtenu et l'encapsulons dans un `Ok()`, +car le résultat de notre fonction est un succès, si tout s'est bien passé. + +### Écriture dans un fichier + +Pour l'écriture dans un fichier, nous nous concentrons sur la fonction +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:write_to_file}} +``` +Nous commençons par créer un fichier à l'aide de la fonction `File::create()` +qui prend en argument le chemin où créer le fichier. +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:create}} +``` +Si un fichier existe déjà +il est écrasé par défaut. Si la création est impossible, une erreur est retournée. +De plus le fichier est ouvert en mode écriture. Il faut noter que nous allons **modifier le fichier**, et il est donc **mutable**. +Ainsi, nous pouvons écrire dans le fichier à l'aide des macros `write!` et `writeln!` +qui s'utilisent comme `print!` et `println!` à l'exception qu'elles prennent +des fichier en argument et retournent une erreur en cas d'échec. +```rust,ignore +{{#include ../../codes/rust_lang/cli/src/io.rs:write}} +``` +A nouveau, toutes les erreurs sont transformées en messages (des chaînes de caractères) +pour simplifier les concepts abordés dans ce code. + +[^1]: Cet identifiant permet d'identifier de façon unique la commande +dans le cas où nous en créerions plusieurs dans la même application ce qui n'est pas le cas +ici. \ No newline at end of file diff --git a/codes/rust_lang/cli/Cargo.toml b/codes/rust_lang/cli/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..9c4c4a46016670aa7335153995bbbf12d46beb70 --- /dev/null +++ b/codes/rust_lang/cli/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.4.0", features = ["derive"] } diff --git a/codes/rust_lang/cli/src/io.rs b/codes/rust_lang/cli/src/io.rs new file mode 100644 index 0000000000000000000000000000000000000000..47d75e27cfc1a00ccccac85cd83f1763ee1e7080 --- /dev/null +++ b/codes/rust_lang/cli/src/io.rs @@ -0,0 +1,185 @@ +use std::{ + fs::File, + io::{BufReader, Read, Write}, +}; + +// ANCHOR: use_clap +use clap::{value_parser, Arg, Command, Parser}; +// ANCHOR_END: use_clap + +// ANCHOR: consts +const COMMAND: &str = "cli"; +const AUTHOR: &str = "Orestis Malaspinas"; +const VERSION: &str = "0.1.0"; +// ANCHOR_END: consts + +use crate::something_or_nothing::find_min; + +// ANCHOR: read_from_urandom +fn read_from_urandom(count: usize) -> Result<Vec<i32>, String> { + // ANCHOR: open + let file = File::open("/dev/urandom").map_err(|_| "Could not open /dev/urandom")?; + // ANCHOR_END: open + // ANCHOR: read + let mut buf_reader = BufReader::new(file); + let mut numbers = vec![0; count * 4]; + buf_reader + .read_exact(&mut numbers) + .map_err(|_| "Could not read numbers")?; + // ANCHOR_END: read + // ANCHOR: convert_to_i32 + Ok(numbers + .chunks(4) + .map(|i| i32::from_be_bytes(i.try_into().unwrap())) + .collect::<Vec<_>>()) + // ANCHOR_END: convert_to_i32 +} +// ANCHOR_END: read_from_urandom + +// ANCHOR: write_to_file +fn write_to_file(output: &str, numbers: &[i32]) -> Result<(), String> { + // ANCHOR: create + let mut file = File::create(output).map_err(|_| format!("Failed to create {output}"))?; + // ANCHOR_END: create + // ANCHOR: write + writeln!(file, "Among the Somethings in the list:") + .map_err(|_| "Failed to write header into file.")?; + for n in numbers { + write!(file, "{n} ").map_err(|_| format!("Failed to write {n} into file."))?; + } + writeln!(file,).map_err(|_| "Failed to write carriage return into file.")?; + writeln!(file, "{}", find_min(numbers).to_string()) + .map_err(|_| "Failed to write minimum value into file.")?; + // ANCHOR_END: write + Ok(()) +} +// ANCHOR_END: write_to_file + +/// Reads i32 from the command line and returns a [Vec] containing +/// these numbers. Returns errors when the parsing fails. +pub fn read_command_line_builder() -> Result<(), String> { + // ANCHOR: matches + let matches = + // ANCHOR: new_command + Command::new(COMMAND) + .author(AUTHOR) + .version(VERSION) + // ANCHOR_END: new_command + // ANCHOR: new_args + .arg( + Arg::new("numbers") // id + .short('n') // version courte -n + .long("numbers") // ou longue --numbers + .help("A list of i32 numbers") // l'aide + .num_args(1..) // combien il y a d'entrées + .allow_negative_numbers(true) // on peut avoir des négatifs + .value_parser(value_parser!(i32)) // on veut s'assurer que ça soit des nombres + .required(false), // optionnel + ) + .arg( + Arg::new("count") + .short('c') + .long("count") + .help("How many random numbers we want?") + .value_parser(value_parser!(usize)) + .conflicts_with("numbers") // impossible d'avoir -c et -n + .required(false), + ) + .arg( + Arg::new("output") + .short('o') + .long("output") + .help("Should we write output in a file?") + .required(false), + ) + // ANCHOR: new_args + .get_matches(); + // ANCHOR_END: matches + + // ANCHOR: numbers_matches + let numbers = + if let Some(count) = + // ANCHOR: get_one_matches + matches.get_one::<usize>("count") + // ANCHOR_END: get_one_matches + { + read_from_urandom(*count)? + } else if let Some(numbers) = + // ANCHOR: get_many_matches + matches.get_many::<i32>("numbers") + // ANCHOR_END: get_many_matches + { + numbers.copied().collect() + } else { + Vec::new() + }; + // ANCHOR_END: numbers_matches + + // ANCHOR: output_matches + if let Some(output) = + // ANCHOR: get_one_string_matches + matches.get_one::<String>("output") + // ANCHOR_END: get_one_string_matches + { + write_to_file(output, &numbers)?; + } else { + println!("Among the Somethings in the list:"); + print_tab(&numbers); + println!("{}", find_min(&numbers).to_string()); + } + // ANCHOR_END: output_matches + + Ok(()) +} + +/// Does not compile without the feature derive +// ANCHOR: derive +#[derive(Parser)] +// ANCHOR: command +#[command(author, version, about, long_about = None)] +// ANCHOR_END: command +struct CliMin { + // ANCHOR: arg + #[arg(short, long, help = "A list of i32 numbers", num_args=1.., allow_negative_numbers=true, value_parser = clap::value_parser!(i32))] + numbers: Option<Vec<i32>>, + // ANCHOR_END: arg + #[arg(short, long, help = "How many random numbers we want?", value_parser = clap::value_parser!(usize), conflicts_with = "numbers")] + count: Option<usize>, + #[arg(short, long, help = "Filename for writing the numbers.")] + output: Option<String>, +} +// ANCHOR_END: derive + +/// Reads i32 from the command line and returns a [Vec] containing +/// these numbers. Returns errors when the parsing fails. +// ANCHOR: read_command_line_derive +pub fn read_command_line_derive() -> Result<(), String> { + // ANCHOR: parse + let cli = CliMin::parse(); + // ANCHOR_END: parse + let numbers = if let Some(count) = cli.count { + read_from_urandom(count)? + } else if let Some(numbers) = cli.numbers { + numbers + } else { + Vec::new() + }; + if let Some(output) = cli.output { + write_to_file(&output, &numbers)?; + } else { + println!("Among the Somethings in the list:"); + print_tab(&numbers); + println!("{}", find_min(&numbers).to_string()); + } + Ok(()) +} +// ANCHOR_END: read_command_line_derive + +/// Prints all the elements of the `tab`. +/// Tab is borrowed here +pub fn print_tab(tab: &Vec<i32>) { + for t in tab { + print!("{} ", t); + } + println!(); +} diff --git a/codes/rust_lang/cli/src/lib.rs b/codes/rust_lang/cli/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..eaabc144a5c0a8c7f7312def661b1e86484bfc37 --- /dev/null +++ b/codes/rust_lang/cli/src/lib.rs @@ -0,0 +1,59 @@ +/*! +cli illustrates the use of [Vec] and the Error Handling with [Option] and [Result]. +It also showcases struct enums. +*/ + +pub mod io; +mod minimum; +pub mod something_or_nothing; + +#[cfg(test)] +mod tests { + use crate::minimum::Minimum; + use crate::something_or_nothing::{find_min, SomethingOrNothing}; + + #[test] + fn test_creation() { + let n1: SomethingOrNothing<i32> = SomethingOrNothing::default(); + assert!(n1 == SomethingOrNothing::default()); + let n2: SomethingOrNothing<i32> = SomethingOrNothing::new(1); + assert!(n2 == SomethingOrNothing::new(1)); + } + + #[test] + #[should_panic] + fn test_failure_creation() { + let n2: SomethingOrNothing<i32> = SomethingOrNothing::new(1); + assert!(n2 == SomethingOrNothing::default()); + assert!(n2 == SomethingOrNothing::new(2)); + } + + #[test] + fn test_min() { + let a = vec![1, 5, -1, 2, 0, 10, 11, 0, 3]; + let min = find_min(&a); + assert!(min == SomethingOrNothing::new(-1)); + } + + #[test] + fn test_min_i32() { + let x = 5; + let y = 10; + assert_eq!(Minimum::min(x, y), x); + assert_eq!(Minimum::min(y, x), x); + assert_eq!(Minimum::min(x, x), x); + assert_eq!(Minimum::min(y, y), y); + } + + #[test] + fn test_min_something_or_nothing() { + let x = SomethingOrNothing::new(5i32); + let y = SomethingOrNothing::new(10i32); + let z = SomethingOrNothing::default(); + assert!(x.min(y) == x); + assert!(y.min(x) == x); + assert!(z.min(y) == y); + assert!(y.min(z) == y); + assert!(z.min(z) == z); + } +} diff --git a/codes/rust_lang/cli/src/main.rs b/codes/rust_lang/cli/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..f0a0a1a29abde0af7daa44bd8cad68507dc7a68c --- /dev/null +++ b/codes/rust_lang/cli/src/main.rs @@ -0,0 +1,6 @@ +use cli::io; + +fn main() -> Result<(), String> { + io::read_command_line_builder()?; + Ok(()) +} diff --git a/codes/rust_lang/cli/src/minimum.rs b/codes/rust_lang/cli/src/minimum.rs new file mode 100644 index 0000000000000000000000000000000000000000..7d7a654b8f2c43bc76e35616813465c6142a0bff --- /dev/null +++ b/codes/rust_lang/cli/src/minimum.rs @@ -0,0 +1,15 @@ +// If we remove Copy, we have a problem with the t in tab +// in the computation of the minimum. +pub trait Minimum: Copy { + fn min(self, rhs: Self) -> Self; +} + +impl Minimum for i32 { + fn min(self, rhs: Self) -> Self { + if self < rhs { + self + } else { + rhs + } + } +} diff --git a/codes/rust_lang/cli/src/something_or_nothing.rs b/codes/rust_lang/cli/src/something_or_nothing.rs new file mode 100644 index 0000000000000000000000000000000000000000..75ffecc1f05373eafb2701c20308b149c0651a94 --- /dev/null +++ b/codes/rust_lang/cli/src/something_or_nothing.rs @@ -0,0 +1,80 @@ +use std::fmt::Display; + +use crate::minimum::Minimum; + +/// An generic enumerated type that encapsulates and Option<T>. +#[derive(Clone, Copy)] +pub struct SomethingOrNothing<T>(Option<T>); + +impl<T: Minimum + Display> SomethingOrNothing<T> { + pub fn new(val: T) -> Self { + SomethingOrNothing(Some(val)) + } + /// A static function that prints the content of a SomethingOrNothing. + pub fn to_string(&self) -> String { + match self.0 { + None => String::from("Nothing."), + Some(val) => format!("Something is: {}", val), + } + } +} + +impl<T> Default for SomethingOrNothing<T> { + /// By Default a [SomethingOrNothing] is a nothing. + fn default() -> Self { + SomethingOrNothing(None) + } +} + +impl<T: PartialEq + Minimum> PartialEq for SomethingOrNothing<T> { + fn eq(&self, other: &Self) -> bool { + match (self.0, other.0) { + (None, None) => true, + (Some(lhs), Some(rhs)) => lhs == rhs, + _ => false, + } + } +} + +impl<T: Minimum + Display> Minimum for SomethingOrNothing<T> { + fn min(self, rhs: Self) -> Self { + match (self.0, rhs.0) { + (None, None) => SomethingOrNothing(None), + (Some(lhs), Some(rhs)) => SomethingOrNothing::new(lhs.min(rhs)), + (None, Some(rhs)) => SomethingOrNothing::new(rhs), + (Some(lhs), None) => SomethingOrNothing::new(lhs), + } + } +} + +/// Computes the minimum of an Array of a type T which implements the [Minimum] trait. +/// Returns a [Some] containing the the minimum value +/// or [None] if no minimum value was found. +/// +/// # Examples +/// +/// ``` +/// # use cli::something_or_nothing::{SomethingOrNothing, find_min}; +/// # fn main() { +/// let tab = vec![10, 32, 12, 43, 52, 53, 83, 2, 9]; +/// let min = find_min(&tab); +/// assert!(min == SomethingOrNothing::new(2)); +/// # } +/// ``` +/// +/// ``` +/// # use cli::something_or_nothing::{SomethingOrNothing, find_min}; +/// # fn main() { +/// let tab: Vec<i32> = vec![]; +/// let min = find_min(&tab); +/// assert!(min == SomethingOrNothing::default()); +/// # } +/// ``` +pub fn find_min<T: Minimum + Display>(tab: &[T]) -> SomethingOrNothing<T> { + let mut minimum: SomethingOrNothing<T> = SomethingOrNothing(None); + // Here is T is Copyable. Which means that t is not moved in the loop + for t in tab { + minimum = minimum.min(SomethingOrNothing::new(*t)); + } + minimum +}