Запись сообщений об ошибках в стандартный поток ошибок вместо стандартного вывода
В данный момент весь наш вывод отправляется в терминал с помощью макроса println!. В большинстве терминалов существует два вида вывода: стандартный вывод (stdout) для общей информации и стандартная ошибка (stderr) для сообщений об ошибках. Это разделение позволяет пользователям направлять успешный вывод программы в файл, но при этом видеть сообщения об ошибках на экране.
Макрос println! может выводить данные только в стандартный вывод, поэтому для записи в стандартный поток ошибок нам нужно использовать что-то другое.
Проверка, куда записываются ошибки
Сначала посмотрим, как в текущем виде minigrep записывает свой вывод в стандартный вывод, включая сообщения об ошибках, которые мы хотим отправлять в стандартный поток ошибок. Мы сделаем это, перенаправив поток стандартного вывода в файл, при этом намеренно вызвав ошибку. Мы не будем перенаправлять стандартный поток ошибок, поэтому любой контент, отправленный в него, по-прежнему будет отображаться на экране.
Ожидается, что командные программы отправляют сообщения об ошибках в стандартный поток ошибок, чтобы мы могли видеть их на экране, даже если перенаправили стандартный вывод в файл. Наша программа сейчас ведёт себя неправильно: мы увидим, что сообщение об ошибке сохраняется в файл!
Чтобы продемонстрировать это поведение, запустим программу с помощью > и указанного пути к файлу _output.txt_, в который мы хотим перенаправить стандартный вывод. Мы не передадим никаких аргументов, что должно вызвать ошибку:
$ cargo run > output.txt
Синтаксис > указывает оболочке записывать содержимое стандартного вывода в _output.txt_ вместо экрана. Мы не увидели ожидавшееся сообщение об ошибке на экране, значит, оно оказалось в файле. Вот что содержит _output.txt_:
Problem parsing arguments: not enough arguments
Да, наше сообщение об ошибке выводится в стандартный вывод. Гораздо полезнее, чтобы подобные сообщения об ошибках выводились в стандартный поток ошибок, и в файл попадал только результат успешного выполнения. Мы это исправим.
Вывод ошибок в стандартный поток ошибок
Мы используем код из Листинга 12-24, чтобы изменить способ вывода сообщений об ошибках. Благодаря рефакторингу, который мы выполнили ранее в этой главе, весь код, выводящий сообщения об ошибках, находится в одной функции main. Стандартная библиотека предоставляет макрос eprintln!, который выводит данные в стандартный поток ошибок, поэтому давайте изменим два места, где мы вызывали println! для вывода ошибок, на eprintln!.
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
eprintln!Теперь запустим программу снова тем же способом, без аргументов и с перенаправлением стандартного вывода через >:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
Теперь мы видим ошибку на экране, а _output.txt_ остаётся пустым, что и ожидается от командной программы.
Запустим программу снова с аргументами, которые не вызывают ошибок, но всё равно перенаправим стандартный вывод в файл:
$ cargo run -- to poem.txt > output.txt
Мы не увидим никакого вывода в терминал, а _output.txt_ будет содержать наши результаты:
Имя файла: output.txt
Are you nobody, too?
How dreary to be somebody!
Это демонстрирует, что теперь мы используем стандартный вывод для успешного результата и стандартный поток ошибок для вывода ошибок, как и положено.
Резюме
В этой главе мы повторили некоторые из основных концепций, изученных до сих пор, и рассмотрели выполнение распространённых операций ввода-вывода в Rust. Используя аргументы командной строки, файлы, переменные окружения и макрос eprintln! для вывода ошибок, вы теперь готовы писать командные приложения. В сочетании с концепциями из предыдущих глав ваш код будет хорошо организован, эффективно хранить данные в соответствующих структурах, корректно обрабатывать ошибки и быть хорошо протестированным.
Далее мы изучим некоторые возможности Rust, на которые повлияли функциональные языки: замыкания и итераторы.