funwithlinux guide

Building Interactive Command Line Tools with Bash

In a world dominated by graphical user interfaces (GUIs) and web apps, the command line remains a powerful, efficient tool for developers, system administrators, and power users. Bash (Bourne Again SHell), the default shell on most Linux and macOS systems, is more than just a tool for running commands—it’s a scripting language capable of building robust, interactive applications. Interactive command line tools (CLIs) built with Bash can automate tasks, prompt users for input, validate choices, and provide real-time feedback, making them indispensable for everything from system administration to DevOps workflows. Unlike compiled languages or heavy frameworks, Bash scripts are lightweight, portable, and require no additional dependencies (beyond a Unix-like environment), making them accessible and easy to distribute. This blog will guide you through creating interactive Bash tools from scratch, covering core concepts like user input, menus, validation, and advanced techniques like progress indicators and error handling. By the end, you’ll have the skills to build tools that feel polished, user-friendly, and professional.

Table of Contents

  1. Understanding Bash for CLI Tools
  2. Core Concepts: Variables and User Input
  3. Creating Interactive Menus
  4. Input Validation and Error Handling
  5. Handling Command-Line Arguments
  6. Progress Indicators and User Feedback
  7. Advanced Techniques
  8. Real-World Example: Interactive Backup Tool
  9. Testing and Debugging
  10. Best Practices
  11. References

1. Understanding Bash for CLI Tools

Bash is the default shell on most Linux distributions and macOS, making it a universal choice for scripting. Its strengths for CLI tools include:

  • Ubiquity: Preinstalled on nearly all Unix-like systems (no setup required).
  • Simplicity: Syntax is readable for beginners, with powerful built-in commands (grep, sed, awk).
  • Interactivity: Native support for user input, menus, and dynamic feedback.
  • Integration: Seamlessly interacts with system utilities (e.g., rsync, git, curl).

While languages like Python or Go offer more features, Bash excels for lightweight, system-focused tools where minimal dependencies and speed matter.

2. Core Concepts: Variables and User Input

At the heart of interactivity is capturing user input. Bash provides the read command for this, along with variables to store data.

Basic User Input with read

The read command reads input from the user and stores it in a variable. Use the -p flag to display a prompt directly:

#!/bin/bash
# interactive_greeting.sh

echo "=== Interactive Greeting ==="
read -p "Enter your name: " username  # Prompt user and store input in $username
echo "Hello, $username! Welcome to the tool."

Output:

=== Interactive Greeting ===
Enter your name: Alice
Hello, Alice! Welcome to the tool.

Silent Input (e.g., Passwords)

Use read -s to hide input (useful for passwords):

read -sp "Enter your password: " password  # -s suppresses output
echo  # Add a newline after input
echo "Password stored (don't worry, we won't show it here!)"

3. Creating Interactive Menus

Menus let users select options without typing commands. Bash’s select loop simplifies menu creation:

Basic Menu with select

#!/bin/bash
# simple_menu.sh

echo "=== File Manager Tool ==="
echo "Please select an option:"

# Define menu options
select choice in "List Files" "Create Directory" "Exit"; do
  case $choice in
    "List Files")
      ls -l
      break  # Exit loop after action
      ;;
    "Create Directory")
      read -p "Enter directory name: " dirname
      mkdir "$dirname" && echo "Directory '$dirname' created!" || echo "Error: Failed to create directory."
      break
      ;;
    "Exit")
      echo "Goodbye!"
      exit 0
      ;;
    *)
      echo "Invalid option. Please enter a number (1-3)."
      ;;
  esac
done

How it works:

  • select choice in ... displays numbered options (e.g., 1) List Files 2) Create Directory 3) Exit).
  • Users input a number, and $choice stores the selected option.
  • The case statement handles logic for each choice.

4. Input Validation and Error Handling

Interactive tools must validate user input to avoid crashes or unintended behavior. Use conditional checks (if, [[ ]]) and regex for validation.

Example: Validate Numeric Input

#!/bin/bash
# age_validator.sh

read -p "Enter your age: " age

# Check if input is a number (using regex)
if [[ $age =~ ^[0-9]+$ ]]; then
  if [ $age -ge 18 ]; then
    echo "You are an adult."
  else
    echo "You are a minor."
  fi
else
  echo "Error: Please enter a valid number."
  exit 1  # Exit with error code 1
fi

Validate File Existence

read -p "Enter a filename to check: " filename
if [ -f "$filename" ]; then
  echo "File '$filename' exists."
else
  echo "Error: File '$filename' not found."
  exit 1
fi

5. Handling Command-Line Arguments

For non-interactive or hybrid tools, parse arguments (e.g., --name, -v) using positional parameters or getopts.

Positional Parameters

Scripts can access arguments via $1, $2, etc. ($0 is the script name):

#!/bin/bash
# greet_arg.sh

# Check if argument is provided
if [ $# -eq 0 ]; then
  echo "Usage: $0 <name>"
  exit 1
fi

echo "Hello, $1!"  # $1 = first argument

Run with: ./greet_arg.sh BobHello, Bob!

Parsing Flags with getopts

For tools with options (e.g., --verbose, --output), use getopts to parse flags:

#!/bin/bash
# advanced_tool.sh

verbose=0
output_file="output.txt"

# Parse options: -v (verbose), -o <file> (output file)
while getopts "vo:" flag; do
  case $flag in
    v) verbose=1 ;;  # Set verbose mode
    o) output_file="$OPTARG" ;;  # $OPTARG = value after -o
    \?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
    :) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
  esac
done

if [ $verbose -eq 1 ]; then
  echo "Verbose mode enabled. Output will be saved to $output_file."
fi

Run with: ./advanced_tool.sh -v -o results.txt

6. Progress Indicators and User Feedback

Long-running tasks need feedback to keep users informed. Use spinners, progress bars, or status messages.

Spinner for Long Tasks

#!/bin/bash
# spinner.sh

spin() {
  local pid=$1
  local delay=0.1
  local spin_chars="|/-\\"

  while [ -d /proc/$pid ]; do  # Check if process is running
    for (( i=0; i<${#spin_chars}; i++ )); do
      echo -ne "\r[${spin_chars:$i:1}] Processing..."
      sleep $delay
    done
  done
  echo -ne "\r[✓] Done!          \n"  # Replace spinner with success message
}

# Simulate a long task (e.g., backup, download)
echo "Starting backup..."
sleep 5 &  # Run sleep in background (replace with your task)
spin $!  # Pass background PID to spinner

How it works:

  • The spin function takes a process ID ($pid) and runs a loop with spinning characters (|/-\).
  • echo -ne "\r" overwrites the current line to animate the spinner.

7. Advanced Techniques

Signal Handling (e.g., Ctrl+C)

Use trap to handle user interrupts (e.g., Ctrl+C) and clean up resources:

#!/bin/bash
# cleanup_on_exit.sh

cleanup() {
  echo -e "\nCleaning up temporary files..."
  rm -f /tmp/tempfile.txt  # Example cleanup
  exit 0
}

# Trap Ctrl+C (SIGINT) and call cleanup
trap cleanup SIGINT

echo "Running... Press Ctrl+C to exit."
sleep 30  # Simulate long task

GUI-Like Menus with dialog

For terminal-based “GUIs,” use dialog (install with sudo apt install dialog on Debian/Ubuntu):

#!/bin/bash
# dialog_menu.sh

# Display a yes/no prompt
if dialog --yesno "Do you want to proceed?" 10 40; then  # 10=height, 40=width
  dialog --msgbox "Great! Let's continue." 10 40
else
  dialog --msgbox "Aborted." 10 40
  exit 0
fi

8. Real-World Example: Interactive Backup Tool

Let’s combine everything into a backup script with:

  • A menu to select directories.
  • Input validation.
  • A progress spinner.
  • Confirmation prompts.
#!/bin/bash
# interactive_backup.sh

set -euo pipefail  # Exit on error, undefined variable, or pipe failure

# Configuration
BACKUP_DIR="/backups"
SPIN_CHARS="|/-\\"

# Spinner function
spin() {
  local pid=$1
  while [ -d /proc/$pid ]; do
    for ((i=0; i<${#SPIN_CHARS}; i++)); do
      echo -ne "\r[${SPIN_CHARS:$i:1}] Backing up..."
      sleep 0.1
    done
  done
  echo -ne "\r[✓] Backup completed!          \n"
}

# Main menu
echo "=== Interactive Backup Tool ==="
select dir in "Documents" "Pictures" "Music" "Custom Path" "Exit"; do
  case $dir in
    "Documents") src="$HOME/Documents" ;;
    "Pictures") src="$HOME/Pictures" ;;
    "Music") src="$HOME/Music" ;;
    "Custom Path")
      read -p "Enter custom directory: " src
      if [ ! -d "$src" ]; then
        echo "Error: Directory '$src' does not exist."
        continue  # Restart menu
      fi
      ;;
    "Exit") echo "Goodbye!"; exit 0 ;;
    *) echo "Invalid option."; continue ;;
  esac

  # Confirm backup
  read -p "Backup '$src' to '$BACKUP_DIR'? (y/N) " confirm
  if [[ $confirm =~ ^[Yy]$ ]]; then
    echo "Starting backup..."
    rsync -av "$src" "$BACKUP_DIR/" &  # Run rsync in background
    spin $!  # Show spinner while rsync runs
    echo "Backup saved to $BACKUP_DIR/$(basename "$src")"
  else
    echo "Backup canceled."
  fi
  break  # Exit menu after backup
done

9. Testing and Debugging

  • Debugging: Use set -x to print commands as they run:
    #!/bin/bash -x  # Enable debugging
    echo "Hello, $USER"
  • Unit Testing: Use shunit2 (a Bash unit testing framework) for automated tests.
  • Common Pitfalls:
    • Forgetting to quote variables (e.g., rm "$file" instead of rm $file to handle spaces).
    • Ignoring exit codes (check $? after commands to ensure success).

10. Best Practices

  1. Shebang: Start scripts with #!/bin/bash (not #!/bin/sh for Bash-specific features).
  2. Comments: Explain complex logic (e.g., # Validate user input with regex).
  3. Error Handling: Use set -e to exit on errors, and trap for cleanup.
  4. Portability: Avoid Bash-specific features (e.g., read -p) if targeting sh shells.
  5. User Feedback: Always inform users of progress or errors.

11. References

With these tools and techniques, you can build powerful, user-friendly CLI tools in Bash. Start small, iterate, and don’t forget to test—your users (and future self) will thank you! 🚀