Work the Shell

Fancy Tricks for Changing Numeric Base

Dave Taylor

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.

Dave Taylor has been hacking shell scripts since the dawn of the computer era. Well, not really, but still, 30 years is a long time! He's the author of the popular Wicked Cool Shell Scripts and Teach Yourself Unix in 24 Hours (new edition just released!). He can be found on Twitter as @DaveTaylor and more generally at his tech site: www.AskDaveTaylor.com.