Contents

文件每一行中删除最后一个字符

1. 概述

从文件中的每一行中删除最后一个字符的问题看起来很简单。然而,在实践中,我们可能会遇到此要求的一些变体。

在本教程中,我们将通过示例解决如何解决此问题。

此外,我们将讨论一些常见的变体。

2.示例文件

为了清楚地解释不同的命令,让我们创建一个包含几行的示例文件:

$ cat input.txt
This is a normal line.
This line has 3 trailing spaces.   
The next line has only 4 spaces:
    
The next line is an empty line:
I am the last line.

我们的 input.txt有几行文本。

此外,一些行包含尾随空格,而尾随空格对这个问题很重要。但是,此信息在上面的输出中并不是那么明显。

我们可以将 -e选项传递给 cat 命令,并要求它在每行的末尾打印一个“ $ ”符号:

$ cat -e input.txt
This is a normal line.$
This line has 3 trailing spaces.   $
The next line has only 4 spaces:$
    $
The next line is an empty line:$
$
I am the last line.$

现在,我们可以清楚地看到输出中的尾随空格。

接下来,让我们仔细看看“从每行中删除最后一个字符”问题及其变体。

3. 删除每行的最后一个字符

首先,让我们看一下如何从每行中删除最后一个字符,而不管它是否为空格。

有很多方法可以解决这个问题。现在,让我们看看一些常见的解决方案。

3.1. 使用纯 Bash

两种 Bash 参数扩展技术可以帮助我们从变量中删除最后一个字符:

接下来,让我们仔细看看这两种方法。

如果我们在子字符串扩展中给出负长度,Bash 将计算从字符串末尾到偏移量的长度。因此,我们可以传递 -1作为 长度以从变量中删除最后一个字符:

$ var="012345"
$ echo ${var:0:-1}
01234

此外,Bash 允许我们在偏移量为“ 0 ”时省略它: ${var::-1}

但是,我们应该记住Bash 的负长度子字符串扩展不适用于空字符串

$ var=""
$ echo ${var::-1}
bash: -1: substring expression < 0

因此,在提取子字符串之前,我们需要检查变量是否为空,例如使用*[ -z “$var” ]*。

接下来,让我们看看*${VAR%word}*扩展如何帮助我们从变量中删除最后一个字符。

在 Bash 中,模式 ’ ? ’ 匹配任何单个字符。由于我们要删除最后一个字符,我们可以使用 ’ ?’ 模式作为后缀:

$ var="012345"
$ echo ${var%?}
01234

删除后缀扩展也适用于空字符串:

$ var=""
$ echo ${var%?} | cat -e
$

我们将采用后缀扩展来解决我们的问题,因为我们的文件包含空行。

好了,至此,我们已经解决了问题的核心部分。剩下的只是遍历行并打印输出:

$ while IFS="" read var; do echo "${var%?}"; done <input.txt | cat -e
This is a normal line$
This line has 3 trailing spaces.  $
The next line has only 4 spaces$
   $
The next line is an empty line$
$
I am the last line$

我们将结果通过管道传递给“ *cat -e”*以清楚地显示尾随空格。如上面的输出所示,每行的最后一个字符已被删除,无论它是否为空格。

3.2. 使用sedawk命令

纯 Bash 解决方案不需要其他软件依赖项。但是,我们必须自己处理每个方面,例如如何循环遍历文件、如何设置IFS 变量等。

如今,大多数现代 Linux 发行版都默认预装了一些方便的文本处理实用程序,例如*sed *和awk

通过使用这些强大的实用程序,我们可以更轻松地解决问题。

接下来,让我们看看如何使用sed解决问题:

$ sed 's/.$//' input.txt | cat -e
This is a normal line$
This line has 3 trailing spaces.  $
The next line has only 4 spaces$
   $
The next line is an empty line$
$
I am the last line$

上面的sed命令使用正则表达式替换来删除每行的最后一个字符。与 Bash 版本相比,  sed解决方案看起来更紧凑。

同样,awk也可以用简短的形式解决问题:

$ awk '{sub(/.$/,"")}1' input.txt | cat -e
This is a normal line$
This line has 3 trailing spaces.  $
The next line has only 4 spaces$
   $
The next line is an empty line$
$
I am the last line$

我们已经解决了从每行中删除最后一个字符的问题,无论它是否是空格。

然而,在实践中,我们经常希望从文件的每一行中删除最后一个非空白字符。

接下来,让我们研究原始问题的一些变体。

4. 从每行中删除最后一个非空白字符

首先,让我们想象一下,如果一行有尾随空格,我们可能会有几个不同的要求:

  • 删除最后一个非空白字符和尾随空格: “example@ ” -> “example“
  • 仅删除最后一个非空白字符并保留尾随空格:“example@ ” -> “example“

下面我们来一一解决这两个变体需求。

4.1. 删除最后一个非空白字符和尾随空白

解决这个问题的一个想法是构建一个正则表达式 (regex) 匹配最后一个非空白字符后跟零个或多个空白字符。 然后我们可以用一个空字符串替换这个模式。

正则表达式模式不难构建。ERE 模式\S ”和“ \s ”分别匹配单个非空白字符和空白字符。它们正是我们要找的。

此外,sed和 awk都支持 ERE。先看看sed是怎么解决的:

$ sed -r 's/\S\s*$//' input.txt | cat -e
This is a normal line$
This line has 3 trailing spaces$
The next line has only 4 spaces$
    $
The next line is an empty line$
$
I am the last line$

我们将-r*选项传递给 GNU sed以告诉它我们在脚本中使用了 ERE。*

正如我们在上面的输出中看到的,最后一个非空白字符和所有尾随空格都已被删除。

此外,仅包含四个空格的行和空行保持不变,因为它们不包含任何非空白字符。

同样,我们可以使用awk命令获得相同的输出:

$ awk '{sub(/\S\s*$/,"")}1' input.txt | cat -e
This is a normal line$
This line has 3 trailing spaces$
The next line has only 4 spaces$
    $
The next line is an empty line$
$
I am the last line$

至此,我们已经解决了这个问题。

接下来,让我们看看如何保留尾随空格。

4.2. 删除最后一个非空白字符但保留尾随空白

我们仍然可以使用正则表达式替换来解决这个问题。但是,首先,让我们看看sed解决方案:

$ sed -r 's/\S(\s*)$/\1/' input.txt | cat -e
This is a normal line$
This line has 3 trailing spaces   $
The next line has only 4 spaces$
    $
The next line is an empty line$
$
I am the last line$

这一次,我们将尾随空格“ \s* ”放在捕获组中

稍后,我们不会像以前那样在替换中用空字符串替换模式。相反,我们在替换中引用捕获组以保留尾随空格。

如上面的输出所示,这些句点和冒号已被删除。但是,我们保留了尾随空格。

因此,我们已经使用sed命令解决了这个问题 。

GNU  awk的不错的 gensub 函数也允许我们处理反向引用。

最后看看awk解决问题的方法:

$ awk '{ $0=gensub(/\S(\s*)$/,"\\1","g") } 1' input.txt | cat -e
This is a normal line$
This line has 3 trailing spaces   $
The next line has only 4 spaces$
    $
The next line is an empty line$
$
I am the last line$

当我们使用 gensub函数时,我们应该记住,sub 和 gsub函数不同,gensub将结果作为新字符串返回

因此,我们需要将结果赋给一个变量。

除此之外,当我们想在gensub函数中引用一个捕获组时,我们必须对索引进行转义,例如,“\1”代表组 1。