How to calculate values in a shell script?

22,170

Solution 1

Your code says to print a string. It doesn't say anywhere that this string is in fact an arithmetic expression that you want evaluated. So you can't reasonably expect your expression to be evaluated.

Your code is suboptimal. $(wc -l) will count the number of matches returned by grep, but there's a simpler way: run grep -c instead. $(ls | wc -l) is an unreliable way of counting the non-dot files in the current directory, because the output of ls isn't reliable; $(set -- *; echo $#) is a reliable way of doing this (assuming there is at least one matching file; if that assumption might not hold, use $(set -- *; if [ -e "$1" ]; then echo $#; else echo 0; fi, but note that this will result in a division by zero below which you should treat as an error condition one way or another). So you can write your code this way:

matches=$(grep -c "bla bla blah" blah*)
files=$(set -- *; echo $#)
echo "Blah: $matches / $files * 100"

or you can inline the computation of the two intermediate values:

echo "Blah: $(grep -c "bla bla blah" blah*) / $(set -- *; echo $#) * 100"

Now, to perform the arithmetic, you can use the shell's built-in arithmetic expansion, but it's limited to integer operations, so the / operator will round down to the nearest integer.

echo "Blah: $(($matches * 100 / $files))"

In ksh93, zsh and yash, but not in other shells, you get floating-point arithmetic if there's something in the expression to force floating-point, such as a floating-point constant. This feature is not present in the Bourne shell, ksh88, pdksh, bash, ash.

echo "Blah: $(($matches * 100.0 / $files))"

The bc utility performs operations on decimal number with arbitrary precision.

echo "Blah: $(echo "scale=2; $matches * 100 / $files" | bc)"

Another standard utility that can perform floating-point computation (with fewer mathematical functions available) is awk.

echo "$matches" "$files" | awk '{print "Blah:", $1 * 100 / $2}'

Solution 2

First of all, you've not specified your shell. I'll presume you're using bash, but please state it in future.

It's also very important that you don't parse the output of ls. There's good documentation on why to not do so here.

Also, what are you attempting to obtain the percentage output of? You don't seem to be attempting to calculate a percentage at the end. For now I just did the exact calculation you listed.

Here is a small script that should be able to do this without the issues mentioned:

#!/bin/bash

_die() {
    printf '%s\n' "${@:2}"
    exit "$1"
}

(( $# )) || _die 1 "Usage: ${0##*/} pattern <dir>"

[[ $2 ]] && _dir=$2 || _dir=.

[[ -d ${_dir} ]] || _die 2 "Directory does not exist: ${_dir}"

for _file in "${_dir}"/*; do
    [[ -f ${_file} ]] && _files+=( "${_file}" )
done

(( ${#_files[@]} )) || _die 3 'No files matched by glob, not attempting to divide by 0.'

# We pass the same files found to grep instead of reglobbing to avoid a race condition.
while IFS= read -r _number_of_matches; do
    (( _total_matches )) && (( _total_matches+=_number_of_matches )) || _total_matches=${_number_of_matches}
done < <(grep -hc "$1" "${_files[@]}")

(( _total_matches )) || _die 4 "Nothing matched by expression: $1"

printf 'Blah: %s\n' "$(bc <<< "${_total_matches}/${#_files[@]}")"

Bear in mind that bc is not portable. If you don't mind using integer arithmetic, you could use the shell to calculate and return it instead of passing it to bc by using $((.

Share:
22,170

Related videos on Youtube

jokerdino
Author by

jokerdino

Updated on September 18, 2022

Comments

  • jokerdino
    jokerdino almost 2 years

    I run this command in the terminal:

    grep "bla bla blah" blah* | echo "Blah: $(wc -l) / $(ls | wc -l) * 100"
    

    And I get this output:

    Blah: 44 / 89 * 100
    

    What I expect to see:

    49.4
    

    Is there a way to obtain the desired output using just the bash commands? I don't prefer scripts I am planning to pipe the output.

  • clerksx
    clerksx over 12 years
    This command is dangerous (race condition).
  • jokerdino
    jokerdino over 12 years
    If there is a way to obtain the result without the usage of scripts, I would be grateful! And yes, I am using bash.
  • jokerdino
    jokerdino over 12 years
    This command does provide me the desired output. Is it really dangerous? Are there any safer alternatives?
  • clerksx
    clerksx over 12 years
    Well, as "dangerous" as a command doing this can be, that is, you are in danger of getting the complete wrong result.
  • clerksx
    clerksx over 12 years
    @jokerdino Put it in a function in your ~/.bashrc then (note, you also will want to source ~/.bashrc from ~/.bash_profile.
  • Gilles 'SO- stop being evil'
    Gilles 'SO- stop being evil' over 12 years
    If you're assuming bash anyway, shopt -s nullglob is a far easier way of handling the case when * doesn't match. Also, bc is standard.
  • Gilles 'SO- stop being evil'
    Gilles 'SO- stop being evil' over 12 years
    @jokerdino If a file bla* is created or removed while you're running this command, then grep and cat may work through different sets of files. I don't know where the cat bla* comes from, though, it doesn't seem to be in the requirements.
  • clerksx
    clerksx over 12 years
    @Gilles Many Unices do not include bc by default (Arch Linux offhand). As for nullglob, I'd still have to do the checks regardless.
  • janmoesen
    janmoesen over 12 years
    You can also set the nullglob shell option to avoid the "no matching files returns the glob pattern" problem. num_files=$(shopt -s nullglob; set -- *; echo $#)
  • rvdginste
    rvdginste over 12 years
    Where is the race condition? The code inside the backticks should be evaluated before the whole argument is passed to "echo"? There are two pieces of code with backticks, and the order in which those are executed does not matter.
  • rvdginste
    rvdginste over 12 years
    Hmm.. you mean that extra files are created or removed while executing the command. Ok, fair enough. That line is just a quick'n dirty way to print some statistics. It is not a full-blown, bulletproof script. Whether it is actually dangerous, depends on the circumstances where you intend to use it.