Конзолни приложения
3 декември 2024
Административни неща
- Второто домашно приключи
- Без лекция на 19 декември, четвъртък
IO типове и trait-ове
std::io::Read
,std::io::Write
: Базови trait-ове за четене и писане
IO типове и trait-ове
std::io::Read
,std::io::Write
: Базови trait-ове за четене и писанеFile
,Stdin
,&[u8]
,BufReader
-- имплементиратRead
BufReader
-- имплементираBufRead
IO типове и trait-ове
std::io::Read
,std::io::Write
: Базови trait-ове за четене и писанеFile
,Stdin
,&[u8]
,BufReader
-- имплементиратRead
BufReader
-- имплементираBufRead
File
,Stdout
иStderr
,Vec<u8>
,BufWriter
-- имплементиратWrite
IO типове и trait-ове
Добре е да ползваме trait-ове когато можем.
impl InputFile {
fn from(mut contents: impl io::Read) -> io::Result<InputFile> {
// ...
}
// или:
fn from<R: io::Read>(mut contents: R) -> io::Result<InputFile> {
// ...
}
}
Така, функцията InputFile::from
може да се извика с файл, но може и със stdin, а може и просто със slice от байтове, идващи от някакъв тестов низ.
"Program to an interface, not an implementation" -- често срещан съвет, който често е прав.
Странична бележка
"Program to an interface, not an implementation" -- не е закон, просто добър съвет.
File
vsTcpStream
File
vsVec<u8>
File
vsStdin
/Stdout
File
-- няма име
SemVer и Cargo
Reference: https://doc.rust-lang.org/cargo/reference/semver.html
String
vs &str
fn from_str(s: &str)
-> Защо&str
, а неString
?
String
vs &str
fn from_str(s: &str)
-> Защо&str
, а неString
?- Със
&str
-- можем да извикаме функцията и със&str
, и със&String
from_str("foobar")
from_str(&String::from("foobar"))
String
vs &str
fn from_str(s: &str)
-> Защо&str
, а неString
?- Със
&str
-- можем да извикаме функцията и със&str
, и със&String
from_str("foobar")
from_str(&String::from("foobar"))
- Аналогично, обикновено използваме
&[T]
като вход на функция, а не&Vec<T>
PathBuf
и Path
PathBuf
и Path
std::path::PathBuf
-- стойност със ownership (катоString
,Vec
)std::path::Path
-- нейния "slice" тип (еквивалент на&str
,&[T]
)
PathBuf
и Path
std::path::PathBuf
-- стойност със ownership (катоString
,Vec
)std::path::Path
-- нейния "slice" тип (еквивалент на&str
,&[T]
)
let home_dir = PathBuf::from("/home/andrew");
println!("{:?}", home_dir);
let home_dir_slice: &Path = home_dir.as_path();
println!("{:?}", home_dir_slice);
"/home/andrew" "/home/andrew"
use std::path::{PathBuf,Path}; fn main() { let home_dir = PathBuf::from("/home/andrew"); println!("{:?}", home_dir); let home_dir_slice: &Path = home_dir.as_path(); println!("{:?}", home_dir_slice); }
PathBuf
и Path
let file_path = PathBuf::from("/home/andrew/file.txt");
println!("{:?}", file_path);
let only_file: &Path = file_path.strip_prefix("/home/andrew").unwrap();
println!("{:?}", only_file);
let standalone_path: &Path = Path::new("file.txt");
println!("{:?}", standalone_path);
"/home/andrew/file.txt" "file.txt" "file.txt"
use std::path::{PathBuf,Path}; fn main() { let file_path = PathBuf::from("/home/andrew/file.txt"); println!("{:?}", file_path); let only_file: &Path = file_path.strip_prefix("/home/andrew").unwrap(); println!("{:?}", only_file); let standalone_path: &Path = Path::new("file.txt"); println!("{:?}", standalone_path); }
PathBuf
и Path
let file_path: &Path = Path::new("file.txt");
println!("{:?}", file_path.to_path_buf());
println!("{:?}", file_path.to_owned());
println!("{:?}", file_path.canonicalize());
"file.txt" "file.txt" Err(Os { code: 2, kind: NotFound, message: "No such file or directory" })
use std::path::Path; fn main() { let file_path: &Path = Path::new("file.txt"); println!("{:?}", file_path.to_path_buf()); println!("{:?}", file_path.to_owned()); println!("{:?}", file_path.canonicalize()); }
Debug
& Display
println!("{:?}", PathBuf::from("file.txt"));
"file.txt"
use std::path::PathBuf; fn main() { println!("{:?}", PathBuf::from("file.txt")); }
println!("{}", PathBuf::from("file.txt"));
error[E0277]: `PathBuf` doesn't implement `std::fmt::Display` --> src/bin/main_7c03549f224654e1a7d4fd9f1586b8229e8e2a81.rs:3:16 | 3 | println!("{}", PathBuf::from("file.txt")); | ^^^^^^^^^^^^^^^^^^^^^^^^^ `PathBuf` cannot be formatted with the default formatter; call `.display()` on it | = help: the trait `std::fmt::Display` is not implemented for `PathBuf` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead = note: call `.display()` or `.to_string_lossy()` to safely print paths, as they may contain non-Unicode data = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0277`. error: could not compile `rust` (bin "main_7c03549f224654e1a7d4fd9f1586b8229e8e2a81") due to 1 previous error
use std::path::PathBuf; fn main() { println!("{}", PathBuf::from("file.txt")); }
Debug
& Display
println!("{}", PathBuf::from("file.txt").display());
println!("{}", Path::new("file.txt").display());
file.txt file.txt
use std::path::{PathBuf,Path}; fn main() { println!("{}", PathBuf::from("file.txt").display()); println!("{}", Path::new("file.txt").display()); }
Пътищата на файловата система не са задължително UTF-8-кодирани. Как точно са кодирани зависи супер много от операционната система.
Пътища и низове
pub fn as_os_str(&self) -> &OsStr
-- Връща ви низ, както е кодиран natively за операционната система.pub fn to_str(&self) -> Option<&str>
-- Връща ви UTF-8 string slice, ако е възможно да се кодира като UTF-8.pub fn to_string_lossy(&self) -> Cow<'_, str>
-- Връща ви низ с въпросчета за невалиден UTF-8 (U+FFFD: �).
Пътища и низове
pub fn as_os_str(&self) -> &OsStr
-- Връща ви низ, както е кодиран natively за операционната система.pub fn to_str(&self) -> Option<&str>
-- Връща ви UTF-8 string slice, ако е възможно да се кодира като UTF-8.pub fn to_string_lossy(&self) -> Cow<'_, str>
-- Връща ви низ с въпросчета за невалиден UTF-8 (U+FFFD: �).- Ако искате да манипулирате пътища -- ползвате методите на
PathBuf
иPath
. - Ако искате да ги показвате на потребителя, викате им
.display()
. - Side note:
std::borrow::Cow
е интересен тип.
Полезни пакети
- walkdir: За ефективно обикаляне на файловата система.
- dirs: За намиране на "стандартни" директории в зависимост от операционната система.
- directories: Подобно на
dirs
, но включва и конфигурационни директории за инсталиране на приложения.
Файлове
std::fs
Файлове
std::fs
File::open
-- отваря път за четене
Файлове
std::fs
File::open
-- отваря път за четенеFile::open<P: AsRef<Path>>(path: P) -> fs::Result<File>
Файлове
std::fs
File::open
-- отваря път за четенеFile::open<P: AsRef<Path>>(path: P) -> fs::Result<File>
AsRef<Path>
-- нещо, което може евтино да се конвертира доPath
, с метода.as_ref()
File::open(PathBuf::from("..."))
File::open(Path::new("..."))
File::open("...")
Файлове
File::create
-- отваря файл за писане- Ако не съществува, ще го създаде, ако съществува, ще премахне съдържанието му
Файлове
fs::OpenOptions::new
-- отваря файл за четене, писане, append-ване, каквото ви душа иска
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open("foo.txt");
Файлове
fs::read_to_string
-- най-лесния и удобен начин за четене на целия файл директно в низ. Ползвайте го смело за тестове, пък и не само. Понякога, просто знаете, че файла няма да е огромен и че ще ви трябва целия in-memory.
Файлове
std::io::stdin() -> Stdin
-- имплементираRead
std::io::stdout() -> Stdout
-- имплементираWrite
std::io::stderr() -> Stderr
-- имплементираWrite
Файлове
std::io::stdin() -> Stdin
-- имплементираRead
std::io::stdout() -> Stdout
-- имплементираWrite
std::io::stderr() -> Stderr
-- имплементираWrite
- Буферират се по редове -- при писане, може да ползвате
.flush()
за да изкарате нещо, което не е цял ред.
Файлове
std::io::stdin() -> Stdin
-- имплементираRead
std::io::stdout() -> Stdout
-- имплементираWrite
std::io::stderr() -> Stderr
-- имплементираWrite
- Буферират се по редове -- при писане, може да ползвате
.flush()
за да изкарате нещо, което не е цял ред. - Lock-ват се автомагично с мутекс, но можете ръчно да викнете
.lock()
за ексклузивен достъп.
include макроси
- Четене на данни по време на компилация.
include макроси
- Четене на данни по време на компилация.
include_bytes!
-- набива някакъв файл в кода като статичен комплект от байтове (&'static [u8; N]
).include_str!
-- набива някакъв файл в кода като&'static str
.
include макроси
- Четене на данни по време на компилация.
include_bytes!
-- набива някакъв файл в кода като статичен комплект от байтове (&'static [u8; N]
).include_str!
-- набива някакъв файл в кода като&'static str
.
const MAIN_JS: &'static str = include_str!("../res/js/main.js");
const MAIN_CSS: &'static str = include_str!("../res/style/main.css");
const GITHUB_CSS: &'static str = include_str!("../res/style/github.css");
С include_bytes!
можете да вкарвате и други ресурси, като картинки, аудио…
std::env
env::args()
-- дава итератор по входа на програмата. Примерно:
$ cargo run one two three
use std::env;
fn main() {
println!("{:?}", env::args().collect::<Vec<String>>());
// => ["target/debug/scratch", "one", "two", "three"]
use std::ffi::OsString;
println!("{:?}", env::args_os().collect::<Vec<OsString>>());
// => ["target/debug/scratch", "one", "two", "three"]
}
std::env
env::var()
-- достъп до environment променливи, може да извади грешка, ако не са валиден unicode, имат \0
байтове и т.н. Ако супер много се налага, има env::var_os()
.
if env::var("DEBUG").is_ok() {
println!("Debug info: ...");
}
$ DEBUG=1 cargo run
Env logger
Удобен проект за по-сериозно log-ване: env_logger + log (пример от друг проект, rust-quickmd)
fn init_logging() {
// Release logging:
// - Warnings and errors
// - No timestamps
// - No module info
#[cfg(not(debug_assertions))]
env_logger::builder().
default_format_module_path(false).
default_format_timestamp(false).
filter_level(log::LevelFilter::Warn).
init();
// Debug logging:
// - All logs
// - Full info
#[cfg(debug_assertions)]
env_logger::builder().
filter_level(log::LevelFilter::Debug).
init();
}
Env logger
Примерна употреба:
debug!("Building HTML:");
debug!(" > home_path = {}", home_path);
debug!(" > scroll_top = {}", scroll_top);
При пускане на програмата в release mode, премахваме много допълнителна информация като модули и timestamp, и показваме всичко от "warning" нагоре.
При пускане в debug mode (стандарното при cargo run
):
[2019-11-29T08:57:33Z DEBUG quickmd::assets] Building HTML:
[2019-11-29T08:57:33Z DEBUG quickmd::assets] > home_path = /home/andrew
[2019-11-29T08:57:33Z DEBUG quickmd::assets] > scroll_top = 0
[2019-11-29T08:57:33Z DEBUG quickmd::ui] Loading HTML:
[2019-11-29T08:57:33Z DEBUG quickmd::ui] > output_path = /tmp/.tmpWq7EYh/output.html
[2019-11-29T08:57:33Z DEBUG quickmd::background] Watching ~/.quickmd.css
По-сложна обработка на аргументи чрез structopt
[dependencies]
structopt = "*"
structopt-derive = "*"
По-сложна обработка на аргументи чрез structopt
$ hangman --wordlist=words.txt --attempts=10 --debug
$ hangman -w words.txt -a 10 -d
$ hangman --help
$ hangman --version
По-сложна обработка на аргументи чрез structopt
use structopt_derive::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(name="hangman", about="A game of Hangman")]
pub struct Options {
#[structopt(short="w", long="wordlist", help="The path to a word list")]
wordlist_path: Option<PathBuf>,
#[structopt(short="a", long="attempts", help="The number of attempts to guess the word", default_value="10")]
attempts: u32,
#[structopt(short="d", long="debug", help="Show debug info")]
debug: bool,
}
Същото става и чрез clap::Parser
#[derive(clap::Parser, Debug)]
#[command(name="hangman", about="A game of Hangman")]
pub struct Options {
#[arg(short='w', long="wordlist", help="The path to a word list")]
pub wordlist_path: Option<PathBuf>,
#[arg(short='a', long="attempts", help="The number of attempts to guess the word", default_value="10")]
pub attempts: u32,
#[arg(short='d', long="debug", help="Show debug info")]
pub debug: bool,
}
Като ви трябва clap = { version = "*", features = ["derive"] }
в Cargo.toml
Вижте примерно https://github.com/clap-rs/clap/blob/25f9fda0a3aac97bce6af78bb1ae1f01aaa7806a/examples/tutorial_derive/01_quick.rs за употреба.
По-сложна обработка на аргументи чрез structopt
$ hangman --help
hangman 0.1.0
A game of Hangman
USAGE:
hangman [FLAGS] [OPTIONS]
FLAGS:
-d, --debug Show debug info
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-a, --attempts <attempts> The number of attempts to guess the word [default: 10]
-w, --wordlist <wordlist-path> The path to a word list
(С cargo run
ще е cargo run -- --help
)
Ограничения на structopt/clap-derive
- Трудно дебъгване на грешки (доста compile-time магия)
- Липса на гъвкавост?
- В краен случай, може би решението е (използвания отдолу пакет) clap
- Като алтернативи вижте:
Локално инсталиране на пакет
cargo install --path .
- Инсталира нещата в
~/.cargo/bin/
hangman -w wordlist.txt
- Може да имаме повече от един executable file. Слагаме ги в
src/bin/
, всеки си има собствен main. Може да ги викаме локално съсcargo run --bin <име-на-файла>
, а при cargo install ще си се инсталират като отделни binary-та. Пример от друг проект: rust-id3-image
Features
Може да си намалите малко броя dependencies, като премахнете default features:
[dependencies]
structopt = { version = "*", default-features = false }
structopt-derive = "*"
Документацията е в clap: https://docs.rs/clap/*/clap/#features-enabled-by-default
Версии
Окей е да използваме *
за версия, когато си пишем кода -- конкретните версии се фиксират в Cargo.lock
. Ако искаме да го публикуваме като пакет в crates.io, използвайки cargo publish
, трябва да фиксираме конкретни версии, примерно:
[dependencies]
dirs = "2.0.2"
rand = "0.7.2"
structopt = { version = "0.3.5", default-features = false }
structopt-derive = "0.3.5"
Може да намерите плъгини за редакторите си, които да ви помогнат с избора на версии: