我是 Bash 的新手。我正尝试通过 Bash 将打印的行记录到数组中。我想对此类数组中的某些元素执行数学运算(即在同一位置添加元素),并最终返回该数组以供函数之外进一步使用。
以下是我一直在摆弄的东西:
linesToArraySum() {
while read line
do
logLine=$line # saves currently logged line in variable logLine
IFS=';' read -a arrayLog <<< $logLine #redirect variable logLine as input for read command. read -a saves word of input string as array. InternalFieldSeparator set as ';' detects elements in input string which are separated by '; ' as words.
for n in 1 3 5 7 9 11
do
arraySum[n]=$((${arraySum[n]} + ${arrayLog[n]})) # define element in arraySum at position n as sum of previous element and element in arrayLog at this position
echo ${arraySum[n]}
done
return arraySum
done
}
如上所述,记录的行通过 ttylog 连续打印,但为了进行故障排除,我们假设我使用以下脚本生成它们:
while [[ $i < 9 ]]
do
i=$(($i + 1))
echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935" | linesToArraySum
done
commandDoSomethingWith_arraySum
我的问题是回显 ${arraySum[n]}在功能上行数转数组总和()总是返回当前值${数组[n]}而不是总和同一列中的值。
我将非常感谢任何针对我的错误提出的提示。\o/
答案1
在 Bash 中,管道(|
)在子壳。对子 shell 内部变量的更改(包括对数组元素的赋值)不会传播回父 shell。在您的测试代码中,您有:
echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935" | linesToArraySum
Shell 函数一般不会在子 shell 中运行,但在这种情况下,您正在linesToArraySum
子 shell 中运行,因为它出现在管道中。在其他一些 shell 中,如 Ksh,管道中最右边的命令不在子 shell 中运行,你的代码实际上可以在这样的 shell 中工作。但是 Bash 甚至在其自己的子 shell 中运行通过管道传输的最后一个命令。
因为linesToArraySum
在子shell中运行,所以arraySum
数组只存在于子shell中,绝不为调用者创建的,并且每次运行管道时都会在新的子 shell 中重新创建。此外,即使在启动子 shell 之前数组已经存在,在子 shell 中对其进行的修改也只会修改子 shell 的副本。
你要做的就是解决这个问题是使用不在子 shell 中运行该函数的方法传递输入linesToArraySum
。一种方法是使用这里是字符串而不是管道:
linesToArraySum <<<"dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"
您可以用它作为循环中那一行的替代品,但我建议用这个或类似的东西替换整个测试循环:
for i in {0..9}; do linesToArraySum <<<"dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"; done
(当然,您可以选择将其写成多行。)
作为Sergiy Kolodyazhnyy 提到,您每行调用一次函数,而不是传递所有行。我上面展示的修复代码并没有改变这一点。由于您编写了linesToArraySum
读取多行的函数,因此您可能希望测试代码对此进行测试。但这是不是为什么中的值arraySum
没有被保留。第一个 Bash 脚本Sergiy Kolodyazhnyy 的回答通过一次将多行输入管道传输到 shell 函数来避免此问题,这样数组的每次修改都发生在同一个子 shell 中。这就是它有效的原因。此外:
- 在
generate_lines | sum_line_tokens
命令完成后,后续命令仍然无法从中读取总和arraySum
,因为数组仍然是在子 shell 中创建的,而该子 shell 会在命令结束时被销毁。 - 只要你使用管道,创建
arraySum
数组前在管道中调用该函数也无法保存值。子 shellarraySum
将从调用者那里收到一份副本,因此在子 shell 中运行的代码将能够访问已分配给它的值,但当它写入数组时,这只会影响子 shell 的数组副本。如果您停止在管道中调用您的函数,那么您无需执行任何其他操作即可使其正常工作!
第二点值得进一步解释,因为它与一个常见的混淆点有关。在 Bash 中,x=foo; IFS= read -r x <<<bar; echo "$x"
打印bar
,但x=foo; echo bar | IFS= read -r x; echo "$x"
打印foo
。将它们放在函数中,用 或 声明变量declare
,local
和/或使用数组不会改变基本原则修改子 shell 中的变量不会为调用者修改它。例如,假设您运行以下定义:
f() { local -ai a=(10 20 30); g() { IFS= read -r 'a[3]'; echo "${a[@]}"; }; echo 40 | g; echo "${a[@]}"; }
然后运行f
。输出表明,a
在调用函数的管道中数组已被修改g
,但执行完命令后修改并未持久化echo 40 | g
:
10 20 30 40
10 20 30
第二个 Bash 脚本的原因是Sergiy Kolodyazhnyy 的回答其工作原理很简单,就是避免使用管道,从而避免sum_line_tokens
在子 shell 中运行其函数。其实现方式是从文件 ( < "$tempfile"
) 重定向输入,而不是使用管道:
generate_lines > "$tempfile"
sum_line_tokens "$1" < "$tempfile"
该脚本包含一个注释,解释道sum_line_tokens
如果在管道中使用它,它将在子 shell 中运行,例如generate_lines | sum_line_tokens
。该评论实际上就是对您整个问题的回答。该脚本中的其他更改(编写main()
函数、在调用使用该数组的函数之前明确创建数组以及使用local
内置函数执行此操作)完全无关紧要。(该脚本作为一个整体是但仍然有用,因为它展示了一种避免使用管道的方法,也展示了一种实现您在评论中询问的相关行为的方法。)
当您放弃将命令放入管道中以防止其在子 shell 中运行时,选择哪种替代方案将取决于具体情况。对于脚本中出现的文本,请使用这里是字符串(如上所示)或这里的文件. 对于另一个命令的输出,写作到临时文件进而阅读从中——就像Sergiy Kolodyazhnyy 的第二个 Bash 脚本——通常是一个合理的选择。你甚至可以创建临时文件作为命名管道和mkfifo
如果您希望它具有与 shell 管道相同的语义和类似的性能特征,则可以使用常规文件。但在大多数情况下,我建议使用流程替代,它实际上在后台为您创建、使用和销毁命名管道:
sum_line_tokens "$1" < <(generate_lines)
要运行该命令,shell:
- 创建一个临时的命名管道。
- 运行
generate_lines
并将其输出重定向到命名管道。 <(generate_lines)
用命名管道的名称替换。- 运行
sum_line_tokens "$1"
并将命名管道的输入重定向到它(由于<
)。
写入命名管道的命令实际上与从命名管道读取的命令同时运行。上面给出的顺序是为了概念上的方便(我必须按某种顺序编写它们)。还请注意:
- 第
<
一个输入重定向第二部分<
是进程替换语法的一部分必须分开。也就是说,...
你想从哪里获取输入的命令,写入< <(...)
,不是<<(...)
。 - 工艺替代做使用子 shell——但是仅有的替代过程。因此
generate_lines
命令是在子 shell 中运行,但sum_line_tokens
不是。如果您尝试修改 中的调用方变量generate_lines
,这些修改之后不会保留。但是,generate_lines
不必这样做。只sum_line_tokens
需要修改之后要使用的变量,因此它不能在子 shell 中运行。 - 进程替换——以及这里的字符串和
[[
——并不是可以移植到所有伯恩风格的贝壳. (此处文件和test
/[
是可移植的。)但数组也不可移植,因此只要您为此使用数组,您就已经没有编写可移植脚本(从跨不同 shell 移植的意义上来说),因此您可能没有理由避免使用进程替换。
您的脚本中还有其他一些错误。由于这些错误在任何脚本中都很容易犯 — — 不仅仅是这个脚本 — — 而且我猜您是为了练习而编写此脚本的,所以我将在这里列出它们。然而,Sergiy Kolodyazhnyy 说,您应该考虑使用类似的工具awk
。许多标准的 Unix 实用程序主要用于逐行处理文本,这awk
是其中之一。
使用 shell 循环处理文本有时是合理的,在极少数情况下甚至是最佳选择。但对于几乎任何可以用标准实用程序完成的任务,最好这样做while
而不是在你的 shell 中写一个使用read
内置命令的循环。shell 是粘合语言如果有外部命令可以完成这项工作,您就应该使用它。
话虽如此,如果您选择继续使用脚本,我建议您改进脚本的以下其他方面:
作为Sergiy Kolodyazhnyy 说,您不能使用
return
它来返回数组。事实上,您甚至不能返回一个简单的变量。您只能返回一个退出代码, 哪个范围应为 0 到 255,并且用途不大。将参数传递给return
或exit
内置函数的主要目的是指示是否存在错误,或者发生了几种可能的错误中的哪一种,或者返回小一把可能的信息片段。(例如,test
/[
内置命令的返回代码表示测试条件是真还是假。)使用您拥有的代码,您应该会看到此错误:-bash: return: arraySum: numeric argument required
-r
使用内置函数时,应该传递read
。否则\
转义会被扩展。这是你极少会想要的情况。因此,请使用read -r line
代替read line
并使用read -ra arrayLog
(或者read -r -a arrayLog
,如果你喜欢这种风格)代替read -a arrayLog
。即使将一行读入单个变量,
IFS=
除非您有特定原因知道您不需要(或不需要),否则请设置。不要使用while read line
,而要使用while IFS= read -r line
。原因是 会从读取的行的开头和结尾read
删除 IFS 空格( -- 中的任何内容)$IFS
。例外情况是如果您确实希望发生这种情况并且--对于 Bash--如果省略变量名。在 Bash 中,read -r
没有变量名相当于IFS= read -r REPLY
。虽然实际上没有错,但您不必在内部使用完整的参数扩展语法
((
))
来使用变量或数组元素的值。避免这样做会使此类表达式更易于阅读。$((arraySum[n] + arrayLog[n]))
优于$((${arraySum[n]} + ${arrayLog[n]}))
。使用
test
、[
和[[
运算<
符执行字典顺序字符串比较,并且不是数值比较。要检查是否$i
小于9
,可以使用[[ $i -lt 9 ]]
。例如,使用i=89
,[[ $i < 9 ]]
返回 true!类似地,你会使用-gt
表示数值大于、-le
表示数值小于或等于,以及-ge
表示数值大于或等于。或者您可能想写(($i < 9))
,这样也可以,就像 一样((i < 9))
。但是,由于在这种情况下你只需要从 到 循环
1
,9
所以它更简单、更清晰、更易于使用一个for
循环和括号扩展({1..9}
)如本文开头所示。
最后,我建议利用静态代码分析通过检查你的 shell 脚本壳牌检测。ShellCheck 将捕获上面列出的大多数错误。许多经验丰富的 shell 脚本编写者经常使用它,但它对新手也非常有用,因为它链接到每个规则的完整解释。
有时 ShellCheck 会将某些实际上正确的内容识别为可能错误。例如,当我在您的脚本上运行它时,它引发了SC2086对于。严格来说<<< $logLine
,在当前支持的 Ubuntu 系统提供的 Bash 版本中,这不是必需的,因为<<<
这里是字符串不受路径名扩展或单词拆分的影响。但是,早期版本没有跳过这些扩展,再加上是个好主意到引用你的变量只要你没有特别的理由不这样做。这是一种常见的模式:即使 ShellCheck 的一些警告表明你可以放心地忽略吧,如果你选择听从它们的话,你会写出更好的代码。
答案2
我擅自编辑了你的函数,并将所有内容放入一个简单的脚本中。问题的核心是你必须回显在 while 循环完成后。另外,bash 函数不会“返回”数组,您必须以某种方式将它们回显到 stdout 或使用主函数并local
在主函数中有一个数组,然后子函数才能访问该数组(这是我在自己的脚本中经常做的事情)。
这是测试结果。对于 9 次迭代,第 1 列始终为 110,我们适当地得到了 990。
$ ./generate_lines.sh
990 1008 1035 1008 1017 1080
脚本如下:
#!/usr/bin/env bash
sum_line_tokens() {
while read line
do
#echo "$line"
logLine=$line # saves currently logged line in variable logLine
# redirect variable logLine as input for read command.
# read -a saves word of input string as array. InternalFieldSeparator set as ';'
# detects elements in input string which are separated by '; ' as words.
IFS=';' read -a arrayLog <<< $logLine
for n in 1 3 5 7 9 11
do
# define element in arraySum at position n as sum of previous element
# and element in arrayLog at this position
arraySum[n]=$(( ${arraySum[n]} + ${arrayLog[n]} ))
#echo "${arraySum[n]}"
done
done
# Functions in bash can only use return to indicate exit status
# This is more like int datatype for C or Java functions. If you want
# to return a string or array, you need to echo it to stdout
echo "${arraySum[@]}"
}
generate_lines(){
while [[ $i < 9 ]]
do
i=$(($i + 1))
echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"
done
}
generate_lines | sum_line_tokens
使用 awk 简化任务
虽然脚本可以工作,但是很长。我们可以用以下方法缩短解决方案awk
:
# again, same thing - the script now generates lines only, no summing.
# We'll pipe it to awk
$ ./generate_lines.sh
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
$ ./generate_lines.sh | awk -F ';' '{for(i=1;i<=11;i++) if(i%2 != 0) sum[i+1]+=$(i+1) }END{for(j in sum) printf "%d\t",sum[j];print ""}'
990 1008 1035 1008 1017 1080
使用主函数和本地数组
我非常提倡main
在脚本中使用函数,因为它可以使一切井井有条,而且作为一个额外的好处,你可以声明一个局部变量,你调用的每个函数main
都会知道这个变量,并且可以覆盖它。
嗯,你的情况有点问题。我们有两个函数,一个生成行,另一个 - 用这些行做一些事情,使用管道有问题 - 管道右侧运行的任何内容都在子 shell 中运行,这意味着当子 shell 退出时,来自子 shell 的所有信息都会消失。参见我以前的问题以供参考。
因此,我们需要一些中立立场 - 要么使用临时文件,要么使用所谓的“命名管道”。在这个例子中,我仅使用了临时文件。如果您需要解析的内容不是太大,您可以始终将所有内容存储在局部变量中,并让两个函数处理同一个变量 - 一个写入变量,一个解析变量。如果文本很长,达到数千行,最好使用一些临时文件。
因此,在此版本的脚本中,我介绍了几件事,包括主函数以及主函数如何获取命令行参数,以及您在评论中要求的内容。基本上,脚本现在获得 1 个命令行参数 - 即您想要的行数,并将其提供给函数sum_line_tokens
。如果没有命令行参数,它会对所有行求和。
测试运行:
$ ./generate_lines.sh 3
330 336 345 336 339 360
$ ./generate_lines.sh 4
440 448 460 448 452 480
脚本本身:
#!/usr/bin/env bash
sum_line_tokens() {
# To perform counting for n number of lines, use a counter variable
# In this case I am using argument passed from command-line
linecount=0
# IFS= and -r for better line reading to ensure that spaces won't mess you up
while IFS='' read -r line
do
# Check if we have arg 1 to function and quit after n lines
if [ -n $1 ] && [ $linecount -eq $1 ]
then
break
fi
logLine=$line
IFS=';' read -a arrayLog <<< $logLine
for n in 1 3 5 7 9 11
do
arraySum[n]=$(( ${arraySum[n]} + ${arrayLog[n]} ))
done
# increment line counter
((linecount++))
done
}
generate_lines(){
while [[ $i < 9 ]]
do
i=$(($i + 1))
echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"
done
}
main(){
# create local array. Any function called from main will know about it
local -a arraySum
# We can't just pipe lines to summing function. Whatever runs on the right-hand side
# of the pipe runs in subshell, which means when that subshell exits, your variables are gone
# See https://askubuntu.com/q/704154/295286
tempfile=$(mktemp)
generate_lines > "$tempfile"
sum_line_tokens "$1" < "$tempfile"
echo "${arraySum[@]}"
rm "$tempfile"
}
# Call main function with the command-line arguments. This works sort of like int main(String[] args) in Java
main "$@"
关于可移植性的说明
当然,因为我们使用了很多特定于 bash 的东西,如果你在bash
不可用的系统上运行它,它将无法工作。这是一个好的脚本吗?是的,它可以完成工作。这是可移植的脚本吗?不是。上面的 awk 解决方案可能更具可移植性。