#!/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 Specify file extensions to search for (comma-separated)." echo " Default: $extensions." echo " -f Specify files to search for." echo " -h Display the help information." echo " -x Specify function names to search for (comma-separated)." echo " -u Specify the path to the unused functions output file." echo " Default: $unused_file." echo " -q Specify the path to the questionable functions output file." echo " Default: $questionable_file." echo " -o Specify the path to the output file." echo " Default: $final_file." echo " -i 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 ""* ]]; 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///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" )