Skip to content
Snippets Groups Projects
Commit 6eed50e3 authored by orestis.malaspin's avatar orestis.malaspin
Browse files

Resolve "CLI, I/O"

parent 007df026
Branches
No related tags found
No related merge requests found
......@@ -14,4 +14,5 @@
- [Part 09](./part09.md)
- [Part 10](./part10.md)
- [Part 11](./part11.md)
- [CLI](./cli.md)
- [Part 12](./part12.md)
# 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
```
`--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)
```
`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
[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"] }
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!();
}
/*!
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);
}
}
use cli::io;
fn main() -> Result<(), String> {
io::read_command_line_builder()?;
Ok(())
}
// 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
}
}
}
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
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment