Files
cflat/cflat
2026-01-29 01:08:41 +01:00

147 lines
4.9 KiB
Bash
Executable File

#!/bin/bash
# --- cflat (c♭) ---
# Flattens C/C++ projects into a single text file.
# https://git.alidavid.hu/david/cflat
VERSION="1.0.0"
SCRIPT_NAME=$(basename "$0")
# 1. HELP FUNCTION
function show_help {
echo "cflat (c♭) v$VERSION"
echo "Flattens C/C++ projects into a single text file."
echo "https://git.alidavid.hu/david/cflat"
echo ""
echo "Usage: ./$SCRIPT_NAME [DIRECTORY] [-e PATTERN]..."
echo ""
echo "Options:"
echo " -e, --exclude Exclude exact path relative to the target directory."
echo " 'aaa.a' -> Excludes ./aaa.a (Target root only)"
echo " 'src/lib' -> Excludes ./src/lib (and its contents)"
echo " '*.txt' -> Excludes ./*.txt (Target root only)"
echo " '*/*.txt' -> Excludes ./*/*.txt (Recursive)"
echo " -h, --help, -? Show this message"
exit 0
}
# 2. ARGUMENT PARSING
TARGET_DIR="."
EXCLUDE_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help|-\?) show_help ;;
-e|--exclude)
if [[ -n "$2" && "$2" != -* ]]; then
# Clean trailing slash from pattern to match find output safely
CLEAN_PAT="${2%/}"
EXCLUDE_ARGS+=("$CLEAN_PAT")
shift 2
else
echo "Error: --exclude requires an argument." >&2; exit 1
fi
;;
-*) echo "Error: Unknown option $1" >&2; exit 1 ;;
*) TARGET_DIR="$1"; shift ;;
esac
done
# Clean target directory (remove trailing slash)
TARGET_DIR=${TARGET_DIR%/}
# 3. BUILD FIND COMMAND
# We anchor every exclusion to the TARGET_DIR.
# This ensures that exclusions are strict (e.g., 'aaa.a' only matches the root file).
# Default ignores (Hidden files, obj, bin, build, and our own output)
BASE_IGNORES=(
-path '*/.*' -o
-path '*/obj' -o
-path '*/bin' -o
-path '*/build' -o
-name "cflat-*.txt" -o
-name "$SCRIPT_NAME"
)
# Build User Excludes
USER_PRUNE_ARGS=()
if [[ ${#EXCLUDE_ARGS[@]} -gt 0 ]]; then
for pattern in "${EXCLUDE_ARGS[@]}"; do
# Construct the strict path match anchored to TARGET_DIR
# e.g., if target is "." and pattern is "file", we match "./file"
MATCH_PATH="$TARGET_DIR/$pattern"
USER_PRUNE_ARGS+=("-path" "$MATCH_PATH" "-o")
done
fi
# Combine logic: ( UserExcludes OR BaseIgnores ) -prune
FINAL_PRUNE_ARGS=("(" "${USER_PRUNE_ARGS[@]}" "${BASE_IGNORES[@]}" ")" "-prune")
# 4. CONFIGURATION
TIMESTAMP=$(date +"%Y-%m-%dT%H-%M-%S")
OUTPUT="cflat-export-$TIMESTAMP.txt"
MAX_SIZE_KB=100
HAS_CLANG=$(command -v clang-format)
# 5. HEADER
echo "Generating export for: $TARGET_DIR"
echo "C-FLAT EXPORT" > "$OUTPUT"
echo "Generated on: $(date)" >> "$OUTPUT"
echo "----------------------------------------" >> "$OUTPUT"
# 6. STATISTICS
echo "PROJECT STATISTICS:" >> "$OUTPUT"
find "$TARGET_DIR" "${FINAL_PRUNE_ARGS[@]}" -o -type f \( -name "*.cpp" -o -name "*.hpp" -o -name "*.c" -o -name "*.h" \) -print0 | xargs -0 -r wc -l | tail -n 1 >> "$OUTPUT"
# 7. KEY DEFINITIONS
echo -e "\nKEY DEFINITIONS FOUND:" >> "$OUTPUT"
find "$TARGET_DIR" "${FINAL_PRUNE_ARGS[@]}" -o -type f -exec grep -nE "class [A-Za-z0-9_]+|struct [A-Za-z0-9_]+" {} + >> "$OUTPUT"
# 8. PROJECT STRUCTURE
echo -e "\nPROJECT STRUCTURE:" >> "$OUTPUT"
# Print all non-pruned paths for the tree visualization
find "$TARGET_DIR" "${FINAL_PRUNE_ARGS[@]}" -o -print | sort | sed -e "s;[^/]*/;|____;g;s;____|; |;g" >> "$OUTPUT"
echo -e "\n--- FILE CONTENTS ---\n" >> "$OUTPUT"
# 9. CONTENT EXPORT
find "$TARGET_DIR" "${FINAL_PRUNE_ARGS[@]}" -o -type f -print | while read -r file; do
if [[ "$file" =~ \.(cpp|hpp|h|c|cc|cxx)$ ]]; then
echo "--- START OF SOURCE FILE: $file ---" >> "$OUTPUT"
if [ -n "$HAS_CLANG" ]; then
clang-format --style=LLVM "$file" >> "$OUTPUT"
else
cat "$file" >> "$OUTPUT"
fi
echo -e "\n--- END OF SOURCE FILE: $file ---\n" >> "$OUTPUT"
elif [[ "$file" == *"Makefile"* ]] || [[ "$file" =~ \.(txt|py|sh|md|cmake)$ ]]; then
echo "--- START OF CONFIG/SCRIPT: $file ---" >> "$OUTPUT"
cat "$file" >> "$OUTPUT"
echo -e "\n--- END OF CONFIG/SCRIPT: $file ---\n" >> "$OUTPUT"
elif grep -qI . "$file"; then
FILE_SIZE=$(du -k "$file" | cut -f1)
if [ "$FILE_SIZE" -lt "$MAX_SIZE_KB" ]; then
echo "--- START OF ASSET FILE: $file ---" >> "$OUTPUT"
cat "$file" >> "$OUTPUT"
echo -e "\n--- END OF ASSET FILE: $file ---\n" >> "$OUTPUT"
fi
fi
done
# 10. AUTO-GITIGNORE
GITIGNORE="$TARGET_DIR/.gitignore"
if [ -f "$GITIGNORE" ]; then
if ! grep -qxF "$SCRIPT_NAME" "$GITIGNORE"; then
echo -e "\n# C-Flat ignores\n$SCRIPT_NAME" >> "$GITIGNORE"
fi
if ! grep -qF "cflat-*.txt" "$GITIGNORE"; then
echo "cflat-*.txt" >> "$GITIGNORE"
fi
fi
echo "Done: Output saved to $OUTPUT"