bash-script-framework
SKILL.md
Bash Script Framework
Create standardized bash scripts with menus, colors, and error handling.
Quick Start
# Create script directory structure
/bash-script-framework init
# Create new script with menu
/bash-script-framework new my-script --menu
# Add to existing scripts directory
/bash-script-framework add utility-script
When to Use
USE when:
- Creating CLI tools for repository
- Building menu-driven automation
- Standardizing script organization
- Cross-platform script development
DON'T USE when:
- Python script is more appropriate
- Simple one-liner needed
- Windows-only environment
Prerequisites
- Bash 4.0+
- Unix-like environment (Linux, macOS, WSL)
Overview
Creates organized bash scripts following workspace-hub patterns:
- Color utilities - Consistent terminal output
- Menu systems - Multi-level navigation
- Error handling - Proper exit codes
- Logging - Timestamped output
- Cross-platform - Linux/macOS/WSL support
Directory Structure
scripts/
├── workspace # Main entry point
├── lib/
│ ├── colors.sh # Color definitions
│ ├── logging.sh # Logging utilities
│ ├── menu.sh # Menu system
│ └── utils.sh # General utilities
├── bash/
│ ├── git/ # Git operations
│ └── dev/ # Development tools
├── python/ # Python utilities
└── powershell/ # Windows scripts
Core Templates
1. Color Library (lib/colors.sh)
#!/bin/bash
# lib/colors.sh - Color definitions for terminal output
# Source this file: source lib/colors.sh
# Reset
NC='\033[0m' # No Color / Reset
# Regular Colors
BLACK='\033[0;30m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[0;37m'
# Bold Colors
BOLD_BLACK='\033[1;30m'
BOLD_RED='\033[1;31m'
BOLD_GREEN='\033[1;32m'
BOLD_YELLOW='\033[1;33m'
BOLD_BLUE='\033[1;34m'
BOLD_MAGENTA='\033[1;35m'
BOLD_CYAN='\033[1;36m'
BOLD_WHITE='\033[1;37m'
# Background Colors
BG_BLACK='\033[40m'
BG_RED='\033[41m'
BG_GREEN='\033[42m'
BG_YELLOW='\033[43m'
BG_BLUE='\033[44m'
BG_MAGENTA='\033[45m'
BG_CYAN='\033[46m'
BG_WHITE='\033[47m'
# Formatting
BOLD='\033[1m'
DIM='\033[2m'
UNDERLINE='\033[4m'
BLINK='\033[5m'
REVERSE='\033[7m'
# Status indicators
SUCCESS="${GREEN}✓${NC}"
FAIL="${RED}✗${NC}"
WARN="${YELLOW}⚠${NC}"
INFO="${BLUE}ℹ${NC}"
ARROW="${CYAN}→${NC}"
# Print colored text
print_color() {
local color="$1"
local message="$2"
echo -e "${color}${message}${NC}"
}
# Status messages
print_success() { echo -e "${SUCCESS} ${GREEN}$1${NC}"; }
print_error() { echo -e "${FAIL} ${RED}$1${NC}" >&2; }
print_warning() { echo -e "${WARN} ${YELLOW}$1${NC}"; }
print_info() { echo -e "${INFO} ${BLUE}$1${NC}"; }
# Headers and separators
print_header() {
local title="$1"
local width=${2:-60}
local line=$(printf '═%.0s' $(seq 1 $width))
echo -e "\n${BOLD_CYAN}╔${line}╗${NC}"
printf "${BOLD_CYAN}║${NC} ${BOLD_WHITE}%-$((width-2))s${NC} ${BOLD_CYAN}║${NC}\n" "$title"
echo -e "${BOLD_CYAN}╚${line}╝${NC}\n"
}
print_separator() {
local width=${1:-60}
local char=${2:-─}
printf "${DIM}%${width}s${NC}\n" | tr ' ' "$char"
}
# Check if colors are supported
supports_colors() {
if [[ -t 1 ]] && [[ -n "$TERM" ]] && [[ "$TERM" != "dumb" ]]; then
return 0
fi
return 1
}
# Disable colors if not supported
if ! supports_colors; then
NC='' RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' WHITE=''
BOLD='' DIM='' SUCCESS='[OK]' FAIL='[FAIL]' WARN='[WARN]' INFO='[INFO]'
fi
2. Logging Library (lib/logging.sh)
#!/bin/bash
# lib/logging.sh - Logging utilities
# Source this file after colors.sh
# Log levels
LOG_LEVEL_DEBUG=0
LOG_LEVEL_INFO=1
LOG_LEVEL_WARN=2
LOG_LEVEL_ERROR=3
# Current log level (default: INFO)
CURRENT_LOG_LEVEL=${CURRENT_LOG_LEVEL:-$LOG_LEVEL_INFO}
# Log file (optional)
LOG_FILE="${LOG_FILE:-}"
# Timestamp format
timestamp() {
date "+%Y-%m-%d %H:%M:%S"
}
# Internal log function
_log() {
local level="$1"
local level_name="$2"
local color="$3"
local message="$4"
if [[ $level -ge $CURRENT_LOG_LEVEL ]]; then
local ts=$(timestamp)
local formatted="[${ts}] [${level_name}] ${message}"
# Output to console
echo -e "${color}${formatted}${NC}"
# Output to log file if configured
if [[ -n "$LOG_FILE" ]]; then
echo "${formatted}" >> "$LOG_FILE"
fi
fi
}
# Log functions
log_debug() { _log $LOG_LEVEL_DEBUG "DEBUG" "$DIM" "$1"; }
log_info() { _log $LOG_LEVEL_INFO "INFO " "$BLUE" "$1"; }
log_warn() { _log $LOG_LEVEL_WARN "WARN " "$YELLOW" "$1"; }
log_error() { _log $LOG_LEVEL_ERROR "ERROR" "$RED" "$1"; }
# Log with custom prefix
log_step() {
local step="$1"
local message="$2"
echo -e "${CYAN}[Step ${step}]${NC} ${message}"
}
log_progress() {
local current="$1"
local total="$2"
local message="${3:-Processing}"
local percent=$((current * 100 / total))
printf "\r${CYAN}%s${NC}: [%3d%%] %d/%d" "$message" "$percent" "$current" "$total"
}
log_progress_done() {
echo "" # New line after progress
}
# Set log level
set_log_level() {
case "$1" in
debug|DEBUG) CURRENT_LOG_LEVEL=$LOG_LEVEL_DEBUG ;;
info|INFO) CURRENT_LOG_LEVEL=$LOG_LEVEL_INFO ;;
warn|WARN) CURRENT_LOG_LEVEL=$LOG_LEVEL_WARN ;;
error|ERROR) CURRENT_LOG_LEVEL=$LOG_LEVEL_ERROR ;;
*) log_error "Unknown log level: $1" ;;
esac
}
# Enable file logging
enable_file_logging() {
LOG_FILE="${1:-logs/script.log}"
mkdir -p "$(dirname "$LOG_FILE")"
log_info "Logging to file: $LOG_FILE"
}
3. Menu System (lib/menu.sh)
#!/bin/bash
# lib/menu.sh - Menu system utilities
# Source after colors.sh
# Display menu and get selection
show_menu() {
local title="$1"
shift
local options=("$@")
print_header "$title"
local i=1
for option in "${options[@]}"; do
if [[ "$option" == "---" ]]; then
print_separator 40
else
printf " ${CYAN}%2d)${NC} %s\n" "$i" "$option"
((i++))
fi
done
echo ""
printf " ${CYAN} 0)${NC} ${DIM}Exit / Back${NC}\n"
echo ""
read -p "$(echo -e ${BOLD}Select option: ${NC})" choice
echo "$choice"
}
# Confirm action
confirm() {
local message="${1:-Are you sure?}"
local default="${2:-n}"
if [[ "$default" == "y" ]]; then
read -p "$(echo -e ${YELLOW}$message [Y/n]: ${NC})" response
response=${response:-y}
else
read -p "$(echo -e ${YELLOW}$message [y/N]: ${NC})" response
response=${response:-n}
fi
[[ "$response" =~ ^[Yy]$ ]]
}
# Prompt for input
prompt_input() {
local message="$1"
local default="${2:-}"
local result
if [[ -n "$default" ]]; then
read -p "$(echo -e ${CYAN}$message [$default]: ${NC})" result
result=${result:-$default}
else
read -p "$(echo -e ${CYAN}$message: ${NC})" result
fi
echo "$result"
}
# Select from list
select_from_list() {
local title="$1"
shift
local items=("$@")
echo -e "\n${BOLD}$title${NC}\n"
local i=1
for item in "${items[@]}"; do
printf " ${CYAN}%2d)${NC} %s\n" "$i" "$item"
((i++))
done
echo ""
read -p "$(echo -e ${BOLD}Select [1-${#items[@]}]: ${NC})" choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#items[@]} ]]; then
echo "${items[$((choice-1))]}"
else
echo ""
fi
}
# Multi-select from list
multi_select() {
local title="$1"
shift
local items=("$@")
local selected=()
echo -e "\n${BOLD}$title${NC}"
echo -e "${DIM}(Enter numbers separated by spaces, or 'all')${NC}\n"
local i=1
for item in "${items[@]}"; do
printf " ${CYAN}%2d)${NC} %s\n" "$i" "$item"
((i++))
done
echo ""
read -p "$(echo -e ${BOLD}Select: ${NC})" choices
if [[ "$choices" == "all" ]]; then
selected=("${items[@]}")
else
for choice in $choices; do
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#items[@]} ]]; then
selected+=("${items[$((choice-1))]}")
fi
done
fi
printf '%s\n' "${selected[@]}"
}
# Wait for keypress
wait_for_key() {
local message="${1:-Press any key to continue...}"
echo -e "\n${DIM}$message${NC}"
read -n 1 -s
}
# Clear screen with header
clear_with_header() {
local title="${1:-Menu}"
clear
print_header "$title" 60
}
4. Utilities (lib/utils.sh)
#!/bin/bash
# lib/utils.sh - General utilities
# Check if command exists
command_exists() {
command -v "$1" &> /dev/null
}
# Check required commands
require_commands() {
local missing=()
for cmd in "$@"; do
if ! command_exists "$cmd"; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_error "Missing required commands: ${missing[*]}"
return 1
fi
return 0
}
# Get script directory
get_script_dir() {
echo "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
}
# Get project root
get_project_root() {
local dir="$(get_script_dir)"
while [[ "$dir" != "/" ]]; do
if [[ -f "$dir/pyproject.toml" ]] || [[ -f "$dir/CLAUDE.md" ]]; then
echo "$dir"
return 0
fi
dir="$(dirname "$dir")"
done
echo "$(pwd)"
}
# Run command with error handling
run_cmd() {
local cmd="$*"
log_debug "Running: $cmd"
if eval "$cmd"; then
return 0
else
local exit_code=$?
log_error "Command failed (exit code $exit_code): $cmd"
return $exit_code
fi
}
# Retry command
retry() {
local max_attempts="${1:-3}"
local delay="${2:-5}"
shift 2
local cmd="$*"
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
log_info "Attempt $attempt/$max_attempts: $cmd"
if eval "$cmd"; then
return 0
fi
if [[ $attempt -lt $max_attempts ]]; then
log_warn "Retrying in ${delay}s..."
sleep "$delay"
fi
((attempt++))
done
log_error "All $max_attempts attempts failed"
return 1
}
# Check if running as root
is_root() {
[[ $EUID -eq 0 ]]
}
# Check OS type
get_os() {
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "macos" ;;
CYGWIN*|MINGW*|MSYS*) echo "windows" ;;
*) echo "unknown" ;;
esac
}
# Check if in git repository
is_git_repo() {
git rev-parse --is-inside-work-tree &> /dev/null
}
# Get current git branch
get_git_branch() {
git rev-parse --abbrev-ref HEAD 2>/dev/null
}
# Cleanup handler
cleanup() {
local exit_code=$?
# Add cleanup tasks here
exit $exit_code
}
# Setup signal handlers
setup_cleanup() {
trap cleanup EXIT INT TERM
}
# Parse arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) VERBOSE=true ;;
-q|--quiet) QUIET=true ;;
-h|--help) show_help; exit 0 ;;
--) shift; break ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*) ARGS+=("$1") ;;
esac
shift
done
}
# File operations
backup_file() {
local file="$1"
if [[ -f "$file" ]]; then
local backup="${file}.bak.$(date +%Y%m%d%H%M%S)"
cp "$file" "$backup"
log_info "Backed up: $file -> $backup"
fi
}
# Ensure directory exists
ensure_dir() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir"
log_debug "Created directory: $dir"
fi
}
5. Main Script Template
#!/bin/bash
# scripts/my-tool - Main entry point
# Description: Tool description here
set -e # Exit on error
# Get script directory and load libraries
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/colors.sh"
source "${SCRIPT_DIR}/lib/logging.sh"
source "${SCRIPT_DIR}/lib/menu.sh"
source "${SCRIPT_DIR}/lib/utils.sh"
# Configuration
VERSION="1.0.0"
TOOL_NAME="My Tool"
# Show help
show_help() {
cat << EOF
${BOLD}${TOOL_NAME}${NC} v${VERSION}
${BOLD}Usage:${NC}
$0 [options] [command]
${BOLD}Commands:${NC}
menu Show interactive menu (default)
status Show status
run Run operation
help Show this help
${BOLD}Options:${NC}
-v, --verbose Enable verbose output
-q, --quiet Suppress output
-h, --help Show this help
${BOLD}Examples:${NC}
$0 # Interactive menu
$0 status # Show status
$0 run --verbose # Run with verbose output
EOF
}
# Show version
show_version() {
echo "${TOOL_NAME} v${VERSION}"
}
# Status command
cmd_status() {
print_header "Status"
print_info "Version: ${VERSION}"
print_info "OS: $(get_os)"
print_info "User: $(whoami)"
if is_git_repo; then
print_success "Git repository: $(get_git_branch)"
else
print_warning "Not a git repository"
fi
}
# Run command
cmd_run() {
print_header "Running Operation"
log_info "Starting operation..."
# Add operation logic here
print_success "Operation completed"
}
# Main menu
main_menu() {
while true; do
clear_with_header "$TOOL_NAME"
choice=$(show_menu "Main Menu" \
"View Status" \
"Run Operation" \
"Settings" \
"---" \
"Help"
)
case "$choice" in
1) cmd_status; wait_for_key ;;
2) cmd_run; wait_for_key ;;
3) settings_menu ;;
4) show_help; wait_for_key ;;
0|"") break ;;
*) print_error "Invalid option"; sleep 1 ;;
esac
done
}
# Settings submenu
settings_menu() {
while true; do
choice=$(show_menu "Settings" \
"Set Log Level" \
"Enable File Logging" \
"View Configuration"
)
case "$choice" in
1)
level=$(select_from_list "Log Level" "debug" "info" "warn" "error")
[[ -n "$level" ]] && set_log_level "$level"
;;
2) enable_file_logging "logs/tool.log" ;;
3) print_info "Config: verbose=$VERBOSE" ;;
0|"") break ;;
esac
done
}
# Main entry point
main() {
# Parse global options
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) set_log_level debug ;;
-q|--quiet) set_log_level error ;;
-h|--help) show_help; exit 0 ;;
--version) show_version; exit 0 ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*) break ;;
esac
shift
done
# Handle commands
local cmd="${1:-menu}"
case "$cmd" in
menu) main_menu ;;
status) cmd_status ;;
run) shift; cmd_run "$@" ;;
help) show_help ;;
*) log_error "Unknown command: $cmd"; show_help; exit 1 ;;
esac
}
# Run main
main "$@"
Usage Examples
Example 1: Create New CLI Tool
# Initialize framework
/bash-script-framework init
# Create tool with menu
/bash-script-framework new repo-manager --menu
# Result: scripts/repo-manager with full menu system
Example 2: Add Simple Script
# Add utility script
/bash-script-framework add backup-tool
# Creates scripts/backup-tool with basic template
Best Practices
- Use set -e - Exit on errors
- Source libraries - Don't duplicate code
- Use functions - Modular, testable code
- Handle signals - Cleanup on exit
- Validate inputs - Check before executing
Related Skills
- python-project-template - Python CLI tools
- yaml-workflow-executor - YAML-driven execution
References
Version History
- 1.0.0 (2026-01-14): Initial release - bash script framework with colors, menus, logging, and utilities
Weekly Installs
11
Repository
vamseeachanta/workspace-hubFirst Seen
Jan 24, 2026
Security Audits
Installed on
claude-code10
gemini-cli9
antigravity9
windsurf9
trae9
codex9