对记录行中的数组进行数学运算

对记录行中的数组进行数学运算

我是 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。将它们放在函数中,用 或 声明变量declarelocal和/或使用数组不会改变基本原则修改子 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,并且用途不大。将参数传递给returnexit内置函数的主要目的是指示是否存在错误,或者发生了几种可能的错误中的哪一种,或者返回小一把可能的信息片段。(例如,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))

    但是,由于在这种情况下你只需要从 到 循环19所以它更简单、更清晰、更易于使用一个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 解决方案可能更具可移植性。

相关内容