411 lines
12 KiB
Bash
411 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
set -uo pipefail
|
|
|
|
# Colors map
|
|
declare -A COLOR_MAP=(
|
|
["Cyan"]="\e[36m"
|
|
["Green"]="\e[32m"
|
|
["Yellow"]="\e[33m"
|
|
["Magenta"]="\e[35m"
|
|
["Blue"]="\e[34m"
|
|
["Red"]="\e[31m"
|
|
["White"]="\e[97m"
|
|
["Gray"]="\e[90m"
|
|
["DarkYellow"]="\e[33m"
|
|
["Reset"]="\e[0m"
|
|
)
|
|
|
|
# Configuration: Type → Extension list, Color
|
|
declare -A TYPE_COLOR
|
|
declare -A TYPE_EXTS
|
|
TYPE_COLOR["C/C++"]="Cyan"
|
|
TYPE_EXTS["C/C++"]='c cpp cc cxx h hpp hh hxx'
|
|
|
|
TYPE_COLOR["OpenGL Shader"]="Blue"
|
|
TYPE_EXTS["OpenGL Shader"]='glsl frag vert comp'
|
|
|
|
TYPE_COLOR["C#"]="Green"
|
|
TYPE_EXTS["C#"]='cs'
|
|
|
|
TYPE_COLOR["Java"]="Yellow"
|
|
TYPE_EXTS["Java"]='java'
|
|
|
|
TYPE_COLOR["Python"]="Magenta"
|
|
TYPE_EXTS["Python"]='py pyw'
|
|
|
|
TYPE_COLOR["JavaScript"]="Blue"
|
|
TYPE_EXTS["JavaScript"]='js jsx ts tsx'
|
|
|
|
TYPE_COLOR["Web"]="Red"
|
|
TYPE_EXTS["Web"]='html htm css scss less sass'
|
|
|
|
TYPE_COLOR["Shell"]="White"
|
|
TYPE_EXTS["Shell"]='sh bash ps1 psm1 psd1'
|
|
|
|
TYPE_COLOR["Config/Data"]="Gray"
|
|
TYPE_EXTS["Config/Data"]='xml json yaml yml ini cfg config'
|
|
|
|
TYPE_COLOR["Other Code"]="DarkYellow"
|
|
TYPE_EXTS["Other Code"]='go rs rb php swift kt scala'
|
|
|
|
# Show usage
|
|
show_usage() {
|
|
printf "Usage: %s [OPTIONS] [DIRECTORY]\n\n" "$(basename "$0")"
|
|
printf "Options:\n"
|
|
printf " -h, --help Show this help message\n"
|
|
printf " -d, --dir DIR Specify directory to scan\n"
|
|
printf "\nExamples:\n"
|
|
printf " %s # Scan current directory\n" "$(basename "$0")"
|
|
printf " %s /path/to/project # Scan specified directory\n" "$(basename "$0")"
|
|
printf " %s -d /path/to/project\n" "$(basename "$0")"
|
|
}
|
|
|
|
# Parse command line arguments
|
|
scan_dir=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help)
|
|
show_usage
|
|
exit 0
|
|
;;
|
|
-d|--dir)
|
|
if [[ -n "${2:-}" ]]; then
|
|
scan_dir="$2"
|
|
shift 2
|
|
else
|
|
printf "Error: --dir requires a directory argument\n" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
-*)
|
|
printf "Error: Unknown option: %s\n" "$1" >&2
|
|
show_usage
|
|
exit 1
|
|
;;
|
|
*)
|
|
if [[ -z "$scan_dir" ]]; then
|
|
scan_dir="$1"
|
|
else
|
|
printf "Error: Multiple directories specified\n" >&2
|
|
exit 1
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Default to current directory if not specified
|
|
if [[ -z "$scan_dir" ]]; then
|
|
scan_dir="$(pwd)"
|
|
fi
|
|
|
|
# Validate directory
|
|
if [[ ! -d "$scan_dir" ]]; then
|
|
printf "Error: Directory does not exist: %s\n" "$scan_dir" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Convert to absolute path using readlink or realpath
|
|
if command -v realpath >/dev/null 2>&1; then
|
|
target_dir="$(realpath "$scan_dir")"
|
|
elif command -v readlink >/dev/null 2>&1 && readlink -f / >/dev/null 2>&1; then
|
|
target_dir="$(readlink -f "$scan_dir")"
|
|
else
|
|
# Fallback: use cd + pwd
|
|
target_dir="$(cd "$scan_dir" && pwd)"
|
|
fi
|
|
|
|
if [[ -z "$target_dir" || ! -d "$target_dir" ]]; then
|
|
printf "Error: Cannot resolve directory: %s\n" "$scan_dir" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Build suffix → type mapping (lowercase, without dot)
|
|
declare -A SUFFIX_TYPE
|
|
for t in "${!TYPE_EXTS[@]}"; do
|
|
for ext in ${TYPE_EXTS[$t]}; do
|
|
ext_lower="$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')"
|
|
SUFFIX_TYPE["$ext_lower"]="$t"
|
|
done
|
|
done
|
|
|
|
# Build regex pattern for find
|
|
# Create a single regex pattern matching all extensions
|
|
all_extensions=()
|
|
for t in "${!TYPE_EXTS[@]}"; do
|
|
for ext in ${TYPE_EXTS[$t]}; do
|
|
all_extensions+=("$ext")
|
|
done
|
|
done
|
|
|
|
# Title
|
|
printf "%b========================================%b\n" "${COLOR_MAP[White]}" "${COLOR_MAP[Reset]}"
|
|
printf "%b Code Line Counter%b\n" "${COLOR_MAP[Yellow]}" "${COLOR_MAP[Reset]}"
|
|
printf "%b========================================%b\n\n" "${COLOR_MAP[White]}" "${COLOR_MAP[Reset]}"
|
|
|
|
printf "%bScanning directory: %s%b\n\n" "${COLOR_MAP[Gray]}" "$target_dir" "${COLOR_MAP[Reset]}"
|
|
|
|
# Statistics variables
|
|
total_lines=0
|
|
total_files=0
|
|
declare -A type_files
|
|
declare -A type_lines
|
|
declare -A ext_files
|
|
declare -A ext_lines
|
|
declare -a FILE_INFO_LIST
|
|
start_time_ns=$(date +%s%N)
|
|
|
|
# Initialize type statistics
|
|
for t in "${!TYPE_EXTS[@]}"; do
|
|
type_files["$t"]=0
|
|
type_lines["$t"]=0
|
|
done
|
|
|
|
printf "%bCollecting files...%b\n" "${COLOR_MAP[Gray]}" "${COLOR_MAP[Reset]}"
|
|
|
|
# Collect ALL regular files first, then filter by extension
|
|
# This avoids any issues with find's -iname pattern matching
|
|
declare -A SEEN_FILES
|
|
ALL_FILES=()
|
|
|
|
# Use find to get all files, then filter in bash
|
|
# This is more reliable than multiple find calls with -iname
|
|
while IFS= read -r -d '' file; do
|
|
# Skip empty
|
|
[[ -z "$file" ]] && continue
|
|
|
|
# Get filename and extension
|
|
filename="${file##*/}"
|
|
|
|
# Skip files without extension
|
|
[[ "$filename" != *.* ]] && continue
|
|
|
|
# Get extension (lowercase, without dot)
|
|
ext="${filename##*.}"
|
|
ext_lower="$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')"
|
|
|
|
# Check if this extension is in our list
|
|
if [[ -n "${SUFFIX_TYPE[$ext_lower]+isset}" ]]; then
|
|
# Deduplicate
|
|
if [[ -z "${SEEN_FILES["$file"]+isset}" ]]; then
|
|
SEEN_FILES["$file"]=1
|
|
ALL_FILES+=("$file")
|
|
fi
|
|
fi
|
|
done < <(find "$target_dir" -type f -print0 2>/dev/null)
|
|
|
|
total_to_process=${#ALL_FILES[@]}
|
|
|
|
if (( total_to_process == 0 )); then
|
|
printf "%bNo source code files found!%b\n" "${COLOR_MAP[Red]}" "${COLOR_MAP[Reset]}"
|
|
printf "%bPlease check if the directory contains source code files.%b\n" "${COLOR_MAP[Gray]}" "${COLOR_MAP[Reset]}"
|
|
exit 0
|
|
fi
|
|
|
|
printf "%bFound %d files to process...%b\n\n" "${COLOR_MAP[Gray]}" "$total_to_process" "${COLOR_MAP[Reset]}"
|
|
|
|
processed=0
|
|
|
|
pad_right() {
|
|
local s="$1" w="$2" len=${#1}
|
|
if (( len < w )); then
|
|
printf "%s%*s" "$s" $((w-len)) ""
|
|
else
|
|
printf "%s" "$s"
|
|
fi
|
|
}
|
|
|
|
count_non_empty_lines() {
|
|
local file="$1"
|
|
local count
|
|
count=$(grep -c '[^[:space:]]' "$file" 2>/dev/null) || count=0
|
|
printf '%d' "$count"
|
|
}
|
|
|
|
# Statistics loop
|
|
for file in "${ALL_FILES[@]}"; do
|
|
((processed++))
|
|
percent=$(( processed * 100 / total_to_process ))
|
|
|
|
base="${file##*/}"
|
|
printf "\rCounting code lines | Processing: %s | Progress: %d / %d | %3d%%" \
|
|
"$(pad_right "$base" 30)" "$processed" "$total_to_process" "$percent"
|
|
|
|
if [[ ! -r "$file" ]]; then
|
|
printf "\n%b Warning: Cannot read file %s%b\n" "${COLOR_MAP[Red]}" "$base" "${COLOR_MAP[Reset]}"
|
|
continue
|
|
fi
|
|
|
|
lines=$(count_non_empty_lines "$file")
|
|
|
|
(( total_lines += lines ))
|
|
(( total_files++ ))
|
|
|
|
# Get extension (lowercase, without dot)
|
|
ext="${base##*.}"
|
|
ext_lower="$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')"
|
|
suffix=".${ext_lower}"
|
|
|
|
t="${SUFFIX_TYPE[$ext_lower]:-}"
|
|
if [[ -n "$t" ]]; then
|
|
(( type_files["$t"] += 1 ))
|
|
(( type_lines["$t"] += lines ))
|
|
|
|
ext_key="${t}|${suffix}"
|
|
(( ext_files["$ext_key"] = ${ext_files["$ext_key"]:-0} + 1 ))
|
|
(( ext_lines["$ext_key"] = ${ext_lines["$ext_key"]:-0} + lines ))
|
|
|
|
file_dir="$(dirname "$file")"
|
|
file_name="$(basename "$file")"
|
|
FILE_INFO_LIST+=("${t}|${suffix}|${file_dir}|${file_name}|${lines}")
|
|
fi
|
|
done
|
|
|
|
printf "\n"
|
|
|
|
# Duration
|
|
end_time_ns=$(date +%s%N)
|
|
duration_ns=$(( end_time_ns - start_time_ns ))
|
|
total_ms=$(( duration_ns / 1000000 ))
|
|
mm=$(( (total_ms / 60000) % 60 ))
|
|
ss=$(( (total_ms / 1000) % 60 ))
|
|
ms=$(( total_ms % 1000 ))
|
|
|
|
# Detailed statistics
|
|
printf "\n%b========================================%b\n" "${COLOR_MAP[White]}" "${COLOR_MAP[Reset]}"
|
|
printf "%b Statistics Details%b\n" "${COLOR_MAP[Yellow]}" "${COLOR_MAP[Reset]}"
|
|
printf "%b========================================%b\n\n" "${COLOR_MAP[White]}" "${COLOR_MAP[Reset]}"
|
|
|
|
types_sorted=()
|
|
while IFS= read -r line; do
|
|
[[ -n "$line" ]] && types_sorted+=("$line")
|
|
done < <(
|
|
for t in "${!TYPE_EXTS[@]}"; do
|
|
if (( ${type_files["$t"]:-0} > 0 )); then
|
|
printf "%s\t%d\n" "$t" "${type_lines["$t"]}"
|
|
fi
|
|
done | sort -k2,2nr | awk -F'\t' '{print $1}'
|
|
)
|
|
|
|
for t in "${types_sorted[@]}"; do
|
|
files=${type_files["$t"]}
|
|
lines=${type_lines["$t"]}
|
|
if (( files > 0 )); then
|
|
percent=0
|
|
if (( total_lines > 0 )); then
|
|
percent=$(awk -v a="$lines" -v b="$total_lines" 'BEGIN{printf("%.2f", (b>0)?(a*100.0/b):0)}')
|
|
fi
|
|
color_name="${TYPE_COLOR[$t]}"
|
|
color="${COLOR_MAP[$color_name]}"
|
|
reset="${COLOR_MAP[Reset]}"
|
|
printf "%b%-15s Files: %4d Lines: %8d (%6s%%)%b\n" "$color" "$t" "$files" "$lines" "$percent" "$reset"
|
|
fi
|
|
done
|
|
|
|
# Summary
|
|
printf "\n%b========================================%b\n" "${COLOR_MAP[White]}" "${COLOR_MAP[Reset]}"
|
|
printf "%bSummary Statistics%b\n" "${COLOR_MAP[Yellow]}" "${COLOR_MAP[Reset]}"
|
|
printf "%b========================================%b\n" "${COLOR_MAP[White]}" "${COLOR_MAP[Reset]}"
|
|
|
|
printf "%bTotal Files: %d%b\n" "${COLOR_MAP[Cyan]}" "$total_files" "${COLOR_MAP[Reset]}"
|
|
printf "%bTotal Lines: %d%b\n" "${COLOR_MAP[Green]}" "$total_lines" "${COLOR_MAP[Reset]}"
|
|
printf "%bProcessed: %d / %d%b\n" "${COLOR_MAP[Gray]}" "$processed" "$total_to_process" "${COLOR_MAP[Reset]}"
|
|
printf "%bDuration: %02d:%02d.%03d%b\n" "${COLOR_MAP[Magenta]}" "$mm" "$ss" "$ms" "${COLOR_MAP[Reset]}"
|
|
|
|
# File size statistics
|
|
total_size_bytes=0
|
|
stat_size() {
|
|
local f="$1"
|
|
if stat --version >/dev/null 2>&1; then
|
|
stat -c %s -- "$f" 2>/dev/null
|
|
else
|
|
stat -f %z -- "$f" 2>/dev/null
|
|
fi
|
|
}
|
|
for f in "${ALL_FILES[@]}"; do
|
|
if sz=$(stat_size "$f"); then
|
|
(( total_size_bytes += sz ))
|
|
fi
|
|
done
|
|
avg_size_bytes=0
|
|
if (( total_files > 0 )); then
|
|
avg_size_bytes=$(( total_size_bytes / total_files ))
|
|
fi
|
|
to_mb=$(awk -v b="$total_size_bytes" 'BEGIN{printf("%.2f", b/1024/1024)}')
|
|
avg_kb=$(awk -v b="$avg_size_bytes" 'BEGIN{printf("%.2f", b/1024)}')
|
|
|
|
printf "%bTotal File Size: %s MB%b\n" "${COLOR_MAP[Blue]}" "$to_mb" "${COLOR_MAP[Reset]}"
|
|
printf "%bAverage File Size: %s KB%b\n" "${COLOR_MAP[Blue]}" "$avg_kb" "${COLOR_MAP[Reset]}"
|
|
|
|
# Export CSV
|
|
printf "\n"
|
|
read -r -p "Export statistics to CSV file? (Y/N) " exportChoice
|
|
if [[ "$exportChoice" == "Y" || "$exportChoice" == "y" ]]; then
|
|
timestamp="$(date +%Y%m%d_%H%M%S)"
|
|
csv_path="${target_dir}/code_stats_${timestamp}.csv"
|
|
|
|
{
|
|
# CSV Header for file details
|
|
printf "Directory,File Name,Language,Extension,Lines\n"
|
|
|
|
# Sort by directory -> language -> extension -> filename
|
|
printf '%s\n' "${FILE_INFO_LIST[@]}" | sort -t'|' -k3,3 -k1,1 -k2,2 -k4,4 | while IFS='|' read -r ftype fsuffix fdir fname flines; do
|
|
[[ -z "$ftype" ]] && continue
|
|
|
|
# Escape fields for CSV
|
|
escaped_dir="${fdir//\"/\"\"}"
|
|
escaped_fname="${fname//\"/\"\"}"
|
|
escaped_type="${ftype//\"/\"\"}"
|
|
escaped_suffix="${fsuffix//\"/\"\"}"
|
|
|
|
printf '"%s","%s","%s","%s",%d\n' \
|
|
"$escaped_dir" "$escaped_fname" "$escaped_type" "$escaped_suffix" "$flines"
|
|
done
|
|
|
|
printf "\n"
|
|
printf -- "--- Extension Summary ---\n"
|
|
printf "Language,Extension,File Count,Line Count,Percentage\n"
|
|
for t in "${types_sorted[@]}"; do
|
|
for key in "${!ext_lines[@]}"; do
|
|
if [[ "$key" == "${t}|"* ]]; then
|
|
suffix="${key#*|}"
|
|
ext_file_count=${ext_files["$key"]:-0}
|
|
ext_line_count=${ext_lines["$key"]:-0}
|
|
if (( ext_file_count > 0 )); then
|
|
ext_percent=$(awk -v a="$ext_line_count" -v b="$total_lines" 'BEGIN{printf("%.2f", (b>0)?(a*100.0/b):0)}')
|
|
escaped_t="${t//\"/\"\"}"
|
|
escaped_s="${suffix//\"/\"\"}"
|
|
printf '"%s","%s",%d,%d,%s\n' "$escaped_t" "$escaped_s" "$ext_file_count" "$ext_line_count" "$ext_percent"
|
|
fi
|
|
fi
|
|
done
|
|
done
|
|
|
|
printf "\n"
|
|
printf -- "--- Language Summary ---\n"
|
|
printf "Language,File Count,Line Count,Percentage\n"
|
|
for t in "${types_sorted[@]}"; do
|
|
files=${type_files["$t"]}
|
|
lines=${type_lines["$t"]}
|
|
if (( files > 0 )); then
|
|
percent=$(awk -v a="$lines" -v b="$total_lines" 'BEGIN{printf("%.2f", (b>0)?(a*100.0/b):0)}')
|
|
escaped_t="${t//\"/\"\"}"
|
|
printf '"%s",%d,%d,%s\n' "$escaped_t" "$files" "$lines" "$percent"
|
|
fi
|
|
done
|
|
|
|
printf "\n"
|
|
printf -- "--- Total ---\n"
|
|
printf "Total Files,Total Lines\n"
|
|
printf "%d,%d\n" "$total_files" "$total_lines"
|
|
|
|
} > "$csv_path"
|
|
|
|
printf "%bStatistics exported to: %s%b\n" "${COLOR_MAP[Green]}" "$csv_path" "${COLOR_MAP[Reset]}"
|
|
fi
|
|
|
|
printf "\n%bStatistics complete! Press any key to exit...%b" "${COLOR_MAP[Gray]}" "${COLOR_MAP[Reset]}"
|
|
read -rsn1 _ 2>/dev/null || true
|
|
printf "\n"
|