【问题标题】:Remove trailing whitespace recursively only at end of file using grep/sed?仅在使用 grep/sed 的文件末尾递归删除尾随空格?
【发布时间】:2011-01-18 17:49:06
【问题描述】:

基本上,我有大约 1,500 个文件,这些文件的最后一个字符不应该是任何类型的空格。

如何检查一堆文件以确保它们不会以某种形式的空格结尾?(换行符、空格、回车符、制表符等)?

【问题讨论】:

  • 文本文件应该以换行符结尾;否则会导致 I/O 系统出现未定义的行为。 Unix 真的不在乎(尽管有些程序会)。其他系统可能不那么宽容。 'sed' 和 'awk' 会添加一个换行符,至少除非你很努力。您必须使用 Perl 或 Python 或 ... 才能在最后获得换行符。
  • @Jonathan Leffler - 好吧,问题是它们是 PHP 文件,并且从中生成了 XML 提要。如果它们在错误的地方包含空格,XML 就会到处乱扔垃圾。
  • 真的吗?这听起来更像是 XML 的问题。而且我仍然认为为了理智起见,您会在最后一行的末尾添加一个换行符。不超过一个换行符 - 我没有问题。但这个问题要求一个比这更严格的条件。我完全赞成没有尾随空格或制表符(问我的团队!),没有多余的空行(我的书中很少有超过 2 个空行;我通常只删除一个空行时间,除非代码的格式确实一致地使用双空行(但从来没有!)。
  • @Jonathan:当空白在 XML 中被视为“重要”时的规则是粗略的,并且取决于包含元素是否在 DTD 中声明为具有“混合”内容:usingxml.com/Basics/XmlSpace在尝试使用 XSLT 转换 XML 时,我自己也遇到了问题。
  • 这是我的 linux 知识中的一个漏洞,因此请多加注意。您不能仅以内存映射方式打开文件并从末尾向后移动并将文件缩小一个以针对最后出现的 \s \r \n 的任何组合吗?从来没有在 linux 中尝试过 mem 映射文件,所以...

标签: linux bash shell unix scripting


【解决方案1】:
awk '{if (flag) print line; line = $0; flag = 1} END {gsub("[[:space:]]+$","",line); printf line}'

编辑:

新版本:

sed 命令删除所有仅包含空格的尾随行,然后awk 命令删除结束换行符。

sed '/^[[:space:]]*$/{:a;$d;N;/\n[[:space:]]*$/ba}' inputfile |
    awk '{if (flag) print line; line = $0; flag = 1} END {printf line}'

缺点是读取文件两次。

编辑 2:

这是一个只读取文件一次的全 awk 解决方案。它以类似于上面的sed 命令的方式累积只有空白的行。

#!/usr/bin/awk -f

# accumulate a run of white-space-only lines so they can be printed or discarded
/^[[:space:]]*$/ {
    accumlines = accumlines nl $0
    nl = "\n"
    accum = 1
    next
}

# print the previous line and any accumulated lines, store the current line for the next pass
{
    if (flag) print line
    if (accum) { print accumlines; accum = 0 }
    accumlines = nl = ""
    line = $0
    flag = 1
}

# print the last line without a trailing newline after removing all trailing whitespace
# the resulting output could be null (nothing rather than 0x00)
# note that we're not print the accumulated lines since they're part of the 
# trailing white-space we're trying to get rid of
END {
    gsub("[[:space:]]+$","",line)
    printf line
}

编辑 3:

  • 删除了不必要的BEGIN 子句
  • lines 更改为accumlines,以便更容易区分line(单数)
  • 添加了 cmets

【讨论】:

  • 这会删除文件末尾的多个包含空格的行,还是只删除最后一行?
  • 对 awk 不太熟悉,但我认为这确实不能正确处理多尾空行的情况。 -1 现在。
  • 好的,我相信最后一个解决方案是正确的,-1 已恢复。虽然不能完全让自己 +1...
  • 在 awk 脚本中使用 END 块不太正确。由于文件的最后一个非空白行打印在此块中,因此应使用 ENDFILE 而不是 END。 END 块在脚本结束时只执行一次,但在处理完每个文件后应该输出最后一个非空白行。此版本在大多数情况下工作方式相同,但在使用就地模式或使用此 awk 脚本处理多个文件时会失败。
  • @infiniteRefactor:在我写这个答案的时候,我不相信添加了ENDFILE的gawk 4已经发布了。另请注意,我的脚本应该在没有 ENDFILE 的 AWK 的非 gawk 版本中运行正常(除了您描述的情况)。
【解决方案2】:

这将删除所有尾随空格:

perl -e '$s = ""; while (defined($_ = getc)) { if (/\s/) { $s .= $_; } else { print $s, $_; $s = ""; } }' < infile > outfile

sed 中可能有一个等价物,但我对 Perl 更熟悉,希望对你有用。基本思路:如果下一个字符是空格,则保存;否则,打印任何保存的字符,后跟刚刚读取的字符。如果我们在读取一个或多个空白字符后点击 EOF,它们将不会被打印。

这将简单地检测尾随空格,如果是,则给出退出代码 1:

perl -e 'while (defined($_ = getc)) { $last = $_; } exit($last =~ /\s/);' < infile > outfile

[编辑] 以上描述了如何检测或更改单个文件。如果您有一个包含要应用更改的文件的大型目录树,则可以将命令放在单独的脚本中:

修复.pl

#!/usr/bin/perl
$s = "";
while (defined($_ = getc)) {
    if (/\s/) { $s .= $_; } else { print $s, $_; $s = ""; }
}

并与find 命令结合使用:

find /top/dir -type f -exec sh -c 'mv "{}" "{}.bak" && fix.pl < "{}.bak" > "{}"' ';'

这会将每个原始文件移动到以“.bak”结尾的备份文件中。 (最好先在一个小的测试文件集上进行测试。)

【讨论】:

  • 所以这是逐个字符而不是逐行读取?顺便说一句,我不认为sed 可以做到这一点。
  • @Dennis:是的。所有读取都将由操作系统缓冲,因此不会非常慢。 (尽管在这种情况下文件似乎由短的文本行组成,但一次读取一行可能会在具有很长行的文件或可能包含很少\n 字符的二进制文件上导致性能下降和高内存使用。)
  • sed 中可能没有等效项;它处理行,因此发出换行符。
  • 呃哦,我担心我没有足够准确地解释我的问题......我拥有的那些 1,500 个文件位于目录和子目录中,您的代码可以用于在给定目录中启动吗?扫描每个子目录?
  • @j_random_hacker - 好的,我会使用 find。想进一步启发我如何使用 find 递归地替换 EOF 处的所有空白行/空格?
【解决方案3】:

从下往上阅读文件可能更容易:

tac filename | 
awk '
    /^[[:space:]]*$/ && !seen {next} 
    /[^[:space:]]/   && !seen {gsub(/[[:space:]]+$/,""); seen=1}
    seen
' | 
tac

【讨论】:

  • Creative :) 但是第 4 行从 /[^[: 开始,而它应该是 /^[[:,我相信 gsub 的第一个参数应该是 /^[[:space:]]+/。不能 +1,因为这将非常低效 - 第二个 tac 从管道获取输入,因此必须等待 awk 的整个结果保存后才能启动。
  • @j_random_hacker:不,插入符号在该行中表示“不”。在它之前的行中,它表示“行首”。 gsub 正在查看行尾的空格,而不是仅包含空格的行。但是,有一个问题。该脚本使用 AWK 的默认输出 print,因此附加了一个换行符。此外,文件内容会被处理 3 次。
  • @Dennis:我现在看到 /[^[: 是(令人惊讶的是!)正确的事情,谢谢,但我认为你对 gsub 部分是错误的——记住,我们是此时仍在处理 reversed 输入,因此我们应该从行的 开头 去除空格。
  • @j_random_hacker:tac 从上到下颠倒,rev 从左到右颠倒。所以$ 是正确的。
【解决方案4】:

只是为了好玩,这是一个简单的 C 答案:

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    int c, bufsize = 100, ns = 0;
    char *buf = malloc(bufsize);

    while ((c = getchar()) != EOF) {
        if (isspace(c)) {
            if (ns == bufsize) buf = realloc(buf, bufsize *= 2);
            buf[ns++] = c;
        } else {
            fwrite(buf, 1, ns, stdout);
            ns = 0;
            putchar(c);
        }
    }

    free(buf);
    return 0;
}

不会比Dennis's awk solution 长多少,而且,我敢说,它更容易理解! :-P

【讨论】:

    【解决方案5】:

    Perl 解决方案:

    # command-line arguments are the names of the files to check.
    # output is names of files that end with trailing whitespace
    for (@ARGV) {
      open F, '<', $_;
      seek F, -1, 2;                # seek to before last char in file
      print "$_\n" if <F> =~ /\s/
    }
    

    【讨论】:

    • 如果我正确地解释了你的代码,那么剩下的几行呢。
    • 哦,我把这个问题解释为只关心文件的结尾,而不是每一行的结尾。
    • @ghostdog74:mob 的解释是正确的——检查 OP 的问题。
    【解决方案6】:
    ruby -e 's=ARGF.read;s.rstrip!;print s' file
    

    基本上,读取整个文件,去掉最后一个空格(如果有),然后打印出内容。所以这个解决方案不适用于非常大的文件。

    【讨论】:

    • 我是否认为这会将整个文件读入内存?
    【解决方案7】:

    您也可以使用man ed 删除文件末尾的尾随空格,使用man dd 删除最后一个换行符(但请记住,ed 将整个文件读入内存并执行就地编辑,无需任何形式以前的备份):

    # tested on Mac OS X using Bash
    while IFS= read -r -d $'\0' file; do
       # remove white space at end of (non-empty) file
       # note: ed will append final newline if missing
       printf '%s\n' H '$g/[[:space:]]\{1,\}$/s///g' wq | ed -s "${file}"
       printf "" | dd  of="${file}" seek=$(($(stat -f "%z" "${file}") - 1)) bs=1 count=1
       #printf "" | dd  of="${file}" seek=$(($(wc -c < "${file}") - 1)) bs=1 count=1
    done < <(find -x "/path/to/dir" -type f -not -empty -print0)
    

    【讨论】:

      【解决方案8】:

      使用man dd 而不使用man ed

      while IFS= read -r -d $'\0' file; do
         filesize="$(wc -c < "${file}")"
         while [[ $(tail -c 1 "${file}" | tr -dc '[[:space:]]' | wc -c) -eq 1 ]]; do
            printf "" | dd  of="${file}" seek=$(($filesize - 1)) bs=1 count=1
            let filesize-=1
         done
      done < <(find -x "/path/to/dir" -type f -not -empty -print0)
      

      【讨论】:

        【解决方案9】:

        版本 2。Linux 语法。正确的命令。

        find /directory/you/want -type f | \ 
        xargs --verbose -L 1 sed -n --in-place -r \
        ':loop;/[^[:space:]\t]/ {p;b;}; N;b loop;'  
        

        版本 1. 删除每行末尾的空格。 FreeBSD 语法。

        find /directory/that/holds/your/files -type f | xargs -L 1  sed  -i '' -E 's/[:         :]+$//'
        

        [: :] 中的空格实际上由一个空格和一个制表符组成。 有了空间,这很容易。你只需按下空格键。要插入制表符,请按 Ctrl-V,然后在 shell 中按 Tab。

        【讨论】:

        • 这只会从每行的末尾修剪空白 - 提问者想要从 file 的末尾修剪所有空白字符(可能跨越 1 行或多行) .
        • 另外sed 采用-e 选项,而不是-E
        • @j_random_hacker:OS X(和 BSD)sed 接受 -E 用于扩展正则表达式(GNU sed 使用 -r)。
        • 确实如此。我没有注意到文件的结尾。明天我会尝试重写。
        • @akond:很高兴看到sed 快到了,-1 恢复了。但是:我认为你的新正则表达式应该是/[^ &lt;-&gt;\n]/&lt;-&gt; = Tab)或/[^[:space:]]/\t 和冒号不应该在那里。似乎还有一个最终的\n 仍然留在输出文件的末尾——是否可以用sed 摆脱它?
        猜你喜欢
        • 1970-01-01
        • 2011-05-25
        • 1970-01-01
        • 2010-09-14
        • 1970-01-01
        • 2021-09-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多