23

I'm trying to write a function that is supposed to convert a time stamp of the form hr:min:sec,ms (i.e 15:41:47,757) to milliseconds. The function is the following:

#!/bin/sh
mili () {

    hr=$(echo "$1" | cut -c1-2)
    echo "hr is: " $hr
    min=$(echo "$1" | cut -c4-5)
    echo "min is: " $min
    sec=$(echo "$1" | cut -c7-8)
    echo "sec is: " $sec
    ms=$(echo "$1" | cut -c10-12)
    echo "ms is: " $ms
    total=$(($hr \* 3600 + $min \* 60 + $sec) \* 1000 + $ms)

    return "$total"
    #echo "Result is: "$total" "
}

mili $1

However, when I run it:

./mili.sh "15:41:47,757"

I get the following output message:

./mili.sh: command substitution: line 15: syntax error near unexpected token 
`\*'
./mili.sh: command substitution: line 15: `($hr \* 3600 + $min \* 60 + $sec) 
\* 1000 + $ms'
./mili.sh: line 17: return: : numeric argument required

I've tried variations of expr with and without single quotes, double quotes, and backticks but can never seem to get it to compute the arithmetic. I can confirm a simple command like this works: expr 2 * 3 but when I try to use something similar in my script it fails.

How can I get it to simply compute my expression?

1
  • Is there any particular reason to use mili and not milli (as in millisecond)? Commented Nov 28, 2021 at 3:44

3 Answers 3

16

Another couple of points:

  1. don't return "$total": a return value is an int between 0 and 255. You need to echo "$total"

  2. you're going to have errors when the hour/minute/second is 08 or 09 -- bash treats numbers with leading zero as octal, and 8 and 9 are invalid octal digits.

    $ mili 11:22:09,456
    hr is:  11
    min is:  22
    sec is:  09
    ms is:  456
    bash: (11 * 3600 + 22 * 60 + 09: value too great for base (error token is "09")
    

I'd write:

mili () {     
    IFS=":,." read -r hr min sec ms <<<"$1"
    echo "hr is:   $hr" >&2
    echo "min is:  $min" >&2
    echo "sec is:  $sec" >&2
    echo "ms is:  $ms" >&2
    echo "$(( ((10#$hr * 60 + 10#$min) * 60 + 10#$sec) * 1000 + 10#$ms ))"
}

where the 10# forces base-10 numbers

then

$ ms=$(mili 11:22:09.456)
hr is:   11
min is:  22
sec is:  09
ms is:  456

$ echo $ms
40929456
2
  • Thank you for the valuable input! I was not aware of this.
    – Tikiyetti
    Commented Aug 18, 2017 at 22:06
  • 1
    10#$hr is clumsy. An alternative ${hr#0} isn't any better. bash is not pretty. Commented Aug 18, 2017 at 22:07
14

Inside arithmetic, * does not need to be escaped. Also, some parentheses were missing. Thus, replace:

total=$(($hr \* 3600 + $min \* 60 + $sec) \* 1000 + $ms)

With:

total=$((($hr * 3600 + $min * 60 + $sec) * 1000 + $ms))

Alternative

The code can be simplified avoiding the need for multiple calls to cut:

mili() {
    IFS=':,' read hr min sec ms <<<"$1"
    echo "hr is: " $hr
    echo "min is: " $min
    echo "sec is: " $sec
    echo "ms is: " $ms
    total=$((($hr * 3600 + $min * 60 + $sec) * 1000 + $ms))
    echo "Total=$total"
    return "$total"
}

Aside: Bash arithmetic and dollar signs

In a Bash arithmetic context, the dollar sign before a variable is optional. For example:

$ a=1; echo "$((1 + a)) and $((1+ $a))"
2 and 2

While some style guides recommend omitting $ in an arithmetic context, there is a key difference. As Chepner points out in the comments, the treatment of undefined variables is very different:

$ unset a
$ echo $((1 + $a))
bash: 1 + : syntax error: operand expected (error token is "+ ")
$ echo $((1 + a))
1

In summary:

  1. If you want an undefined variable to default to zero, then omit the $.

  2. If you want an undefined variable to be replace with nothing, possibly causing an invalid expression, then include the $.

In the shell function mili, an undefined variable hr, min, etc., would indicate a code error and we might want an error message to warn us about it and we would want to include the $. In other circumstances where a default value of zero is reasonable, we would not and omitting the $ would be correct.

5
  • 1
    Also, inside $((...)) you can use variable names without $ -- total=$(( ((hr * 60 + min) * 60 + sec) * 1000 + ms )) -- I find adding a spaces inside the double parentheses help readability. Commented Aug 18, 2017 at 21:49
  • 1
    Wow...Worked like a charm. Thanks man, really appreciate it. facepalm
    – Tikiyetti
    Commented Aug 18, 2017 at 21:52
  • @glennjackman Yes, you are right & many style guides prefer the removal of $. (I would prefer it if bash was designed for notational consistency.)
    – John1024
    Commented Aug 18, 2017 at 21:59
  • 1
    There is an ever-so-subtle difference between using $ and not: with $, the variable is expanded before the expression is evaluated. The empty string produced by an undefined variable will produce an invalid expression, or at least a different expression than you expected will be evaluated. Without the $, the string is assumed to be a variable during evaluation, with undefined variables defaulting to integer values of 0.
    – chepner
    Commented Aug 22, 2017 at 16:06
  • 1
    @chepner That is an excellent point! I just added discussion of that to the answer.
    – John1024
    Commented Aug 22, 2017 at 18:18
1

Here's a crazy alternative:

$ mili () {
    IFS=., read -r time ms <<<"$1"
    ms3=$(cut -c 1-3 <<<"${ms}000")
    echo "$(date -u -d "1970-01-01 $time" +%s)$ms3"
}

$ mili 15:41:47,757
56507757

$ mili 15:41:47,75
56507750

$ mili 15:41:47
56507000
1
  • I'd incorporate $ms3 into the format string to avoid echo and a command substitution: date -u -d "1970-01-01 $time" +"%s$ms3".
    – chepner
    Commented Aug 22, 2017 at 16:07

Not the answer you're looking for? Browse other questions tagged or ask your own question.