Created
July 23, 2025 14:37
-
-
Save karlb/7b9b9c51bbcf253b27dd27e3405e26d1 to your computer and use it in GitHub Desktop.
Run shellcheck on just recipes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # justfile-shellcheck.sh - Extract and lint bash code from justfile recipes | |
| # Set DEFAULT_OPTIONS to your liking | |
| set -euo pipefail | |
| JUSTFILE="${1:-justfile}" | |
| TEMP_DIR=$(mktemp -d) | |
| SHELL_TYPE="${2:-bash}" # Default to bash, can be overridden | |
| DEFAULT_OPTIONS="--exclude=SC2086,SC2046" | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| cleanup() { | |
| rm -rf "$TEMP_DIR" | |
| } | |
| trap cleanup EXIT | |
| usage() { | |
| echo "Usage: $0 [justfile] [shell_type]" | |
| echo " justfile - Path to justfile (default: ./justfile)" | |
| echo " shell_type - Shell type for shebang (default: bash)" | |
| echo "" | |
| echo "Examples:" | |
| echo " $0" | |
| echo " $0 my.justfile" | |
| echo " $0 justfile sh" | |
| exit 1 | |
| } | |
| log() { | |
| echo -e "${BLUE}[INFO]${NC} $1" | |
| } | |
| warn() { | |
| echo -e "${YELLOW}[WARN]${NC} $1" | |
| } | |
| error() { | |
| echo -e "${RED}[ERROR]${NC} $1" | |
| } | |
| success() { | |
| echo -e "${GREEN}[SUCCESS]${NC} $1" | |
| } | |
| # Check if justfile exists | |
| if [[ ! -f "$JUSTFILE" ]]; then | |
| error "Justfile not found: $JUSTFILE" | |
| usage | |
| fi | |
| # Check if shellcheck is installed | |
| if ! command -v shellcheck &> /dev/null; then | |
| error "shellcheck is not installed. Please install it first." | |
| echo " - Ubuntu/Debian: apt install shellcheck" | |
| echo " - macOS: brew install shellcheck" | |
| echo " - Fedora: dnf install ShellCheck" | |
| exit 1 | |
| fi | |
| log "Parsing justfile: $JUSTFILE" | |
| log "Using shell type: $SHELL_TYPE" | |
| log "Temporary directory: $TEMP_DIR" | |
| # Parse justfile and extract recipes | |
| extract_recipes() { | |
| local justfile="$1" | |
| local current_recipe="" | |
| local in_recipe=false | |
| local recipe_content="" | |
| local recipes_found=0 | |
| local recipe_shebang="" | |
| while IFS= read -r line || [[ -n "$line" ]]; do | |
| # Skip settings and top-level comments when not in recipe | |
| if [[ "$in_recipe" == false ]]; then | |
| if [[ "$line" =~ ^[[:space:]]*set[[:space:]] ]] || | |
| [[ "$line" =~ ^[[:space:]]*# ]] || | |
| [[ "$line" =~ ^[[:space:]]*$ ]]; then | |
| continue | |
| fi | |
| fi | |
| # Check if this is a recipe definition | |
| # Pattern: recipe_name [parameters...]: | |
| if [[ "$line" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*([[:space:]]+[^:]*)?:[[:space:]]*$ ]]; then | |
| # Save previous recipe if we were in one | |
| if [[ "$in_recipe" == true && -n "$current_recipe" ]]; then | |
| save_recipe "$current_recipe" "$recipe_content" "$recipe_shebang" | |
| ((recipes_found++)) | |
| fi | |
| # Start new recipe | |
| current_recipe=$(echo "$line" | sed 's/\([^[:space:]]*\).*/\1/') | |
| recipe_content="" | |
| recipe_shebang="" | |
| in_recipe=true | |
| log "Found recipe: $current_recipe" | |
| continue | |
| fi | |
| # If we're in a recipe, collect the content | |
| if [[ "$in_recipe" == true ]]; then | |
| # Check if this line starts a new recipe (not indented, contains colon) | |
| if [[ ! "$line" =~ ^[[:space:]] ]] && [[ "$line" =~ : ]] && [[ ! "$line" =~ ^[[:space:]]*# ]]; then | |
| # This is a new recipe, save current and start over | |
| if [[ -n "$current_recipe" ]]; then | |
| save_recipe "$current_recipe" "$recipe_content" "$recipe_shebang" | |
| ((recipes_found++)) | |
| fi | |
| # Start the new recipe | |
| current_recipe=$(echo "$line" | sed 's/\([^[:space:]]*\).*/\1/') | |
| recipe_content="" | |
| recipe_shebang="" | |
| log "Found recipe: $current_recipe" | |
| continue | |
| fi | |
| # Handle indented content (recipe body) | |
| if [[ "$line" =~ ^[[:space:]]+.* ]] || [[ "$line" =~ ^[[:space:]]*$ ]]; then | |
| # Check if this is a shebang line within the recipe | |
| if [[ "$line" =~ ^[[:space:]]+#!/ ]]; then | |
| # Extract the shebang and store it separately | |
| recipe_shebang=$(echo "$line" | sed 's/^[[:space:]]*//') | |
| else | |
| # Regular recipe content - remove one level of indentation | |
| cleaned_line=$(echo "$line" | sed 's/^[[:space:]]\{1,4\}//') | |
| recipe_content+="$cleaned_line"$'\n' | |
| fi | |
| else | |
| # Non-indented, non-recipe line - could be end of recipe | |
| if [[ -n "$current_recipe" ]]; then | |
| save_recipe "$current_recipe" "$recipe_content" "$recipe_shebang" | |
| ((recipes_found++)) | |
| in_recipe=false | |
| current_recipe="" | |
| recipe_content="" | |
| recipe_shebang="" | |
| fi | |
| fi | |
| fi | |
| done < "$justfile" | |
| # Handle the last recipe if file doesn't end with a non-indented line | |
| if [[ "$in_recipe" == true && -n "$current_recipe" ]]; then | |
| save_recipe "$current_recipe" "$recipe_content" "$recipe_shebang" | |
| ((recipes_found++)) | |
| fi | |
| if [[ $recipes_found -eq 0 ]]; then | |
| warn "No recipes found in $justfile" | |
| return 1 | |
| fi | |
| log "Extracted $recipes_found recipes" | |
| } | |
| save_recipe() { | |
| local recipe_name="$1" | |
| local content="$2" | |
| local recipe_shebang="$3" | |
| # Skip recipes with no shell content (e.g., just @just --list) | |
| local shell_content=$(echo "$content" | grep -v '^[[:space:]]*@' | tr -d '[:space:]') | |
| if [[ -z "$shell_content" ]]; then | |
| log "Skipping recipe '$recipe_name' (no shell code to lint)" | |
| return | |
| fi | |
| local script_file="$TEMP_DIR/${recipe_name}.sh" | |
| # Use recipe's shebang if present, otherwise use default | |
| if [[ -n "$recipe_shebang" ]]; then | |
| echo "$recipe_shebang" > "$script_file" | |
| else | |
| echo "#!/bin/$SHELL_TYPE" > "$script_file" | |
| fi | |
| echo "# Extracted from justfile recipe: $recipe_name" >> "$script_file" | |
| echo "" >> "$script_file" | |
| # Filter out just-specific syntax while preserving shell code | |
| echo "$content" | while IFS= read -r line; do | |
| # Skip lines that start with @ (just commands) | |
| if [[ "$line" =~ ^[[:space:]]*@ ]]; then | |
| continue | |
| fi | |
| # Replace just template variables {{var}} with placeholder | |
| # This prevents shellcheck from complaining about invalid syntax | |
| processed_line=$(echo "$line" | sed 's/{{[^}]*}}/\$JUST_VAR/g') | |
| echo "$processed_line" >> "$script_file" | |
| done | |
| chmod +x "$script_file" | |
| } | |
| run_shellcheck() { | |
| local total_files=0 | |
| local passed_files=0 | |
| local failed_files=0 | |
| log "Running ShellCheck on extracted recipes..." | |
| echo "" | |
| for script_file in "$TEMP_DIR"/*.sh; do | |
| if [[ ! -f "$script_file" ]]; then | |
| continue | |
| fi | |
| ((total_files++)) | |
| local recipe_name=$(basename "$script_file" .sh) | |
| echo -e "${BLUE}Checking recipe: $recipe_name${NC}" | |
| echo "----------------------------------------" | |
| # Add shellcheck options to ignore some just-specific issues | |
| if shellcheck -e SC2154 $DEFAULT_OPTIONS "$script_file"; then | |
| success "✓ $recipe_name passed ShellCheck" | |
| ((passed_files++)) | |
| else | |
| error "✗ $recipe_name failed ShellCheck" | |
| ((failed_files++)) | |
| fi | |
| echo "" | |
| done | |
| if [[ $total_files -eq 0 ]]; then | |
| warn "No shell scripts found to check" | |
| return 0 | |
| fi | |
| # Summary | |
| echo "========================================" | |
| log "ShellCheck Results Summary:" | |
| echo " Total recipes: $total_files" | |
| echo " Passed: $passed_files" | |
| echo " Failed: $failed_files" | |
| if [[ $failed_files -gt 0 ]]; then | |
| echo "" | |
| warn "Some recipes failed ShellCheck. Review the output above for details." | |
| return 1 | |
| else | |
| echo "" | |
| success "All recipes passed ShellCheck!" | |
| return 0 | |
| fi | |
| } | |
| # Show help if requested | |
| if [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]]; then | |
| usage | |
| fi | |
| # Main execution | |
| main() { | |
| log "Starting justfile ShellCheck analysis..." | |
| if extract_recipes "$JUSTFILE"; then | |
| run_shellcheck | |
| else | |
| error "Failed to extract recipes from justfile" | |
| exit 1 | |
| fi | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment