Issue #263, March 2016

bconvert—ways to convert numeric bases from the command line.

In my last article, I did what business writers would call a “deep
dive”
into `getopts` and different ways to parse starting flags and arguments in
shell scripts. I know you've tested that in your latest programs,
which is great, because it's something that you could find yourself
using quite a lot.

In this article, I'm covering something that's a bit more 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.

Copyright © 1994 - 2018 Linux Journal. All rights reserved.