Progressively shorter and cleaner Bash conditionals

In my PercBar Bash package, there's a part where I calculate the positions of percentage labels.

I want to show you a progression.

A progression

01

Here's how I'd have written it when I was starting to learn Bash:

if [ $bfull -lt 2 ]; then
    ltpad=0
elif [ $pfull -lt 10 ]; then
    ltpad=$(echo "($bfull - 2)/2" | bc)
elif [ $pfull -lt 100 ]; then
    ltpad=$(echo "($bfull - 3)/2" | bc)
else
    ltpad=$(echo "($bnorm - 4)/2" | bc)
fi

if [ $bempt -lt 2 ]; then
    rtpad=-1
elif [ $pfull -lt 10 ]; then
    rtpad=$(echo "($bempt - 2)/2" | bc)
elif [ $pfull -lt 100 ]; then
    rtpad=$(echo "($bempt - 3)/2" | bc)
else
    rtpad=$(echo "($bnorm - 4)/2" | bc)
fi

if [ $pfull -lt 10 ]; then
    ctpad=$(echo $bfull - 2 + $rtpad - $ltpad | bc)
elif [ $pfull -lt 100 ]; then
    ctpad=$(echo $bfull - 3 + $rtpad - $ltpad | bc)
else
    ctpad=$(echo $bnorm - 4 + $rtpad - $ltpad | bc)
fi

02

Then later I'd learn that unless your integers are really large, bc is not needed — Bash has its own syntax for that:

if [ $bfull -lt 2 ]; then
    ltpad=0
elif [ $pfull -lt 10 ]; then
    ltpad=$((($bfull - 2)/2))
elif [ $pfull -lt 100 ]; then
    ltpad=$((($bfull - 3)/2))
else
    ltpad=$((($bnorm - 4)/2))
fi

if [ $bempt -lt 2 ]; then
    rtpad=-1
elif [ $pfull -lt 10 ]; then
    rtpad=$((($bempt - 2)/2))
elif [ $pfull -lt 100 ]; then
    rtpad=$((($bempt - 3)/2))
else
    rtpad=$((($bnorm - 4)/2))
fi

if [ $pfull -lt 10 ]; then
    ctpad=$(($bfull - 2 + $rtpad - $ltpad))
elif [ $pfull -lt 100 ]; then
    ctpad=$(($bfull - 3 + $rtpad - $ltpad))
else
    ctpad=$(($bnorm - 4 + $rtpad - $ltpad))
fi

03

I'd then learn:

  • that it's bad practice — and potentially unsafe — not to quote variables;
  • and that I don't need $ when inside ((…)).
if [ "$bfull" -lt 2 ]; then
    ltpad=0
elif [ "$pfull" -lt 10 ]; then
    ltpad=$(((bfull - 2)/2))
elif [ "$pfull" -lt 100 ]; then
    ltpad=$(((bfull - 3)/2))
else
    ltpad=$(((bnorm - 4)/2))
fi

if [ "$bempt" -lt 2 ]; then
    rtpad=-1
elif [ "$pfull" -lt 10 ]; then
    rtpad=$(((bempt - 2)/2))
elif [ "$pfull" -lt 100 ]; then
    rtpad=$(((bempt - 3)/2))
else
    rtpad=$(((bnorm - 4)/2))
fi

if [ "$pfull" -lt 10 ]; then
    ctpad=$((bfull - 2 + rtpad - ltpad))
elif [ "$pfull" -lt 100 ]; then
    ctpad=$((bfull - 3 + rtpad - ltpad))
else
    ctpad=$((bnorm - 4 + rtpad - ltpad))
fi

04

Eventually I'd wonder: why am I not also using that same Bash syntax for the tests?

That'd be shorter, more readable, faster:

if ((bfull < 2)); then
    ltpad=0
elif ((pfull < 10)); then
    ltpad=$(((bfull - 2)/2))
elif ((pfull < 100)); then
    ltpad=$(((bfull - 3)/2))
else
    ltpad=$(((bnorm - 4)/2))
fi

if ((bempt < 2)); then
    rtpad=-1
elif ((pfull < 10)); then
    rtpad=$(((bempt - 2)/2))
elif ((pfull < 100)); then
    rtpad=$(((bempt - 3)/2))
else
    rtpad=$(((bnorm - 4)/2))
fi

if ((pfull < 10)); then
    ctpad=$((bfull - 2 + rtpad - ltpad))
elif ((pfull < 100)); then
    ctpad=$((bfull - 3 + rtpad - ltpad))
else
    ctpad=$((bnorm - 4 + rtpad - ltpad))
fi

05

But this still takes a lot of screen space.

Why not align it and make it easier to compare the conditionals?

if   ((bfull < 2))  ; then ltpad=0
elif ((pfull < 10)) ; then ltpad=$(((bfull - 2)/2))
elif ((pfull < 100)); then ltpad=$(((bfull - 3)/2))
else                       ltpad=$(((bnorm - 4)/2))
fi

if   ((bempt < 2))  ; then rtpad=-1
elif ((pfull < 10)) ; then rtpad=$(((bempt - 2)/2))
elif ((pfull < 100)); then rtpad=$(((bempt - 3)/2))
else                       rtpad=$(((bnorm - 4)/2))
fi

if   ((pfull < 10)) ; then ctpad=$((bfull - 2 + rtpad - ltpad))
elif ((pfull < 100)); then ctpad=$((bfull - 3 + rtpad - ltpad))
else                       ctpad=$((bnorm - 4 + rtpad - ltpad))
fi

06

Then I'd learn that I can combine : with $_ and get rid of much of those assignment repetitions:

if   ((bfull < 2))  ; then : 0
elif ((pfull < 10)) ; then : "$(((bfull - 2)/2))"
elif ((pfull < 100)); then : "$(((bfull - 3)/2))"
else                       : "$(((bnorm - 4)/2))"
fi
ltpad="$_"

if   ((bempt < 2))  ; then : -1
elif ((pfull < 10)) ; then : "$(((bempt - 2)/2))"
elif ((pfull < 100)); then : "$(((bempt - 3)/2))"
else                       : "$(((bnorm - 4)/2))"
fi
rtpad="$_"

if   ((pfull < 10)) ; then : "$((bfull - 2 + rtpad - ltpad))"
elif ((pfull < 100)); then : "$((bfull - 3 + rtpad - ltpad))"
else                       : "$((bnorm - 4 + rtpad - ltpad))"
fi
ctpad="$_"

07

And then get rid of still more repetitions:

if   ((bfull < 2))  ; then : 0
elif ((pfull < 10)) ; then : "$((bfull - 2))"
elif ((pfull < 100)); then : "$((bfull - 3))"
else                       : "$((bnorm - 4))"
fi
ltpad=$((_ / 2))

if   ((bempt < 2))  ; then : -2
elif ((pfull < 10)) ; then : "$((bempt - 2))"
elif ((pfull < 100)); then : "$((bempt - 3))"
else                       : "$((bnorm - 4))"
fi
rtpad=$((_ / 2))

if   ((pfull < 10)) ; then : "$((bfull - 2))"
elif ((pfull < 100)); then : "$((bfull - 3))"
else                       : "$((bnorm - 4))"
fi
ctpad=$((_ + rtpad - ltpad))

which is exactly how it was just as I was preparing PercBar for publishing.

Staring at that, a question occurred to me: why am I even dealing with those conditionals outside arithmetic evaluations? After all, we're still dealing only with integers, and Bash's arithmetic evaluation can deal with conditionals.

Wouldn't the noise of all the $ and quoting go away?

08

That led me to the current version. Behold:

((_= bfull < 2   ? 0
  :  pfull < 10  ? bfull - 2
  :  pfull < 100 ? bfull - 3
  :                bnorm - 4 ,
  ltpad= _ / 2               ,
  _= bempt < 2   ? -2
  :  pfull < 10  ? bempt - 2
  :  pfull < 100 ? bempt - 3
  :                bnorm - 4 ,
  rtpad= _ / 2               ,
  _= pfull < 10  ? bfull - 2
  :  pfull < 100 ? bfull - 3
  :                bnorm - 4 ,
  ctpad= _ + rtpad - ltpad))

which I find delightfully clean!

Note that the colons in this last one have nothing to do with the colons in the previous:

  • here, inside ((…)), ? means then, and : means else;
  • whereas there, outside ((…)), the : is the null command that does nothing and always succeed.

Also note that:

  • inside ((…)) I can directly assign _ as a throwaway variable that can be reused;
  • whereas that wouldn't quite work outside ((…)).

It wouldn't work? Why not?

Because the devil is in the details, and Bash has its share of oddities.

Look into it

If you use Bash but aren't familiar with its arithmetic evaluation syntax, look into it.

See also

If you liked this, you might also enjoy FizzBuzz Obsessions III.

📆 2025-11-08