#!/usr/bin/env bash ############################################################################# # Bash Documentation Generator with Animated GIFs # # Generate sexy, comprehensive README.md files with embedded asciinema GIFs # for any POSIX-compliant executables (those with --help) # # Usage: # doc_bash_generate.sh [OPTIONS] [executable...] # doc_bash_generate.sh [OPTIONS] "*.sh" # # Arguments: # executable One or more executables or glob patterns # # Options: # -o, --output FILE Output README.md path (default: ./README.md) # -t, --title TITLE Documentation title (default: auto-generated) # --no-gif Skip GIF generation (faster, text only) # --gif-only Only generate GIFs, don't update README # -h, --help Show this help message # # Examples: # doc_bash_generate.sh css_*.sh # doc_bash_generate.sh -o docs/README.md *.sh # doc_bash_generate.sh --title "My Awesome Tools" script1.sh script2.sh # # Dependencies: # asciinema For terminal recording # agg For converting asciinema to GIF (cargo install agg) # ############################################################################# set -euo pipefail # ============================================================================ # Color Definitions # ============================================================================ RED="" GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET="" COLORS=0 if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then COLORS=$(tput colors 2>/dev/null || echo 0) if [[ ${COLORS:-0} -ge 8 ]]; then RED=$(tput setaf 1 2>/dev/null || echo "") GREEN=$(tput setaf 2 2>/dev/null || echo "") YELLOW=$(tput setaf 3 2>/dev/null || echo "") BLUE=$(tput setaf 4 2>/dev/null || echo "") MAGENTA=$(tput setaf 5 2>/dev/null || echo "") CYAN=$(tput setaf 6 2>/dev/null || echo "") BOLD=$(tput bold 2>/dev/null || echo "") DIM=$(tput dim 2>/dev/null || echo "") RESET=$(tput sgr0 2>/dev/null || echo "") fi fi # ============================================================================ # Configuration # ============================================================================ OUTPUT_FILE="$PWD/README.md" DOC_TITLE="" SKIP_GIF=false GIF_ONLY=false declare -a EXECUTABLES=() TEMP_DIR="" # ============================================================================ # Helper Functions # ============================================================================ print_usage() { cat << EOF ${BOLD}Bash Documentation Generator with Animated GIFs${RESET} Generate sexy, comprehensive README.md files with embedded asciinema GIFs. ${BOLD}USAGE:${RESET} $(basename "$0") [OPTIONS] [executable...] ${BOLD}ARGUMENTS:${RESET} executable One or more executables or glob patterns ${BOLD}OPTIONS:${RESET} -o, --output FILE Output README.md path (default: ./README.md) -t, --title TITLE Documentation title (default: auto-generated) --no-gif Skip GIF generation (faster, text only) --gif-only Only generate GIFs, don't update README -h, --help Show this help message ${BOLD}EXAMPLES:${RESET} $(basename "$0") css_*.sh $(basename "$0") -o docs/README.md *.sh $(basename "$0") --title "My Awesome Tools" script1.sh script2.sh ${BOLD}DEPENDENCIES:${RESET} asciinema Terminal session recorder agg Asciinema to GIF converter (cargo install agg) ${BOLD}NOTES:${RESET} Demos are automatically generated by running ${DIM}--help${RESET} on each command. GIF recordings are created in a temporary directory and cleaned up after. EOF } error() { echo "${RED}${BOLD}Error:${RESET} $1" >&2 exit 1 } info() { echo "${BLUE}${BOLD}==>${RESET} $1" >&2 } success() { echo "${GREEN}${BOLD}[OK]${RESET} $1" >&2 } warning() { echo "${YELLOW}${BOLD}[WARN]${RESET} $1" >&2 } banner() { echo "" >&2 echo "${BOLD}${MAGENTA}================================================================${RESET}" >&2 echo "${BOLD}${MAGENTA} $1${RESET}" >&2 echo "${BOLD}${MAGENTA}================================================================${RESET}" >&2 echo "" >&2 } cleanup() { if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" fi } trap cleanup EXIT check_dependencies() { local missing=() if ! command -v asciinema >/dev/null 2>&1; then missing+=("asciinema") fi if ! command -v agg >/dev/null 2>&1 && [[ "$SKIP_GIF" == "false" ]]; then warning "agg not found. Install with: cargo install agg" warning "Continuing without GIF generation..." SKIP_GIF=true fi if [[ ${#missing[@]} -gt 0 ]]; then error "Missing required dependencies: ${missing[*]}" fi } # ============================================================================ # Help Parsing Functions # ============================================================================ get_command_help() { local cmd="$1" if [[ ! -x "$cmd" ]]; then warning "Command not executable: $cmd" return 1 fi # Try --help, then -h, then fail local help_output if help_output=$("$cmd" --help 2>&1); then echo "$help_output" return 0 elif help_output=$("$cmd" -h 2>&1); then echo "$help_output" return 0 else warning "Command doesn't support --help: $cmd" return 1 fi } extract_description() { local help_text="$1" # Try to find first non-empty line after header/title echo "$help_text" | grep -v "^#" | grep -v "^-" | grep -v "^=" | \ grep -E "^[A-Z]" | head -n 1 | sed 's/^[[:space:]]*//' } extract_usage() { local help_text="$1" # Find USAGE section echo "$help_text" | sed -n '/USAGE:/,/^$/p' | grep -v "USAGE:" | \ sed 's/^[[:space:]]*//' } extract_examples() { local help_text="$1" # Find EXAMPLES section echo "$help_text" | sed -n '/EXAMPLES:/,/^[A-Z]/p' | grep -v "EXAMPLES:" | \ grep -v "^[A-Z]" | sed 's/^[[:space:]]*//' } # ============================================================================ # Asciinema Recording Functions # ============================================================================ extract_example_commands() { local help_text="$1" local cmd_name="$2" # Extract example commands from EXAMPLES section # Look for lines that start with the command name or common patterns echo "$help_text" | sed -n '/EXAMPLES:/,/^[A-Z]/p' | \ grep -E "^\s*($cmd_name|#|$)" | \ grep -v "^#" | \ grep -v "EXAMPLES:" | \ grep -v "^[A-Z]" | \ sed 's/^[[:space:]]*//' | \ grep -v "^$" | \ head -n 2 } create_demo() { local cmd="$1" local cmd_name=$(basename "$cmd") # Create demo file in temp directory local demo_file="$TEMP_DIR/${cmd_name%.sh}.demo" # Get help text to extract examples local help_text=$("$cmd" --help 2>&1 || true) # Extract example commands from EXAMPLES section local examples=$(echo "$help_text" | sed -n '/EXAMPLES:/,/^[A-Z]/p' | \ grep -E "^\s*$cmd_name" | head -n 2 | sed 's/^[[:space:]]*//') cat > "$demo_file" << 'DEMO_HEADER' #!/usr/bin/env bash # Auto-generated demo echo "" echo "=== Demo: $CMD_NAME ===" echo "" DEMO_HEADER # If we have examples, run them if [[ -n "$examples" ]]; then echo '# Show examples' >> "$demo_file" echo 'echo ""' >> "$demo_file" while IFS= read -r example; do if [[ -n "$example" ]]; then # Show the command being run echo "echo '$ $example'" >> "$demo_file" echo 'echo ""' >> "$demo_file" # Run the command with timeout (15 seconds max) # Replace the script name with full path local full_example="${example/$cmd_name/$cmd}" cat >> "$demo_file" << EOF timeout 15s $full_example 2>&1 || { exit_code=\$? if [ \$exit_code -eq 124 ]; then echo "[Example timed out after 15s - computation too intensive for demo]" else echo "[Command requires additional setup or arguments]" fi } EOF echo 'echo ""' >> "$demo_file" fi done <<< "$examples" else # Fallback to showing help if no examples found echo 'echo "$ $CMD_NAME --help | head -n 20"' >> "$demo_file" echo 'echo ""' >> "$demo_file" echo "$cmd --help 2>&1 | head -n 20" >> "$demo_file" fi echo 'echo ""' >> "$demo_file" # Replace placeholder with actual command name sed -i "s|\$CMD_NAME|$cmd_name|g" "$demo_file" chmod +x "$demo_file" echo "$demo_file" } record_demo() { local cmd="$1" local output_cast="$2" local demo_script="$3" info "Recording demo for $(basename "$cmd")..." # Create a sandbox directory for demo execution local sandbox_dir="$TEMP_DIR/sandbox" mkdir -p "$sandbox_dir" # Create a wrapper script that runs the demo in sandbox local wrapper="$TEMP_DIR/demo_wrapper.sh" cat > "$wrapper" << 'WRAPPER_EOF' #!/usr/bin/env bash set -e # Set terminal size for consistent output export COLUMNS=100 export LINES=30 # Change to sandbox directory for safe execution cd "$3" # Add some style echo -e "\033[1;36m" echo "========================================" echo " Demo: $(basename "$1")" echo "========================================" echo -e "\033[0m" echo "" sleep 1 # Source the demo script source "$2" echo "" sleep 2 WRAPPER_EOF chmod +x "$wrapper" # Record with asciinema (redirect all output to avoid contamination) asciinema rec \ --command "$wrapper '$cmd' '$demo_script' '$sandbox_dir'" \ --overwrite \ --cols 100 \ --rows 30 \ "$output_cast" >/dev/null 2>&1 if [[ $? -eq 0 ]]; then success "Recorded: $output_cast" return 0 else warning "Failed to record demo for $(basename "$cmd")" return 1 fi } convert_to_gif() { local cast_file="$1" local gif_file="$2" info "Converting to GIF: $(basename "$gif_file")..." if command -v agg >/dev/null 2>&1; then agg \ --font-size 14 \ --speed 1.5 \ --theme monokai \ "$cast_file" \ "$gif_file" >/dev/null 2>&1 if [[ $? -eq 0 ]]; then success "Created GIF: $(basename "$gif_file")" return 0 fi fi warning "Failed to convert to GIF: $(basename "$cast_file")" return 1 } # ============================================================================ # README Generation Functions # ============================================================================ generate_readme_header() { local title="$1" local output_dir=$(dirname "$OUTPUT_FILE") cat << EOF # $title > Comprehensive documentation with usage examples and demos This documentation was auto-generated using [\`doc_bash_generate.sh\`](https://github.com/yourusername/yourrepo). ## Table of Contents EOF } generate_toc_entry() { local cmd="$1" local cmd_name=$(basename "$cmd") local slug=$(echo "$cmd_name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g') echo "- [\`$cmd_name\`](#$slug)" } generate_command_section() { local cmd="$1" local gif_path="$2" local help_text="$3" local cmd_name=$(basename "$cmd") cat << EOF --- ## \`$cmd_name\` EOF # Add description local description=$(extract_description "$help_text") if [[ -n "$description" ]]; then echo "$description" echo "" fi # Add GIF if exists if [[ -f "$gif_path" ]]; then local gif_rel_path="docs/img/$(basename "$gif_path")" cat << EOF ### Demo ![Demo of $cmd_name]($gif_rel_path) EOF fi # Add usage section cat << EOF ### Usage \`\`\`bash $cmd_name [OPTIONS] [ARGUMENTS] \`\`\` EOF # Add full help output cat << EOF ### Options
Click to expand full help output \`\`\` $help_text \`\`\`
EOF # Add examples if available local examples=$(extract_examples "$help_text") if [[ -n "$examples" ]]; then cat << EOF ### Examples \`\`\`bash $examples \`\`\` EOF fi } generate_readme_footer() { cat << EOF --- ## Installation All scripts are available in this repository. Make sure they are executable: \`\`\`bash chmod +x *.sh \`\`\` Add them to your PATH for easy access: \`\`\`bash export PATH="\$PATH:\$(pwd)" \`\`\` ## Dependencies Common dependencies for these scripts: - \`bash\` (4.0+) - \`bc\` - For floating-point arithmetic Specific dependencies are listed in each command's help output. ## Contributing Contributions are welcome! Please ensure: - Scripts follow POSIX conventions - Include comprehensive \`--help\` output with usage examples ## License MIT License - See LICENSE file for details. --- *Documentation generated on $(date '+%Y-%m-%d %H:%M:%S %Z') by [\`doc_bash_generate.sh\`](https://github.com/yourusername/yourrepo)* EOF } # ============================================================================ # Main Processing Functions # ============================================================================ process_command() { local cmd="$1" local cmd_name=$(basename "$cmd") local output_dir=$(dirname "$OUTPUT_FILE") local img_dir="$output_dir/docs/img" info "Processing: ${BOLD}$cmd_name${RESET}" # Create image directory mkdir -p "$img_dir" # Get help text local help_text if ! help_text=$(get_command_help "$cmd" 2>&1); then warning "Skipping $cmd_name - no help available" return 1 fi # Setup paths local cast_file="$TEMP_DIR/${cmd_name%.sh}.cast" local gif_file="$img_dir/${cmd_name%.sh}.gif" # Handle GIF generation if [[ "$SKIP_GIF" == "false" ]]; then # Create demo script in temp directory local demo_script=$(create_demo "$cmd") # Record and convert if record_demo "$cmd" "$cast_file" "$demo_script"; then convert_to_gif "$cast_file" "$gif_file" || true fi fi # Return data for README generation (base64 encode help text to preserve newlines) local encoded_help=$(echo "$help_text" | base64 -w 0) echo "$cmd|$gif_file|$encoded_help" } build_readme() { local -a command_data=("$@") info "Building README.md..." local title="${DOC_TITLE:-"Script Documentation"}" local readme_content="" # Generate header readme_content+=$(generate_readme_header "$title") readme_content+=$'\n' # Generate TOC for data in "${command_data[@]}"; do # Split data using bash parameter expansion to avoid IFS issues local cmd="${data%%|*}" local rest="${data#*|}" local gif_path="${rest%%|*}" local encoded_help="${rest#*|}" readme_content+=$(generate_toc_entry "$cmd") readme_content+=$'\n' done readme_content+=$'\n' # Generate command sections for data in "${command_data[@]}"; do # Split data using bash parameter expansion to avoid IFS issues local cmd="${data%%|*}" local rest="${data#*|}" local gif_path="${rest%%|*}" local encoded_help="${rest#*|}" local help_text=$(echo "$encoded_help" | base64 -d 2>/dev/null || echo "") readme_content+=$(generate_command_section "$cmd" "$gif_path" "$help_text") readme_content+=$'\n' done # Generate footer readme_content+=$(generate_readme_footer) # Write to file echo "$readme_content" > "$OUTPUT_FILE" success "README generated: $OUTPUT_FILE" } # ============================================================================ # Argument Parsing # ============================================================================ parse_arguments() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) print_usage exit 0 ;; -o|--output) OUTPUT_FILE="$2" shift 2 ;; -t|--title) DOC_TITLE="$2" shift 2 ;; --no-gif) SKIP_GIF=true shift ;; --gif-only) GIF_ONLY=true shift ;; -*) error "Unknown option: $1" ;; *) # Handle glob patterns if [[ "$1" == *"*"* ]] || [[ "$1" == *"?"* ]]; then # Expand glob for file in $1; do if [[ -f "$file" ]]; then EXECUTABLES+=("$file") fi done else if [[ -f "$1" ]]; then EXECUTABLES+=("$1") else warning "File not found: $1" fi fi shift ;; esac done if [[ ${#EXECUTABLES[@]} -eq 0 ]]; then error "No executables specified. Use --help for usage information." fi } # ============================================================================ # Main Script Logic # ============================================================================ main() { banner "Bash Documentation Generator" parse_arguments "$@" check_dependencies # Create temp directory TEMP_DIR=$(mktemp -d) info "Output: $OUTPUT_FILE" info "Commands: ${#EXECUTABLES[@]}" echo "" # Process all commands local -a command_data=() for cmd in "${EXECUTABLES[@]}"; do local result if result=$(process_command "$cmd"); then command_data+=("$result") fi echo "" done if [[ ${#command_data[@]} -eq 0 ]]; then error "No commands were successfully processed" fi # Build README unless gif-only mode if [[ "$GIF_ONLY" == "false" ]]; then build_readme "${command_data[@]}" fi echo "" banner "Documentation Complete!" echo "${BOLD}Generated Files:${RESET}" if [[ "$GIF_ONLY" == "false" ]]; then echo " ${GREEN}*${RESET} $OUTPUT_FILE" fi if [[ "$SKIP_GIF" == "false" ]]; then local img_dir="$(dirname "$OUTPUT_FILE")/docs/img" if [[ -d "$img_dir" ]]; then local gif_count=$(find "$img_dir" -name "*.gif" 2>/dev/null | wc -l) echo " ${GREEN}*${RESET} $gif_count GIF(s) in $img_dir" fi fi echo "" success "All done! Check out your sexy new documentation!" } main "$@"