All Your Base in Bash: Complete Solution & Deep Dive Guide

Code Debug

All Your Base: A Complete Guide to Number Base Conversion in Bash Scripting

Master number base conversion in Bash by creating a script to transform a sequence of digits from an input base to a target base. This involves converting the number to a decimal (base-10) intermediate, then repeatedly dividing by the target base to find the new digits.

Ever felt like you're speaking a different language than your computer? In a way, you are. Computers fundamentally operate in binary (base-2), a world of ones and zeros, while we humans comfortably navigate our daily lives using decimal (base-10). This numerical disconnect is at the very heart of modern computing, from the colors on your screen (hexadecimal) to the permissions on a file (octal).

Imagine you're a professor, and your students, brilliant as they are, submit their work in a different number system each week. Your challenge isn't that they're wrong, but that you need a way to translate their answers into a base you understand. What if you could build a universal translator for numbers? A tool that could seamlessly convert between any base? This guide will show you exactly how to build that tool from scratch, using nothing but the raw power of a Bash script, a core skill from the kodikra.com Bash learning roadmap.


What Exactly is Number Base Conversion?

Before we dive into scripting, it's crucial to understand the foundational concept: positional notation. It's a system we use every day without a second thought. Base conversion is simply the process of taking a number represented in one positional system and expressing its equivalent value in another.

Understanding Positional Notation

Positional notation is a method of representing numbers where the value of a digit depends on its position within the number. The base, or radix, of a number system is the number of unique digits used to represent numbers.

In our familiar decimal (base-10) system, we use ten digits (0-9). The number 42 isn't just "four and two"; it's a shorthand for:

(4 × 10¹) + (2 × 10⁰) = 40 + 2 = 42

Each position represents a power of the base (10). The rightmost digit is the 10⁰ (ones) place, the next is the 10¹ (tens) place, and so on. The same principle applies to any base.

Common Bases in Computing

While we live in a base-10 world, computers and programmers frequently interact with other bases:

  • Base-2 (Binary): The native language of computers. Uses only two digits: 0 and 1. The decimal number 42 is 101010 in binary.
  • Base-8 (Octal): An older system often used for file permissions in Unix-like systems (e.g., chmod 755). It uses digits 0-7.
  • Base-16 (Hexadecimal): Widely used for representing memory addresses, color codes (e.g., #FFFFFF for white), and other byte-level data. It uses digits 0-9 and letters A-F to represent values 10-15.

The ability to convert between these bases is not just an academic exercise; it's a practical skill for anyone working close to the hardware or system-level configurations.


Why Master Base Conversion in Bash?

You might wonder why you'd build this in Bash when tools like Python or dedicated calculators exist. The answer lies in understanding the algorithm and the environment. Bash is the lingua franca of Linux and macOS systems, making it the perfect tool for tasks that need to be universally available without external dependencies.

Real-World Scenarios for SysAdmins and DevOps

Mastering this concept in Bash unlocks several practical applications:

  • Parsing Low-Level Data: Analyzing data from network packets, device registers, or binary files often requires interpreting values in different bases.
  • Automating System Configuration: Scripts that manage network subnets (using CIDR notation) or set file permissions can benefit from custom base conversion logic.
  • Debugging: When debugging memory dumps or embedded systems, you're often faced with hexadecimal values that need to be understood in a decimal context.
  • Building Lightweight Tools: Create powerful, dependency-free command-line utilities that can be easily shared and run on any server.

By implementing the algorithm yourself, as prescribed in the kodikra.com exclusive curriculum, you move from being a user of tools to a creator of them, deepening your understanding of both mathematics and scripting.


How the Universal Conversion Algorithm Works

The magic of converting between any two arbitrary bases (where the base is 2 or greater) is a two-step process that uses decimal (base-10) as a universal intermediate language. You never convert directly from, say, base-3 to base-7. Instead, you convert from base-3 to base-10, and then from base-10 to base-7.

Step 1: Converting Any Base to Decimal (Base-10)

To convert a number from any input base (from_base) to its decimal equivalent, you process its digits from left to right. This process is an application of Horner's method, which is an efficient way to evaluate polynomials.

The formula is: decimal_value = (...((d₀ * from_base + d₁) * from_base + d₂) * ... * from_base + dₙ)

Let's trace this with an example: converting 101010 from base-2 to base-10.

  1. Start with a decimal value of 0.
  2. Read the first digit (1): decimal = (0 * 2) + 1 = 1
  3. Read the second digit (0): decimal = (1 * 2) + 0 = 2
  4. Read the third digit (1): decimal = (2 * 2) + 1 = 5
  5. Read the fourth digit (0): decimal = (5 * 2) + 0 = 10
  6. Read the fifth digit (1): decimal = (10 * 2) + 1 = 21
  7. Read the sixth digit (0): decimal = (21 * 2) + 0 = 42

The final decimal value is 42. This iterative process is simple, elegant, and perfectly suited for a loop in a script.

    ● Start with digits in `from_base`
    │   e.g., [1, 0, 1] in base-2
    │
    ▼
  ┌───────────────────┐
  │ Initialize decimal=0 │
  └─────────┬─────────┘
            │
            ▼
  ┌───────────────────┐
  │ Loop through each digit │
  │ from left to right  │
  └─────────┬─────────┘
            │
            ├─→ For digit `d`:
            │   decimal = (decimal * from_base) + d
            │
            ▼
    ◆ More digits?
   ╱           ╲
  Yes           No
  │              │
  └──────────────┤
                 │
                 ▼
  ┌───────────────────┐
  │ Final decimal value │
  │   e.g., 5 in base-10  │
  └───────────────────┘
                 │
                 ▼
              ● End

Step 2: Converting Decimal (Base-10) to Any Target Base

To convert a decimal number to a target base (to_base), we use the inverse process: repeated division and remainder.

The algorithm is as follows:

  1. Take the decimal number.
  2. Divide it by the to_base.
  3. The remainder of this division is the rightmost digit of your new number.
  4. The integer quotient from the division becomes your new decimal number.
  5. Repeat steps 2-4 until the quotient is 0.

Let's trace this by converting the decimal number 42 to base-3.

  1. 42 / 3 = 14 with a remainder of 0. Our first digit is 0.
  2. New number is 14. 14 / 3 = 4 with a remainder of 2. Our second digit is 2.
  3. New number is 4. 4 / 3 = 1 with a remainder of 1. Our third digit is 1.
  4. New number is 1. 1 / 3 = 0 with a remainder of 1. Our fourth digit is 1.

The quotient is now 0, so we stop. The remainders, read in reverse order of calculation (from bottom to top), give us the final number: 1120 in base-3.

    ● Start with decimal value
    │   e.g., 42 in base-10
    │   Target base `to_base` = 3
    │
    ▼
  ┌───────────────────┐
  │ Initialize result = "" │
  └─────────┬─────────┘
            │
            ▼
    ◆ decimal > 0?
   ╱           ╲
  Yes           No
  │              │
  ├─→ remainder = decimal % to_base ─────→ Prepend remainder to result
  │   decimal = decimal / to_base          e.g., result = "0", "2 0", "1 2 0", "1 1 2 0"
  │                                      
  └──────────────┤
                 │
                 ▼
  ┌───────────────────┐
  │   Final result    │
  │ e.g., "1 1 2 0"   │
  └───────────────────┘
                 │
                 ▼
              ● End

The Complete Bash Solution: A Deep Dive Code Walkthrough

Now, let's translate this two-step algorithm into a robust Bash script. The following solution from the kodikra.com module is clean, efficient, and follows best practices for shell scripting. We will dissect it piece by piece to understand every decision.

Full Script from the kodikra.com Module


#!/bin/bash

# Function to validate input and output bases
validate_bases() {
    if (( $1 < 2 )); then
        echo "input base must be >= 2" >&2
        exit 1
    fi
    if (( $3 < 2 )); then
        echo "output base must be >= 2" >&2
        exit 1
    fi
}

# Function to validate each digit against the input base
validate_digit() {
    local digit=$1
    local base=$2
    if [[ ! "$digit" =~ ^[0-9]+$ ]]; then
        echo "input has invalid digit" >&2
        exit 1
    fi
    if (( digit < 0 || digit >= base )); then
        echo "input has invalid digit" >&2
        exit 1
    fi
}

main() {
    # Check for correct number of arguments
    if (( $# != 3 )); then
        echo "Usage: all_your_base.sh <input base> <input digits> <output base>" >&2
        exit 1
    fi

    validate_bases "$1" "$2" "$3"

    local -i from_base=$1
    local from_digits=$2
    local -i to_base=$3

    # We're going to use an unquoted variable to take
    # advantage of word splitting. However, we don't want to
    # be affected by pathname expansion, so we must turn off
    # that feature temporarily.
    set -f
    local digits=( $from_digits )
    set +f

    # Handle empty digit list
    if (( ${#digits[@]} == 0 )); then
        echo "0"
        exit 0
    fi

    # Step 1: Convert from input base to decimal
    local -i decimal=0
    for digit in "${digits[@]}"; do
        validate_digit "$digit" "$from_base"
        decimal=$((decimal * from_base + digit))
    done

    # Step 2: Convert from decimal to output base
    if (( decimal == 0 )); then
        echo "0"
        exit 0
    fi

    local result=""
    while (( decimal > 0 )); do
        local -i remainder=$((decimal % to_base))
        result="$remainder $result"
        decimal=$((decimal / to_base))
    done

    # Print the final result, removing the trailing space
    echo "${result% }"
}

main "$@"

Part 1: The main Function and Initial Setup

The script is wrapped in a main function, a common practice that improves readability and prevents code from running automatically if the script is sourced by another script. The main "$@" at the end executes the function, passing all command-line arguments to it.


main() {
    # Check for correct number of arguments
    if (( $# != 3 )); then
        echo "Usage: all_your_base.sh <input base> <input digits> <output base>" >&2
        exit 1
    fi

    validate_bases "$1" "$2" "$3"

    local -i from_base=$1
    local from_digits=$2
    local -i to_base=$3
    # ...
}
  • $# is a special Bash variable that holds the count of command-line arguments. The script expects exactly three.
  • Error messages are redirected to standard error (>&2), which is the correct channel for errors.
  • exit 1 signals that the script terminated with an error.
  • local -i declares the base variables as integers. This tells Bash to treat their values as numbers, enabling arithmetic operations without explicit casting.

Part 2: Robust Input Validation

A script is only as good as its error handling. These two functions ensure the input is sane before any calculations begin.


validate_bases() {
    if (( $1 < 2 )); then
        echo "input base must be >= 2" >&2; exit 1
    fi
    if (( $3 < 2 )); then
        echo "output base must be >= 2" >&2; exit 1
    fi
}

validate_digit() {
    local digit=$1
    local base=$2
    if [[ ! "$digit" =~ ^[0-9]+$ ]]; then
        echo "input has invalid digit" >&2; exit 1
    fi
    if (( digit < 0 || digit >= base )); then
        echo "input has invalid digit" >&2; exit 1
    fi
}
  • validate_bases ensures both the input and output bases are 2 or greater, as number systems with a base less than 2 are not meaningful in this context.
  • validate_digit performs two checks: first, it uses a regular expression (=~) to ensure the digit is actually a number. Second, it confirms the digit's value is valid for the given base (e.g., in base-8, digits can only be 0-7).

Part 3: The Core Logic - From Input Base to Decimal

This section is where the real work begins. It implements the first half of our algorithm.


    # Turn off pathname expansion (globbing)
    set -f
    local digits=( $from_digits )
    # Turn it back on
    set +f

    # Handle empty digit list
    if (( ${#digits[@]} == 0 )); then
        echo "0"
        exit 0
    fi

    local -i decimal=0
    for digit in "${digits[@]}"; do
        validate_digit "$digit" "$from_base"
        decimal=$((decimal * from_base + digit))
    done
  • The set -f Trick: This is a crucial and clever piece of Bash scripting. The input digits arrive as a single space-separated string (e.g., "1 0 1 0"). To turn this into a Bash array, we use local digits=( $from_digits ). The unquoted $from_digits undergoes "word splitting". However, if the string contained a character like *, Bash would try to replace it with filenames (pathname expansion). set -f temporarily disables this feature, ensuring our digits are interpreted literally. We immediately turn it back on with set +f.
  • The loop iterates through each digit in the array, validates it, and applies the Horner's method formula: decimal = (decimal * from_base + digit). This is done inside Bash's arithmetic expansion syntax: $((...)).

Part 4: The Final Step - From Decimal to Target Base

With the number now in a universal decimal format, we convert it to the target base using the repeated division algorithm.


    if (( decimal == 0 )); then
        echo "0"
        exit 0
    fi

    local result=""
    while (( decimal > 0 )); do
        local -i remainder=$((decimal % to_base))
        result="$remainder $result"
        decimal=$((decimal / to_base))
    done
  • A special case handles an input of 0.
  • The while loop continues as long as our decimal value is greater than zero.
  • % is the modulo operator, which gives us the remainder.
  • / in an integer context performs integer division.
  • The Prepend Trick: Notice the line result="$remainder $result". By placing the new remainder at the beginning of the result string in each iteration, we cleverly build the final number in the correct order. This avoids needing a separate step to reverse the digits at the end.

Part 5: Handling the Output

The final step is to present the result cleanly.


    # Print the final result, removing the trailing space
    echo "${result% }"
  • Our prepend trick leaves an extra space at the end of the string (e.g., "1 1 2 0 ").
  • ${result% } is a Bash parameter expansion feature. It removes the shortest matching suffix pattern from the variable. Here, it removes a single trailing space, giving us a perfectly formatted output like "1 1 2 0".

Custom Script vs. Built-in Linux Tools

While building this script is an invaluable learning experience, it's also important to know about existing tools for day-to-day tasks. The goal of this kodikra module is to teach you the *how* and *why*, not just to get an answer.

When to Use bc or printf

For quick, one-off conversions on the command line, Linux provides powerful utilities:

  • bc (Basic Calculator): An arbitrary-precision calculator language. It can handle base conversions natively.
    # Convert 42 (decimal) to base 2
    $ echo "obase=2; 42" | bc
    101010
    
    # Convert 101010 (binary) to base 10
    $ echo "ibase=2; 101010" | bc
    42
  • printf: The shell's built-in printf command can convert from decimal to octal (%o) and hexadecimal (%x).
    # Convert 255 (decimal) to hex
    $ printf "%x\n" 255
    ff

Pros and Cons Analysis

Let's compare our custom script to using a tool like bc.

Feature Custom Bash Script Using bc
Learning Value Excellent. Forces you to understand and implement the core algorithm. Low. It's a black box that just gives you the answer.
Dependencies None. Requires only Bash, which is ubiquitous on Linux/macOS. Requires the bc package to be installed (it usually is, but not always).
Flexibility High. You can customize input/output formats, add logging, or integrate it into larger scripts. Moderate. It's powerful but has its own specific syntax.
Performance Slower for very large numbers due to shell arithmetic overhead. Faster. It's a compiled C program optimized for math.
Error Handling Customizable and explicit. You control every error message. Can be cryptic or silent on some errors.

Frequently Asked Questions (FAQ)

What is positional notation?
Positional notation is a system where a digit's value is determined by its position within a number, multiplied by a power of the system's base. For example, in the decimal number 123, the '1' represents 100 (1 * 10²), not just one.

Why is it necessary to convert to a decimal (base-10) intermediate?
Using base-10 as an intermediate simplifies the problem immensely. It provides a common ground. The algorithms for converting any base to decimal and from decimal to any base are straightforward and universally applicable. Creating direct conversion algorithms for every possible pair of bases (e.g., base-3 to base-17) would be incredibly complex.

What does the `set -f` command do in the script?
The set -f command temporarily disables a shell feature called "pathname expansion" or "globbing." This prevents the shell from interpreting special characters like * or ? in the input digits as wildcards for filenames, ensuring the script handles the input literally and safely.

Can this script handle bases greater than 10, like hexadecimal?
Not in its current form. This script is designed for bases where digits are represented by standard numbers (0-9). To support hexadecimal (base-16) or higher, you would need to modify the script to handle alphabetic digits (A-F) for both input and output, adding a mapping layer to convert between characters and their numeric values.

Is Bash efficient for this kind of numerical computation?
For small to moderately sized integers, Bash is perfectly adequate. However, for high-performance computing or numbers exceeding the shell's maximum integer size (which is typically a 64-bit signed integer), Bash is not the right tool. Compiled languages like C, Go, or Rust, or even higher-level tools like Python with its arbitrary-precision integers, would be far more efficient.

What are the limitations of this script?
The primary limitations are the maximum integer size supported by your system's version of Bash and the fact that it only handles numeric digits. It does not support negative numbers or fractional parts. The purpose of this exercise is to master the algorithm for positive integers within a typical scripting context.

Conclusion: Your Next Step in Bash Mastery

You've successfully journeyed through the logic, theory, and practical implementation of number base conversion in Bash. By building this tool from the ground up, you've done more than just solve a problem; you've gained a deeper appreciation for how numbers are represented and manipulated at a fundamental level. You've also mastered several advanced Bash techniques, including robust error handling, array manipulation, and safe variable expansion.

This foundational knowledge is a stepping stone. The principles of algorithmic thinking and careful implementation you've practiced here are applicable to countless other challenges you'll face as a developer, system administrator, or DevOps engineer. The command line is your canvas, and Bash is your brush.

Ready to continue your journey? Dive deeper into our Bash Scripting Guide to discover more powerful techniques and build even more sophisticated tools.

Disclaimer: The code and explanations in this guide are based on the exclusive learning curriculum from kodikra.com and are compatible with Bash version 4.0 and newer. Behavior may vary on older versions of Bash.


Published by Kodikra — Your trusted Bash learning resource.