Skip to content

Instantly share code, notes, and snippets.

@bentruyman
Last active January 17, 2025 18:30
Show Gist options
  • Save bentruyman/ff7818ac549c9a1a8b20abf89c7603ae to your computer and use it in GitHub Desktop.
Save bentruyman/ff7818ac549c9a1a8b20abf89c7603ae to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -euo pipefail
#=============================================================================
# NAME
# print-code
#
# DESCRIPTION
# Outputs the contents of matching files in a directory or Git repository,
# excluding ignored and binary files. Each file's content is wrapped with
# separators and includes the filename as a header.
#
# USAGE
# print-code [OPTIONS]
#
# OPTIONS
# -i, --include <patterns> Comma-separated glob patterns to include
# (defaults to '*').
# -d, --directory <path> Directory or Git repository path (defaults to
# current directory).
# -e, --exclude <patterns> Comma-separated glob patterns to exclude
# (defaults to none).
# -h, --help Show this help message and exit.
#
# EXAMPLES
# print-code -i "*.go,*.md" -d /path/to/repo -e "*.json"
#=============================================================================
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Options:
-i, --include <patterns> Comma-separated glob patterns to include (default "*")
-d, --directory <path> Directory or Git repository path (default current directory)
-e, --exclude <patterns> Comma-separated glob patterns to exclude (default none)
-h, --help Show this help message and exit
Examples:
$(basename "$0") -i "*.go,*.md" -d /path/to/repo -e "*.json"
EOF
}
# Default values
glob="*"
repo="$(pwd)"
exclude_glob=""
# Parse flags
while [[ $# -gt 0 ]]; do
case "$1" in
-i | --include)
glob="${2:-}"
shift 2
;;
-d | --directory)
repo="${2:-}"
shift 2
;;
-e | --exclude)
exclude_glob="${2:-}"
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
echo "Error: Unknown option $1"
usage
exit 1
;;
esac
done
# Validate directory
if [[ ! -d "$repo" ]]; then
echo "Error: '$repo' is not a valid directory."
exit 1
fi
# Navigate to the specified directory
pushd "$repo" >/dev/null || {
echo "Error: Could not enter directory '$repo'."
exit 1
}
# Split include globs
IFS=',' read -r -a globs <<<"$glob"
# Split exclude globs
IFS=',' read -r -a exclude_globs <<<"$exclude_glob"
if [[ -d ".git" ]]; then
# Git repository handling
git_args=()
for g in "${globs[@]}"; do
git_args+=("--" "$g")
done
# Gather files not ignored by git (cached & untracked)
mapfile -t all_files < <(git ls-files --cached --others --exclude-standard -- "${git_args[@]}")
# Exclude certain files if requested
if [[ -n "$exclude_glob" ]]; then
for excl in "${exclude_globs[@]}"; do
# Convert simple '*' patterns into a grep-friendly pattern
# (Not perfect for every case, but works for most typical use-cases)
pattern="$(echo "$excl" | sed 's/\*/.*/g')"
all_files=($(printf "%s\n" "${all_files[@]}" | grep -v -E "$pattern"))
done
fi
# Check if any files matched
if [[ ${#all_files[@]} -eq 0 ]]; then
echo "No files matched the patterns '$glob' excluding '$exclude_glob' in repository '$repo'."
popd >/dev/null || exit
exit 0
fi
# Filter out binary files using git grep
mapfile -t text_files < <(git grep -Il . -- "${all_files[@]}")
else
# Non-Git repository handling
find_expr=""
for ((i = 0; i < ${#globs[@]}; i++)); do
find_expr+="-name \"${globs[i]}\""
if [[ $i -lt $((${#globs[@]} - 1)) ]]; then
find_expr+=" -o "
fi
done
# Append exclude patterns
for excl in "${exclude_globs[@]}"; do
find_expr+=" ! -name \"$excl\""
done
# Perform the find command
eval "mapfile -t all_files < <(find . -type f \( $find_expr \))"
# Check if any files matched
if [[ ${#all_files[@]} -eq 0 ]]; then
echo "No files matched the patterns '$glob' excluding '$exclude_glob' in directory '$repo'."
popd >/dev/null || exit
exit 0
fi
# Filter out binary files using the `file` command
text_files=()
for file in "${all_files[@]}"; do
if file "$file" | grep -q "text"; then
text_files+=("$file")
fi
done
# Check if any text files matched
if [[ ${#text_files[@]} -eq 0 ]]; then
echo "No text files matched the patterns '$glob' excluding '$exclude_glob' in directory '$repo'."
popd >/dev/null || exit
exit 0
fi
fi
# Iterate through each text file and output its contents with headers
for file in "${text_files[@]}"; do
echo "## $file"
echo '```'
cat "$file"
echo '```'
echo
done
# Return to the original directory
popd >/dev/null || exit
@bentruyman
Copy link
Author

bentruyman commented Jan 16, 2025

Usage

The most basic usage looks like the following:

$ ./print-code.sh

I just add the script to my $PATH so it can be invoked as simply copy-code.

When run from the root of a git repo, this will print all text files in the repo that aren't in .gitignore in a format like:

## package.json
```
{
  "name": "my-kewl-project",
  "description": "very kewl project",
  ...
}
```

## index.js
```
console.log("Hello World");
```

Basically, all of your repo's files get an H2 (##) heading with its contents surrounded by code fences.

On macOS, you can pipe the output of this to something like pbcopy:

$ print-code | pbcopy

You can also filter which files are included/excluded, as well as the directory the script should start from:

$ print-code -i "*.go,*.md" -d /path/to/repo -e "*.json"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment