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 "$@"