如何以 POSIX 方式计算字符串变量中的行数?

如何以 POSIX 方式计算字符串变量中的行数?

我知道我可以在 Bash 中执行此操作:

wc -l <<< "${string_variable}"

基本上,我发现的所有内容都涉及<<<Bash 操作符。

但在 POSIX shell 中,<<<未定义,我几个小时以来一直无法找到替代方法。我很确定有一个简单的解决方案,但不幸的是,到目前为止我还没有找到。

答案1

简单的答案是,这wc -l <<< "${string_variable}"printf "%s\n" "${string_variable}" | wc -l.

<<<实际上,管道工作的方式有所不同:<<<创建一个临时文件作为命令的输入传递,而|创建一个管道。在 bash 和 pdksh/mksh 中(但不在 ksh93 或 zsh 中),管道右侧的命令在子 shell 中运行。但在这种特殊情况下,这些差异并不重要。

请注意,就行计数而言,这假设变量不为空并且不以换行符结尾。当变量是命令替换的结果时,不以换行符结尾,因此在大多数情况下您会得到正确的结果,但空字符串会得到 1。

var=$(somecommand); wc -l <<<"$var"和之间有两个区别somecommand | wc -l:使用命令替换和临时变量会去除末尾的空白行,忘记输出的最后一行是否以换行符结束(如果命令输出有效的非空文本文件,则总是如此) ,如果输出为空则加一。如果您想同时保留结果和计数行,可以通过附加一些已知文本并在末尾将其剥离来实现:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"

答案2

不符合 shell 内置功能,使用外部实用程序,例如grepawkPOSIX 兼容选项,

string_variable="one
two
three
four"

使用 withgrep来匹配行的开头

printf '%s' "${string_variable}" | grep -c '^'
4

awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

请注意,某些 GNU 工具(尤其是 GNU)grep不考虑POSIXLY_CORRECT=1运行该工具的 POSIX 版本的选项。受设置变量影响的唯一行为是grep命令行标志的处理顺序有所不同。从文档(GNUgrep手册)来看,似乎

POSIXLY_CORRECT

如果设置,grep 的行为将符合 POSIX 的要求;否则, grep其行为更像其他 GNU 程序。 POSIX 要求文件名后面的选项必须被视为文件名;默认情况下,此类选项被排列到操作数列表的前面并被视为选项。

如何在 grep 中使用 POSIXLY_CORRECT?

答案3

Here-string<<<几乎是here-document 的单行版本<<。前者不是标准功能,但后者是。<<在这种情况下你也可以使用。这些应该是等效的:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

但请注意,两者都在末尾添加了一个额外的换行符$somevar,例如此打印6,即使变量只有五行:

s=$'foo\n\n\nbar\n\n'
wc -l <<< "$s"

使用printf,您可以决定是否需要额外的换行符:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

但请注意,wc仅计算完整行(或字符串中换行符的数量)。grep -c ^还应该计算最后的行片段。

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

${var%...}(当然,您也可以通过使用扩展在循环中一次删除一行来完全计算 shell 中的行数...)

答案4

在那些令人惊讶的频繁情况下,您实际需要做的是处理所有非空以某种方式(包括对它们进行计数)在变量内的行,您可以将 IFS 设置为换行符,然后使用 shell 的分词机制将非空行分开。

例如,下面是一个小 shell 函数,它对所有提供的参数中的非空行进行总计:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

这里使用括号而不是大括号来构成函数体的复合命令。这使得该函数在子 shell 中执行,这样就不会在每次调用时污染外界的 IFS 变量和路径名扩展设置。

如果你想迭代非空行,你可以这样做:

IFS='
'
set -f
for line in $lines
do
    printf '[%s]\n' $line
done

以这种方式操作 IFS 是一种经常被忽视的技术,也可以方便地执行诸如解析可能包含制表符分隔的列式输入中的空格的路径名之类的操作。但是,您确实需要注意,故意删除通常包含在 IFS 默认设置 space-tab-newline 中的空格字符可能最终会在您通常希望看到它的地方禁用分词。

例如,如果您使用变量为类似的东西构建复杂的命令行ffmpeg,则您可能希望-vf scale=$scale仅在变量scale设置为非空时才包含。通常您可以通过以下方式实现此目的${scale:+-vf scale=$scale},但如果 IFS 在完成此参数扩展时不包含其通常的空格字符,-vf则 和之间的空格scale=将不会用作单词分隔符,并将ffmpeg全部-vf scale=$scale作为单个参数传递,它不会理解。

要解决此问题,您需要确保在进行扩展之前更正常地设置 IFS ${scale},或者进行两次扩展:${scale:+-vf} ${scale:+scale=$scale}. shell 在命令行初始解析过程中执行的单词拆分(与处理这些命令行的扩展阶段期间执行的拆分相反)不依赖于 IFS。

如果您要做这种事情,那么其他可能值得您花时间的事情是创建两个全局 shell 变量来仅保存一个选项卡和一个换行符:

t=' '
n='
'

这样,您就可以在需要制表符和换行符的扩展中包含$tand ,而不用在所有代码中添加带引号的空格。$n如果您希望在没有其他机制的 POSIX shell 中完全避免引用空格,printf那么可以提供帮助,尽管您确实需要一些摆弄来解决命令扩展中尾随换行符的删除问题:

nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}

有时,将 IFS 设置为每个命令的环境变量效果很好。例如,下面是一个循环,它从制表符分隔的输入文件的每一行读取允许包含空格和缩放因子的路径名:

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

在这种情况下,read内置函数将 IFS 设置为只是一个制表符,因此它也不会在空格上拆分它读取的输入行。但IFS=$t set -- $lines 工作:shell$lines在构建set内置参数时扩展执行命令,因此以仅在内置函数本身执行期间应用的方式对 IFS 进行临时设置来得太晚了。这就是为什么我上面给出的代码片段在单独的步骤中设置了 IFS,以及为什么它们必须处理保留它的问题。

相关内容