45

I have written a function to prompt for input and return the result. In this version, the returned string includes a trailing newline from the user. I would like to return the input with that newline (and just that newline) removed:

fn read_with_prompt(prompt: &str) -> io::Result<String> {
    let stdout = io::stdout();
    let reader = io::stdin();
    let mut input = String::new();
    print!("{}", prompt);
    stdout.lock().flush().unwrap();
    reader.read_line(&mut input)?;

    // TODO: Remove trailing newline if present
    Ok(input)
}

The reason for only removing the single trailing newline is that this function will also be used to prompt for a password (with appropriate use of termios to stop echoing) and if someone's password has trailing whitespace, this should be preserved.

After much fussing about how to actually remove a single newline at the end of a string, I ended up using trim_right_matches. However that returns a &str. I tried using Cow to deal with this but the error still says that the input variable doesn't live long enough.

fn read_with_prompt<'a>(prompt: &str) -> io::Result<Cow<'a, str>> {
    let stdout = io::stdout();
    let reader = io::stdin();
    let mut input = String::new();
    print!("{}", prompt);
    stdout.lock().flush().unwrap();
    reader.read_line(&mut input)?;

    let mut trimmed = false;
    Ok(Cow::Borrowed(input.trim_right_matches(|c| {
        if !trimmed && c == '\n' {
            trimmed = true;
            true
        }
        else {
            false
        }
    })))
}

Error:

error[E0515]: cannot return value referencing local variable `input`
  --> src/lib.rs:13:5
   |
13 |       Ok(Cow::Borrowed(input.trim_right_matches(|c| {
   |       ^                ----- `input` is borrowed here
   |  _____|
   | |
14 | |         if !trimmed && c == '\n' {
15 | |             trimmed = true;
16 | |             true
...  |
20 | |         }
21 | |     })))
   | |________^ returns a value referencing data owned by the current function

Based on previous questions along these lines it seems this is not possible. Is the only option to allocate a new string that has the trailing newline removed? It seems there should be a way to trim the string without copying it (in C you'd just replace the '\n' with '\0').

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
Wes
  • 2,166
  • 1
  • 20
  • 22

6 Answers6

43

You can use String::pop or String::truncate:

fn main() {
    let mut s = "hello\n".to_string();
    s.pop();
    assert_eq!("hello", &s);

    let mut s = "hello\n".to_string();
    let len = s.len();
    s.truncate(len - 1);
    assert_eq!("hello", &s);
}
malbarbo
  • 10,717
  • 1
  • 42
  • 57
  • 4
    Note that this doesn't handle \r\n newlines and also assumes the last character is a newline (which may be true in the question but not in the general case). – Matthew D. Scholefield May 08 '22 at 05:40
31

A cross-platform way of stripping a single trailing newline without reallocating the string is this:

fn trim_newline(s: &mut String) {
    if s.ends_with('\n') {
        s.pop();
        if s.ends_with('\r') {
            s.pop();
        }
    }
}

This will strip either "\n" or "\r\n" from the end of the string, but no additional whitespace.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • 3
    Kind of annoying that we have to do this instead of it being baked into the language.. but I guess it does allow for OS optimization? I'm pretty new to all of this stuff. – Matthew S Mar 13 '20 at 03:48
  • 1
    If you want to iterate over the lines of a file or buffer, you can use `BufRead::lines`, which will [trim the newline characters for you](https://doc.rust-lang.org/1.42.0/src/std/io/mod.rs.html#2397-2402). So the most common case is indeed covered by the standard library. – Sven Marnach Mar 13 '20 at 09:40
24

with strip_suffix

This removes one trailing \r\n or \n:

fn strip_trailing_newline(input: &str) -> &str {
    input
        .strip_suffix("\r\n")
        .or(input.strip_suffix("\n"))
        .unwrap_or(input)
}

If there are multiple newlines, only the last one is stripped off.

If there is no newline at the end of the string, the string is unchanged.

Some tests:

#[test]
fn strip_newline_works(){
    assert_eq!(strip_trailing_newline("Test0\r\n\r\n"), "Test0\r\n");
    assert_eq!(strip_trailing_newline("Test1\r\n"), "Test1");
    assert_eq!(strip_trailing_newline("Test2\n"), "Test2");
    assert_eq!(strip_trailing_newline("Test3"), "Test3");
}
Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
13

A more generic solution than the accepted one, that works with any kind of line ending:

fn main() {
    let mut s = "hello\r\n".to_string();
    let len_withoutcrlf = s.trim_right().len();
    s.truncate(len_withoutcrlf);
    assert_eq!("hello", &s);
}
Sander
  • 323
  • 3
  • 9
  • 10
    "If someone's password has trailing whitespace this should be preserved." However trim_right() will trim spaces too. – Robert Dec 07 '18 at 18:55
  • True. A better solution would be to pattern match on the last two characters/bytes and determine the truncation index. But that won't remove multiple newlines. The requirements aren't clear enough to decide on that. – Sander Sep 07 '19 at 15:35
0

Edit: I just realized that the OP was looking to not make a copy of the string... so just making a note that this does in fact copy the string. :(

I'm a Rust newbie, so I don't know when this function was introduced, but consider using the String::lines method. It looks like it should work cross-platform in a reliable way and in my local development, appeared to behave as the OP was looking for.

use std::io::stdin;

fn main() {
    println!("Enter a line of text:");
    let mut buf = String::new();
    stdin().read_line(&mut buf).expect("Failed to read input.");
    let my_str = buf.lines()
        .next().expect("Could not read entry.");
    println!("You entered: [{}]", my_str);
}

Reference: https://doc.rust-lang.org/stable/std/string/struct.String.html#method.lines

cuelindar
  • 1
  • 2
0

In the case where you already have an owned String, you don't need a new allocation just to strip the trailing new line.

Here's a cross-platform example that removes multiple trailing \r\n or \n in place:

fn strip_trailing_nl(input: &mut String) {
    let new_len = input
        .char_indices()
        .rev()
        .find(|(_, c)| !matches!(c, '\n' | '\r'))
        .map_or(0, |(i, _)| i + 1);
    if new_len != input.len() {
        input.truncate(new_len);
    }
}

Now, let's test it (playground link: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ec2f6f60bdde32ccfeb8fa0c63a06f54):

#[test]
fn this_works() {
    let mut s = "\n".to_string();
    strip_trailing_nl(&mut s);
    assert_eq!(s, "");

    let mut s = "\r\n".to_string();
    strip_trailing_nl(&mut s);
    assert_eq!(s, "");

    let mut s = "Hello, World".to_string();
    strip_trailing_nl(&mut s);
    assert_eq!(s, "Hello, World");

    let mut s = "Hello, World\n".to_string();
    strip_trailing_nl(&mut s);
    assert_eq!(s, "Hello, World");

    let mut s = "Hello, World\r\n".to_string();
    strip_trailing_nl(&mut s);
    assert_eq!(s, "Hello, World");

    let mut s = "Hello, World\n\n\r\n\r\n".to_string();
    strip_trailing_nl(&mut s);
    assert_eq!(s, "Hello, World");

    let mut s = "".to_string();
    strip_trailing_nl(&mut s);
    assert_eq!(s, "");
}
Miraclx
  • 371
  • 3
  • 8