Fancy Tricks for Changing Numeric Base
In this article, I'm covering something that's a little abstruse: converting numeric bases within shell scripts. There are really four commonly used numeric bases to consider: binary, octal, decimal and hexadecimal. You're used to working in base-10, so 10 = 1 * 10**1 + 0 and 100 = 1 * 10**2 + 0 * 10**1 + 0.
That maps to other numeric bases, so 1010 base-2 or binary is really 1 * 2**3 + 0 * 2**2 + 1 * 2**1 + 0 or 8 + 0 + 2 + 0 = 10 decimal. Octal is the same thing, so 33 base-8 converts to decimal as 3 * 8**1 + 3 = 27.
Hexadecimal presents a different challenge because a base-16 numbering system doesn't fit neatly into our Arabic numerals 0, 1, 2, ... 9. "Hex", as it's known informally, adds A, B, C, D, E and F, so that the decimal value 10 is represented in Hex as "A". That's where the math gets interesting, so 33 base-16 = 3 * 16**1 + 3 = 48 + 3 = 51.
The long, complicated way to create a base conversion utility is therefore to disassemble every value given and apply the translation shown, then have an internal value that's a common base (probably base-10), then have another routine that converts the common base to the desired output base.
There are smarter ways to do this, as I'll discuss, but for now,
let's look at the bc
command, which supports users specifying both
the input and output numeric bases. bc
, the binary calculator, is a bit
tricky to work with as it's an old-school UNIX command. As I discuss
at length in my book Wicked Cool Shell Scripts, the most common way to
work with the crude but interactive bc
program is
to use echo
to send
it the commands needed, as demonstrated here:
$ echo '333 * 0.35' | bc
116.55
Useful (particularly since expr
and $((
))
can't work with floats and
decimal values), but where this gets really interesting is with those
input and output numeric bases.
Let's say I want to confirm a conversion I listed earlier, by converting 33 hex into decimal. This is easily done:
$ echo 'ibase=16; 33' | bc
51
That's simple. Now, let's do something bigger and more complicated:
$ echo 'ibase=16; FEF33D9' | bc
267334617
ibase
is the input numeric base. The output base is specified as
obase
. And that's it—easy enough!
So let's take the same hex value as input but force the output to octal instead of the default decimal:
$ echo 'ibase=16; obase=8; FEF33D9' | bc
1773631731
Would you rather work in binary? You can do that too:
$ echo 'ibase=16; obase=2; FEF33D9' | bc
1111111011110011001111011001
That's a lot of ones and zeroes, for sure. It makes me think of Interstellar, but that's another article entirely!
Armed with this knowledge, it's pretty easy to push out a rudimentary shell script that converts between any of binary, octal, decimal and hexadecimal:
ibase=10; obase=10 # set up defaults
usage() {
echo "Usage: $(basename $0) -i base -o base value" 1>&2
echo " where base can be 2, 8, 10 or 16." 1>&2
exit 1
}
while getopts "i:o:" value ; do
case "$value" in
i) ibase=$OPTARG
(( ibase == 2 || ibase == 8 || ibase == 10 ||
ibase == 16 )) || usage
;;
o) obase=$OPTARG
(( obase == 2 || obase == 8 || obase == 10 ||
obase == 16 )) || usage
;;
*) usage ;;
esac
done
shift $(( OPTIND - 1 ))
echo Converting $1 from base-$ibase to base-$obase\:
echo "obase=$obase; ibase=$ibase; $1" | bc
exit 0
Almost the entire program is involved with parsing and checking input values, which isn't that uncommon with well written shell scripts. Notice some shortcuts I include in the script too, notably the test structure:
(( condition || condition )) || usage
This is the same as saying "if not condition1 and not condition 2 ; then
; usage", just more succinct. Also, as I discussed in my last
article, note the
use of OPTARG
to get the argument value and
OPTIND
with the shift
command
to axe all of the parameters so that $1
will be the value to convert.
A few quick runs of the program reveal that it's working fine:
$ bconvert.sh -i 16 33
Converting 33 from base-16 to base-10:
51
$ bconvert.sh -i 16 -o 2 33
Converting 33 from base-16 to base-2:
110011
$ bconvert.sh -i 2 -o 16 110011
Converting 110011 from base-2 to base-16:
33
Notice the last two examples demonstrate the mirror function of converting between 33 base-16 and 110011 base-2. It works!
A common numeric notation in the Linux world is to recognize that numbers prefaced with a zero are octal, and those prefaced with "0x" are hexadecimal. (Binary isn't particularly useful so it's not included in the common notation.) Here are a few examples: 0700, 0xFFc39. You could modify the script to accept these as inputs and infer the appropriate base, but I'm going to leave that as an exercise for you, dear reader.
There's another way you can convert values without involving
bc
—by utilizing the
printf
command-line program. If you know C programming,
you're already familiar with printf()
and
scanf()
, but unfortunately,
only the output function is available at the shell command line. Usage
is quite similar, however, as you can see in this quick example:
$ printf "> %d <\n" 42
> 42 <
In this case, the format string (argument #1) details the desired output,
with %d
indicating that a decimal value will be printed, then argument
2 is that value, 42.
Where this gets interesting is because you actually can use other values in the format string to force octal or hexadecimal:
$ printf "octal: %o\nhex: %x\n" 42 42
octal: 52
hex: 2a
Because of the notational convention mentioned earlier for non-decimal numbers in the shell, you also can specify an octal or hexadecimal value too:
$ printf "%o\n" 0500
500
Wait, what happened in that last example? It's simple: I specified that I wanted octal (base-8) output, but by using the leading zero, I also indicated that I was specifying a value in octal too. Ergo, 0500 = 500.
That's nice, but no binary, which is a definite limitation.
But, I'm not done yet. There's one more way you can convert values,
and it's actually directly within the shell. It turns out that using the
$(( ))
notation, you actually can specify a numeric
base for numbers!
This is something I stumbled across recently, having had no idea that this was even a capability of the shell, but check this out, a quick conversion of 33 base-16 to decimal:
$ echo $((16#33))
51
Not only that, but the leading zero and leading "0x" are both valid too:
$ echo $(( 0xFF ))
255
If you don't care about binary values, you can see that there are three completely different ways to convert numeric bases from within a shell script. Now take what I've shown here and do something really slick!
In a future article, I'll explore some other shortcuts for conditional statements that let you skip the mundane "if condition ; then XX else XX fi" notational sequence.
Note: the bconvert.sh script is available for download at http://www.linuxjournal.com/files/linuxjournal.com/code/bconvert