Nimbin[12]?Useful / find_js_functions.sh

find_js_functions.sh Useful

Search directory for JS functions and their execution.
Useful for finding unused functions.
Identify normal, arrow, and async functions in the search.
Execute with -h to get help and additional instructions

wget https://git.christianimmanuel.de/scripts/raw/Useful/find_js_functions.sh && chmod 740 find_js_functions.sh
find_js_functions.sh 18.8 KB · 573 lines raw
#!/bin/bash

##########################################
### Search directory for JS functions and their execution.
### Useful for finding unused functions.
### Identify normal, arrow, and async functions in the search.
### Execute with -h to get help and additional instructions


cols=$(tput cols)
header_line=$(printf '─%.0s' {1..38})

find_functions_dir="/tmp/findFunctions"
found_functions_dir="/tmp/foundFunctions"

final_file="$found_functions_dir/result.txt"
unused_file="$found_functions_dir/unused.txt"
questionable_file="$found_functions_dir/questionable.txt"
main_director=""

declare -A pid_files
declare -a file_array=()
declare -a ignore_file_array=()


use_parallel=false
sorted_paths=""
extensions="php,html,js"
global_functions=""
TOTAL=0

print_help() {
    echo "------------------------------------------------------"
    echo "find_js_functions.sh - Find and Analyze JavaScript Functions"
    echo "------------------------------------------------------"
    echo
    echo "Description:"
    echo "The find_js_functions.sh script is searching and analyzing JavaScript functions within files."
    echo
    echo "Usage:"
    echo "To use the script, provide the desired options and file paths using the command line. Here is the general format:"
    echo
    echo "  find_js_functions.sh [options] [directory]"
    echo
    echo "Options:"
    echo "  -a               Search for all files in the specified directories."
    echo "  -e <ext>         Specify file extensions to search for (comma-separated)."
    echo "                   Default: $extensions."
    echo "  -f <files>       Specify files to search for."
    echo "  -h               Display the help information."
    echo "  -x <names>       Specify function names to search for (comma-separated)."
    echo "  -u <file>        Specify the path to the unused functions output file."
    echo "                   Default: $unused_file."
    echo "  -q <file>        Specify the path to the questionable functions output file."
    echo "                   Default: $questionable_file."
    echo "  -o <file>        Specify the path to the output file."
    echo "                   Default: $final_file."
    echo "  -i <files/dirs>  Specify files or directories to ignore (comma-separated)."
    echo
    echo "Example:"
    echo "  find_js_functions.sh -e js,xhtml -x func1,func2,func3 -u unused.txt -q questionable.txt -i ignore.js,path/ignore /path/to/directory"
    echo
    echo "Note: If the 'directory' parameter is not provided, the script will search in the current directory."
}

# Function to clean exportet vars
cleanup() {
  unset find_functions_dir found_functions_dir final_file unused_file questionable_file main_director
  unset -f clear_php_files search_execution
  exit 1
}

# Trap the SIGINT signal (Ctrl+C) and call the cleanup function
trap cleanup SIGINT

clear_php_files() {
    # Set the input file name
    input_file="$1"
    if [[ ! $file == *.php ]]; then
        cat "$file"
        return
    fi

    # Set a flag to indicate if we are inside the block to be removed
    inside_block=0

    # Read the file line by line
    while IFS= read -r line; do
        # Check if the line contains "<?php"
        if [[ $line == *"<?php"* ]]; then
            # Set the flag to indicate that we are inside the block
            inside_block=1
            # Print the line
            echo "$line"
        elif [[ $line == *"?>"* ]]; then
            # Set the flag to indicate that we are outside the block
            inside_block=0
            # Print the line
            echo "$line"
        elif [ "$inside_block" -eq 0 ]; then
            # Print the line if we are outside the block
            echo "$line"
        fi  
    done <<< "$(sed "s/<?php.*?>/<PHP>/g" "$input_file")"
}

# Fix paths in the given arrays
check_paths() {
    fix_path() {
        local -n array_name=$1
        for ((i=0; i<${#array_name[@]}; i++)); do
            local path="${array_name[$i]}"
            if [[ ! $path = /* ]]; then
                array_name[$i]="$main_directory/$path"
            fi
        done
    }
    
    fix_path file_array
    fix_path ignore_file_array
}


# Function to remove files that match ignore patterns
remove_ignored_files() {
    local filtered_files=()
    for file in "${file_array[@]}"; do
        local ignore_match=false
        for ignore_file in "${ignore_file_array[@]}"; do
            if [[ "$file" == "$ignore_file" || "$file" == "$ignore_file"* ]]; then
                ignore_match=true
                break
            fi
        done
        if [[ "$ignore_match" == false ]]; then
            filtered_files+=("$file")
        fi
    done
    file_array=("${filtered_files[@]}")
}

search_files() {
    local directory="$1"

    IFS=',' read -ra ext_array <<< "$extensions"

    # Construct the -name option for each extension
    name_options=""
    for extension in "${ext_array[@]}"; do
        name_options+=" -o -name \"*.$extension\""
    done

    # Remove the leading "-o" from the name_options
    name_options="${name_options# -o}"

    # Use the constructed name_options in the find command
    find_cmd="find \"$directory\" -type f \($name_options \) | sort"
    local files=$(eval "$find_cmd")


    # Save the files in the global array
    while IFS= read -r file; do
        file_array+=("$file")
    done <<< "$files"
}

# Function to modify the file path
modify_filepath() {
    # Remove the initial directory path
    filepath="${1#$main_directory/}"
    echo "$filepath"
}

# Function to search for arrow functions
search_for_arrow_functions() {
    local filepath="$1"

    # Search for arrow functions using grep and sed, remove leading spaces
    if [[ $filepath == *.php ]]; then
        sed '/<\?php/,/\?>/d' "$filepath" | grep -Eo '^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*=[[:space:]]*)?(async[[:space:]]*)?\([^)]*\)[[:space:]]*=>[[:space:]]*' | sed -E 's/^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*=[[:space:]]*)?(async[[:space:]]*)?\(([^)]*)\)[[:space:]]*=>[[:space:]]*$/\0/' | sed 's/^[ \t]*//;s/[ \t]*$//'
        sed '/<\?php/,/\?>/d' "$filepath" | grep -o 'function [a-zA-Z_][a-zA-Z0-9_]*([^)]*) {'
    else
        grep -Eo '^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*=[[:space:]]*)?(async[[:space:]]*)?\([^)]*\)[[:space:]]*=>[[:space:]]*' "$filepath" | sed -E 's/^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*=[[:space:]]*)?(async[[:space:]]*)?\(([^)]*)\)[[:space:]]*=>[[:space:]]*$/\0/' | sed 's/^[ \t]*//;s/[ \t]*$//'
        grep -o 'function [a-zA-Z_][a-zA-Z0-9_]*([^)]*) {' "$filepath"
    fi
}

# Function to search for function execution
search_execution() {
    local file="$1"
    local func_name="$2"
    local in_file="$3"
    local func_count_file="$4"
    local functions
    local count=0

    # Store the function matches in a variable
    functions=$(clear_php_files "$file" | sed -E "s/\bfunction[[:space:]]+$func_name\(/\(/g" | grep -no "$func_name([^)]*)" | sed 's/^\([0-9]\+\):/\1 \t/' | sed 's/^/      /')
    if [ -z "$functions" ]; then
        functions=$(clear_php_files "$file" | sed -E "s/\b${func_name}[[:space:]]+=//g; s/\bfunction[[:space:]]+\b${func_name}//g" | grep -E "(^|[,[:space:]]|[()])($func_name)($|[,[:space:]]|[()])"  | grep -v "^$" | sed 's/^\([0-9]\+\):/\1 \t/' | sed 's/^/      /')

        if [[ ! -f "$find_functions_dir/$func_name.lock" ]]; then
            if ! grep -qF "$func_name -> $in_file" "$questionable_file"; then
                echo "$func_name -> $in_file" >> "$questionable_file"
            fi
        fi
    else
        touch "$find_functions_dir/$func_name.lock"
        if grep -qF "$func_name -> $in_file" "$questionable_file"; then
          sed -i "/$func_name -> .*$/d" "$questionable_file"
        fi
    fi

    # Output the matches
    if [ -n "$functions" ]; then
        echo -e "    \033[1;36mFile:\033[0m ${file#$main_directory/}"
        echo "$functions"
        count=$(wc -l <<< "$functions")
    fi

    # Save the function count to a temporary file
    echo "$count" >> "$func_count_file"
}

# Function to search all files for function execution
search_for_function_execution() {
    local line="$1"
    local input_file="$2"
    local function_name=$(echo "$line" | awk '{print $1}')
    if [[ "$function_name" = "function" ]]; then
        function_name=$(echo "$line" | awk '{print $2}' | cut -d'(' -f1 )
    fi
    local functions_count=0
    if [[ -z "$function_name" ]]; then
        return
    fi

    if [[ "$use_parallel" == "true" ]]; then
        functions_count_file="$(mktemp)"
        parallel --will-cite search_execution {} "$function_name" "$input_file" "$functions_count_file" ::: "${file_array[@]}"
        functions_count=$(awk '{sum += $1} END {print sum}' "$functions_count_file")
        rm "$functions_count_file"
    else
        functions_count_file="$(mktemp)"
        for file in "${file_array[@]}"; do
            search_execution "$file" "$function_name" "$input_file" "$functions_count_file"
        done
        functions_count=$(awk '{sum += $1} END {print sum}' "$functions_count_file")
        rm "$functions_count_file"
    fi

    if [ "$functions_count" -eq 0 ]; then
        echo -e "      \033[1;31mCount: $functions_count\033[0m"
    else
        echo -e "      \033[1;33mCount: $functions_count\033[0m"
    fi

    # If no functions were found, write to unused_functions file
    if [ $functions_count -eq 0 ]; then
        echo "$function_name -> $input_file" >> "$unused_file"
    fi
}

# Function to sort the unused_functions file by file path
sort_unused_functions() {
    if [ -s "$unused_file" ]; then
        sort -t '>' -k 2 -o "$unused_file" "$unused_file"
    fi
}

# Function to iterate through files
iterate_file() {
    local file="$1"
    local functions
    if [ -n "$global_functions" ]; then
        functions="$global_functions"
    else
        functions=$(search_for_arrow_functions "$file")
    fi

    search_res=""
    while IFS= read -r line; do
        search="$(search_for_function_execution "$line" "$file")"
        if [ -n "$search" ]; then
            search_res="$search_res  \033[1;32mFunction:\033[0m $line\n$search\n\n"
        fi
    done <<< "$functions"

    output_file="${file//\//-}.txt"
    if [ -n "$search_res" ]; then
        if [ ! -s "$filename" ]; then
            echo -e "\n\033[1m${header_line}\033[0m" >> "$find_functions_dir/$output_file"
            echo -e "\033[1m=== File: $(modify_filepath $file)\033[0m\n" >> "$find_functions_dir/$output_file"
        fi
        echo -e "$search_res" | sed '/^$/d' >> "$find_functions_dir/$output_file"
    else 
        echo -e "$(modify_filepath $file) has no functions" | sed '/^$/d' >> "$find_functions_dir/$output_file"
    fi
}

# Function to iterate through files and subdirectories
iterate_directory() {
    local dir="$1"
    local title="$2"

    # Print the sorted file paths with a title
    if [ ${#file_array[@]} -gt 0 ]; then
        title="${title/main}"

        # Iterate through the sorted file paths
        process_file() {
            file="$1"
            SECONDS=0
            iterate_file "$file"
            output_file="${file//\//-}.txt"
            if [ -s "$find_functions_dir/$output_file" ]; then
                cat "$find_functions_dir/$output_file"
                cat "$find_functions_dir/$output_file" >> "$final_file"
            fi
            elapsed_time=$SECONDS

            # Format the output
            if [ $SECONDS -gt 0 ]; then
                minutes=$((elapsed_time / 60))
                seconds=$((elapsed_time % 60))
                printf "Runtime: %02d:%02d\n" "$minutes" "$seconds"
            fi
        }
        if [ -n "$global_functions" ]; then
            process_file "all"
        else
            for file in "${file_array[@]}"; do
                process_file "$file"
            done
        fi
    fi
}

# Function to print the unused functions in a table-like format
print_unused_functions() {
    if [ -s "$unused_file" ]; then
        sort_unused_functions

        echo -e "\n\033[1;31m${header_line}\033[0m"
        echo -e "\033[1;31m=== Not found\033[0m\n"

        # Get the longest function name within the unused functions
        longest_function_name=$(awk -F " -> " '{ print length($1) }' "$unused_file" | sort -nr | head -1)
        longest_function_name=$((longest_function_name + 1))  # Add 1 for spacing

        # Print the table header
        printf "%-${longest_function_name}s%s\n" "Function Name" "Input File"
        printf "%-${longest_function_name}s%s\n" "-------------" "----------"

        # Print each unused function with proper spacing
        while IFS= read -r line; do
            function_name=$(echo "$line" | awk -F " -> " '{ print $1 }')
            input_file=$(echo "$line" | awk -F " -> " '{ print $2 }')
            printf "%-${longest_function_name}s%s\n" "$function_name" "$input_file"
        done < "$unused_file"

        # Get the line count of the unused functions file
        line_count=$(wc -l < "$unused_file")

        # Print the unused functions line count in red
        echo -e "\nCount: $line_count"
    else
        echo -e "\n\033[0;31m${header_line}\033[0m"
        echo -e "\033[32mNo unused functions found.\033[0m"
    fi
}

# Function to print the unused functions in a table-like format
print_questionable_functions() {
    if [ -s "$questionable_file" ]; then
        echo -e "\n\033[1;33m${header_line}\033[0m"
        echo -e "\033[1;33m=== Questionable found\033[0m\n"

        # Get the longest function name within the unused functions
        longest_function_name=$(awk -F " -> " '{ print length($1) }' "$questionable_file" | sort -nr | head -1)
        longest_function_name=$((longest_function_name + 1))  # Add 1 for spacing

        # Print the table header
        printf "%-${longest_function_name}s%s\n" "Function Name" "Input File"
        printf "%-${longest_function_name}s%s\n" "-------------" "----------"

        # Print each unused function with proper spacing
        while IFS= read -r line; do
            function_name=$(echo "$line" | awk -F " -> " '{ print $1 }')
            input_file=$(echo "$line" | awk -F " -> " '{ print $2 }')
            printf "%-${longest_function_name}s%s\n" "$function_name" "$input_file"
        done < "$questionable_file"

        # Get the line count of the unused functions file
        line_count=$(wc -l < "$questionable_file")

        # Print the unused functions line count in red
        echo -e "\nCount: $line_count"
    else
        echo -e "\n\033[1;35m${header_line}\033[0m"
        echo -e "\033[32mNo questionable functions found.\033[0m"
    fi
}

clear_questionable_file() {
    if [ -z "$unused_file" ] || [ -z "$questionable_file" ] || [ ! -f "$unused_file" ] || [ ! -f "$questionable_file" ]; then
        exit 1
    fi

    temp_file=$(mktemp)

    # Read each line from the unused file
    while IFS= read -r unused_line; do
        # Extract the function name from the unused line
        unused_function=$(echo "$unused_line" | cut -d' ' -f1)

        # Remove any lines from the questionable file that contain the function name
        grep -vF "$unused_function" "$questionable_file" > "$temp_file"
        mv "$temp_file" "$questionable_file"
    done < "$unused_file" 2>/dev/null
}

# Main function
main() {

    export find_functions_dir found_functions_dir final_file unused_file questionable_file main_director

    export -f clear_php_files search_execution

    main_directory="${1:-$(pwd)}"

    # Create the directory /tmp/findFunctions if it doesn't exist and ensure it's empty
    mkdir -p "$find_functions_dir"
    mkdir -p "$found_functions_dir"
    rm -f "$find_functions_dir"/*
    rm -f "$found_functions_dir"/*

    > "$final_file"
    > "$unused_file"
    > "$questionable_file"



    # Check if the 'parallel' command exists
    if command -v parallel >/dev/null 2>&1; then
        read -p "The 'parallel' command is available. Do you want to use it? (y/n): " choice
        case $choice in
            [Yy]*)
                use_parallel=true
                echo "Using parallel."
                ;;
            [Nn]*)
                use_parallel=false
                echo "Not using parallel."
                ;;
            *)
                echo "Using parallel."
                use_parallel=true
                ;;
            esac
    else
        echo "'parallel' command not found. Not using parallel."
        use_parallel=false
    fi

    # Get the total path for file_array and remove_ignored_files
    check_paths

    # List all files in file_array
    if [ ${#file_array[@]} -eq 0 ]; then
        search_files "$main_directory"
    fi

    # Remove ignored files from file_array
    remove_ignored_files

    # Call the recursive function to iterate through the main directory and its subdirectories
    iterate_directory "$main_directory" "main"

    # Print the questionable functions
    clear_questionable_file

    echo
    echo

    # Print the questionable functions
    print_questionable_functions


    # Print the unused functions
    print_unused_functions

    # Provide information about the output file
    sed -i 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$final_file"

    if [ -f "$final_file" ]; then
        line_count=$(wc -l < "$final_file")
        echo -e "\n\033[1m${header_line}\033[0m"
        echo -e "\033[1m=== Output File Information ===\033[0m\n"
        echo "Search:       $final_file"
        if [[ -s "$questionable_file" ]]; then
            echo "Questionable: $questionable_file"
        fi
        echo "Unused:       $unused_file"
    else
        echo -e "\n\033[1m${header_line}\033[0m"
        echo -e "\033[1m=== Output File Information\033[0m\n"
        echo "No output file found."
    fi


    echo
    echo -e "Total functions found: $(grep "Function:" /tmp/foundFunctions/result.txt | wc -l)"


    # Cleanup: Remove the /tmp/findFunctions directory and its contents
    rm -rf "$find_functions_dir"
    cleanup
}




# Process command line options
while getopts "e:x:ha:f:o:u:q:i:" option; do
    case $option in
        e)
            extensions="$OPTARG"
            ;;
        h)
            print_help
            exit 0
            ;;
        x)
            g_functions="$OPTARG"
            ;;
        a)
            extensions="*"
            ;;
        f)
            IFS=',' read -ra file_array <<< "$OPTARG"
            ;;
        o)
            final_file="$OPTARG"
            ;;
        u)
            unused_file="$OPTARG"
            ;;
        q)
            questionable_file="$OPTARG"
            ;;
        i)
            IFS=',' read -ra ignore_file_array <<< "$OPTARG"
            ;;
        \?)
            echo "Invalid option: -$OPTARG" >&2
            print_help
            exit 0
            ;;
    esac
done


if [ -n "$g_functions" ]; then
    IFS=',' read -ra func_array <<< "$g_functions"
    global_functions=$(printf "%s\n" "${func_array[@]}")
fi

# Get the directory argument (remaining non-option arguments)
shift $((OPTIND - 1))
# Call the main function with the provided directory argument or default to the current working directory
( main "$1" )