7

These are possible output formats for ps h -eo etime

21-18:26:30
   15:28:37
      48:14
      00:01

How to parse them into seconds?

  • Please assume at least 3 digits for the days part as I don't know how long it can be.
  • The output will be egreped to one only line so no need for a loop.
Michael Jaros
  • 4,586
  • 1
  • 22
  • 39
Clodoaldo Neto
  • 118,695
  • 26
  • 233
  • 260

15 Answers15

15

Yet another bash solution, which works any number of fields:

ps -p $pid -oetime= | tr '-' ':' | awk -F: '{ total=0; m=1; } { for (i=0; i < NF; i++) {total += $(NF-i)*m; m *= i >= 2 ? 24 : 60 }} {print total}'

Explanation:

  1. replace - to : so that string becomes 1:2:3:4 instead of '1-2:3:4', set total to 0 and multiplier to 1
  2. split by :, start with the last field (seconds), multiply it by m = 1, add to total second number, m becomes 60 (seconds in a minute)
  3. add minutes fields multiplied by 60, m becomes 3600
  4. add hours * 3600
  5. add days * 3600 * 24
Fluffy
  • 27,504
  • 41
  • 151
  • 234
  • 2
    Of all the solutions, I believe this is the most elegant one, in terms of maintainability, portability, and simplicity of code. – aemus May 17 '15 at 16:08
  • 1
    Very useful and working bit of code. I just wanted to know what the `NF` variable is inside the FOR loop. Thanks! – ipruthi Oct 15 '16 at 16:18
  • NF = Number of Fields (in the line currently being processed) – JCCyC Feb 04 '19 at 17:04
14

With awk:

#!/usr/bin/awk -f  
BEGIN { FS = ":" }
{
  if (NF == 2) {
    print $1*60 + $2
  } else if (NF == 3) {
    split($1, a, "-");
    if (a[2] != "" ) {
      print ((a[1]*24+a[2])*60 + $2) * 60 + $3;
    } else {
      print ($1*60 + $2) * 60 + $3;
    }
  }
}

Run with :

awk -f script.awk datafile

Output:

1880790
55717
2894
1

And finally, if you want to pipe to the parser, you can do something like this:

ps h -eo etime | ./script.awk
user000001
  • 32,226
  • 12
  • 81
  • 108
  • I'd be careful using the above example. While it looks to pass the test criteria presented, it provides an incorrect answer for a value where the day is specified and the hour is zero: `06-00:15:30` returns an answer of `22530` which is clearly incorrect (the answer is `519330`). – markeissler Feb 10 '15 at 20:32
  • Ugh. Nothing wrong with your answer (upvoted). But just Ugh, seriously, Unix? Seriously? – Henley Sep 15 '15 at 19:26
  • @markeissler Yup you are right. Changed the condition so that it works with zero hours – user000001 Sep 16 '15 at 12:29
  • 1
    this works for me out of the box, thanks. i needed it to be able to check the number of seconds my process has been running and see if it's greater than a threshold I defined. – Michael Martinez Jan 20 '17 at 00:33
13

Here's mine Perl one liner:

ps -eo pid,comm,etime | perl -ane '@t=reverse split(/[:-]/,$F[2]); $s=$t[0]+$t[1]*60+$t[2]*3600+$t[3]*86400; print "$F[0]\t$F[1]\t$F[2]\t$s\n"'

Undefined values are rendering to zero, so they won't have effect on the sum of seconds.

Fluffy
  • 27,504
  • 41
  • 151
  • 234
Andor
  • 5,523
  • 5
  • 26
  • 24
12

Think I might be missing the point here but the simplest way of doing this is:

ps h -eo etimes

Note the 's' on the end of etime.

James Thomas
  • 121
  • 1
  • 2
6

Try to use my solution with sed+awk:

ps --pid $YOUR_PID -o etime= | sed 's/:\|-/ /g;' |\ 
awk '{print $4" "$3" "$2" "$1}' |\
awk '{print $1+$2*60+$3*3600+$4*86400}'

it splits the string with sed, then inverts the numbers backwards ("DD hh mm ss" -> "ss mm hh DD") and calculates them with awk.

Also you can play with sed and remove all non-numeric characters from input string:

sed 's/[^0-9]/ /g;' | awk '{print $4" "$3" "$2" "$1}' | awk '{print $1+$2*60+$3*3600+$4*86400}'
bolD
  • 141
  • 1
  • 4
  • It's clear but inefficient in comparison to to other alternatives. I would use it if I just needed to execute it once. – Felix Oct 17 '18 at 11:30
3

I've implemented a 100% bash solution as follows:

#!/usr/bin/env bash

etime_to_seconds() {
  local time_string="$1"
  local time_string_array=()
  local time_seconds=0
  local return_status=0

  [[ -z "${time_string}" ]] && return 255

  # etime string returned by ps(1) consists one of three formats:
  #         31:24 (less than 1 hour)
  #      23:22:38 (less than 1 day)
  #   01-00:54:47 (more than 1 day)
  #

  # convert days component into just another element
  time_string="${time_string//-/:}"

  # split time_string into components separated by ':'
  time_string_array=( ${time_string//:/ } )

  # parse the array in reverse (smallest unit to largest)
  local _elem=""
  local _indx=1
  for(( i=${#time_string_array[@]}; i>0; i-- )); do
    _elem="${time_string_array[$i-1]}"
    # convert to base 10
    _elem=$(( 10#${_elem} ))
    case ${_indx} in
      1 )
        (( time_seconds+=${_elem} ))
        ;;
      2 )
        (( time_seconds+=${_elem}*60 ))
        ;;
      3 )
        (( time_seconds+=${_elem}*3600 ))
        ;;
      4 )
        (( time_seconds+=${_elem}*86400 ))
        ;;
    esac
    (( _indx++ ))
  done
  unset _indx
  unset _elem

  echo -n "$time_seconds"; return $return_status
}

main() {
  local time_string_array=( "31:24" "23:22:38" "06-00:15:30" "09:10" )

  for timeStr in "${time_string_array[@]}"; do

      local _secs="$(etime_to_seconds "$timeStr")"
      echo "           timeStr: "$timeStr""
      echo "  etime_to_seconds: ${_secs}"
  done

}

main
markeissler
  • 649
  • 7
  • 9
  • There seems to ben a error at while parsing 08 and 09 in line 33 (( time_seconds+=${_elem} )). You get : "09: value too great for base (error token is "09")" or "08: value too great for base (error token is "08")". The reason for this is, since the numbers start with 0 they are being interpreted as hex notation. If you use (( time_seconds+=$((10#$_elem)) )) instead, the error disappears, since it indicates the base you are using (in this case base 10). – aemus May 17 '15 at 15:32
  • Thanks @aemus. I updated the answer to better incorporate your fix. – markeissler May 18 '15 at 21:27
2

Here's a PHP alternative, readable and fully unit-tested:

//Convert the etime string $s (as returned by the `ps` command) into seconds
function parse_etime($s) {
    $m = array();
    preg_match("/^(([\d]+)-)?(([\d]+):)?([\d]+):([\d]+)$/", trim($s), $m); //Man page for `ps` says that the format for etime is [[dd-]hh:]mm:ss
    return
        $m[2]*86400+    //Days
        $m[4]*3600+     //Hours
        $m[5]*60+       //Minutes
        $m[6];          //Seconds
}
rinogo
  • 8,491
  • 12
  • 61
  • 102
2
#!/bin/bash
echo $1 | sed 's/-/:/g' |  awk -F $':' -f <(cat - <<-'EOF'
  {
    if (NF == 1) {
        print $1
    }
    if (NF == 2) {
        print $1*60 + $2
    }
    if (NF == 3) {
        print $1*60*60 + $2*60 + $3;
    }
    if (NF == 4) {
        print $1*24*60*60 + $2*60*60 + $3*60 + $4;
    }
    if (NF > 4 ) {
        print "Cannot convert datatime to seconds"
        exit 2
    }
  }
EOF
) < /dev/stdin

Then to run use:

ps -eo etime | ./script.sh 
2
[[ $(ps -o etime= REPLACE_ME_WITH_PID) =~ ((.*)-)?((.*):)?(.*):(.*) ]]
printf "%d\n" $((10#${BASH_REMATCH[2]} * 60 * 60 * 24 + 10#${BASH_REMATCH[4]} * 60 * 60 + 10#${BASH_REMATCH[5]} * 60 + 10#${BASH_REMATCH[6]}))

Pure BASH. Requires BASH 2+ (?) for the BASH_REMATCH variable. The regex matches any of the inputs and places the matched strings into the array BASH_REMATCH, which parts of are used to compute number of seconds.

Larry
  • 310
  • 2
  • 11
1

Ruby version:

def psETime2Seconds(etime)
  running_secs = 0
  if etime.match(/^(([\d]+)-)?(([\d]+):)?([\d]+):([\d]+)$/)
    running_secs += $2.to_i * 86400 # days
    running_secs += $4.to_i * 3600  # hours
    running_secs += $5.to_i * 60    # minutes
    running_secs += $6.to_i         # seconds
  end
  return running_secs
end
JonB
  • 836
  • 1
  • 11
  • 15
1

Another bash option as a function; uses tac and bc for math.

function etime2sec () {
   # 21-18:26:30
   #    15:28:37
   #       48:14
   #       00:01
etimein=$1
hassec=no ; hasmin=no ; hashr=no ; hasday=no
newline=`echo "${etimein}" | tr ':' '-' | tr '-' ' ' | tac -s " " | tr '\n' ' '`
for thispiece in $(echo "${etimein}" | tr ':' '-' | tr '-' ' ' | tac -s " " | tr '\n' ' ') ; do
  if [[ $hassec = no ]] ; then
    totsec=$thispiece
    hassec=yes
  elif [[ $hasmin = no ]] ; then
    totsec=`echo "$totsec + ($thispiece * 60)" | bc`
    hasmin=yes
  elif [[ $hashr = no ]] ; then
    totsec=`echo "$totsec + ($thispiece * 3600)" | bc`
    hashr=yes
  elif [[ $hasday = no ]] ; then
    totsec=`echo "$totsec + ($thispiece * 86400)" | bc`
    hashr=yes
  fi
done
echo $totsec
}
Dave
  • 21
  • 1
0

I just had to add my version, heavily based on the elegant perl one-liner by @andor (beautiful perl code!)

  • time : total CPU time since beginning (? or some computation of it, that is decayed if cpu usage goes down? I am not sure.... a high number signals a cpu intensive process, though)
  • etime: total time elapsed since the process started
  • the 2 ways for tail : on linux : tail +2 doesn't work. On solaris, tail -n +2 doesn't work. So I try both to be sure.

Here is how to compute the times and also sort your processes by their mean CPU usage over time

ps -eo pid,comm,etime,time | { tail +2 2>/dev/null || tail -n +2 ;} | perl -ane '
    @e=reverse split(/[:-]/,$F[2]); $se=$e[0]+$e[1]*60+$e[2]*3600+$e[3]*86400;
    @t=reverse split(/[:-]/,$F[3]); $st=$t[0]+$t[1]*60+$t[2]*3600+$t[4]*86400; 
    if ( $se == 0 ) { $pct=0 ; } else { $pct=$st/$se ;};
    printf "%s\t%s\t%s(%sseconds)\t%s(%sseconds)\t%.4f%%\n",$F[0],$F[1],$F[2],$se,$F[3],$st,$pct*100;
   '  | sort -k5,5n
Olivier Dulac
  • 3,695
  • 16
  • 31
0

Works on AIX 7.1:

ps -eo etime,pid,comm | awk '{if (NR==1) {print "-1 ",$0} else {str=$1; sub(/-/, ":", str="0:0:"str); n=split(str,f,":"); print 86400*f[n-3]+3600*f[n-2]+60*f[n-1]+f[n]," ",$0}}' | sort -k1n
Paul Roub
  • 36,322
  • 27
  • 84
  • 93
0

A version for Python:

ex=[
    '21-18:26:30',
    '06-00:15:30',
    '15:28:37',
    '48:14',
    '00:01'
    ]

def etime_to_secs(e):
    t=e.replace('-',':').split(':')
    t=[0]*(4-len(t))+[int(i) for i in t]
    return t[0]*86400+t[1]*3600+t[2]*60+t[3]

for e in ex:
    print('{:11s}: {:d}'.format(e, etime_to_secs(e)))
0

I hoped I can find a solution that doesn't involve scripts that I need to test and maintain.

My solution is to calculate etime as the difference between current time and start time. I understand it is not the most efficient but for what I need it readability is more relevant than performance.

start_time_seconds_since_1970=$(date +%s -d $(ps -o stime= -p "<pid>"))
current_time_seconds_since_1970=$(date +%s)
etime_seconds=$((current_time_seconds_since_1970 - start_time_seconds_since_1970))
Emanuel George Hategan
  • 1,123
  • 1
  • 13
  • 22