mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
wc: avoid excess String allocations
print_stats will now take advantage of the buffer built into io::stdout(). We can also waste fewer lines on show! by making a helper macro.
This commit is contained in:
parent
c3b06e10a6
commit
38b4825e7f
3 changed files with 41 additions and 81 deletions
|
@ -17,7 +17,7 @@ use countable::WordCountable;
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
use utf8::{BufReadDecoder, BufReadDecoderError};
|
use utf8::{BufReadDecoder, BufReadDecoderError};
|
||||||
use uucore::{format_usage, help_about, help_usage, show};
|
use uucore::{format_usage, help_about, help_usage, show};
|
||||||
use word_count::{TitledWordCount, WordCount};
|
use word_count::WordCount;
|
||||||
|
|
||||||
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
|
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ use std::fs::{self, File};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use uucore::error::{UError, UResult, USimpleError};
|
use uucore::error::{FromIo, UError, UResult};
|
||||||
use uucore::quoting_style::{escape_name, QuotingStyle};
|
use uucore::quoting_style::{escape_name, QuotingStyle};
|
||||||
|
|
||||||
/// The minimum character width for formatting counts when reading from stdin.
|
/// The minimum character width for formatting counts when reading from stdin.
|
||||||
|
@ -621,93 +621,72 @@ fn compute_number_width(inputs: &[Input], settings: &Settings) -> usize {
|
||||||
fn wc(inputs: &[Input], settings: &Settings) -> UResult<()> {
|
fn wc(inputs: &[Input], settings: &Settings) -> UResult<()> {
|
||||||
let mut total_word_count = WordCount::default();
|
let mut total_word_count = WordCount::default();
|
||||||
|
|
||||||
let (number_width, are_stats_visible, total_row_title) =
|
let (number_width, are_stats_visible) = match settings.total_when {
|
||||||
if settings.total_when == TotalWhen::Only {
|
TotalWhen::Only => (1, false),
|
||||||
(1, false, None)
|
_ => (compute_number_width(inputs, settings), true),
|
||||||
} else {
|
};
|
||||||
let number_width = compute_number_width(inputs, settings);
|
|
||||||
let title = Some(String::from("total"));
|
|
||||||
|
|
||||||
(number_width, true, title)
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_total_row_visible = settings.total_when.is_total_row_visible(inputs.len());
|
let is_total_row_visible = settings.total_when.is_total_row_visible(inputs.len());
|
||||||
|
|
||||||
for input in inputs {
|
for input in inputs {
|
||||||
let word_count = match word_count_from_input(input, settings) {
|
let word_count = match word_count_from_input(input, settings) {
|
||||||
CountResult::Success(word_count) => word_count,
|
CountResult::Success(word_count) => word_count,
|
||||||
CountResult::Interrupted(word_count, error) => {
|
CountResult::Interrupted(word_count, err) => {
|
||||||
show!(USimpleError::new(
|
show!(err.map_err_context(|| input.path_display()));
|
||||||
1,
|
|
||||||
format!("{}: {}", input.path_display(), error)
|
|
||||||
));
|
|
||||||
word_count
|
word_count
|
||||||
}
|
}
|
||||||
CountResult::Failure(error) => {
|
CountResult::Failure(err) => {
|
||||||
show!(USimpleError::new(
|
show!(err.map_err_context(|| input.path_display()));
|
||||||
1,
|
|
||||||
format!("{}: {}", input.path_display(), error)
|
|
||||||
));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
total_word_count += word_count;
|
total_word_count += word_count;
|
||||||
let result = word_count.with_title(input.to_title());
|
|
||||||
|
|
||||||
if are_stats_visible {
|
if are_stats_visible {
|
||||||
if let Err(err) = print_stats(settings, &result, number_width) {
|
let maybe_title = input.to_title();
|
||||||
show!(USimpleError::new(
|
let maybe_title_str = maybe_title.as_deref();
|
||||||
1,
|
if let Err(err) = print_stats(settings, &word_count, maybe_title_str, number_width) {
|
||||||
format!(
|
let title = maybe_title_str.unwrap_or("<stdin>");
|
||||||
"failed to print result for {}: {}",
|
show!(err.map_err_context(|| format!("failed to print result for {title}")));
|
||||||
&result.title.unwrap_or_else(|| String::from("<stdin>")),
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_total_row_visible {
|
if is_total_row_visible {
|
||||||
let total_result = total_word_count.with_title(total_row_title);
|
let title = are_stats_visible.then_some("total");
|
||||||
if let Err(err) = print_stats(settings, &total_result, number_width) {
|
if let Err(err) = print_stats(settings, &total_word_count, title, number_width) {
|
||||||
show!(USimpleError::new(
|
show!(err.map_err_context(|| "failed to print total".into()));
|
||||||
1,
|
|
||||||
format!("failed to print total: {err}")
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Although this appears to be returning `Ok`, the exit code may
|
// Although this appears to be returning `Ok`, the exit code may have been set to a non-zero
|
||||||
// have been set to a non-zero value by a call to `show!()` above.
|
// value by a call to `record_error!()` above.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_stats(
|
fn print_stats(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
result: &TitledWordCount,
|
result: &WordCount,
|
||||||
|
title: Option<&str>,
|
||||||
number_width: usize,
|
number_width: usize,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let mut columns = Vec::new();
|
let mut stdout = io::stdout().lock();
|
||||||
|
|
||||||
if settings.show_lines {
|
let maybe_cols = [
|
||||||
columns.push(format!("{:1$}", result.count.lines, number_width));
|
(settings.show_lines, result.lines),
|
||||||
}
|
(settings.show_words, result.words),
|
||||||
if settings.show_words {
|
(settings.show_chars, result.chars),
|
||||||
columns.push(format!("{:1$}", result.count.words, number_width));
|
(settings.show_bytes, result.bytes),
|
||||||
}
|
(settings.show_max_line_length, result.max_line_length),
|
||||||
if settings.show_chars {
|
];
|
||||||
columns.push(format!("{:1$}", result.count.chars, number_width));
|
|
||||||
}
|
let mut space = "";
|
||||||
if settings.show_bytes {
|
for (_, num) in maybe_cols.iter().filter(|(show, _)| *show) {
|
||||||
columns.push(format!("{:1$}", result.count.bytes, number_width));
|
write!(stdout, "{space}{num:number_width$}")?;
|
||||||
}
|
space = " ";
|
||||||
if settings.show_max_line_length {
|
|
||||||
columns.push(format!("{:1$}", result.count.max_line_length, number_width));
|
|
||||||
}
|
|
||||||
if let Some(title) = &result.title {
|
|
||||||
columns.push(title.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(io::stdout().lock(), "{}", columns.join(" "))
|
if let Some(title) = title {
|
||||||
|
writeln!(stdout, "{space}{title}")
|
||||||
|
} else {
|
||||||
|
writeln!(stdout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,19 +29,3 @@ impl AddAssign for WordCount {
|
||||||
*self = *self + other;
|
*self = *self + other;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WordCount {
|
|
||||||
pub fn with_title(self, title: Option<String>) -> TitledWordCount {
|
|
||||||
TitledWordCount { title, count: self }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This struct supplements the actual word count with an optional title that is
|
|
||||||
/// displayed to the user at the end of the program.
|
|
||||||
/// The reason we don't simply include title in the `WordCount` struct is that
|
|
||||||
/// it would result in unnecessary copying of `String`.
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct TitledWordCount {
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub count: WordCount,
|
|
||||||
}
|
|
||||||
|
|
|
@ -375,7 +375,7 @@ fn test_read_from_directory_error() {
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
const STDERR: &str = ".: Is a directory";
|
const STDERR: &str = ".: Is a directory";
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const STDERR: &str = ".: Access is denied";
|
const STDERR: &str = ".: Permission denied";
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
const STDOUT: &str = " 0 0 0 .\n";
|
const STDOUT: &str = " 0 0 0 .\n";
|
||||||
|
@ -392,10 +392,7 @@ fn test_read_from_directory_error() {
|
||||||
/// Test that getting counts from nonexistent file is an error.
|
/// Test that getting counts from nonexistent file is an error.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_read_from_nonexistent_file() {
|
fn test_read_from_nonexistent_file() {
|
||||||
#[cfg(not(windows))]
|
|
||||||
const MSG: &str = "bogusfile: No such file or directory";
|
const MSG: &str = "bogusfile: No such file or directory";
|
||||||
#[cfg(windows)]
|
|
||||||
const MSG: &str = "bogusfile: The system cannot find the file specified";
|
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
.args(&["bogusfile"])
|
.args(&["bogusfile"])
|
||||||
.fails()
|
.fails()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue