Fix broken pipe when dumping links (#339)

When piping the output of lychee's `--dump` output to another program,
we can run into issues with broken pipes as described in
https://github.com/rust-lang/rust/issues/46016
and https://gabebw.com/blog/2019/10/13/learning-rust-by-candlelight
To avoid this, we use the underlying writeln macro and check the
returned `ErrorKind`.
This commit is contained in:
Matthias 2021-09-20 12:12:35 +02:00 committed by GitHub
parent 94cae4e9e5
commit f2d7abbc29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -62,7 +62,7 @@
use ring as _;
use std::fs::File;
use std::io::{self, BufRead};
use std::io::{self, BufRead, Write};
use std::iter::FromIterator;
use std::{collections::HashSet, fs, str::FromStr, time::Duration};
@ -70,7 +70,7 @@ use anyhow::{anyhow, Context, Result};
use headers::{authorization::Basic, Authorization, HeaderMap, HeaderMapExt, HeaderName};
use http::StatusCode;
use indicatif::{ProgressBar, ProgressStyle};
use lychee_lib::{ClientBuilder, ClientPool, Collector, Input, Response};
use lychee_lib::{ClientBuilder, ClientPool, Collector, Input, Request, Response};
use openssl_sys as _; // required for vendored-openssl feature
use regex::RegexSet;
use ring as _; // required for apple silicon
@ -212,12 +212,8 @@ async fn run(cfg: &Config, inputs: Vec<Input>) -> Result<i32> {
.map_err(|e| anyhow!(e))?;
if cfg.dump {
for link in links {
if !client.filtered(&link.uri) {
println!("{}", link);
}
}
return Ok(ExitCode::Success as i32);
let exit_code = dump_links(links.iter().filter(|link| !client.filtered(&link.uri)));
return Ok(exit_code as i32);
}
let pb = if cfg.no_progress {
@ -264,17 +260,7 @@ async fn run(cfg: &Config, inputs: Vec<Input>) -> Result<i32> {
pb.finish_and_clear();
}
let stats_formatted = fmt(&stats, &cfg.format)?;
if let Some(output) = &cfg.output {
fs::write(output, stats_formatted).context("Cannot write status output to file")?;
} else {
if cfg.verbose && !stats.is_empty() {
// separate summary from the verbose list of links above
println!();
}
// we assume that the formatted stats don't have a final newline
println!("{}", stats_formatted);
}
write_stats(&stats, cfg)?;
if stats.is_success() {
Ok(ExitCode::Success as i32)
@ -283,6 +269,41 @@ async fn run(cfg: &Config, inputs: Vec<Input>) -> Result<i32> {
}
}
/// Dump all detected links to stdout without checking them
fn dump_links<'a>(links: impl Iterator<Item = &'a Request>) -> ExitCode {
let mut stdout = io::stdout();
for link in links {
// Avoid panic on broken pipe.
// See https://github.com/rust-lang/rust/issues/46016
// This can occur when piping the output of lychee
// to another program like `grep`.
if let Err(e) = writeln!(stdout, "{}", &link) {
if e.kind() != io::ErrorKind::BrokenPipe {
eprintln!("{}", e);
return ExitCode::UnexpectedFailure;
}
}
}
ExitCode::Success
}
/// Write final statistics to stdout or to file
fn write_stats(stats: &ResponseStats, cfg: &Config) -> Result<()> {
let formatted = fmt(stats, &cfg.format)?;
if let Some(output) = &cfg.output {
fs::write(output, formatted).context("Cannot write status output to file")?;
} else {
if cfg.verbose && !stats.is_empty() {
// separate summary from the verbose list of links above
println!();
}
// we assume that the formatted stats don't have a final newline
println!("{}", stats);
}
Ok(())
}
fn read_header(input: &str) -> Result<(String, String)> {
let elements: Vec<_> = input.split('=').collect();
if elements.len() != 2 {