使用 POSIX 实用程序或 GNU coreutils 或 Perl 计算尾随换行符
Count trailing newlines with POSIX utilities or GNU coreutils or Perl
我正在寻找方法来计算来自可能是二进制数据的尾随换行符的数量:
- 从标准输入读取
- 或已经在 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
是字符串的一部分时(无论如何它只在从 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
,只要它们在尾随 NUL
s
之前、之内或之后
或:
报错
将 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 变量
$&
被分配给匹配的子字符串。
我正在寻找方法来计算来自可能是二进制数据的尾随换行符的数量:
- 从标准输入读取
- 或已经在 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
是字符串的一部分时(无论如何它只在从 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
,只要它们在尾随 NUL
s
或:
报错
将 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 变量
$&
被分配给匹配的子字符串。