Mastering Advanced Control Structures
Continuing with our take on advanced shell scripting using bash – we are diving in deeper. You can find the first tutorial here at: Advanced Shell Scripting – part 1
Control structures form the backbone of any programming language, and Bash is no exception. While you may be familiar with basic if
statements and for
loops, advanced Bash scripting involves leveraging more sophisticated control structures to handle complex logic and flow control. Let’s explore some of these advanced techniques that will elevate your scripting skills.
Case Statements for Elegant Option Handling
The case
statement in Bash provides a clean and efficient way to handle multiple conditions, especially when dealing with command-line options or menu-driven scripts. Here’s an example of how you can use a case
statement:
read -p "Enter your favorite color (red/blue/green): " color
case $color in
red)
echo "Red like a rose!"
;;
blue)
echo "Blue like the sky!"
;;
green)
echo "Green like grass!"
;;
*)
echo "That's not a color I know!"
;;
esac
This script prompts the user for a color and responds differently based on the input. The
*)
pattern acts as a catch-all for any input that doesn’t match the specified patterns.
Select Loops for Interactive Menus
The select
loop is a powerful construct for creating interactive menus in Bash. It automatically generates a numbered menu from a list of items and prompts the user to make a selection. Here’s an example:
PS3="Select a fruit: "
select fruit in "Apple" "Banana" "Cherry" "Quit"
do
case $fruit in
"Apple")
echo "An apple a day keeps the doctor away!"
;;
"Banana")
echo "Bananas are rich in potassium!"
;;
"Cherry")
echo "Cherries are delicious in pies!"
;;
"Quit")
echo "Goodbye!"
break
;;
*)
echo "Invalid selection. Please try again."
;;
esac
done
This script creates a menu of fruits and responds to the user’s selection. The
PS3
variable sets the prompt for the menu.
Until Loops for Condition-Based Iteration
While while
loops are commonly used for condition-based iteration, until
loops offer an alternative that can sometimes lead to more intuitive code. An until
loop continues until its condition becomes true. Here’s an example:
counter=0
until [ $counter -ge 5 ]
do
echo "Counter: $counter"
((counter++))
done
This script will print the counter value until it reaches 5.
Nested Loops and Conditional Statements
Advanced scripts often require combining multiple control structures. Nesting loops and conditional statements allows you to create complex logic flows. Here’s an example that combines a for
loop with nested if
statements:
for file in *.txt
do
if [ -f "$file" ]; then
if [ -s "$file" ]; then
echo "$file is a non-empty text file"
else
echo "$file is an empty text file"
fi
else
echo "$file is not a regular file"
fi
done
This script iterates over all
.txt
files in the current directory, checks if each is a regular file, and then determines if it’s empty or not.
Conditional Execution with && and ||
The &&
(AND) and ||
(OR) operators allow you to chain commands based on their success or failure. This can lead to more concise scripts:
mkdir new_directory && cd new_directory || echo "Failed to create and enter new directory"
This command attempts to create a new directory and change into it. If either operation fails, it prints an error message.
By mastering these advanced control structures, you’ll be able to write more sophisticated and efficient Bash scripts. These techniques allow you to handle complex decision-making processes and create more interactive and responsive scripts. Practice incorporating these structures into your scripts to take full advantage of Bash’s powerful scripting capabilities.
Leveraging Functions for Modular Scripting
Functions are a cornerstone of modular programming, allowing you to encapsulate reusable code and improve the overall structure and readability of your scripts. In advanced Bash scripting, mastering functions can significantly enhance your ability to create complex, maintainable scripts. Let’s explore the various aspects of working with functions in Bash.
Defining and Calling Functions
In Bash, you can define a function using either of these syntaxes:
function greet() {
echo "Hello, $1!"
}
# Or
greet() {
echo "Hello, $1!"
}
To call a function, simply use its name followed by any arguments:
greet "World"
Passing Arguments to Functions
Bash functions can accept arguments, which are accessed within the function using positional parameters ($1
, $2
, etc.). Here’s an example:
calculate_sum() {
local result=$(($1 + $2))
echo "The sum of $1 and $2 is $result"
}
calculate_sum 5 3
Returning Values from Functions
Bash functions don’t return values in the traditional sense. Instead, you can use the echo
command to output a value, which can then be captured when calling the function:
get_square() {
local num=$1
echo $((num * num))
}
result=$(get_square 5)
echo "The square of 5 is $result"
Alternatively, you can use the return
keyword to set the function’s exit status, which can be accessed using $?
:
is_even() {
if (( $1 % 2 == 0 )); then
return 0 # True (even)
else
return 1 # False (odd)
fi
}
is_even 4
if [ $? -eq 0 ]; then
echo "4 is even"
else
echo "4 is odd"
fi
Local Variables in Functions
To prevent variable name conflicts and maintain proper scoping, use the local
keyword to declare variables that are only accessible within the function:
process_data() {
local data=$1
local result=$(echo $data | tr '[:lower:]' '[:upper:]')
echo $result
}
data="hello"
processed=$(process_data $data)
echo "Original: $data, Processed: $processed"
Recursive Functions
Bash supports recursive functions, which can be useful for tasks like directory traversal or mathematical calculations:
factorial() {
if [ $1 -le 1 ]; then
echo 1
else
local prev=$(factorial $(($1 - 1)))
echo $(($1 * prev))
fi
}
result=$(factorial 5)
echo "Factorial of 5 is $result"
Function Libraries
For larger projects, you can create function libraries to organize and reuse your code. Create a separate file with your functions:
functions.sh
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
validate_number() {
if [[ $1 =~ ^[0-9]+$ ]]; then
return 0
else
return 1
fi
}
Then source this file in your main script:
!/bin/bash
source functions.sh
log_message "Script started"
if validate_number "123"; then
log_message "Valid number"
else
log_message "Invalid number"
fi
By leveraging functions effectively, you can create more organized, reusable, and maintainable Bash scripts. Functions allow you to break down complex tasks into smaller, manageable pieces, making your code easier to understand and debug. As you continue to develop your Bash scripting skills, make it a habit to identify repetitive code blocks and convert them into functions for improved modularity and efficiency.
Summarizing this module – for example if you will want to create a simple script that does purge all accounts that are inactive for 90 days – then you can easily do this via:
#!/bin/bash
INACTIVE_DAYS=90
CUTOFF_DATE=$(date -d "$INACTIVE_DAYS days ago" +%Y-%m-%d)
for USER in $(awk -F: '{ if ($3 >= 1000) print $1 }' /etc/passwd); do
LAST_LOGIN=$(lastlog -u $USER | awk 'NR==2 {print $4, $5, $6}')
if [[ ! -z "$LAST_LOGIN" ]]; then
LOGIN_DATE=$(date -d "$LAST_LOGIN" +%Y-%m-%d)
if [[ "$LOGIN_DATE" < "$CUTOFF_DATE" ]]; then
usermod -L $USER
echo "$USER has been disabled due to inactivity."
fi
fi
done
So in coming articles we will be discussing more on how to work with texts and how to make edits on the go – so stay tuned!
[…] https://pixelhowl.com/advanced-shell-scripting-part-2-automation-server-management/ […]
[…] Part 2: https://pixelhowl.com/advanced-shell-scripting-part-2-automation-server-management/ […]
[…] Chapter 2: https://pixelhowl.com/advanced-shell-scripting-part-2-automation-server-management. […]