【问题标题】:Count trailing newlines with POSIX utilities or GNU coreutils or Perl使用 POSIX 实用程序或 GNU coreutils 或 Perl 计算尾随换行符
【发布时间】:2022-01-07 01:17:10
【问题描述】:

我正在寻找从可能是二进制数据中计算尾随换行数的方法:

  • 从标准输入读取
  • 或已经在一个shell变量中(当然“二进制”至少不包括0x0) 使用 POSIX 或 coreutils 实用程序或 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 是字符串一部分的特殊情况(无论如何,它只在从标准输入读取时起作用,而不是在通过变量在外壳中给出字符串时),结果是 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 之前、之中或之后

或:
报错

【问题讨论】:

  • 我尝试使用 sed 进行一些操作,例如删除所有尾随换行符的行并计算剩余的行,但无法使其正常工作。
  • 如果数据是“可能是二进制的”,你将如何区分合法的0x0A 和换行符?
  • @JimGarrison 你认为他们可以通过什么方式来区分?
  • 不会有任何区别。任何 \n 都将被视为换行符。
  • @EdMorton 在复制和粘贴过程中出现拼写错误。已更正。

标签: shell perl awk sed posix


【解决方案1】:

RT 使用 GNU awk 并且不会一次将所有输入读入内存:

$ 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

【讨论】:

  • @calestyo 您应该在我对您的问题的回答中添加测试用例,并使用所有这些测试用例测试解决方案,因为其中一些测试用例的某些答案将失败。您可能还需要考虑必须将所有输入读入内存的答案之间的差异,因此对于大输入可能会失败与一次只读取 1 行的答案。
  • 添加了测试用例,...检查了所有当前的解决方案,似乎都产生了相同的结果。
  • 坏消息...看来您的解决方案仅适用于 gawk - mawk 和 original-awk 给出不同的结果。见pastebin.com/9VdXcL6H
  • @calestyo 并非所有发布的解决方案都会产生相同的结果,您一定错过了一些,例如给定printf '' | sed -Ezn '${s/.*[^\n]//;s/.*/wc -l <<!\n&!/ep}'stackoverflow.com/a/70617379/1745001 中的第一个sed 命令将不产生输出而不是0stackoverflow.com/a/70616543/1745001 中的最后一个perl 命令,因为相同的输入将打印一个空行。我还没有尝试过每个输入的其余答案。
  • @calestyo 这一点都不是坏消息,因为我在回答中特别指出,RT 需要 GNU awk (gawk)。
【解决方案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 $1'

# From standard input (Note: The herestring adds an extra newline)
perl -0777 -nE '/(\n*)\z/; say length($1) - 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 $1' 

还有一种更详细的方式,它不涉及像-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 }'

【讨论】:

  • perl -sE'$i =~ /(\n*)\z/; say length $1' -- -i "$original_string"
  • 请注意,-E 不向前兼容。我建议在除了一次性代码之外的所有代码中都反对它。你可以改用-M5.010 -e
  • 和我自己赢了那个小赌注。
  • @ikegami "注意 -E 不向前兼容" 你的意思是将来say 不会成为一个功能?根据perldoc perlrun,选项-E 启用所有可选功能。而且由于未来可选功能可能会发生变化,所以不确定-E未来会做什么?
  • @HåkonHægland 我的意思是可以添加一个会导致程序停止工作的功能。 -E 专门用于启用不向后兼容的东西
【解决方案3】:

使用 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 修饰符,这个新的模式空间被执行,模式空间被结果替换并打印(感谢p 修饰符)。

编辑

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

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

【讨论】:

  • 不错。只有一种情况它不起作用,即空字符串。我认为也可以将其写为 sed --zero-terminated -E -n '${s/^.*[^\n]//;p}' | wc -l ... 甚至适用于空字符串。
  • @calestyo 是的,你是对的,我忘了考虑这个案例。您使用管道连接到wc -l 的版本更好。我将对此进行编辑。
  • btw: -E 不是必须的,p 后面应该跟 ;更加正确(尽管这使用了 1-2 个 GNU 扩展)
【解决方案4】:

另一个 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 变量$&amp; 分配给匹配的子字符串。

【讨论】:

  • 这似乎不适用于单个 [尾随] 换行符的情况。 printf '\n' => 0(应为 1), printf '\n\n' => 1(应为 2)。当 '\n' 前面有一些字母时也是如此: printf 'a\n' => 0 (应该是 1), printf 'a\n\n' => 1 (应该是 2)。
  • 感谢您的礼貌反馈。我可能误解了trailing newlines 的定义。我已经用更正的答案更新了我的答案。 BR。
猜你喜欢
  • 2016-03-19
  • 2023-04-09
  • 1970-01-01
  • 1970-01-01
  • 2020-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多