Repository: uutils/coreutils (Rust)

Issue:
wc: fails to append a dash to output line when a dash is given on the command-line
Environment: Ubuntu 18.04, GNU `wc` version 8.28

`wc` is failing to append a dash to the output line when a dash is given on the command-line to represent reading from `stdin`.

What happens now: no difference when calling `wc` with and without a dash
```sh
$ cat tests/fixtures/wc/alice_in_wonderland.txt | ./target/debug/coreutils wc -clmw
   5  57 302 302
$ cat tests/fixtures/wc/alice_in_wonderland.txt | ./target/debug/coreutils wc -clmw -
   5  57 302 302
```

What I expected to happen: GNU prints the dash as the "name" of the file
```
$ cat tests/fixtures/wc/alice_in_wonderland.txt | wc -clmw
      5      57     302     302
$ cat tests/fixtures/wc/alice_in_wonderland.txt | wc -clmw -
      5      57     302     302 -
```

(There are also two issues with the spacing, those issues are represented in issues #2173 and #2186.)


Discussion:
I'm working on a fix.



Patch:
diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs
index 33b2ba5ec96..226608d4091 100644
--- a/src/uu/wc/src/wc.rs
+++ b/src/uu/wc/src/wc.rs
@@ -104,6 +104,34 @@ fn get_usage() -> String {
     )
 }
 
+enum StdinKind {
+    /// Stdin specified on command-line with "-".
+    Explicit,
+
+    /// Stdin implicitly specified on command-line by not passing any positional argument.
+    Implicit,
+}
+
+/// Supported inputs.
+enum Input {
+    /// A regular file.
+    Path(String),
+
+    /// Standard input.
+    Stdin(StdinKind),
+}
+
+impl Input {
+    /// Converts input to title that appears in stats.
+    fn to_title(&self) -> Option<&str> {
+        match self {
+            Input::Path(path) => Some(path),
+            Input::Stdin(StdinKind::Explicit) => Some("-"),
+            Input::Stdin(StdinKind::Implicit) => None,
+        }
+    }
+}
+
 pub fn uumain(args: impl uucore::Args) -> i32 {
     let usage = get_usage();
 
@@ -144,18 +172,27 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
         .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true))
         .get_matches_from(args);
 
-    let mut files: Vec<String> = matches
+    let mut inputs: Vec<Input> = matches
         .values_of(ARG_FILES)
-        .map(|v| v.map(ToString::to_string).collect())
+        .map(|v| {
+            v.map(|i| {
+                if i == "-" {
+                    Input::Stdin(StdinKind::Explicit)
+                } else {
+                    Input::Path(ToString::to_string(i))
+                }
+            })
+            .collect()
+        })
         .unwrap_or_default();
 
-    if files.is_empty() {
-        files.push("-".to_owned());
+    if inputs.is_empty() {
+        inputs.push(Input::Stdin(StdinKind::Implicit));
     }
 
     let settings = Settings::new(&matches);
 
-    if wc(files, &settings).is_ok() {
+    if wc(inputs, &settings).is_ok() {
         0
     } else {
         1
@@ -198,32 +235,35 @@ fn word_count_from_reader<T: WordCountable>(
     Ok(total)
 }
 
-fn word_count_from_path(path: &str, settings: &Settings) -> WcResult<WordCount> {
-    if path == "-" {
-        let stdin = io::stdin();
-        let stdin_lock = stdin.lock();
-        word_count_from_reader(stdin_lock, settings, path)
-    } else {
-        let path_obj = Path::new(path);
-        if path_obj.is_dir() {
-            Err(WcError::IsDirectory(path.to_owned()))
-        } else {
-            let file = File::open(path)?;
-            word_count_from_reader(file, settings, path)
+fn word_count_from_input(input: &Input, settings: &Settings) -> WcResult<WordCount> {
+    match input {
+        Input::Stdin(_) => {
+            let stdin = io::stdin();
+            let stdin_lock = stdin.lock();
+            word_count_from_reader(stdin_lock, settings, "-")
+        }
+        Input::Path(path) => {
+            let path_obj = Path::new(path);
+            if path_obj.is_dir() {
+                Err(WcError::IsDirectory(path.to_owned()))
+            } else {
+                let file = File::open(path)?;
+                word_count_from_reader(file, settings, path)
+            }
         }
     }
 }
 
-fn wc(files: Vec<String>, settings: &Settings) -> Result<(), u32> {
+fn wc(inputs: Vec<Input>, settings: &Settings) -> Result<(), u32> {
     let mut total_word_count = WordCount::default();
     let mut results = vec![];
     let mut max_width: usize = 0;
     let mut error_count = 0;
 
-    let num_files = files.len();
+    let num_inputs = inputs.len();
 
-    for path in &files {
-        let word_count = word_count_from_path(&path, settings).unwrap_or_else(|err| {
+    for input in &inputs {
+        let word_count = word_count_from_input(&input, settings).unwrap_or_else(|err| {
             show_error!("{}", err);
             error_count += 1;
             WordCount::default()
@@ -235,18 +275,22 @@ fn wc(files: Vec<String>, settings: &Settings) -> Result<(), u32> {
         // formatting each count as a string for output.
         max_width = max(max_width, word_count.bytes.to_string().len());
         total_word_count += word_count;
-        results.push(word_count.with_title(path));
+        results.push(word_count.with_title(input.to_title()));
     }
 
     for result in &results {
         if let Err(err) = print_stats(settings, &result, max_width) {
-            show_warning!("failed to print result for {}: {}", result.title, err);
+            show_warning!(
+                "failed to print result for {}: {}",
+                result.title.unwrap_or("<stdin>"),
+                err
+            );
             error_count += 1;
         }
     }
 
-    if num_files > 1 {
-        let total_result = total_word_count.with_title("total");
+    if num_inputs > 1 {
+        let total_result = total_word_count.with_title(Some("total"));
         if let Err(err) = print_stats(settings, &total_result, max_width) {
             show_warning!("failed to print total: {}", err);
             error_count += 1;
@@ -315,10 +359,10 @@ fn print_stats(
         )?;
     }
 
-    if result.title == "-" {
-        writeln!(stdout_lock)?;
+    if let Some(title) = result.title {
+        writeln!(stdout_lock, " {}", title)?;
     } else {
-        writeln!(stdout_lock, " {}", result.title)?;
+        writeln!(stdout_lock)?;
     }
 
     Ok(())
diff --git a/src/uu/wc/src/wordcount.rs b/src/uu/wc/src/wordcount.rs
index 785e57eff00..9e2a81fcac3 100644
--- a/src/uu/wc/src/wordcount.rs
+++ b/src/uu/wc/src/wordcount.rs
@@ -103,7 +103,7 @@ impl WordCount {
         (word_count, char_count)
     }
 
-    pub fn with_title(self, title: &str) -> TitledWordCount {
+    pub fn with_title(self, title: Option<&str>) -> TitledWordCount {
         TitledWordCount { title, count: self }
     }
 
@@ -120,12 +120,12 @@ impl WordCount {
     }
 }
 
-/// This struct supplements the actual word count with a title that is displayed
-/// to the user at the end of the program.
+/// 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 unneccesary copying of `String`.
 #[derive(Debug, Default, Clone)]
 pub struct TitledWordCount<'a> {
-    pub title: &'a str,
+    pub title: Option<&'a str>,
     pub count: WordCount,
 }


Test patch:
diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs
index 87a86fca4e4..b61d7e3aac7 100644
--- a/tests/by-util/test_wc.rs
+++ b/tests/by-util/test_wc.rs
@@ -36,6 +36,15 @@ fn test_stdin_default() {
         .stdout_is(" 13 109 772\n");
 }
 
+#[test]
+fn test_stdin_explicit() {
+    new_ucmd!()
+        .pipe_in_fixture("lorem_ipsum.txt")
+        .arg("-")
+        .run()
+        .stdout_is(" 13 109 772 -\n");
+}
+
 #[test]
 fn test_utf8() {
     new_ucmd!()