如何POSIX-ly计算字符串变量中的行数?


10

我知道我可以在Bash中做到这一点:

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

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

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

Answers:


11

简单的答案是,这wc -l <<< "${string_variable}"是ksh / bash / zsh的快捷方式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%.}"

1
@Inian Keeping wc -l完全等同于原始值:<<<$foo将换行符添加到$foo(即使$foo为空)值。我在回答中解释了为什么这可能不是我们想要的,但却是要问的。
吉尔斯(Gillles)“所以-别再作恶了” '18

2

使用诸如POSIX兼容选项之类的外部实用程序grepawk与之兼容的Shell内置程序,

string_variable="one
two
three
four"

grep行首匹配

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

awk

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

请注意,某些GNU工具(尤其是GNU)grepPOSIXLY_CORRECT=1支持运行该工具的POSIX版本的选项。在grep受影响通过设置变量的唯一行为将在命令行标志的顺序的处理的差异。从文档(GNU grep手册)看来,

POSIXLY_CORRECT

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

请参阅如何在grep中使用POSIXLY_CORRECT?


2
当然wc -l这里仍然可行吗?
Michael Homer

@MichaelHomer:根据我的观察,wc -l需要一个合适的换行符分隔流(末尾带有'\ n`以便正确计数)。一个人不能使用简单的FIFO来与一起使用printf,例如,printf '%s' "${string_variable}" | wc -l可能无法按预期工作,但<<<由于\n此字符串附加了尾随
Inian

1
那是printf '%s\n'在做什么,然后再将其取出...
Michael Homer

1

这里的字符串<<<几乎是这里文档的单行版本<<。前者不是标准功能,但后者是标准功能。<<在这种情况下,您也可以使用。这些应该等效:

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%...}扩展来一次循环地删除一行来完全计数外壳中的行...)


0

在那些实际需要做的令人惊讶的频繁情况下,您实际上需要以某种方式处理变量中的所有非空行(包括对它们进行计数),可以将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作为单个参数传递,这是不明白的。

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

如果您打算做这种事情,那么值得您花些时间的事情是创建两个全局shell变量以仅包含一个制表符和一个换行符:

t=' '
n='
'

这样,你可以只包含$t$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 行不通的:外壳程序会执行命令之前$lines构建set内置参数时扩展,因此以仅在内置程序本身执行期间适用的方式对IFS进行临时设置为时已晚。这就是为什么我首先给出的代码片段在单独的步骤中设置了IFS的原因,以及为什么它们必须处理保留它的问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.