Bash Scripting Series — Level 6

Bash Scripting Series — Level 6

Introduction

Welcome to Level 6 of this Bash scripting tutorial series. In the previous stages of learning Bash, you may have explored variables, commands, conditional statements, loops, and basic script execution. This level builds on those concepts by combining several Bash features into a single interactive program.

The objective of this lab is to create a basic resource management script. Instead of executing a single command, the script allows a user to select a resource and perform an action on it. The available resources are a web server, a database, and a cache. Users can choose to start a resource, stop it, or check its simulated status.

Although the script does not control actual infrastructure, it demonstrates programming patterns used when building system administration utilities, deployment scripts, monitoring tools, and automation workflows.

By the end of this tutorial, you should understand how to organize Bash code into functions, manage collections with arrays, accept and validate user input, select actions using a case statement, and use Bash's arithmetic capabilities to process data.

1. Understanding the Script Structure

The script begins with the following line:

#!/bin/bash

This is known as the shebang. It tells the operating system which interpreter should execute the script when it is launched as an executable file. In this case, the interpreter is Bash, located at /bin/bash.

The distinction between Bash and other shells is important. Bash supports features such as indexed arrays, the [[ ... ]] conditional construct, and the $RANDOM variable. These features are not guaranteed to work identically in a minimal POSIX shell such as sh.

Next, the script defines an array:

RESOURCES=("WebServer" "Database" "Cache")

An array stores multiple values under a single variable name. Instead of creating three separate variables for three different resources, the script groups them into one collection.

Bash arrays use zero-based indexing, meaning the first element is at index 0, the second at index 1, and the third at index 2.

For example:

echo "${RESOURCES[0]}"
echo "${RESOURCES[1]}"
echo "${RESOURCES[2]}"

The output is:

WebServer
Database
Cache

Notice the use of curly braces and double quotes. The syntax ${RESOURCES[0]} identifies a particular array element, while quoting expansions helps preserve the value as a single argument if it contains spaces or special characters.

Arrays are especially useful in scripts that manage multiple services, hosts, files, users, or configuration options.

2. Creating Functions for Reusable Operations

The script defines separate functions for starting, stopping, and checking resources.

Consider the first function:

start_resource() {
    local resource=$1
    echo "Starting $resource..."
    echo "Resource $resource is now running."
}

A function is a named block of commands that can be called whenever its functionality is needed. Functions reduce duplication and make a script easier to maintain.

The function is declared using its name followed by parentheses and a block enclosed in braces.

The most important line is:

local resource=$1

In Bash, positional parameters provide access to arguments passed to a function. $1 represents the first argument, $2 the second, and so forth. Here, the function expects to receive one resource name.

When the script executes:

start_resource "$resource"

the selected resource is passed to the function as its first argument.

The local keyword declares resource as a variable local to that function. This prevents the function's internal variable from unnecessarily changing a variable of the same name in the surrounding scope.

This technique is useful when functions are called repeatedly with different arguments. For example, the same function can handle a web server, database, or cache without needing a separate function for each resource.

The stop_resource function follows the same pattern:

stop_resource() {
    local resource=$1
    echo "Resource $resource is now stopped."
}

It accepts a resource name and prints a message indicating that the resource has stopped.

The important technical distinction is that these functions currently simulate operations using output messages. They do not actually start or stop services. Real service management would require suitable commands, permissions, error handling, and verification of the target service's state.

3. Simulating Resource Status with Random Numbers

The status function introduces a new concept:

status_resource() {
    local resource=$1
    local status=$((RANDOM % 2))

    if [ "$status" -eq 0 ]; then
        status="running"
    else
        status="stopped"
    fi

    echo "The Status of $resource: $status"
}

The variable RANDOM is a Bash-provided pseudo-random number generator. Referencing it produces an integer in the range from 0 to 32767.

The expression:

$((RANDOM % 2))

uses arithmetic expansion. The $(( ... )) syntax evaluates an arithmetic expression and substitutes its numerical result into the surrounding command.

The modulo operator % returns the remainder after division. Since dividing an integer by two produces a remainder of either zero or one, the result is limited to those two values.

For example:

echo $((5 % 2))
echo $((8 % 2))

The first expression returns 1, while the second returns 0.

The status function uses that result in an if statement:

if [ "$status" -eq 0 ]; then
    status="running"
else
    status="stopped"
fi

The -eq operator performs a numerical equality comparison. If the calculated value is zero, the script assigns the string running. Otherwise, it assigns stopped.

This provides a simple way to demonstrate conditional logic without connecting to real infrastructure.

However, there is an important limitation: the status is randomized every time the function is called. If the user previously selected “Start resource,” the next status check does not remember that operation. The script does not maintain persistent resource state.

In a real monitoring script, status would typically be retrieved from a service manager, process check, network probe, monitoring API, or another authoritative source. Random numbers should not be used to determine the actual operational status of infrastructure.

4. Displaying and Selecting Resources with Arrays and Loops

The choose_resource function combines arrays, loops, input handling, and conditional validation.

choose_resource() {
    echo "Available resources:"

    for i in "${!RESOURCES[@]}"; do
        echo "$((i+1))) ${RESOURCES[$i]}"
    done

    read -rp "Enter the number of the resource: " res_choice

    if [[ $res_choice -ge 1 && $res_choice -le ${#RESOURCES[@]} ]]; then
        resource="${RESOURCES[$((res_choice-1))]}"
    else
        echo "Invalid choice"
        exit 1
    fi
}

The first important expression is:

"${!RESOURCES[@]}"

The [@] notation expands to all elements or indices of an array, depending on the surrounding syntax. In this case, the exclamation mark requests the array's indices, not its values.

Because the resource array has three elements, the loop iterates over indices 0, 1, and 2.

The expression:

$((i+1))

adds one to each index. This displays menu options numbered 1, 2, and 3, which is more intuitive for a user than zero-based numbering.

The corresponding resource is displayed through:

${RESOURCES[$i]}

The result is a numbered list:

Available resources:
1) WebServer
2) Database
3) Cache

The next command gathers the user's selection:

read -rp "Enter the number of the resource: " res_choice

The read builtin reads a line of input and stores it in the variable res_choice.

Its options have specific purposes:

  • -r prevents backslashes from being interpreted as escape characters.

  • -p displays a prompt before reading input.

Keeping the prompt and input operation in one command makes the script easier to read.

The function then checks whether the chosen number falls within the permitted range.

[[ $res_choice -ge 1 && $res_choice -le ${#RESOURCES[@]} ]]

The ${#RESOURCES[@]} expression returns the number of elements in the array. In this example, its value is 3.

The -ge operator means greater than or equal to, while -le means less than or equal to. The && operator requires both conditions to be true.

This means the accepted range is from 1 to 3.

If the selection is valid, the script converts the user's one-based menu number into a zero-based array index:

resource="${RESOURCES[$((res_choice-1))]}"

For example, selecting option 1 results in index 0, which selects WebServer. Selecting option 3 results in index 2, which selects Cache.

This conversion is a fundamental pattern when building command-line interfaces around Bash arrays.

One improvement worth remembering is that a numeric range check is not the same as complete input validation. Letters, empty input, or unexpected values can cause comparison errors. A more robust implementation would first verify that the input contains only the expected digits, and then check the range.

5. Building an Interactive Menu with a Case Statement

After defining the functions, the script displays the main menu:

echo "Select an action:"
echo "1) Start resource"
echo "2) Stop resource"
echo "3) Check status"

read -p "Enter your choice: " choice

The variable choice stores the selected action. The script then uses a case statement to decide which operation should run.

case $choice in
    1)
        choose_resource
        start_resource "$resource"
        ;;
    2)
        choose_resource
        stop_resource "$resource"
        ;;
    3)
        choose_resource
        status_resource "$resource"
        ;;
    *)
        echo "Invalid choice"
        ;;
esac

The case statement is an alternative to writing multiple if, elif, and else conditions. It compares a value against a set of patterns and executes the matching block.

Each numbered option is followed by a closing parenthesis. The ;; terminator marks the end of that option's commands and prevents execution from falling through into the next case.

The wildcard pattern * acts as the default branch. It handles inputs that do not match options 1, 2, or 3.

The esac keyword closes the statement; it is effectively case written backward.

Every valid action follows the same sequence: ask the user to select a resource, then call the corresponding function with the selected resource name.

This separation between the action and the target is an important design principle. It makes the code easier to extend without duplicating resource-selection logic.

6. Understanding Variable Scope and Script Termination

One detail deserves additional attention: the resource variable inside choose_resource.

Unlike the other functions, choose_resource does not declare resource using local. Consequently, assigning a value to resource creates or updates a variable in the surrounding shell scope.

This allows the selected value to remain available after choose_resource finishes, so the main script can pass it to start_resource, stop_resource, or status_resource.

Although this works for the lab, implicit reliance on global variables can make larger scripts harder to understand. A more maintainable design might have the selection function return the chosen name through standard output or use another explicit method of passing data.

The script also contains:

exit 1

This command terminates the entire script with exit status 1, conventionally indicating an error. It does not merely exit the choose_resource function.

By comparison, return can end a function while allowing the calling script to continue. Understanding this distinction is important when designing functions that validate input or report failures.

The default branch of the main case statement only prints an error. It does not explicitly set a non-zero exit status, so an invalid action can still leave the script with a successful final command status. A production-quality implementation should handle error reporting and exit codes deliberately.

7. Running the Script in a Linux Environment

Save the lab as Q5.bash. Open a terminal in the directory where the file is located and make it executable:

chmod +x Q5.bash

Run it with:

./Q5.bash

Alternatively, invoke Bash directly:

bash Q5.bash

The second method does not require the executable permission bit because the file is passed as an argument to the Bash interpreter.

An example interaction might look like this:

Select an action:
1) Start resource
2) Stop resource
3) Check status
Enter your choice: 1

Available resources:
1) WebServer
2) Database
3) Cache
Enter the number of the resource: 1

Starting WebServer...
Resource WebServer is now running.

Your output will depend on your selections. Choosing the status option will produce either running or stopped, because the script generates a new pseudo-random result for each status check.

8. Practical Exercises and Further Improvements

Once you understand the existing implementation, try extending it rather than simply copying the code.

Exercise 1: Improve input validation. Reject letters, empty strings, and numbers outside the allowed range without producing shell comparison errors. Consider using Bash's =~ regular-expression operator to check that the input contains only permitted digits.

Exercise 2: Add another resource. Add an ApplicationServer or Firewall element to RESOURCES. Observe how the resource list and valid maximum selection change automatically because the code uses the array length.

Exercise 3: Add another action. Introduce an action that lists all available resources or displays a simulated summary of the infrastructure. Add an appropriate case to the main menu.

Exercise 4: Implement persistent state. Replace the random status calculation with a mechanism that records the current simulated state of each resource. Think about how a start operation should affect a later status check. This requires more than printing a message: the script must store and update state.

Exercise 5: Introduce error handling. Define how the program should respond to invalid selections, failed operations, and missing dependencies. Consider using meaningful exit codes and checking whether external commands succeed.

For real system administration, additional safeguards are essential. A service-management script should verify the target resource, check permissions, handle command failures, and distinguish between the requested action and its confirmed result.

Conclusion

Level 6 brings together several essential Bash scripting concepts in one interactive lab. You have seen how arrays organize related values, functions make operations reusable, loops display collections, arithmetic expansion calculates indices, and conditional statements validate choices. You also explored how read collects input and how case provides a structured way to dispatch actions.

The most important lesson is that a useful script is more than a collection of commands. It requires clear control flow, predictable variable handling, sensible input validation, and an understanding of what the program actually does.

This example is deliberately simulated, but the design patterns are transferable to more advanced automation tasks. With additional validation, explicit state management, and appropriate error handling, the same structure can serve as the foundation for a more capable command-line administration tool.

In the next level, consider moving from simulated operations toward more reliable automation, including loops for repeated interaction, function return codes, and carefully checked system commands.

Comments

Popular posts from this blog

Information disclosure in version control history

Linux AAA

Hashcat LAB