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
    May 16 at 13:29
  • Wouldn't this be a matter of testing against ^(3\.[6-9]|[4-9]\.|[0-9]{2,}\.)? Or something similar.
    – Kusalananda
    May 16 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. May 16 at 13:38
  • 2
    @StéphaneChazelas, even Debian have awk installed :D May 16 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
    May 16 at 13:42

2 Answers 2

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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