Bash: 如何将变量用作数组名称

5

我正在解析日志文件,并为每个用户创建关联数组,其中包括行号和最后一个字段(总登录时间)。日志文件的行如下所示:

jww3321   pts/2        cpe-76-180-64-22 Mon Oct 18 23:29 - 00:27  (00:58)
jpd8635   pts/1        cpe-74-67-131-24 Mon Oct 18 23:22 - 03:49  (04:26)

第一个字段(jww3321)将是数组的名称,数组中的第一条目将是(1,00:58),下一个将是(2,(用户的下一个时间))。为了获得正确的键,我需要保存列表的长度,并在将下一个值添加到用户数组时将其加1。到目前为止,我的代码如下:

cat lastinfo.txt | while read line
do
    uname=`echo "$line" | awk '{print $1;}'`
    count=`echo "${#$uname[@]}"`
    echo "$count"
done

我尝试使用间接引用,但是遇到了这个错误:

l8t1: line 7: ${#$uname[@]}: bad substitution

有什么建议吗?

1
我认为使用另一种语言来完成这个任务会更容易。 :) - sarnold
你认为哪种语言更适合这个任务?我发现在这种操作方面,Bash非常强大。 - user1034850
关联数组在哪里?看起来你正在使用普通的整数索引数组。使用关联数组的例子是:uname [jww332] =“1,00:58”。 - ata
4个回答

4

我不确定我是否正确理解了你想要做的事情,尤其是“关联”部分(我看不到关联数组在哪里使用),但这段代码实现了我所理解的你想要做的事情:

#!/bin/bash
while IFS=" " read user time; do
    eval "item=\${#$user[@]} ; $user[\$item]=\(\$((\$item + 1)),$time\)"
    [[ "${arraynames[@]}" =~ $user ]] || arraynames[${#arraynames[@]}]=$user
done< <(sed -r 's/^ *([[:alnum:]]*) .*\((.*)\)$/\1 \2/')

for arrayname in ${arraynames[@]}; do
    eval "array=(\${$arrayname[@]})"
    echo "$arrayname has ${#array[@]} entries:"
    for item in ${!array[@]}; do
        echo "$arrayname[$item] = ${array[$item]}"
    done
    echo
done

它从标准输入读取。我已经使用像这样的示例文件进行了测试:
jww3321 pts/2 cpe-76-180-64-22 Mon Oct 18 23:29 - 00:27 (00:58) jpd8635 pts/1 cpe-74-67-131-24 Mon Oct 18 23:22 - 03:49 (04:26) jww3321 pts/2 cpe-76-180-64-22 Mon Oct 18 23:29 - 00:27 (01:58) jpd8635 pts/1 cpe-74-67-131-24 Mon Oct 18 23:22 - 03:49 (05:26)
输出结果如下:
jww3321有两个条目: jww3321[0] = (1,00:58) jww3321[1] = (2,01:58)
jpd8635有两个条目: jpd8635[0] = (1,04:26) jpd8635[1] = (2,05:26)
请注意,仅使用标准整数索引数组。在Bash中,到目前为止,在左侧间接数组引用始终涉及使用eval(uuuuuuhhhh,幽灵般的声音),在右侧您可以通过${!}替换和命令替换$()轻松获得。
关于eval的经验法则:转义希望在eval时间扩展的内容,并且不要转义希望在eval时间之前扩展的内容。每当您对最终被评估的内容感到怀疑时,请复制该行并将eval更改为echo。
编辑:回答sarnold的评论,一种无需eval的方法:
#!/bin/bash
while IFS=" " read user time; do
    array=$user[@] array=( ${!array} ) item=${#array[@]}
    read $user[$item] <<< "\($(($item + 1)),$time\)"
    [[ "${arraynames[@]}" =~ $user ]] || arraynames[${#arraynames[@]}]=$user
done< <(sed -r 's/^ *([[:alnum:]]*) .*\((.*)\)$/\1 \2/')

for arrayname in ${arraynames[@]}; do
    array=$arrayname[@] array=( ${!array} )
    echo "$arrayname has ${#array[@]} entries:"
    for item in ${!array[@]}; do
        echo "$arrayname[$item] = ${array[$item]}"
    done
    echo
done

哎呀,使用 eval 来完成看起来如此微不足道的任务似乎有些可惜,但实际上使用标准 shell 脚本很容易做到。谢谢! - sarnold
@sarnold:实际上,在bash中有一种方法可以完全不使用eval来进行*任何类型的间接引用,尽管在处理数组时有些笨拙,但总体而言还是很好的。但是开始揭开“神话”——eval是在bash中进行间接引用的唯一方法的说法还是很好的。更新后添加了非eval版本。 - ata

2

您没有创建关联数组。错误与${#$uname[@]}的语法有关:删除第二个美元符号。


2

bash 中,您可以使用 eval

eval count=`echo "$\{#$uname[@]\}"`

响应。

eval count="$\{#$uname[@]\}"

1

我喜欢使用bash(1),我认为它足够适用于“小”任务。我经常对它在小空间内完成的工作量感到印象深刻。但是我认为其他编程语言可以提供更友好的数据结构。十年前,我会毫不犹豫地使用perl(1),但现在我已经不喜欢将哈希存储为其他哈希值的语法了。Python也很容易上手,但我目前更熟悉Ruby,所以这里有一个类似于你正在处理的东西:

#!/usr/bin/ruby -w

users = Hash.new() do |hash, key|
    hash[key] = Array.new()
end

lineno = 0

while(line = DATA.gets) do
    lineno+=1
    username, _ptr, _loc, _dow, _mon, _date, _in, _min, _out, time =
        line.split()
    u = users[username]
    minutes = 60 * Integer(time[1..2]) + Integer(time[4..5])
    u << [lineno, minutes]
end

users.each() do |user, list| 
    total = list.inject(0) { |sum, entry| sum + entry[1] }
    puts "#{user} was on #{list.length} times for a total of #{total} minutes"
end

__END__
jww3321   pts/2        cpe-76-180-64-22 Mon Oct 18 23:29 - 00:27  (00:58)
jpd8635   pts/1        cpe-74-67-131-24 Mon Oct 18 23:22 - 03:49  (04:26)
jww3321   pts/2        cpe-76-180-64-22 Mon Oct 18 23:29 - 00:27  (00:58)
jpd8635   pts/1        cpe-74-67-131-24 Mon Oct 18 23:22 - 03:49  (04:26)
jww3321   pts/2        cpe-76-180-64-22 Mon Oct 18 23:29 - 00:27  (00:58)
jpd8635   pts/1        cpe-74-67-131-24 Mon Oct 18 23:22 - 03:49  (04:26)

__END__(以及相应的DATA)只是为了使其成为一个自包含的示例。如果您选择使用它,请将DATA替换为STDIN,并删除__END__及其后面的所有内容。

由于我主要是用C思考,所以这可能不是最典型的Ruby示例,但它确实演示了哈希(关联数组)可以具有每个键的数组(比它本该更简单),展示了如何附加到数组(u << ...),展示了一些简单的数学运算,展示了对哈希的简单迭代(users.each() do...),甚至使用了一些高阶函数list.inject(0) { .. })来计算sum。是的,可以使用更常见的循环结构来计算总和,但“对此列表的所有元素执行此操作”的优雅之处使其成为易于选择的构造。

当然,我不知道你从last(1)命令中获取的数据是用来做什么的,但这个ruby(1)脚本似乎比相应的bash(1)脚本更容易。最终,我想看看它,只是为了自己的学习。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接