5

I would like to save the output of the uptime command into a csv file in a Bash script. Since the uptime command has different output formats based on the time since the last reboot I came up with a pretty heavy solution based on case, but there is surely a more elegant way of doing this.

uptime output:

 8:58AM  up 15:12, 1 user, load averages: 0.01, 0.02, 0.00

desired result:

15:12,1 user,0.00 0.02 0.00,

current code:

case "`uptime | wc -w | awk '{print $1}'`" in
#Count the number of words in the uptime output

10)
    #e.g.:  8:16PM  up  2:30, 1 user, load averages: 0.09, 0.05, 0.02
    echo -n `uptime | awk '{ print $3 }' | awk '{gsub ( ",","" ) ; print $0 }'`","`uptime | awk '{ print $4,$5 }' | awk '{gsub ( ",","" ) ; print $0 }'`","`uptime | awk '{ print $8,$9,$10 }' | awk '{gsub ( ",","" ) ; print $0 }'`","
    ;;

12)
    #e.g.: 1:41pm  up 105 days, 21:46,  2 users,  load average: 0.28, 0.28, 0.27
    echo -n `uptime | awk '{ print $3,$4,$5 }' | awk '{gsub ( ",","" ) ; print $0 }'`","`uptime | awk '{ print $6,$7 }' | awk '{gsub ( ",","" ) ; print $0 }'`","`uptime | awk '{ print $10,$11,$12 }' | awk '{gsub ( ",","" ) ; print $0 }'`","
    ;;

13)
    #e.g.: 12:55pm  up 105 days, 21 hrs,  2 users,  load average: 0.26, 0.26, 0.26
    echo -n `uptime | awk '{ print $3,$4,$5,$6 }' | awk '{gsub ( ",","" ) ; print $0 }'`","`uptime | awk '{ print $7,$8 }' | awk '{gsub ( ",","" ) ; print $0 }'`","`uptime | awk '{ print $11,$12,$13 }' | awk '{gsub ( ",","" ) ; print $0 }'`","
    ;;
esac

4 Answers 4

13

I suggest you go directly to the source instead of parsing uptime(1) output.

  • Uptime is in /proc/uptime
  • Load average is in /proc/loadavg
  • Number of users is a bit more involved, see Wikipedia:utmp, but the w(1) or who(1) commands will help you.

The following is not exactly the desired output you asked for, but you get the idea:

$ echo $(cut -d ' ' -f 1 </proc/uptime),$(w -h | wc -l),$(cut -d ' ' -f 1-3 </proc/loadavg),
8545883.49,4,0.00 0.01 0.05

That means 8.55e6 seconds (almost 99 days), 4 users, load average.

1
  • 2
    Sorry the question was incorrectly tagged "linux", I am on a FreeBSD system (FreeNAS), but I get your point and the approach would be similar: parsing /sbin/sysctl kern.boottime and /sbin/sysctl vm.loadavg Thanks for the hint!
    – Keek
    Commented Aug 23, 2014 at 8:01
1

The following code will suits all kind of uptime outputs. Hope this will help you,

uptime=`uptime`
upt=`echo $uptime | grep -ohe 'up .*user*' | awk '{gsub ( "user*","" ); print $0 }' | sed 's/,//g' | sed -r 's/(\S+\s+){1}//' | awk '{$NF=""}1'`
usrs=`echo $uptime | grep -ohe '[0-9.*] user[s,]'| sed 's/,//g'`
ldt=`echo $uptime | grep -ohe 'load average[s:][: ].*' | sed 's/,//g' | awk '{ print $3" "$4" "$5"," }'`
echo $upt, $usrs, $ldt
1

I'll add an Awk approach. First I'll directly answer your question:

uptime | 
awk 'BEGIN {FS=","}                                                 # Use commas as the field separator
     function trim(s,d) {gsub("(^ +| +$|"d")", "", s); return s}    # Function to trim spaces and delete pattern d in input s
     {
        days=trim($1, ".* up ")     # Trim spaces and remove everything before and including " up " on 1st field
        h_m=trim($2)                # Trim spaces on 2nd field
        users=trim($3)              # . . .
        l1=trim($4, ".*: ")
        l2=trim($5)
        l3=trim($6)
        printf "%s %s,%s,%s %s %s,\n", days, h_m, users, l1, l2, l3
     }'

So, the one-liner with most unnecessary whitespace, comments, and variables removed; trim renamed to t; and not stripping (non-existent) trailing spaces would be:

uptime | awk 'BEGIN{FS=","}function t(s,d){gsub("(^ +|"d")","",s);return s}{printf "%s %s,%s,%s %s %s,\n",t($1,".* up "),t($2),t($3),t($4,".*: "),t($5),t($6)}'

Also there's the sed-only approach:

uptime | sed -E -e 's/(.* up|[^,]*: )//g' -e 's/, +/,/g' -e 's/(([0-9]+ days),)?([^,]*),/\2 \3,/' -e 's/^ +//' -e 's/([^,]+),([^,]+),([^,]+)$/\1 \2 \3,/'

Uptime Only

For anyone like me who only cares about the uptime in this case, here's the way to get output that will look like uptime: 32d 7h 9m (at 13:38:22). The values for time, days, hours, and minutes are stored in variables, so you can adjust the printf statement at the end to make it print however you want.
NOTE: This is more or less an example of how you would parse the output of uptime in Awk to get day, hour, minute, and time variables to use. This can be expanded upon to include the rest of the uptime output. You can see it gets a bit complex because uptime is fancy and will output 7 min instead of 00:07 if it detects that, etc. Skip below if you just want uptime in a smallish command.

uptime | 
awk 'BEGIN {FS=","}                             # Use commas as the field separator
     #   Function to trim spaces and delete pattern d in input s
     function trim(s,d) {gsub("(^ +| +$|"d")", "", s); return s}
     #   Function to set hours and mins variables from time in x:xx format (split on : then trim leading 0 from mins)
     function CoSp(s) {split(trim(s), B, /:/); sub(/^0/, "", B[2]); if(B[2]==""){B[2]=0} hours=B[1]; mins=B[2]}
     {
        up=" up ";                              # Set variable for long pattern that is needed frequently
        days=0; hours=0; mins=0                 # Set variables so that printf is more readable
        if ($1 ~ /.*days.*/) {                  # If it has days
            split(trim($1, " days"), A, up)     # Trim spaces and remove " days" on 1st field and split on " up "
            days=A[2]
            if ($2 ~ /hrs/) {                   # If only hours
                hours=trim($2, " hrs")
            } else if ($2 ~ /min/) {            # If only minutes
                mins=trim($2, " min")
            } else {                            # If has hours and minutes
                CoSp($2)
            }
        } else if ($1 ~ /hrs/) {                # If only hours
            split(trim($1, " hrs"), A, up)
            hours=A[2]
        } else if ($1 ~ /min/) {                # If only minutes
            split(trim($1, " min"), A, up)
            mins=A[2]
        } else {
            split(trim($1), A, up)
            CoSp(A[2])
        }
        time=A[1];
        printf "uptime: %sd %sh %sm (at %s)\n", days, hours, mins, time
     }'

You can get it smaller if you don't care about capturing the time, but at that point it's better to just calculate based off /proc/uptime (as referenced in another answer). This will get you the uptime in the form uptime: 27d 9h 13m 13s 320ms:

awk 'FNR==1{                       # Only process 1st line of file
        seconds=$1
        ms=seconds*1000%1000
        secs=int(seconds%60)
        mins=int(seconds/60)
        hours=int(mins/60)
        days=int(hours/24)
        hours=hours%24
        mins=mins%60
        printf "uptime: %sd %sh %sm %ss %sms\n", days, hours, mins, secs, ms
     }' /proc/uptime

This is the one-liner for that, without unnecessary whitespace and comments:

awk 'FNR==1{ms=$1*1000%1000;secs=int($1%60);mins=int($1/60);hours=int(mins/60);days=int(hours/24);hours=hours%24;mins=mins%60;printf "uptime: %sd %sh %sm %ss %sms\n", days, hours, mins, secs, ms}' /proc/uptime

And if you can safely assume that /proc/uptime only ever has 1 line, you reduce the variables to just their first letter, and you modify the math a bit, you get this one-liner:

awk '{ms=$1*1000%1000;s=int($1%60);m=int($1/60%60);h=int($1/3600%24);d=int($1/86400);printf "uptime: %sd %sh %sm %ss %sms\n", d, h, m, s, ms}' /proc/uptime

And you can always remove any calculations you don't care about. For example if you want to remove milliseconds and round seconds (instead of just removing the int() from around seconds and leaving it as a float):

awk '{s=int($1%60+.5);m=int($1/60%60);h=int($1/3600%24);d=int($1/86400);printf "uptime: %sd %sh %sm %ss %sms\n", d, h, m, s}' /proc/uptime
0

FWIW:

$ echo -e "\n$(uptime)\n" ; uptime \
| sed -nre 's/.+up +([\:0-9]+), +([^ ] [^,]+), +.*: +([^ ]+), +([^ ]+), +([^ ]+)$/\1, \2, \3 \4 \5/p'

 12:58:47 up  5:53,  2 users,  load average: 0,21, 0,09, 0,10

5:53, 2 users, 0,21 0,09 0,10

Note that the decimal separator for numbers depends on your locale settings.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .