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
((…)),?meansthen, and:meanselse; - 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