Skip to content

Instantly share code, notes, and snippets.

@dereckmezquita
Last active June 17, 2026 18:48
Show Gist options
  • Select an option

  • Save dereckmezquita/1e02490d47816046cb4bf5cf7fcd7fe6 to your computer and use it in GitHub Desktop.

Select an option

Save dereckmezquita/1e02490d47816046cb4bf5cf7fcd7fe6 to your computer and use it in GitHub Desktop.
Used for merging a bunch of code from a project together into one file with context; useful for feeding LLMs.
function merge_files() {
# Default values
local output_file="" # empty => write to stdout (default)
local to_clipboard=false # -c => copy to clipboard instead of stdout
local extensions=()
local extra_excluded_dirs=()
local extra_excluded_files=()
# Robust default ignore list. These are matched by NAME at any depth
# (recursively), so e.g. a "test" or "node_modules" folder is dropped no
# matter how deep it sits in a monorepo.
local default_excluded_dirs=(
# version control
.git .svn .hg
# JavaScript / Node / Bun / Yarn / pnpm + bundler output
node_modules bower_components .yarn .pnp .npm
.next .nuxt .svelte-kit .turbo .parcel-cache .cache
dist build out coverage
# Python / Poetry / virtualenvs
.venv venv env __pycache__ .pytest_cache .mypy_cache .ruff_cache
.tox .eggs "*.egg-info" .ipynb_checkpoints site-packages
# R / renv / packrat
renv packrat .Rproj.user
# Swift / Xcode
.build .swiftpm DerivedData "*.xcodeproj" "*.xcworkspace" Pods Carthage
# other language build/output dirs
target vendor .gradle .terraform
# editors / OS
.idea .vscode
)
# File basenames (globs allowed) to skip even when their extension matches.
# Mostly lockfiles and minified bundles that add noise, not signal.
local default_excluded_files=(
package-lock.json yarn.lock pnpm-lock.yaml bun.lockb
poetry.lock Pipfile.lock composer.lock Cargo.lock renv.lock
"*.min.js" "*.min.css"
)
# Help message
local usage="Usage: merge_files -e EXTENSIONS [OPTIONS]
Required:
-e, --extensions EXT Comma-separated list of extensions (e.g., py,js,ts)
Options:
-o, --output FILE Write to FILE instead of stdout (default: stdout)
-c, --clipboard Copy merged output to the clipboard (via pbcopy)
-x, --exclude DIRS Comma-separated dir NAMES to ignore recursively
(added to the built-in defaults)
-X, --exclude-files F Comma-separated file names/globs to ignore
(added to the built-in defaults)
-h, --help Show this help message
By default output goes to stdout (pipe it, or use -o to save a file).
Examples:
merge_files -e py,js # print merged code to stdout
merge_files -e py -o all.txt # save to a file
merge_files -e py,js,ts -c # copy straight to the clipboard
merge_files -e R,r -x test,fixtures # ignore every test/ dir recursively
merge_files -e py,js,ts | pbcopy # straight to clipboard"
# Helper: split a comma-separated value into an array, appending to $1.
_mf_split_into() {
local __name="$1" __val="$2"
if [[ -n "$ZSH_VERSION" ]]; then
eval "${__name}+=(\"\${(@s:,:)__val}\")"
else
local __oldIFS=$IFS part
IFS=','
for part in $__val; do
eval "${__name}+=(\"\$part\")"
done
IFS=$__oldIFS
fi
}
# Check if no arguments provided
if [[ $# -eq 0 ]]; then
echo "Error: No arguments provided" >&2
echo "$usage" >&2
return 1
fi
# Parse arguments
local extensions_provided=false
while [[ $# -gt 0 ]]; do
case $1 in
-o|--output)
output_file="$2"
shift 2
;;
-c|--clipboard)
to_clipboard=true
shift
;;
-e|--extensions)
_mf_split_into extensions "$2"
extensions_provided=true
shift 2
;;
-x|--exclude)
_mf_split_into extra_excluded_dirs "$2"
shift 2
;;
-X|--exclude-files)
_mf_split_into extra_excluded_files "$2"
shift 2
;;
-h|--help)
echo "$usage"
return 0
;;
*)
echo "Error: Unknown option: $1" >&2
echo "$usage" >&2
return 1
;;
esac
done
# Check if extensions were provided
if ! $extensions_provided || [[ ${#extensions[@]} -eq 0 ]]; then
echo "Error: No extensions provided. Use -e or --extensions option." >&2
echo "$usage" >&2
return 1
fi
# -o and -c are mutually exclusive destinations.
if $to_clipboard && [[ -n "$output_file" ]]; then
echo "Error: -c/--clipboard cannot be combined with -o/--output." >&2
return 1
fi
if $to_clipboard && ! command -v pbcopy >/dev/null 2>&1; then
echo "Error: -c/--clipboard requires pbcopy, which was not found." >&2
return 1
fi
# Merge user-supplied excludes onto the defaults (additive, not replacing).
local excluded_dirs=("${default_excluded_dirs[@]}" "${extra_excluded_dirs[@]}")
local excluded_files=("${default_excluded_files[@]}" "${extra_excluded_files[@]}")
# Comment map
declare -A comment_map=(
[py]="#"
[js]="//"
[ts]="//"
[sh]="#"
[cpp]="//"
[java]="//"
[rb]="#"
[php]="#"
[pl]="#"
[R]="#"
[r]="#"
[swift]="//"
[go]="//"
[rs]="//"
[c]="//"
[h]="//"
[yaml]="#"
[yml]="#"
[ini]="#"
[conf]="#"
[md]="<!--"
[html]="<!--"
[css]="/*"
)
local DEFAULT_COMMENT="#"
# Build the find expression.
# 1) Prune directories matched by NAME (recursive at any depth).
local prune_expr=("(" "-type" "d" "(")
local first=true dir
for dir in "${excluded_dirs[@]}"; do
$first || prune_expr+=("-o")
prune_expr+=("-name" "$dir")
first=false
done
prune_expr+=(")" "-prune" ")")
# 2) Match files by extension.
local name_expr=("(")
first=true
local ext
for ext in "${extensions[@]}"; do
$first || name_expr+=("-o")
name_expr+=("-name" "*.$ext")
first=false
done
name_expr+=(")")
# 3) Exclude noisy files by basename/glob, plus the output file itself so
# it never gets merged into itself.
local notfile_expr=()
local f
for f in "${excluded_files[@]}"; do
notfile_expr+=("!" "-name" "$f")
done
[[ -n "$output_file" ]] && notfile_expr+=("!" "-name" "${output_file##*/}")
# Emit merged content to stdout; caller redirects to a file if requested.
_mf_emit() {
find . "${prune_expr[@]}" -o \( -type f "${name_expr[@]}" "${notfile_expr[@]}" -print \) \
| sort | while IFS= read -r file; do
local extension="${file##*.}"
local comment_prefix="${comment_map[$extension]}"
[[ -z "$comment_prefix" ]] && comment_prefix="$DEFAULT_COMMENT"
printf '%s %s\n\n' "$comment_prefix" "$file"
cat "$file"
printf '\n'
done
}
if [[ -n "$output_file" ]]; then
_mf_emit > "$output_file"
if [[ ! -s "$output_file" ]]; then
rm -f "$output_file"
echo "Warning: No files found matching extensions: ${extensions[*]}" >&2
return 0
fi
echo "Merged [${extensions[*]}] into $output_file" >&2
elif $to_clipboard; then
local merged
merged="$(_mf_emit)"
if [[ -z "$merged" ]]; then
echo "Warning: No files found matching extensions: ${extensions[*]}" >&2
return 0
fi
printf '%s' "$merged" | pbcopy
echo "Merged [${extensions[*]}] into the clipboard" >&2
else
_mf_emit
fi
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment