Absolute integers: Bash's ternary operator and other oddities
So one day I was thinking of a function to return the absolute value of an integer. The usefulness of this is questionable, but never mind that.
It started as a small exercise in languages comparison.
First I wrote it in Python:
#!/usr/bin/env python3 # Calculate the absolute value of an integer. import sys def int_abs(int): if int<0: y=-1*int else: y=int return y print(int_abs(int(sys.argv[1]))) raise SystemExit
Then in Haskell, which looks as clear as it can get:
-- Calculate the absolute value of an integer. module Intabs where intAbs :: Int -> Int intAbs x | x < 0 = -x | otherwise = x
Then in Emacs Lisp:
(defun int-abs (x) "Calculate the absolute value of an integer." (when (integerp x) (if (< x 0) (- x) x)))
Also clear.
And finally in Bash, using only its native syntax (no bc
):
# Calculate the absolute value of an integer. isInt() [[ "$1" =~ ^-?[0-9]+$ ]] intAbs() (isInt "$1" && echo "$(("$1"<0?10#"${1#-}":10#"$1"))")
Now, this is not quite as legible, is it? Wouldn't blame you for avoiding it.
A short example and explanation of the last construct:
echo "$((4+3))" # 7 -- numerical evaluation echo "$((3<5?10:20))" # 10 -- ternary operation: if 3<5, then 10, else 20. echo "${1#-}" # Remove "-" from the beginning of "$1" if it exists
And that 10#
? It says the number is decimal. Otherwise, if it happens to have leading zeros, Bash tries to read it as octal.
So there we have it: Bash has a C-like ternary test! You can do things like this:
foo() { : "$((x = "$1", w = 3, z = (x < 5 ? 10 : 20) * x + w))" echo "$z" ;} foo 2 #=> 23 = 10 * 2 + 3 foo 7 #=> 143 = 20 * 7 + 3
Anyway, I wondered about different ways of writing this simple absolute integer function in Bash. I ended up with 10 different versions — all of which turned out to be... even less legible.
In the end, this became an exploration of alternatives in Bash syntax, as well as of over-engineering meta-functions to report the comparison of tests. By which I mean that if we run this:
intAbs.sh -t
we'd get this:
INPUT SHOULD intAbs1 intAbs2 intAbs3 intAbs4 intAbs5 intAbs6 intAbs7 intAbs8 intAbs9 intAbs10 12 12 12 12 12 12 12 12 12 12 12 (?) 12 -13 13 13 13 13 13 13 13 13 13 13 (?) 13 022 22 22 22 22 22 22 22 22 22 22 (?) 18 -023 23 23 23 23 23 23 23 23 23 23 (?) 19 0042 42 42 42 42 42 42 42 42 42 42 (?) 34 -0043 43 43 43 43 43 43 43 43 43 43 (?) 35 99 99 99 99 99 99 99 99 99 99 99 (?) 99 0 0 0 0 0 0 0 0 0 0 0 (?) 0 000 0 0 0 0 0 0 0 0 0 0 (?) 0 -0 0 0 0 0 0 0 0 0 0 0 (?) 0 -000 0 0 0 0 0 0 0 0 0 0 (?) 0 ----- 1 2 3 3.5 -2.4 ab a 1 b
(UPDATE: not anymore: newer versions of Bash now throw an error when the
number after "base#" is negative, so the first four functions no longer work.)
Here is the code:
#!/usr/bin/env bash # # SPDX-FileCopyrightText: © flandrew # SPDX-License-Identifier: GPL-3.0-or-later # # intAbs.sh — Absolute integers in Bash — Version: 0.1.1 # If input is an integer, return its absolute value. Otherwise, nothing. # # An exercise in esoteric Bashery, with tidy output of test results for # various alternative functions that are all supposed to return exactly the # same values. ## User variables # That's the data to test INPUT=(12 -13 022 -023 0042 -0043 "99" 0 000 -0 -000 "-----" "1 2 3" 3.5 -2.4 ab "a 1 b") # That's what the result should be SHOULD=(12 13 22 23 42 43 99 0 0 0 0 "" "" "" "" "" "") # That's how your main function should be called, plus a growing numerical # prefix from 1 to n. FUNCN="intAbs" ## Internal variables # This is the $c in the available $FUNCN{1..$c} (though those braces here # wouldn't expand...) FUNCOUNT="$(< "$0" grep -oP "(?<=^"$FUNCN")\d+" | sort -rn | sed 1q)" ## Support functions isInt() [[ "$1" =~ ^-?[0-9]+$ ]] ## Core function versions # Those are expected to be RIGHT: intAbs1() { isInt "$1" && echo "$(($1<0?10#${1#-}:10#$1))";} # (Is the previous one safe to leave unquoted? Not good practice.) intAbs2() { isInt "$1" && echo "$(("$1"<0?10#"${1#-}":10#"$1"))";} intAbs3() { isInt "$1" && echo "$(("$1"<0 ?10#"${1#-}" :10#"$1"))" } intAbs4() { isInt "$1" && echo "$(("$1">=0 ?10#"$1" :10#"${1#-}"))" } intAbs5() { isInt "$1" && { case "${1:0:1}" in -) : "${1:1}" ;; *) : "$1" esac sed -E "s/^0+/0/ s/^0([^0])/\1/" <<< "$_" } } intAbs6() { isInt "$1" && { case "${1:0:1}" in -) : "${1:1}" ;; *) : "$1" esac case "${_:0:1}" in 0) until [[ "$_" == "${_#0}" ]] do : "${_#0}" done sed "s/^$/0/" <<< "$_" ;; *) echo "$_" esac } } intAbs7() { isInt "$1" && { case "${1:0:1}" in -) : "${1:1}" ;; *) : "$1" esac case "${_:0:1}" in 0) : "${_#"${_%%[^0]*}"}" sed "s/^$/0/" <<< "$_" ;; *) echo "$_" esac } } intAbs8() { isInt "$1" && { local x case "${1:0:1}" in -) : "${1:1}" ;; *) : "$1" esac case "${_:0:1}" in 0) : "${_#"${_%%[^0]*}"}" : "${x=$_}" : "$((${#x}==0?0:x))" ;; esac echo "$_" } } intAbs9() if isInt "$1"; then local x case "${1:0:1}" in -) : "${1:1}" ;; *) : "$1" esac ((${_:0:1}=="0")) && { : "${_#"${_%%[^0]*}"}" : "${x=$_}" : "$((${#x}==0?0:x))" } printf "%d\n" "$_" fi # Expected to be WRONG: # WRONG! Because 0042 is read as \o42 (octal)... intAbs10() { isInt "$1" && echo "(?) $(("$1"<0?"${1#-}":"$1"))";} ## Testing functions outdata() { # Think only Lisp has macros? # To be fair, quasi-quoting (i.e. Lisp's all quoted + selective ,var) # is nicer than its inverse (i.e. Bash's all unquoted + selective \$var) . <(cat <<EOM echo "$1" for n in "\${$1[@]}" do echo "\$n" done EOM ) } testone() { # testone function testdataarray . <(cat <<EOM echo "$1" for n in "\${$2[@]}" do $1 "\$n" | (grep . || echo) done EOM ) } _testall() { # _testall functionrootname funcount testdataarray expectedresultsarray echo "outdata $3" echo "outdata $4" for ((;c++<"$2";)) do echo "testone $1$c $3" done } testall() { # _testall functionrootname funcount testdataarray expectedresultsarray . <(_testall "$@" | sed -E "s/.*/<(&)/ 1 s/^/paste\n/" | sed -E "s/.*/& \\\\/ $ s/$/\n/") | column -ts$'\t' } ## Main main() { case "$1" in -t|--test|test) testall intAbs "$FUNCOUNT" INPUT SHOULD ;; *) intAbs5 "$1" esac } main "$@" exit 0
Well, I guess we have to admit that if immediate clarity is what you want, then your calculation of absolute integers needs to be in some non-Bash language...
But some of these syntaxes are handy. Could you understand it?
Look into man bash
, then search there (with /STRING
) for:
/Parameter Expansion /Arithmetic Expansion /_<SPACE> <--- a literal space /: \[arguments\]
And did you notice anything odd about the function intAbs9
? Anything supposedly... missing?
And yet, it works. Here's why. Search man bash
for:
/Shell Function Definitions /Compound Commands
Have fun.