task.sh

Download
bash 414 lines 9.9 KB
  1#!/usr/bin/env bash
  2set -euo pipefail
  3
  4# Task Runner - Modern build automation for bash
  5# Discovers and runs tasks defined as task::* functions
  6# Supports dependencies, help generation, and colored output
  7
  8# ============================================================================
  9# Configuration
 10# ============================================================================
 11
 12SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 13TASK_PREFIX="task::"
 14EXECUTED_TASKS=()
 15
 16# Colors
 17RED='\033[0;31m'
 18GREEN='\033[0;32m'
 19YELLOW='\033[1;33m'
 20BLUE='\033[0;34m'
 21MAGENTA='\033[0;35m'
 22CYAN='\033[0;36m'
 23BOLD='\033[1m'
 24NC='\033[0m'
 25
 26# ============================================================================
 27# Utility Functions
 28# ============================================================================
 29
 30log_info() {
 31    echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $*"
 32}
 33
 34log_success() {
 35    echo -e "${GREEN}[$(date '+%H:%M:%S')]${NC}$*"
 36}
 37
 38log_error() {
 39    echo -e "${RED}[$(date '+%H:%M:%S')]${NC}$*" >&2
 40}
 41
 42log_task() {
 43    echo -e "\n${CYAN}${BOLD}==>${NC} ${BOLD}$*${NC}"
 44}
 45
 46# ============================================================================
 47# Task Discovery and Help
 48# ============================================================================
 49
 50# Get all defined tasks
 51list_tasks() {
 52    declare -F | awk '{print $3}' | grep "^${TASK_PREFIX}" | sed "s/^${TASK_PREFIX}//"
 53}
 54
 55# Extract help comment from task function
 56get_task_help() {
 57    local task_name=$1
 58    local func_name="${TASK_PREFIX}${task_name}"
 59
 60    # Look for ## comment before the function
 61    local help_text
 62    help_text=$(awk "/^## /{comment=\$0; sub(/^## /, \"\", comment)}
 63                     /^${func_name}\(\)/{if(comment) print comment; comment=\"\"}" "${BASH_SOURCE[0]}")
 64
 65    echo "${help_text:-No description available}"
 66}
 67
 68# Show usage information
 69show_usage() {
 70    cat << EOF
 71${BOLD}Task Runner${NC} - Build automation for bash projects
 72
 73${BOLD}Usage:${NC}
 74  ./task.sh [options] <task> [<task>...]
 75
 76${BOLD}Options:${NC}
 77  -h, --help     Show this help message
 78  -l, --list     List all available tasks
 79
 80${BOLD}Available Tasks:${NC}
 81EOF
 82
 83    local tasks
 84    tasks=$(list_tasks)
 85
 86    if [[ -z "$tasks" ]]; then
 87        echo "  (no tasks defined)"
 88        return
 89    fi
 90
 91    while IFS= read -r task; do
 92        local help
 93        help=$(get_task_help "$task")
 94        printf "  ${GREEN}%-15s${NC} %s\n" "$task" "$help"
 95    done <<< "$tasks"
 96
 97    echo
 98    echo "${BOLD}Examples:${NC}"
 99    echo "  ./task.sh build        # Run the build task"
100    echo "  ./task.sh clean build  # Run multiple tasks"
101    echo "  ./task.sh deploy       # Run task with dependencies"
102}
103
104# ============================================================================
105# Dependency Management
106# ============================================================================
107
108# Declare task dependencies
109depends_on() {
110    for dep in "$@"; do
111        if ! task_exists "$dep"; then
112            log_error "Dependency not found: $dep"
113            exit 1
114        fi
115
116        if ! has_executed "$dep"; then
117            log_info "Running dependency: $dep"
118            run_task "$dep"
119        fi
120    done
121}
122
123# Check if task exists
124task_exists() {
125    local task_name=$1
126    declare -f "${TASK_PREFIX}${task_name}" > /dev/null
127}
128
129# Check if task has been executed
130has_executed() {
131    local task_name=$1
132    for executed in "${EXECUTED_TASKS[@]}"; do
133        if [[ "$executed" == "$task_name" ]]; then
134            return 0
135        fi
136    done
137    return 1
138}
139
140# Mark task as executed
141mark_executed() {
142    local task_name=$1
143    EXECUTED_TASKS+=("$task_name")
144}
145
146# ============================================================================
147# Task Execution
148# ============================================================================
149
150# Run a single task
151run_task() {
152    local task_name=$1
153    local func_name="${TASK_PREFIX}${task_name}"
154
155    if ! task_exists "$task_name"; then
156        log_error "Task not found: $task_name"
157        log_info "Run './task.sh --list' to see available tasks"
158        exit 1
159    fi
160
161    if has_executed "$task_name"; then
162        log_info "Task already executed: $task_name (skipping)"
163        return 0
164    fi
165
166    log_task "Running task: $task_name"
167
168    local start_time
169    start_time=$(date +%s)
170
171    # Execute the task
172    if "$func_name"; then
173        local end_time
174        end_time=$(date +%s)
175        local duration=$((end_time - start_time))
176
177        mark_executed "$task_name"
178        log_success "Task completed: $task_name (${duration}s)"
179    else
180        log_error "Task failed: $task_name"
181        exit 1
182    fi
183}
184
185# ============================================================================
186# Task Definitions
187# ============================================================================
188
189## Clean build artifacts and temporary files
190task::clean() {
191    log_info "Removing build artifacts..."
192
193    # Simulate cleaning
194    rm -rf "$SCRIPT_DIR/dist" "$SCRIPT_DIR/build" "$SCRIPT_DIR"/*.log || true
195    mkdir -p "$SCRIPT_DIR/dist" "$SCRIPT_DIR/build"
196
197    log_info "Clean complete"
198}
199
200## Install project dependencies
201task::deps() {
202    log_info "Installing dependencies..."
203
204    # Simulate dependency installation
205    local deps=("shellcheck" "bats" "jq")
206
207    for dep in "${deps[@]}"; do
208        if command -v "$dep" &> /dev/null; then
209            log_info "✓ $dep already installed"
210        else
211            log_info "✗ $dep not found (would install)"
212        fi
213    done
214
215    log_info "Dependencies checked"
216}
217
218## Run linting checks (ShellCheck)
219task::lint() {
220    depends_on deps
221
222    log_info "Running ShellCheck..."
223
224    if command -v shellcheck &> /dev/null; then
225        if shellcheck "$0"; then
226            log_info "Lint passed"
227        else
228            log_error "Lint failed"
229            return 1
230        fi
231    else
232        log_info "ShellCheck not installed, skipping"
233    fi
234}
235
236## Run unit tests
237task::test() {
238    depends_on deps lint
239
240    log_info "Running tests..."
241
242    # Simulate test execution
243    local test_files=("utils" "config" "main")
244    local passed=0
245    local total=${#test_files[@]}
246
247    for test in "${test_files[@]}"; do
248        log_info "Testing $test..."
249        sleep 0.2
250
251        # Simulate random test result
252        if [[ $((RANDOM % 10)) -gt 1 ]]; then
253            ((passed++))
254        fi
255    done
256
257    log_info "Tests: $passed/$total passed"
258
259    if [[ $passed -eq $total ]]; then
260        return 0
261    else
262        log_error "Some tests failed"
263        return 1
264    fi
265}
266
267## Build the project
268task::build() {
269    depends_on clean test
270
271    log_info "Building project..."
272
273    # Simulate build steps
274    log_info "Compiling sources..."
275    sleep 0.3
276    echo "#!/bin/bash" > "$SCRIPT_DIR/build/app"
277    echo "echo 'Hello from built app'" >> "$SCRIPT_DIR/build/app"
278    chmod +x "$SCRIPT_DIR/build/app"
279
280    log_info "Generating documentation..."
281    sleep 0.2
282    echo "# Project Documentation" > "$SCRIPT_DIR/build/README.md"
283
284    log_info "Creating archives..."
285    sleep 0.2
286    tar -czf "$SCRIPT_DIR/build/app.tar.gz" -C "$SCRIPT_DIR/build" app README.md
287
288    log_info "Build complete"
289}
290
291## Create distribution package
292task::package() {
293    depends_on build
294
295    log_info "Creating distribution package..."
296
297    # Create package structure
298    local pkg_dir="$SCRIPT_DIR/dist/myapp-1.0.0"
299    mkdir -p "$pkg_dir"/{bin,lib,doc}
300
301    # Copy files
302    cp "$SCRIPT_DIR/build/app" "$pkg_dir/bin/"
303    cp "$SCRIPT_DIR/build/README.md" "$pkg_dir/doc/"
304
305    # Create installer script
306    cat > "$pkg_dir/install.sh" << 'EOF'
307#!/bin/bash
308echo "Installing myapp..."
309mkdir -p /usr/local/bin
310cp bin/app /usr/local/bin/myapp
311echo "Installation complete"
312EOF
313    chmod +x "$pkg_dir/install.sh"
314
315    # Create tarball
316    tar -czf "$SCRIPT_DIR/dist/myapp-1.0.0.tar.gz" -C "$SCRIPT_DIR/dist" myapp-1.0.0
317
318    log_info "Package created: dist/myapp-1.0.0.tar.gz"
319}
320
321## Deploy to production
322task::deploy() {
323    depends_on package
324
325    log_info "Deploying to production..."
326
327    # Simulate deployment steps
328    log_info "Uploading package..."
329    sleep 0.5
330
331    log_info "Running remote install..."
332    sleep 0.3
333
334    log_info "Verifying deployment..."
335    sleep 0.2
336
337    log_info "Deployment complete"
338    log_success "Application deployed successfully!"
339}
340
341## Run development server (no dependencies)
342task::dev() {
343    log_info "Starting development server..."
344    log_info "Server running at http://localhost:8000"
345    log_info "Press Ctrl+C to stop"
346
347    # Simulate server (would normally run indefinitely)
348    sleep 2
349    log_info "Development server stopped"
350}
351
352## Show project status and info
353task::status() {
354    log_info "Project Status"
355    echo
356    echo "  Directory:  $SCRIPT_DIR"
357    echo "  Git branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'not a git repo')"
358    echo "  Files:      $(find "$SCRIPT_DIR" -type f | wc -l | tr -d ' ')"
359    echo
360
361    if [[ -d "$SCRIPT_DIR/dist" ]]; then
362        echo "  Build artifacts:"
363        ls -lh "$SCRIPT_DIR/dist" | tail -n +2 | awk '{print "    " $9 " (" $5 ")"}'
364    fi
365}
366
367# ============================================================================
368# Main Entry Point
369# ============================================================================
370
371main() {
372    # Handle options
373    case "${1:-}" in
374        -h|--help)
375            show_usage
376            exit 0
377            ;;
378        -l|--list)
379            echo "${BOLD}Available tasks:${NC}"
380            list_tasks | while read -r task; do
381                echo "  - $task"
382            done
383            exit 0
384            ;;
385        "")
386            log_error "No task specified"
387            echo
388            show_usage
389            exit 1
390            ;;
391    esac
392
393    # Run all specified tasks
394    local overall_success=true
395
396    for task_name in "$@"; do
397        if ! run_task "$task_name"; then
398            overall_success=false
399            break
400        fi
401    done
402
403    echo
404    if [[ "$overall_success" == true ]]; then
405        log_success "All tasks completed successfully"
406        exit 0
407    else
408        log_error "Task execution failed"
409        exit 1
410    fi
411}
412
413main "$@"