Reputation: 51
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
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
Reputation: 14147
Within bash
you could use eval
:
eval count=`echo "$\{#$uname[@]\}"`
resp.
eval count="$\{#$uname[@]\}"
Upvotes: 2
Reputation: 242443
You are not creating associative arrays. The error is related to the syntax of ${#$uname[@]}
: delete the second dollar sign.
Upvotes: 2
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