user1034850
user1034850

Reputation: 51

Bash: Use a variable as an array name

I am parsing a log file and creating associative arrays for each user with the line number and the last field (total time logged in). The lines of the log file look like this:

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)

Where the first field (jww3321) will be the array name and the first entry in the array will be (1,00:58), the next will be (2,(the next time for user)). In order to obtain the proper keys I need to save the the length of list and add one to it when I add the next value to a user array. My code so far looks like this:

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

I have tried using indirect references but I am stuck with this error:

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

Any suggestions?

Upvotes: 5

Views: 6042

Answers (4)

ata
ata

Reputation: 2065

I'm not sure if I understood correctly what you are trying to do, specifically the "associative" part (I can't see where an associative array is used), but this code does what I UNDERSTAND that you want to do:

#!/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

It reads from stdin. I've tested it with an example file like this:

    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)

Output:

    jww3321 has 2 entries:
    jww3321[0] = (1,00:58)
    jww3321[1] = (2,01:58)

    jpd8635 has 2 entries:
    jpd8635[0] = (1,04:26)
    jpd8635[1] = (2,05:26)

Note that only standard integer-indexed arrays are used. In Bash, as of now, indirect array references in the left side always involve using eval (uuuuuuhhhh, ghostly sound), in the right side you can get away with ${!} substitution and command substitution $().

Rule of thumb with eval: escape what you want to be expanded at eval time, and don't escape what you want to be expanded before eval time. Any time you're in doubt about what ends up being eval'd, make a copy of the line and change eval for echo.

edit: to answer sarnold's comment, a way to do this without 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

Upvotes: 4

bmk
bmk

Reputation: 14147

Within bash you could use eval:

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

resp.

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

Upvotes: 2

choroba
choroba

Reputation: 242443

You are not creating associative arrays. The error is related to the syntax of ${#$uname[@]}: delete the second dollar sign.

Upvotes: 2

sarnold
sarnold

Reputation: 104120

I like bash(1), I think it's fair enough for "small" tasks. I'm often impressed with how much work it can get done in small spaces. But I think other languages can provide friendlier datastructures. A decade ago, I would have used perl(1) for this without thinking twice but I've grown to dislike the syntax of storing hashes as values in other hashes. Python would be pretty easy too, but I know Ruby better than Python at this point, so here's something similar to what you're working on:

#!/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)

The __END__ (and corresponding DATA) are just to make this a self-contained example. If you choose to use this, replace DATA with STDIN and delete __END__ and everything following it.

Since I mostly think in C, this might not by the most idiomatic Ruby example, but it does demonstrate how a hash (associative array) can have an array for each key (which is sadly more complicated than it could be), shows how to append to the array (u << ...), shows some simple mathematics, shows some simple iteration over the hash (users.each() do ...), and even uses some higher order functions (list.inject(0) { .. }) to calculate a sum. Yes, the sum could be calculated with a more-usual looping construct, but there's something about the elegence of "do this operation on all elements of this list" that makes it an easy construct to choose.

Of course, I don't know what you're really doing with the data from the last(1) command, but this ruby(1) seems easier than the corresponding bash(1) script would be. (I'd like to see it, in the end, just for my own education.)

Upvotes: 1

Related Questions