Line Up in Bash: Complete Solution & Deep Dive Guide

Tabs labeled

Bash Scripting from Zero to Hero: Handling Ordinal Numbers Like a Pro

This complete guide explains how to build a robust Bash script to convert numbers into ordinal form (e.g., 1st, 2nd, 21st). We explore the logic behind handling exceptions like 11th, 12th, and 13th using Bash's powerful case statement, command-line arguments, and secure string formatting with printf.

Have you ever found yourself wrestling with text manipulation in a shell script? You're trying to generate a user-friendly report, a log file with a unique name, or just a simple, personalized message, but you hit a wall. The problem isn't complex logic; it's the seemingly simple task of making text sound natural. You need to turn the number 1 into "1st", 2 into "2nd", and 23 into "23rd", but also correctly handle those tricky teens like 11 ("11th") and 12 ("12th"). This small detail can be the difference between a clunky script and a professional, polished tool.

This is a common frustration for developers and system administrators alike. You know Bash is powerful, but navigating its string manipulation and conditional logic can feel unintuitive. Fear not. This guide will walk you through a practical, real-world problem from the exclusive kodikra.com curriculum. We will dissect a clean, efficient solution, transforming you from someone who avoids complex string logic in Bash to someone who masters it. You will learn not just *how* to solve the problem, but *why* certain tools and techniques are chosen, empowering you to build more sophisticated and elegant shell scripts.


What is the Ordinal Number Challenge in Bash?

At its core, this challenge is about converting a cardinal number (e.g., 1, 2, 3) into its ordinal form (1st, 2nd, 3rd). This is a classic text processing task that highlights the importance of handling rules and their exceptions—a fundamental concept in programming.

The scenario, drawn from a kodikra.com module, involves a friend named Yaʻqūb who works at a busy deli. To manage the customer queue, he wants to issue tickets with a personalized message. Instead of just a number, he wants something like, "Mary, you are the 1st customer..." This requires a script that can take a name and a number and dynamically generate the correct ordinal suffix.

The Rules of the Game

The English language rules for ordinal numbers are specific and form the basis of our script's logic:

  • Numbers ending in 1 receive an "st" suffix (e.g., 1st, 21st, 31st).
  • Numbers ending in 2 receive an "nd" suffix (e.g., 2nd, 22nd, 42nd).
  • Numbers ending in 3 receive an "rd" suffix (e.g., 3rd, 23rd, 53rd).
  • All other numbers receive a "th" suffix (e.g., 4th, 9th, 20th).

The Crucial Exceptions

Here's where it gets interesting. There's a major exception to the rules above involving the "teens":

  • Numbers ending in 11, 12, or 13, regardless of the final digit, always use the "th" suffix. For example, 11th (not 11st), 12th (not 12nd), and 13th (not 13rd). This also applies to numbers like 111th, 112th, and 113th.

Our Bash script must be intelligent enough to check for these exceptions *before* applying the general rules. This makes it a perfect exercise for mastering conditional logic and pattern matching in a shell environment.


Why Use Bash for This Text Processing Task?

In a world with powerful languages like Python, Go, and Rust, you might wonder why we'd choose Bash for a task like this. While those languages are certainly capable, Bash offers a unique set of advantages for this specific type of problem, making it a highly practical and efficient choice.

Lightweight and Ubiquitous

Bash is the default shell on virtually every Linux distribution and macOS. It's available out-of-the-box with no need for compilers, interpreters, or dependency management for simple tasks. This makes scripts incredibly portable and easy to deploy. You can write the script on one machine and be confident it will run on another with minimal to no changes.

Superior for CLI Integration

The script's purpose is to be a command-line utility. Bash is the native language of the command line. It excels at:

  • Argument Parsing: Handling command-line arguments (like the customer's name and number) is a fundamental feature, not an add-on library.
  • Piping and Redirection: While not used in this specific solution, the output of our script could easily be piped to another command, saved to a file, or integrated into a larger workflow.
  • System Automation: This kind of script can be easily called by other system tools, cron jobs, or monitoring agents, making it a perfect building block for automation.

Built-in Pattern Matching

Bash's case statement provides powerful, built-in glob-style pattern matching. As we'll see in the code walkthrough, this allows for incredibly concise and readable logic for checking the last digits of a number. Achieving the same result in other languages might require string slicing, modulus arithmetic, or regular expressions, which can be more verbose for this specific use case.

For quick, text-centric command-line tools, Bash often provides the straightest and most efficient path from problem to solution. Master the fundamentals with our complete Bash guide to unlock its full potential for system administration and automation.


How the Bash Solution Works: A Deep Dive

Now, let's break down the elegant solution from the kodikra learning path. We will analyze it piece by piece to understand the purpose of every line and command. This detailed walkthrough will illuminate key Bash concepts that are applicable to a wide range of scripting challenges.

The Complete Script

Here is the full source code we will be dissecting. This script is designed to be executed from the command line, taking the name and number as arguments.


#!/usr/bin/env bash

# This function takes a number as its first argument
# and returns the number with the correct ordinal suffix.
ordinal() {
    # The case statement checks the number against several patterns.
    # The order of these patterns is critical.
    case $1 in
        # First, check for the exceptions: numbers ending in 11, 12, or 13.
        # The '*' is a glob pattern that matches any characters.
        # The '|' acts as an OR operator.
        *11|*12|*13)
            echo "${1}th"
            ;;
        # If not an exception, check for numbers ending in 1.
        *1)
            echo "${1}st"
            ;;
        # Check for numbers ending in 2.
        *2)
            echo "${1}nd"
            ;;
        # Check for numbers ending in 3.
        *3)
            echo "${1}rd"
            ;;
        # The catch-all pattern. If none of the above match, it must be 'th'.
        *)
            echo "${1}th"
            ;;
    esac
}

# The main function orchestrates the script's execution.
main() {
    # Use printf for safe, formatted output.
    # %s is a placeholder for a string.
    # The first %s is replaced by the first argument ($1, the name).
    # The second %s is replaced by the output of the ordinal function.
    printf "%s, you are the %s customer we serve today. Thank you!" "$1" "$(ordinal "$2")"
}

# This line calls the main function, passing all command-line
# arguments to it. "$@" expands to all arguments, quoted to
# handle spaces correctly.
main "$@"

Line-by-Line Code Walkthrough

1. The Shebang: #!/usr/bin/env bash

This is the first and most important line in any shell script. It's called a "shebang." It tells the operating system which interpreter to use to execute the file. Using /usr/bin/env bash is more portable than the hardcoded path /bin/bash because it finds the bash executable in the user's PATH environment variable, accommodating systems where Bash might be installed in a different location.

2. The `ordinal` Function

Functions in Bash are a way to group reusable code. The ordinal() function is the logical core of our script. It takes one argument—the number to be converted—and is responsible for figuring out the correct suffix.


ordinal() {
    case $1 in
        # ... patterns ...
    esac
}

Inside the function, $1 refers to the *first argument passed to the function*, not the script itself. This is a key concept of scope in Bash scripting.

3. The `case` Statement: Elegant Pattern Matching

The case statement is the star of the show. It's a control structure that compares a value (in this case, $1) against a series of patterns until it finds a match, at which point it executes the corresponding block of code. It's often a cleaner and more readable alternative to a long chain of if-elif-else statements.

Let's look at the logic flow:

    ● Start (Input number, e.g., "12", "21")
    │
    ▼
  ┌──────────────────┐
  │  `case $1 in`    │
  └────────┬─────────┘
           │
           ▼
    ◆ Ends in 11, 12, or 13?
   ╱ (e.g., "11", "112")
  Yes
  │
  ▼
┌──────────────┐
│ Append "th"  │
└──────────────┘
           ╲
            No
             │
             ▼
        ◆ Ends in 1?
       ╱ (e.g., "1", "21")
      Yes
      │
      ▼
    ┌──────────────┐
    │ Append "st"  │
    └──────────────┘
               ╲
                No
                 │
                 ▼
            ◆ Ends in 2?
           ╱ (e.g., "2", "22")
          Yes
          │
          ▼
        ┌──────────────┐
        │ Append "nd"  │
        └──────────────┘
                   ╲
                    No
                     │
                     ▼
                 ◆ Ends in 3?
                ╱ (e.g., "3", "33")
               Yes
               │
               ▼
             ┌──────────────┐
             │ Append "rd"  │
             └──────────────┘
                        ╲
                         No (Default)
                          │
                          ▼
                        ┌──────────────┐
                        │ Append "th"  │
                        └──────────────┘
  • *11|*12|*13) echo "${1}th" ;;
    This is the most critical pattern and it comes first. The * is a wildcard (glob) that matches any sequence of characters. So, *11 matches "11", "111", "211", and so on. The | acts as an "OR", allowing us to combine the three exception patterns into one line. If a match is found, the script appends "th" and the ;; terminates the case block.
  • *1) echo "${1}st" ;;
    This pattern only runs if the first one didn't match. It checks for any number ending in 1. Because the `*11` case was handled first, we can be sure that any number matching here is a valid "st" case (like 1, 21, 31).
  • *2) echo "${1}nd" ;; and *3) echo "${1}rd" ;;
    These follow the same logic for numbers ending in 2 and 3, having already excluded 12 and 13.
  • *) echo "${1}th" ;;
    The final * is a catch-all pattern. It matches anything that wasn't caught by the previous patterns. This handles all other numbers (4-10, 14-20, etc.) and correctly appends "th".

4. The `main` Function and Execution

While not strictly necessary for a simple script, using a main function is excellent practice. It clearly defines the script's entry point and improves organization.


main() {
    printf "%s, you are the %s customer we serve today. Thank you!" "$1" "$(ordinal "$2")"
}

main "$@"
  • printf for Safety: The printf command is used to print the final sentence. It's generally safer than echo because it doesn't interpret backslash escapes or other characters in its arguments unless you explicitly tell it to. The %s placeholders are filled in order by the subsequent arguments.
  • Command Substitution: The magic happens with "$(ordinal "$2")". The $() syntax is called command substitution. Bash executes the command inside the parentheses (ordinal "$2"), captures its standard output (the result of the echo in the function), and substitutes it into the printf command's argument list.
  • Script Arguments: Inside main, $1 refers to the first argument passed to the script (the name), and $2 refers to the second (the number).
  • The Grand Finale - main "$@": This line executes the main function. "$@" is a special variable that expands to all the command-line arguments passed to the script. Quoting it is crucial because it ensures that arguments containing spaces (e.g., a name like "Mary Ann") are treated as a single argument.

Running the Script and Seeing it in Action

To use this script, you would first save the code into a file, for example, lineup.sh. Then, you need to make it executable.

Step 1: Make the Script Executable

Open your terminal and use the chmod (change mode) command:


chmod +x lineup.sh

Step 2: Execute with Different Inputs

Now you can run the script from your terminal, providing a name and a number as arguments.

Example 1: The "st" rule


$ ./lineup.sh Mary 1
Mary, you are the 1st customer we serve today. Thank you!

$ ./lineup.sh David 21
David, you are the 21st customer we serve today. Thank you!

Example 2: The "nd" and "rd" rules


$ ./lineup.sh Susan 2
Susan, you are the 2nd customer we serve today. Thank you!

$ ./lineup.sh Peter 33
Peter, you are the 33rd customer we serve today. Thank you!

Example 3: The "th" exception rule


$ ./lineup.sh John 11
John, you are the 11th customer we serve today. Thank you!

$ ./lineup.sh Emily 12
Emily, you are the 12th customer we serve today. Thank you!

Example 4: The default "th" rule


$ ./lineup.sh Michael 4
Michael, you are the 4th customer we serve today. Thank you!

$ ./lineup.sh Sarah 99
Sarah, you are the 99th customer we serve today. Thank you!

This demonstrates the script's complete execution flow from command-line input to final formatted output.

    ● Start
    │
    ▼
  ┌──────────────────┐
  │ ./lineup.sh "John" "12" │
  └────────┬─────────┘
           │
           ▼
    `main "$@"` is called
    ($1="John", $2="12")
           │
           ▼
  ┌──────────────────┐
  │ `printf "...", "$1", ...` │
  └────────┬─────────┘
           │
           ├─ "$1" is "John"
           │
           └─ Calls `ordinal "$2"`
              │
              ▼
            ┌──────────────┐
            │ `ordinal "12"` │
            └──────┬───────┘
                   │
                   ▼
              `case "12" in`
              Matches `*12` pattern
                   │
                   ▼
            ┌──────────────┐
            │ `echo "12th"`  │
            └──────┬───────┘
                   │
                   ▼
  Output "12th" is captured
  by command substitution
           │
           ▼
  ┌──────────────────┐
  │ `printf "...", "John", "12th"` │
  └────────┬─────────┘
           │
           ▼
    ● Final Output to stdout

Improving the Script: Adding Input Validation

The provided solution is clean and correct, but a production-ready script should always include error handling. What happens if the user forgets to provide arguments, or provides too many? Our current script would produce a malformed sentence.

We can add a simple check at the beginning of our main function to validate the number of arguments.

Optimized and Hardened Solution


#!/usr/bin/env bash

# This function remains the same.
ordinal() {
    case $1 in
        *11|*12|*13) echo "${1}th" ;;
        *1) echo "${1}st" ;;
        *2) echo "${1}nd" ;;
        *3) echo "${1}rd" ;;
        *) echo "${1}th" ;;
    esac
}

main() {
    # --- NEW: Input Validation ---
    # "$#" holds the count of positional parameters (arguments).
    # We check if the number of arguments is not equal to 2.
    if [ "$#" -ne 2 ]; then
        # Print a usage message to standard error (>&2)
        echo "Usage: $0 <name> <number>" >&2
        # Exit with a non-zero status code to indicate an error.
        exit 1
    fi
    # --- End of Validation ---

    printf "%s, you are the %s customer we serve today. Thank you!" "$1" "$(ordinal "$2")"
}

main "$@"

What We Added:

  • if [ "$#" -ne 2 ]; then ... fi: This is the core of the validation. $# is a special Bash variable that contains the number of arguments passed to the script. The condition -ne means "not equal to". If the argument count is not exactly 2, the code inside the if block is executed.
  • echo "Usage: ..." >&2: We print a helpful usage message. $0 is another special variable that holds the name of the script itself. The >&2 redirects this message to standard error (stderr), which is the correct stream for error messages, separating them from normal program output (stdout).
  • exit 1: This command immediately terminates the script. By convention, an exit code of 0 means success, while any non-zero code indicates an error. This is useful for other scripts or tools that might be calling our script and need to know if it completed successfully.

This small addition makes the script significantly more robust and user-friendly, guiding users on how to use it correctly and preventing unexpected behavior.


Pros and Cons of This Bash Approach

Every technical solution involves trade-offs. While this Bash script is excellent for its intended purpose, it's important to understand its strengths and weaknesses to know when to use it and when to reach for a different tool. This perspective is key to growing as a developer and is a central theme in the comprehensive Bash learning path at kodikra.com.

Pros (Strengths) Cons (Weaknesses)
Highly Portable: Runs on nearly any Unix-like system (Linux, macOS, BSD) without any setup, as Bash is a standard component. Limited Data Structures: Bash lacks the rich, built-in data structures of languages like Python (e.g., dictionaries, complex objects), making more advanced logic cumbersome.
Extremely Lightweight: The script has a near-instant startup time and minimal memory footprint, making it ideal for frequent, small tasks. Verbose Error Handling: While possible, robust error handling and type checking (e.g., ensuring the second argument is actually a number) requires more explicit, manual code.
Readable Pattern Matching: The case statement with glob patterns is arguably more intuitive and readable for this specific string-ending logic than regex or arithmetic solutions. No Built-in Testing Framework: Writing automated tests for Bash scripts requires external tools like Bats, whereas many modern languages have testing frameworks integrated into their standard tooling.
Seamless CLI Integration: Natively handles command-line arguments, pipes, and redirection, making it a perfect "glue" for connecting other command-line tools. Scalability for Complexity: As the logic grows (e.g., adding support for different languages), the script can become difficult to maintain compared to an object-oriented or modular approach in another language.

Frequently Asked Questions (FAQ)

Why use the case statement instead of a series of if-elif-else checks?
For matching a single variable against multiple patterns, case is generally considered more readable and efficient in Bash. It flattens the logic, avoiding deeply nested if statements. The glob-style pattern matching (*1) is also more concise than the string manipulation that would be required in an if condition.
What exactly does "$@" mean and why is it quoted?
$@ represents all the positional parameters (command-line arguments) passed to the script or function. When unquoted ($@), it splits arguments containing spaces. When quoted ("$@"), it expands each argument as a separate, single word. This correctly preserves arguments like "Mary Ann" as one entity instead of two. Always quoting "$@" is a critical best practice.
What is the purpose of #!/usr/bin/env bash? Why not just #!/bin/bash?
#!/bin/bash uses a hardcoded path to the Bash executable. This works on most systems, but fails if Bash is installed elsewhere (e.g., /usr/local/bin/bash). #!/usr/bin/env bash tells the system to look for the bash executable in the directories listed in the user's PATH environment variable, making the script more portable across different system configurations.
How can I make the script handle non-numeric input for the number?
You could add another validation check using a regular expression. For example: if ! [[ "$2" =~ ^[0-9]+$ ]]; then echo "Error: Second argument must be a number." >&2; exit 1; fi. This check ensures the second argument consists of one or more digits before proceeding.
Is printf really that much better than echo?
Yes, for reliability. The behavior of echo can vary between different shells and versions, especially in how it handles options (like -n or -e) and backslash sequences. printf is defined by the POSIX standard and behaves consistently. It also cleanly separates the format string from the data, preventing user input from being accidentally interpreted as code or options, which is a potential security risk.
Can this script's logic handle numbers larger than 999?
Absolutely. The logic is based on pattern matching the *end* of the string (e.g., *11, *1). It doesn't care what precedes the final digits. Therefore, it will work correctly for 101st, 111th, 5412th, and any other integer without modification.
How would this logic differ in a language like Python?
In Python, you would likely use string methods like endswith() or integer arithmetic with the modulo operator (%). For example: if number % 100 in [11, 12, 13]: suffix = 'th'. While the core logic is similar, the syntax and tools are different. Python also has more advanced libraries for internationalization (i18n) if you needed to support ordinals in other languages.

Conclusion: From a Simple Problem to a Powerful Skill

We began with a simple request from a fictional deli owner and ended with a deep understanding of core Bash scripting principles. This journey through the kodikra.com ordinal numbers module has taught us more than just how to append "st" or "th" to a number. It has provided a masterclass in essential shell scripting techniques: function organization, robust argument handling, the power and elegance of the case statement, safe output formatting with printf, and the critical importance of input validation.

The solution is a testament to the philosophy of using the right tool for the job. For creating lightweight, portable, and efficient command-line utilities, Bash remains an indispensable skill for any developer, system administrator, or DevOps engineer. The patterns and practices explored here—from the shebang to the main "$@" call—form the bedrock of high-quality shell scripting.

By mastering these fundamentals, you are now better equipped to automate tasks, build powerful command-line tools, and write scripts that are not just functional, but also reliable, readable, and robust. This is a significant step forward in your journey as a programmer.

Disclaimer: All code snippets and examples are based on the latest stable versions of Bash (v5.x) as of the time of writing. While the core logic is highly portable, behavior in very old versions of Bash (pre-v4.0) may vary slightly.


Published by Kodikra — Your trusted Bash learning resource.