Last active
July 3, 2025 09:03
-
-
Save isurfer21/bec2c4c5d66f4e9024babbd9b39aeb36 to your computer and use it in GitHub Desktop.
Here's a Bash script that recursively traverses a directory, reads each file, and concatenates them into a single output file. It adds a comment at the top of each file's content using the appropriate syntax based on the file extension.
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 | |
# Function to get relative path from given absoule path & base path | |
get_relative_path() { | |
local absolute_path="$1" | |
local base_path="$2" | |
# Remove trailing slashes for consistency | |
absolute_path="${absolute_path%/}" | |
base_path="${base_path%/}" | |
# Convert both paths to arrays | |
IFS='/' read -r -a absolute_parts <<< "$absolute_path" | |
IFS='/' read -r -a base_parts <<< "$base_path" | |
# Find the common prefix length | |
local common_length=0 | |
while [[ $common_length -lt ${#base_parts[@]} && $common_length -lt ${#absolute_parts[@]} && "${base_parts[$common_length]}" == "${absolute_parts[$common_length]}" ]]; do | |
((common_length++)) | |
done | |
# Calculate the number of directories to go up from the base path | |
local up_dirs=$(( ${#base_parts[@]} - common_length - 1 )) | |
# Construct the relative path | |
local relative_path="" | |
for ((i = 0; i < up_dirs; i++)); do | |
relative_path+="../" | |
done | |
# Add the remaining parts of the absolute path | |
for ((i = common_length; i < ${#absolute_parts[@]}; i++)); do | |
relative_path+="${absolute_parts[$i]}" | |
if [[ $i -lt $((${#absolute_parts[@]} - 1)) ]]; then | |
relative_path+="/" | |
fi | |
done | |
echo "$relative_path" | |
} | |
# Function to get directory name from path | |
get_dirname() { | |
local input_path="$1" | |
local resolved_path | |
# Check if realpath exists | |
if command -v realpath >/dev/null 2>&1; then | |
# Try to resolve the path fully | |
resolved_path=$(realpath "$input_path" 2>/dev/null) | |
elif command -v readlink >/dev/null 2>&1; then | |
resolved_path=$(readlink -f "$input_path" 2>/dev/null) | |
else | |
# Fallback: use the input path as is | |
resolved_path="$input_path" | |
fi | |
# If resolution failed (empty), fallback to input path | |
if [ -z "$resolved_path" ]; then | |
resolved_path="$input_path" | |
fi | |
# Check if the resolved path is a directory | |
if [ -d "$resolved_path" ]; then | |
# Return the directory name itself | |
echo "$resolved_path" | |
else | |
# Get dirname of the resolved path | |
dirname "$resolved_path" | |
fi | |
} | |
# Function to determine comment syntax based on file extension | |
get_comment_prefix() { | |
case "$1" in | |
*.py|*.sh|*.pl|*.rb|*.r|*.yaml|*.yml|*.toml|*.ini|*.conf|*.cfg) echo "#" ;; | |
*.c|*.cpp|*.h|*.java|*.js|*.ts|*.css|*.go|*.swift|*.kt|*.scala|*.cs|*.fs) echo "//" ;; | |
*.vb) echo "'" ;; # Visual Basic uses single quote for comments | |
*.html|*.xml|*.cshtml|*.razor) echo "<!--" ;; | |
*.php) echo "//" ;; | |
*.sql) echo "--" ;; | |
*) echo "#" ;; # Default to hash | |
esac | |
} | |
# Function to create the output file name and create the file | |
create_output_file() { | |
local input_directory="$1" | |
local current_directory | |
local current_dir_name | |
local relative_path | |
local output_filename | |
# Get the real path of the current directory | |
current_directory=$(get_dirname "$(pwd)") | |
current_dir_name=$(basename "$current_directory") # Get the last part of the current directory | |
# Get the parent directory | |
parent_directory=$(dirname "$input_directory") | |
# Extract the part from the parent directory to the input directory | |
relative_path="${input_directory#$parent_directory/}" | |
# Get the last part of the relative path | |
relative_dir_name=$(basename "$relative_path") | |
# Create the output filename | |
if [[ "$relative_dir_name" == "$current_dir_name" ]]; then | |
output_filename="${current_dir_name}" # No __ suffix if they are the same | |
else | |
output_filename="${current_dir_name}__${relative_dir_name}" | |
fi | |
echo "$output_filename" # Return the output filename | |
} | |
# Check if input directory is provided and exists | |
if [ -z "$1" ] || [ ! -d "$1" ]; then | |
echo "Error: Please provide a valid directory path." | |
exit 1 | |
fi | |
# Get the current directory path | |
current_directory=$(pwd) | |
# Get the input directory path | |
input_dirpath="$(realpath $1)" | |
# Get output filename from input directory path | |
output_filename=$(create_output_file "$input_dirpath") | |
# Output file | |
output_file="./$output_filename.txt" | |
> "$output_file" # Clear the output file if it exists | |
output_file_absolute_path=$(realpath $output_file) | |
# Traverse all files in the directory and subdirectories | |
find "$input_dirpath" -type f -not -path '*/.*' | while IFS= read -r file; do | |
# Skip the output file if it matches the current file | |
if [[ "$file" == "$output_file_absolute_path" ]]; then | |
continue | |
fi | |
ext="${file##*.}" | |
comment_prefix=$(get_comment_prefix "$file") | |
filepath=$(get_relative_path $file $current_directory) | |
if [[ "$comment_prefix" == "<!--" ]]; then | |
echo "${comment_prefix} @file $filepath -->" >> "$output_file" | |
else | |
echo "${comment_prefix} @file $filepath" >> "$output_file" | |
fi | |
cat "$file" >> "$output_file" | |
echo -e "\n" >> "$output_file" | |
done | |
echo "Concatenation complete. Output saved to $output_file" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment