使用 POSIX 实用程序或 GNU coreutils 或 Perl 计算尾随换行符

Count trailing newlines with POSIX utilities or GNU coreutils or Perl

我正在寻找方法来计算来自可能是二进制数据的尾随换行符的数量:

这应该可以工作没有临时文件或 FIFO。

当输入在 shell 变量中时,我已经有了以下(可能很丑但)可行的解决方案:

original_string=$'abc\n\n\def\n\n\n'
string_without_trailing_newlines="$( printf '%s' "${original_string}" )"
printf '%s' $(( ${#original_string}-${#string_without_trailing_newlines} ))

在上面的例子中给出 3

上面的想法只是减去字符串长度并使用命令替换的“功能”,它会丢弃任何尾随的换行符。

测试用例:

printf ''             |  function   results in: 0
printf '\n'           |  function   results in: 1
printf '\n\n'         |  function   results in: 2
printf '\n\n\n'       |  function   results in: 3
printf 'a'            |  function   results in: 0
printf 'a\n'          |  function   results in: 1
printf 'a\n\n'        |  function   results in: 2
printf '\na\n\n'      |  function   results in: 2
printf 'a\n\nb\n'     |  function   results in: 1

对于特殊情况,当 NUL 是字符串的一部分时(无论如何它只在从 stdin 读取时起作用,而不是在通过变量在 shell 中提供字符串时),结果是 undefined 但通常应该是:

printf '\n\x00\n\n'   |  function   results in: 1
printf 'a\n\n\x00\n'  |  function   results in: 2

计算新行直到 NUL

或:

printf '\n\x00\n\n'   |  function   results in: 2
printf 'a\n\n\x00\n'  |  function   results in: 1

计算 NUL

中的换行符

或:

printf '\n\x00\n\n'   |  function   results in: 3
printf 'a\n\n\x00\n'  |  function   results in: 3

忽略任何“尾随”NUL,只要它们在尾随 NULs

之前、之内或之后

或:
报错

将 GNU awk 用于 RT 并且不立即将所有输入读入内存:

$ printf 'abc\n\n\def\n\n\n' | awk '/./{n=NR} END{print NR-n+(n && (RT==RS))}'
3

$ printf 'a\n' | awk '/./{n=NR} END{print NR-n+(n && (RT==RS))}'
1

$ printf 'a' | awk '/./{n=NR} END{print NR-n+(n && (RT==RS))}'
0

$ printf '' | awk '/./{n=NR} END{print NR-n+(n && (RT==RS))}'
0

$ printf '\n' | awk '/./{n=NR} END{print NR-n+(n && (RT==RS))}'
1

$ printf '\n\n' | awk '/./{n=NR} END{print NR-n+(n && (RT==RS))}'
2

一些基于perl的解决方案:

#!/usr/bin/env bash

original_string=$'abc\n\n\ndef\n\n\n'

# From a shell variable. Look ma, no pipes!
input="$original_string" perl -E '$ENV{input} =~ /(\n*)\z/; say length '

# From standard input (Note: The herestring adds an extra newline)
perl -0777 -nE '/(\n*)\z/; say length() - 1' <<<"$original_string"

# Or in a shell without herestrings (But then you're also not getting the
# above $'' quoting syntax)
printf "%s" "$original_string" | perl -0777 -nE '/(\n*)\z/; say length ' 

还有一种更冗长的方式,它不涉及像 -0777 那样将输入作为单个块读取(除非根本没有换行符),适用于大量数据:

printf "abc\n\ndef\n\n\n" | perl -nE '
  if (/^\n\z/) { # Nothing but a newline
    $blank++
  } elsif (/\n\z/) { # Data that ends in a newline; reset counter to 1
    $blank = 1
  } else { # No newline (Last line is missing one?); reset counter to 0
    $blank = 0
  }
  END { say $blank }'

对于 GNU sed,我们可以使用 -z 选项,加上替换命令的 e 修饰符,并将所有这些打包到一个 sed 脚本中:

$ printf 'abc\n\n\def\n\n\n' | sed -Ezn '${s/.*[^\n]//;s/.*/wc -l <<!\n&!/ep}'
3

或者,如果字符串在变量中:

$ printf '%s' "$original_string" | sed -Ezn '${s/.*[^\n]//;s/.*/wc -l <<!\n&!/ep}'
3

解释:

  • -z 选项告诉 sed 输入行由 NUL 字符而不是换行符终止。

  • -n 选项禁用自动打印。

  • 2 个替换命令仅应用于最后一行($ 地址),即最后一个 NUL 字符之后的所有内容,或者如果没有 NUL 字符,则完整的输入字符串。

  • 第一个替换命令删除除尾随换行符之外的所有内容。

  • 第二个替换命令将这些尾随换行符替换为:

    wc -l <<!
    
    
    
    !
    

    here-document 中的行数与输入中尾随的换行符一样多。当使用 e 修饰符时,这个新模式 space 被执行,模式 space 被结果替换并打印(感谢 p 修饰符)。

编辑

正如 OP 所注意到的,当输入为空字符串而不是预期的 0 时,这根本不会产生任何输出。一个更简单的版本,也适用于空字符串可能是:

$ printf '%s' "$original_string" | sed -zn '${s/.*[^\n]//;p;}' | wc -l

另一个 perl 解决方案怎么样:

echo -ne 'abc\n\n\def\n\n\n' | perl -0777 -ne '/\n*$/; print length($&), "\n";'
=> 3
echo -ne '\n' | perl -0777 -ne '/\n*$/; print length($&), "\n";'
=> 1
echo -ne '\n\n' | perl -0777 -ne '/\n*$/; print length($&), "\n";'
=> 2
echo -ne 'a\n\n' | perl -0777 -ne '/\n*$/; print length($&), "\n";'
=> 2
echo -ne 'a' | perl -0777 -ne '/\n*$/; print length($&), "\n";'
=> 0
  • -0777 选项告诉 perl 一次性处理所有输入行。
  • -ne选项与sed类似。
  • 正则表达式 \n*$ 匹配输入字符串的尾随换行符。
  • Perl 变量 $& 被分配给匹配的子字符串。