从字符串中删除单个尾随换行符而不克隆。

45

我写了一个函数来提示输入并返回结果。 在这个版本中,返回的字符串包括用户输入的尾随换行符。 我想返回已删除该换行符(仅该换行符)的输入:

我编写了一段提示输入并返回结果的函数。 在这个版本中,返回的字符串包含用户输入中的换行符。 我希望返回已经删除该换行符(仅限该换行符)的输入内容:

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)
}

仅删除单个尾随换行符的原因是,该函数也将用于提示输入密码(使用termios适当地停止回显),如果某人的密码有尾随空格,则应保留。

在烦恼如何实际上删除字符串末尾的单个换行符时,我最终使用了trim_right_matches。但是它返回一个&str。我试图使用Cow来解决这个问题,但错误仍然显示input变量的寿命不够长。

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[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

根据之前类似的问题,似乎这是不可能的。唯一的选择是分配一个新字符串,删除尾随换行符吗?似乎应该有一种在不复制字符串的情况下修剪字符串的方法(在C语言中,您只需用'\0'替换'\n')。

6个回答

43

您可以使用String::popString::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);
}

4
请注意,此代码不处理\r\n换行符,并且假定最后一个字符是换行符(这在问题中可能是正确的,但在一般情况下不是)。 - Matthew D. Scholefield

31

一个跨平台的方法,可以在不重新分配字符串的情况下去掉单个尾随换行符,如下:

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

这将从字符串末尾删除"\n""\r\n",但不会删除额外的空格。


3
有点烦人的是我们必须这样做,而不是将其嵌入语言中...但我想这确实可以进行操作系统优化吧?我对所有这些都很新。 - Matthew S
1
如果您想迭代文件或缓冲区的行,可以使用 BufRead::lines,它会自动为您删除换行符。因此,标准库确实覆盖了最常见的情况。 - Sven Marnach

25

使用strip_suffix

此方法可以移除一个尾部的\r\n\n

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

如果存在多个换行符,只会剥离最后一个。

如果字符串末尾没有换行符,则字符串不会改变。

一些测试:

#[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");
}

8
测试方案很出色!! - Mihai Galos

13
比已被接受的解决方案更加通用的解决方案,适用于任何类型的行尾:
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);
}

10
如果密码末尾有空格,应该保留这些空格。然而,trim_right()函数也会去掉空格。 - Robert
真的。更好的解决方案是对最后两个字符/字节进行模式匹配,并确定截断索引。但这不会删除多个换行符。要决定是否这样做,需求不够清晰。 - Sander

1
在您已经拥有一个 String 的情况下,您不需要新分配空间来删除尾随的换行符。
以下是一个跨平台示例,可以就地删除多个尾随的\r\n\n
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);
    }
}

现在,让我们来测试一下(playground链接: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, "");
}

0

编辑:我刚意识到 OP 寻求的是复制字符串...所以只是注意一下,这确实复制了字符串。 :(

我是 Rust 初学者,所以不知道这个函数是什么时候引入的,但考虑使用 String::lines 方法。它看起来应该能够跨平台可靠地工作,并且在我的本地开发中,似乎表现出了 OP 寻求的行为。

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);
}

参考资料:https://doc.rust-lang.org/stable/std/string/struct.String.html#method.lines


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接