Skip to content

Instantly share code, notes, and snippets.

@isurfer21
Last active July 3, 2025 09:03
Show Gist options
  • Save isurfer21/bec2c4c5d66f4e9024babbd9b39aeb36 to your computer and use it in GitHub Desktop.
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.
#!/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