2

I'm needing to write a script on some embedded Linux systems that don't have bc installed on them and won't be able to have it installed. The script I'm writing is essentially a monitoring script that takes certain corrective actions at different load values. For example, at load average 1.5, do something.

I am wondering if there's a simple way to take the load average variable and just multiply it by 100 or simply move the decimal place to the right two spaces and pad with 0s if necessary, that would then make this integer arithmetic and normal bash (()) arithmetic expansion could take over.

Right now, I'm breaking the floating point number its truncated integer and the decimal as a whole number(e.g. 1.5, LOAD1_INT=1, LOAD1_DECIMAL=50) but would like to simplify it if possible.

Current (complicated) version:

CRIT_LOAD=3.5
if [[ $CRIT_LOAD =~ ^[0-9]{1,2}\.[0-9]{1,2}$ ]]; then
    # Since bash can't handle floating point arithmetic, break $CRIT_LOAD float into 2 separate integers
    CRIT_LOAD_INT=$(echo $CRIT_LOAD | cut -d'.' -f1)
    CRIT_LOAD_DECIMAL=$(echo $CRIT_LOAD | cut -d'.' -f2)
elif [[ $CRIT_LOAD =~ ^[0-9]{1,2}$ ]]; then
    # If $CRIT_LOAD is already an int, update variables so Monitor code works unchanged
    CRIT_LOAD_INT=$CRIT_LOAD
    CRIT_LOAD_DECIMAL=0
else
    # Set a default value of 1.0 if we can't parse CRIT_LOAD value
    CRIT_LOAD_INT=1
    CRIT_LOAD_DECIMAL=0
fi
LOAD1=$(cat /proc/loadavg | cut -d' ' -f1)
LOAD1_INT=$(echo $LOAD1 | cut -d'.' -f1)
LOAD1_DECIMAL=$(echo $LOAD1 | cut -d'.' -f2)

# Current load int is already higher than critical threshold int
if (( LOAD1_INT > CRIT_LOAD_INT )); then
    log "CRITICAL: Load values have exceeded threshold."
elif (( LOAD1_INT == CRIT_LOAD_INT )); then
    # If current load int is same as crit threshold int, compare decimals
    if (( LOAD1_DECIMAL > CRIT_LOAD_DECIMAL )); then
        log "CRITICAL: Load values have exceeded threshold."
    fi
fi

Is there a way to just pare down all this code by simply turning loadavg (e.g. 1.50) into an int (e.g. 150)? Again, without using the bc utility as it't not available on these systems.

EDIT: I wound up taking the printf command @ilkkachu suggested and modified it into a function for use in my code. I chose this route over the awk command as there are other places in this code where calling a function to emulate floating point arithmetic simplifies code readability and re-usability. Marking his answer as solution.

function dec_to_int() {
    DECIMAL=$1
    SCALE_FACTOR=$2
    # printf removes decimal and allows $SCALE_FACTOR additional spaces to be included, 0-pads numbers that would be too small otherwise
    # NOTE: printf will round number if the values it keeps are greater than the scale factor
    # e.g. SCALE_FACTOR=2, 1.759 -> 176
    SCALED_INT=$(printf "%.0f\n" "${DECIMAL}e${SCALE_FACTOR}")
    echo $SCALED_INT
}

LOAD1=$(cat /proc/loadavg | cut -d' ' -f1)
LOAD1=$(dec_to_int $LOAD1 2)
9
  • 5
    Does your system have awk, perl, python, nodejs, ruby, zsh?
    – jesse_b
    Commented May 16, 2022 at 13:29
  • Wouldn't this be a matter of testing against ^(3\.[6-9]|[4-9]\.|[0-9]{2,}\.)? Or something similar.
    – Kusalananda
    Commented May 16, 2022 at 13:33
  • 1
    @jesse_b even Debian doesn't have bc installed out of the box. See also pax and m4 which are often omitted. Commented May 16, 2022 at 13:38
  • 2
    @StéphaneChazelas, even Debian have awk installed :D Commented May 16, 2022 at 13:41
  • Your numerical comparison of the DECIMAL part works only if the number of decimal digits is the same. When you have e.g. 3.5 and 3.25, you might get an unexpected result if you compare 5 with 25.
    – Bodo
    Commented May 16, 2022 at 13:42

3 Answers 3

4

If I understand your code correct, you want to log a message when the first value of /proc/loadavg is greater than CRIT_LOAD. This check including the parsing of /proc/loadavg can be done with a single awk command.

if ! awk -v crit_load=3.5 '$1>crit_load { exit 1 }' /proc/loadavg
then
    log "CRITICAL: Load values have exceeded threshold."
fi

The awk script reads (all lines from) /proc/loadavg and if the first field is greater than crit_load it exits with code 1 (error, false), otherwise it exits with code 0 (OK, true).

If the threshold value is already in a shell variable you can, of course, use this for the awk variable assignment.

CRIT_LOAD=3.5
if ! awk -v crit_load="$CRIT_LOAD" '$1>crit_load { exit 1 }' /proc/loadavg
then # ...

If necessary, the awk script could be extended to check other fields /proc/loadavg as well. This would be more efficient and consistent than running several awk commands and combining the results in the shell script.

If you want to distinguish more cases, the script can be changed to use different exit codes.

awk -v crit_load=3.5 -v warn_load=1.5 '$1>crit_load { exit 2 } $1>warn_load { exit 1 }' /proc/loadavg
case "$?" in
    2) do_something ;;
    1) do_other ;;
    0) do_nothing ;;
    *) handle_unknown_value ;;
esac
2

If you want/need to do it purely in Bash (or even Dash or Busybox), you can take a page from How to do integer & float calculations, in bash or other languages/frameworks? and abuse the fact that printf should be able to parse a floating point number and print it another out. Appending a suffix like e2 would scale the number up by 100 (as long as the number doesn't already have an E-suffix).

E.g. this would print a scaled up by 100, or 123:

a=1.23
printf "%.0f\n" "${a}e2"

Or, reading from /proc/loadavg:

read avg1 avg5 avg15 rest < /proc/loadavg
scaled_load=$(printf "%.0f\n" "${avg1}e2")

(or printf -v scaled_load "%.0f\n" "${avg1}e2" instead of the command substitution in Bash)

Then again, as long as loadavg contains only two digits after the decimal point, you could just drop the point from the middle and turn 1.23 directly into 123.

The linked post contains a few other programs that can also be used for floating point arithmetic. Even if your installation doesn't have bc, it might have e.g. awk.

1

Do you have dc?

Otherwise maybe something like this. It takes the first two decimals after the dot. It then takes the integer and multiplies it by 100, after which the two decimals (which are now not decimals anymore) are added up to that number.

loadavg="1.50"
mult100="100"
DECIMALS=$( echo "${loadavg#*.}" | cut -c 1-2 )
INT=$( echo $loadavg | cut -f 1 -d . )
NEW_INT=$(( $INT * $mult100 ))
NEW_LOADAVG=$(( $NEW_INT + $DECIMALS ))
echo "$NEW_LOADAVG"

No printf required. Works also with larger numbers.

loadavg="147372.52772830"
mult100="100"
DECIMALS=$( echo "${loadavg#*.}" | cut -c 1-2 )
INT=$( echo $loadavg | cut -f 1 -d . )
NEW_INT=$(( $INT * $mult100 ))
NEW_LOADAVG=$(( $NEW_INT + $DECIMALS ))
echo "$NEW_LOADAVG"

It always rounds down; just multiplication of number by 100 and removing the leftover decimals.

If it needs to be rounded to nearest integer, then use

loadavg="147372.52772830"
mult100="100"
DECIMALS=$( echo "${loadavg#*.}" | cut -c 1-2 )
INT=$( echo $loadavg | cut -f 1 -d . )
NEW_INT=$(( $INT * $mult100 ))
ROUNDING=$( echo "${loadavg#*.}" | cut -c 3 )
if [[ $ROUNDING -ge 5 ]]; then NEW_LOADAVG=$(( $NEW_INT + $DECIMALS + 1 )); else NEW_LOADAVG=$(( $NEW_INT + $DECIMALS )); fi
echo "$NEW_LOADAVG"

If it needs to be rounded up, then use

loadavg="147372.52772830"
mult100="100"
DECIMALS=$( echo "${loadavg#*.}" | cut -c 1-2 )
INT=$( echo $loadavg | cut -f 1 -d . )
NEW_INT=$(( $INT * $mult100 ))
ROUNDING=$( echo "${loadavg#*.}" | cut -c 3 )
if [[ $ROUNDING -gt 0 ]]; then NEW_LOADAVG=$(( $NEW_INT + $DECIMALS + 1 )); else NEW_LOADAVG=$(( $NEW_INT + $DECIMALS )); fi
echo "$NEW_LOADAVG"

edit: ah solved already -_-'

4
  • (1) You have overlooked a potential problem identified in a comment by Bodo — 1.7 will be treated as if it were 1.07. You can fix this by saying DECIMALS=$( echo "${loadavg#*.}00" | cut -c 1-2 ) (note the added 00). (2) IMHO, the NEW_INT variable just makes this longer and more cumbersome, and not more clear. I suggest NEW_LOADAVG=$(( $INT * $mult100 + $DECIMALS )), or $(( ( $INT * $mult100 ) + $DECIMALS )) if you want to be pedantic. … (Cont’d) Commented Sep 4, 2022 at 7:20
  • (Cont’d) …  (2a) I do not see much benefit in having the mult100 variable.  Oh, sure, next week the requirement might be to handle four decimal places, and so you’d want to multiply the integer part by 10,000 — but would you really want to change the definition of mult100 to be 10000?  That would be terribly confusing.  I you suggest that you define POW=2 and TEN_POW=100 — maybe even compute TEN_POW from POW — then change cut -c 1-2 to cut -c 1-"$POW".  … (Cont’d) Commented Sep 4, 2022 at 7:20
  • (Cont’d) …  (3) I believe that your rounding code is more complicated than it needs to be.  I suggest NEW_LOADAVG=$(( $INT * $mult100 + $DECIMALS )), then if [[ $ROUNDING -ge 5 ]]; then (( NEW_LOADAVG++ )); fi.  (4) Your up-rounding code will round 3.0077 up to 3.01, but 3.0007 down to 3.00 (should also be 3.01). Commented Sep 4, 2022 at 7:21
  • (Cont’d) …  (5) If your input (loadavg) is something like .72, then INT will be set to an empty string.  Of course, this will cause subsequent calculations to fail.  (You should test for that and set it to 0.) … … … … … … … … … … … … … … … … … … … … … … … … … … … … … … … PS.  System-defined variables, like HOME and PATH, typically have names that are all upper case.  Therefore, it is commonly advised not to use all-upper-case names for user-defined variables, to reduce the chance of collision. Commented Sep 4, 2022 at 19:18

You must log in to answer this question.

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