5

I have a script that reads from /proc/stat and calculates CPU usage. There are three relevant lines in /proc/stat:

cpu  1312092 24 395204 12582958 77712 456 3890 0 0 0
cpu0 617029 12 204802 8341965 62291 443 2718 0 0 0
cpu1 695063 12 190402 4240992 15420 12 1172 0 0 0

Currently, my script only reads the first line and calculates usage from that:

cpu=($( cat /proc/stat | grep '^cpu[^0-9] ' ))
unset cpu[0]
idle=${cpu[4]}
total=0
for value in "${cpu[@]}"; do
  let total=$(( total+value ))
done

let usage=$(( (1000*(total-idle)/total+5)/10 ))
echo "$usage%"

This works as expected, because the script only parses this line:

cpu  1312092 24 395204 12582958 77712 456 3890 0 0 0

It's easy enough to get only the lines starting with cpu0 and cpu1

cpu=$( cat /proc/stat | grep '^cpu[0-9] ' )

but I don't know how to iterate over each line and apply this same process. Ive tried resetting the internal field separator inside a subshell, like this:

cpus=$( cat /proc/stat | grep '^cpu[0-9] ' )
(
IFS=$'\n'
for cpu in $cpus; do
    cpu=($cpu)
    unset cpu[0]
    idle=${cpu[4]}
    total=0
    for value in "${cpu[@]}"; do
        let total=$(( total+value ))
    done
    let usage=$(( (1000*(total-idle)/total+5)/10 ))
    echo -n "$usage%"
done
)

but this gets me a syntax error

line 18: (1000*(total-idle)/total+5)/10 : division by 0 (error token is "+5)/10 ")

If I echo the cpu variable in the loop it looks like it's separating the lines properly. I looked at this thread and I think Im assigning the cpu variable to an array properly but is there another error Im not seeing?

I put my script into whats wrong with my script and it doesnt show me any errors apart from a warning about using cat within $(), s o I'm stumped.

Community
  • 1
  • 1
Michael A
  • 4,391
  • 8
  • 34
  • 61
  • I think you should try doing this in `awk`. – Staven Dec 09 '13 at 04:51
  • @Staven I'll look into that. Is something like this pretty much a one liner in awk? – Michael A Dec 09 '13 at 04:53
  • @user30588 I added an `awk` solution too in my answer – janos Dec 09 '13 at 05:02
  • @janos Thanks for the reminder. Your bash answer is helpful, although the `awk` script in the other answer is superior to the one-line `awk` script in your answer. – Michael A Dec 17 '13 at 22:23
  • @user30588 you're welcome. I don't know what you mean by "superior" though, his solution is equivalent, only the syntax is different. – janos Dec 17 '13 at 23:01
  • @janos The non-one-liner code is more readable, and takes advantage of the fact that `awk` allows for floating-point division. The `(1000 * (total - idle) / total + 5) / 10` hack that I used in my bash code (and that's in your `awk` code) isn't necessary in `awk`. It just adds clutter. – Michael A Dec 17 '13 at 23:03
  • @user30588 hm, ok that's true – janos Dec 17 '13 at 23:19

2 Answers2

6

Change this line in the middle of your loop:

IFS=' ' cpu=($cpu)

You need this because outside of your loop you're setting IFS=$'\n', but with that settingcpu($cpu)` won't do what you expect.

Btw, I would write your script like this:

#!/bin/bash -e

grep ^cpu /proc/stat | while IFS=$'\n' read cpu; do
    cpu=($cpu)
    name=${cpu[0]}
    unset cpu[0]
    idle=${cpu[4]}
    total=0
    for value in "${cpu[@]}"; do
        ((total+=value))
    done
    ((usage=(1000 * (total - idle) / total + 5) / 10))
    echo "$name $usage%"
done

The equivalent using awk:

awk '/^cpu/ { total=0; idle=$5; for (i=2; i<=NF; ++i) { total += $i }; print $1, int((1000 * (total - idle) / total + 5) / 10) }' < /proc/stat
janos
  • 120,954
  • 29
  • 226
  • 236
  • 3
    Everything is a one-liner if you delete the newlines. – Staven Dec 09 '13 at 05:08
  • Don't know why you said that. It was natural for me to write this on one line, I'm not arguing it's better to write on one line. I see you wrote pretty much the same thing but expanded to multiple lines, which is more readable. – janos Dec 17 '13 at 22:59
3

Because the OP asked, an awk program.

awk '
    /cpu[0-9] .*/ {
        total = 0
        idle = $5
        for(i = 0; i <= NF; i++) { total += $i; }
        printf("%s: %f%%\n", $1, 100*(total-idle)/total);
    }
' /proc/stat

The /cpu[0-9] .*/ means "execute for every line matching this expression". The variables like $1 do what you'd expect, but the 1st field has index 1, not 0: $0 means the whole line in awk.

Staven
  • 3,113
  • 16
  • 20
  • Does awk not have the rounding issues that bash has? That's why I used `(1000 * (total - idle) / total + 5) / 10` instead of just `100*(total-idle)/total` – Michael A Dec 09 '13 at 05:26
  • @user30588 I don't know. What rounding issues does Bash have? Regardless though, I've put the wrong format letter in the printf, fixed now. – Staven Dec 09 '13 at 05:36
  • `bash` doesn't have rounding issues so much as it only performs integer division: `a/b` is always an integer, with the remainder thrown out. – chepner Dec 09 '13 at 16:15
  • 1
    Then no, `awk` doesn't have such a limitation. Try `awk 'BEGIN { print 1/2; }' /dev/null`, for example. – Staven Dec 10 '13 at 14:35