Skip to content

Commit 206fe29

Browse files
committed
update original
1 parent 35d75f5 commit 206fe29

6 files changed

Lines changed: 353 additions & 5 deletions

File tree

rust-cookbook/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [Command Line](cli.md)
1717
- [Argument Parsing](cli/arguments.md)
1818
- [ANSI Terminal](cli/ansi_terminal.md)
19+
- [Environment Variables](cli/env.md)
1920
- [Compression](compression.md)
2021
- [Working with Tarballs](compression/tar.md)
2122
- [Concurrency](concurrency.md)

rust-cookbook/src/cli.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
| Recipe | Crates | Categories |
44
|--------|--------|------------|
5-
| [Parse command line arguments][ex-clap-basic] | [![clap-badge]][clap] | [![cat-command-line-badge]][cat-command-line] |
5+
| [Parse command line arguments][ex-argument-basic] | [![std-badge]][std] | [![cat-command-line-badge]][cat-command-line] |
6+
| [Clap Basic Argument Parsing][ex-argument-clap-basic] | [![clap-badge]][clap] | [![cat-command-line-badge]][cat-command-line] |
67
| [ANSI Terminal][ex-ansi_term-basic] | [![ansi_term-badge]][ansi_term]| [![cat-command-line-badge]][cat-command-line] |
8+
| [Environment Variables][ex-env] | [![std-badge]][std]| [![cat-command-line-badge]][cat-command-line] |
9+
| [Load Config Example][ex-load-config] | [![std-badge]][std]| [![cat-command-line-badge]][cat-command-line] |
710

8-
[ex-clap-basic]: cli/arguments.html#parse-command-line-arguments
911
[ex-ansi_term-basic]: cli/ansi_terminal.html#ansi-terminal
12+
[ex-argument-basic]: cli/arguments.html#basic-argument-parsing
13+
[ex-argument-clap-basic]: cli/arguments.html#clap-basic
14+
[ex-env]: cli/env.html
15+
[ex-load-config]: cli/env.html#loading-config-from-the-environment
1016

11-
{{#include links.md}}
17+
{{#include links.md}}

rust-cookbook/src/cli/arguments.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# Clap basic
1+
# Parse command line arguments
22

3+
{{#include arguments/basic.md}}
34
{{#include arguments/clap-basic.md}}
45

56
{{#include ../links.md}}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
## Basic Argument Parsing
2+
This section covers a basic implementation of commandline argument parsing.
3+
Use [`args`] to get the arguments this process was started with:
4+
5+
```rust,edition2024
6+
use std::env;
7+
8+
fn main() {
9+
let args: Vec<String> = env::args().collect();
10+
assert!(!args.is_empty());
11+
12+
// Skip the program name, it's usually the first argument
13+
let args: Vec<String> = env::args().skip(1).collect();
14+
}
15+
```
16+
17+
Some args, e.g. file paths, may not be valid UTF-8. Use [`args_os`] to handle those safely:
18+
19+
```rust,edition2024
20+
use std::{env, ffi::OsString};
21+
22+
fn main() {
23+
let os_args: Vec<OsString> = env::args_os().collect();
24+
assert!(!os_args.is_empty());
25+
}
26+
```
27+
28+
## Calculate File Stats
29+
In this section we parse CLI args and compute per-file stats (line/word counts). A mini [`wc`]-like
30+
utility
31+
32+
Flags:
33+
- --lines, -l Count lines per file
34+
- --words, -w Count words per file
35+
- --paths, -p Remaining args treated as file paths
36+
37+
> `-p` acts as a terminator. Everything after it is consumed as file paths and parsing stops
38+
> immediately.
39+
40+
`CliArgs` holds the parsed state of the process args. It uses [`args_os`] over [`args`] to
41+
safely handle non-UTF-8 paths, which can occur on Linux. [`skip(1)`] drops the bin name since it is
42+
always the first arg. Once `-p` flag is is encountered, all remaining args are collected into paths.
43+
44+
`Stats<'p>` represents the computed result for a single file. It borrows filepath directly from
45+
`CliArgs` rather than cloning it, with the lifetime 'p tying each Stats instance to its source
46+
`CliArgs`. Both `lines` and `words` are `Option<usize>`, where `None` means the flag was absent and
47+
the stat was never computed, while `Some(n)` means the flag was set and the count is `n`. This
48+
distinction matters because it avoids confusing "not requested" with a legitimate count of zero.
49+
50+
`stat_files` is the core logic. Iterates over `cli_args.paths` and builds a `Vec<Stats<'_>>`.
51+
Non-files and IO errors are skipped silently via `continue` for simplicity. Each file is read
52+
to a string buffer with fs::[`read_to_string()`] for processing. Line count uses
53+
content.[`lines()`].[`count()`]. Word count splits each line on whitespace via
54+
[`split_whitespace()`], counts tokens per line, then sums across all lines, which correctly handles
55+
tabs and multiple consecutive spaces. Both stats are computed only if their respective flag was set,
56+
so there is no wasted work.
57+
58+
The result is output as one line per file. Stats omitted by the user are excluded from the output
59+
entirely rather than printed as zero or a placeholder.
60+
61+
```rust,edition2024,no_run
62+
use std::env;
63+
use std::fs;
64+
use std::path::{Path, PathBuf};
65+
66+
// Represents the arguments passed to this process during initialization
67+
#[derive(Debug)]
68+
struct CliArgs {
69+
lines: bool,
70+
words: bool,
71+
paths: Vec<PathBuf>,
72+
}
73+
74+
impl CliArgs {
75+
fn parse() -> Self {
76+
let mut lines = false;
77+
let mut words = false;
78+
let mut paths = Vec::new();
79+
80+
let args = env::args_os().skip(1);
81+
let mut args = args.into_iter();
82+
while let Some(arg) = args.next() {
83+
match arg.to_str().unwrap_or_default() {
84+
"-l" | "--lines" => lines = true,
85+
"-w" | "--words" => words = true,
86+
"-p" | "--paths" => {
87+
paths.extend(args.by_ref().map(PathBuf::from));
88+
break;
89+
}
90+
_ => {}
91+
}
92+
}
93+
94+
CliArgs {
95+
lines,
96+
words,
97+
paths,
98+
}
99+
}
100+
}
101+
102+
// Represents the statistics of a given filepath
103+
struct Stats<'p> {
104+
filepath: &'p Path,
105+
lines: Option<usize>,
106+
words: Option<usize>,
107+
}
108+
109+
fn stat_files(cli_args: &CliArgs) -> Vec<Stats<'_>> {
110+
let mut stat_collection: Vec<Stats> = Vec::new();
111+
// If no flag is set, do no work
112+
if !cli_args.words && !cli_args.lines {
113+
return stat_collection;
114+
}
115+
116+
for filepath in &cli_args.paths {
117+
// Skip anything that ain't a file
118+
if !filepath.is_file() {
119+
continue;
120+
}
121+
122+
let Ok(content) = fs::read_to_string(filepath) else {
123+
continue;
124+
};
125+
126+
let lines = match cli_args.lines {
127+
true => Some(content.lines().count()),
128+
false => None,
129+
};
130+
131+
let words = match cli_args.words {
132+
true => Some(
133+
content
134+
.lines()
135+
.map(|line| line.split_whitespace().count())
136+
.sum(),
137+
),
138+
false => None,
139+
};
140+
141+
let stats = Stats {
142+
filepath,
143+
lines,
144+
words,
145+
};
146+
147+
stat_collection.push(stats);
148+
}
149+
150+
stat_collection
151+
}
152+
153+
fn main() {
154+
let cli_args = CliArgs::parse();
155+
let stats = stat_files(&cli_args);
156+
// Print individual file statistics
157+
for stat in stats {
158+
println!(
159+
"Path: {:?} {} {}",
160+
stat.filepath,
161+
if let Some(words) = stat.words {
162+
format!("Words: {}", words)
163+
} else {
164+
"".to_string()
165+
},
166+
if let Some(lines) = stat.lines {
167+
format!("Lines: {}", lines)
168+
} else {
169+
"".to_string()
170+
},
171+
);
172+
}
173+
}
174+
```
175+
176+
Example Usage:
177+
178+
```bash
179+
cargo run --release -- -l -w -p src/main.rs Cargo.toml
180+
# Path: "src/main.rs" Words: 312 Lines: 84
181+
# Path: "Cargo.toml" Words: 21 Lines: 9
182+
183+
cargo run --release -- -l -p src/main.rs
184+
# Path: "src/main.rs" Lines: 84
185+
```
186+
187+
[`args_os`]: https://doc.rust-lang.org/std/env/fn.args_os.html
188+
[`args`]: https://doc.rust-lang.org/std/env/fn.args.html
189+
[`count()`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.count
190+
[`env`]: https://doc.rust-lang.org/std/env/index.html
191+
[`lines()`]: https://doc.rust-lang.org/std/primitive.str.html#method.lines
192+
[`read_to_string()`]: https://doc.rust-lang.org/std/fs/fn.read_to_string.html
193+
[`skip(1)`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.skip
194+
[`split_whitespace()`]: https://doc.rust-lang.org/std/primitive.str.html#method.split_whitespace
195+
[`wc`]: https://www.man7.org/linux/man-pages/man1/wc.1.html

rust-cookbook/src/cli/arguments/clap-basic.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Parse command line arguments
1+
## Clap Basic
22

33
[![clap-badge]][clap] [![cat-command-line-badge]][cat-command-line]
44

rust-cookbook/src/cli/env.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Environment Variables
2+
The [`env`] module lets you inspect and manipulate the process environment, including env vars, CLI
3+
args, and working directories.
4+
5+
## Get Environment Variables
6+
[`var`] fetches an env var from the current process. It returns `Result<String, VarError>`, so you
7+
decide how to handle missing values:
8+
9+
```rust,edition2024,no_run
10+
use std::env;
11+
12+
fn main() {
13+
// If not set, we use a sensible default value
14+
let mode = env::var("MODE").unwrap_or_else(|_| "INFO".to_string());
15+
}
16+
```
17+
18+
## Set Environment Variables
19+
20+
[`set_var`] is safe in single-threaded programs, and always safe on Windows. Avoid it in
21+
multi-threaded programs on other OSes. [`See safety note`].
22+
23+
```rust,edition2024,no_run
24+
use std::env;
25+
26+
fn main() {
27+
// SAFETY: No other thread is currently manipulating the environment
28+
unsafe {
29+
env::set_var("MODE", "debug");
30+
}
31+
}
32+
```
33+
34+
## Remove Environment Variables
35+
36+
Same threading rules apply as [`set_var`]. [`See safety note first`].
37+
38+
```rust,edition2024,no_run
39+
use std::env;
40+
41+
fn main() {
42+
// SAFETY: No other thread is currently manipulating the environment
43+
unsafe {
44+
env::remove_var("MODE");
45+
}
46+
}
47+
```
48+
49+
## Loading Config from the Environment
50+
51+
A common pattern is loading config at startup from env vars, falling back to sensible defaults when
52+
vars are missing or invalid. This approach keeps the program runnable without requiring every var to
53+
be set, which is useful during development.
54+
55+
The chain `.ok().and_then(|v| ...)` is used here intentionally:
56+
- `.ok()` converts Result to Option, discarding the error
57+
- `.and_then()` applies a fallible transform, like parsing, flattening the result
58+
- `.unwrap_or_else(|_| DEFAULT)` then supplies the fallback. [`unwrap_or_else`] only allocates
59+
during the evaluation of the else branch.
60+
61+
```rust,edition2024
62+
use std::env;
63+
64+
#[derive(Debug)]
65+
enum LogLevel {
66+
Info,
67+
Warn,
68+
Error,
69+
Debug,
70+
Trace,
71+
}
72+
73+
// Handles conversion of string env variables to LogLevel enum.
74+
impl TryFrom<String> for LogLevel {
75+
type Error = String;
76+
77+
fn try_from(value: String) -> Result<Self, String> {
78+
let level = match value.to_lowercase().as_str() {
79+
"info" => LogLevel::Info,
80+
"debug" => LogLevel::Debug,
81+
"warn" => LogLevel::Warn,
82+
"error" => LogLevel::Error,
83+
"trace" => LogLevel::Trace,
84+
other => return Err(format!("Unknown log level: {}", other)),
85+
};
86+
Ok(level)
87+
}
88+
}
89+
90+
#[allow(dead_code)]
91+
#[derive(Debug)]
92+
struct Config {
93+
server_url: String,
94+
log_level: LogLevel,
95+
server_port: u16,
96+
}
97+
98+
const DEFAULT_SERVER_URL: &str = "http://127.0.0.1";
99+
const DEFAULT_SERVER_PORT: u16 = 8080_u16;
100+
const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Info;
101+
102+
impl std::default::Default for Config {
103+
fn default() -> Self {
104+
Self {
105+
server_url: DEFAULT_SERVER_URL.into(),
106+
log_level: DEFAULT_LOG_LEVEL,
107+
server_port: DEFAULT_SERVER_PORT,
108+
}
109+
}
110+
}
111+
112+
impl Config {
113+
fn load() -> Self {
114+
let server_url =
115+
env::var("BACKEND_SERVER_URL").unwrap_or_else(|_| DEFAULT_SERVER_URL.into());
116+
let server_port = env::var("BACKEND_SERVER_PORT")
117+
.ok()
118+
.and_then(|v| v.parse::<u16>().ok())
119+
.unwrap_or(DEFAULT_SERVER_PORT);
120+
121+
let log_level = env::var("BACKEND_LOG_LEVEL")
122+
.ok()
123+
.and_then(|v| v.try_into().ok())
124+
.unwrap_or(DEFAULT_LOG_LEVEL);
125+
126+
Config {
127+
server_url,
128+
server_port,
129+
log_level,
130+
}
131+
}
132+
}
133+
134+
fn main() {
135+
let cfg = Config::load();
136+
dbg!(cfg);
137+
}
138+
```
139+
140+
[`See safety note first`]: https://doc.rust-lang.org/std/env/fn.remove_var.html
141+
[`See safety note`]: https://doc.rust-lang.org/std/env/fn.set_var.html
142+
[`env`]: https://doc.rust-lang.org/std/env/index.html
143+
[`set_var`]: https://doc.rust-lang.org/std/env/fn.set_var.html
144+
[`unwrap_or_else`]: https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_else
145+
[`var`]: https://doc.rust-lang.org/std/env/fn.var.html

0 commit comments

Comments
 (0)