Printify
This script captures snapshots of your project files into Markdown and copies them to the clipboard (when on mac).
Quick Start¶
- Make the script executable:
- Run it from your project root:
- Find the snapshot in
snapshots/data-N.mdand in your clipboard.
Configuration¶
- INCLUDE_EXTENSIONS: file types to include (default:
py,md,sh,txt). - IGNORE_DIRS: directories to skip (default:
snapshots,.venv,.git). - IGNORE_PATTERNS: filename patterns to skip (default:
*.pyc,.env*).
Script¶
Show full script
#!/usr/bin/env bash
set -euo pipefail
#
# ─── USER CONFIGURATION ────────────────────────────────────────────────────────
#
# Default kept file extensions (no leading dot). Leave empty for “all”.
DEFAULT_KEEP_EXTENSIONS=(py md txt)
# Default directories to skip entirely
DEFAULT_SKIP_DIRS=(snapshots .venv .git)
# default filename/path globs to skip
default_skip_files=('*.pyc' '.env*')
# this script’s filename (so we don’t snapshot ourselves)
self_script="$(basename "$0")"
#
# ─── runtime filters (can be overridden / extended by flags) ─────────────────
#
keep_extensions=("${default_keep_extensions[@]}")
keep_dirs=()
skip_files=("${default_skip_files[@]}")
skip_dirs=("${default_skip_dirs[@]}")
show_help() {
cat <<'eof'
usage:
snapshot.sh [options]
options:
--keep ext [ext ...] replace kept extensions with 1+ extensions
(example: --keep py md txt)
--keep-dir dir [dir ...] only include files inside these directories
(example: --keep-dir src docs)
--skip pattern [pattern ...] add file/path globs to skip
(example: --skip '*.pyc' '.env*')
--skip-dir dir [dir ...] add directories to skip entirely
(example: --skip-dir .git node_modules)
-h, --help show this help
notes:
- --keep replaces the default extension keep-list.
- --keep-dir, --skip, and --skip-dir append to the defaults.
- directories are interpreted relative to the current working directory.
eof
}
die() {
echo "❌ $*" >&2
exit 1
}
normalize_dir() {
local dir="$1"
dir="${dir#./}"
dir="${dir%/}"
printf '%s' "$dir"
}
lower() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
}
#
# ─── argument parsing ──────────────────────────────────────────────────────────
#
keep_seen=0
while [[ $# -gt 0 ]]; do
case "$1" in
--keep)
shift
if [[ $# -eq 0 || "$1" == --* ]]; then
die "--keep requires at least one extension"
fi
if [[ $keep_seen -eq 0 ]]; then
keep_extensions=()
keep_seen=1
fi
while [[ $# -gt 0 && "$1" != --* ]]; do
keep_extensions+=("${1#.}")
shift
done
;;
--keep-dir)
shift
if [[ $# -eq 0 || "$1" == --* ]]; then
die "--keep-dir requires at least one directory"
fi
while [[ $# -gt 0 && "$1" != --* ]]; do
keep_dirs+=("$(normalize_dir "$1")")
shift
done
;;
--skip)
shift
if [[ $# -eq 0 || "$1" == --* ]]; then
die "--skip requires at least one file pattern"
fi
while [[ $# -gt 0 && "$1" != --* ]]; do
skip_files+=("$1")
shift
done
;;
--skip-dir)
shift
if [[ $# -eq 0 || "$1" == --* ]]; then
die "--skip-dir requires at least one directory"
fi
while [[ $# -gt 0 && "$1" != --* ]]; do
skip_dirs+=("$(normalize_dir "$1")")
shift
done
;;
-h|--help)
show_help
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
#
# ─── platform detection ────────────────────────────────────────────────────────
#
os="$(uname -s)"
if [[ "$os" == "darwin" ]]; then
clip_mode="pbcopy-only"
elif [[ "$os" == "linux" ]] \
&& grep -qe '^id=debian' /etc/os-release 2>/dev/null \
&& grep -qe 'version_id="12"' /etc/os-release 2>/dev/null; then
clip_mode="auto"
else
clip_mode="auto"
echo "⚠️ unrecognized os or distro—using generic clipboard-auto mode." >&2
fi
#
# ─── prepare snapshot path ─────────────────────────────────────────────────────
#
mkdir -p snapshots
shopt -s nullglob
last=0
for f in snapshots/data-*.md; do
num="${f##*data-}"
num="${num%.md}"
if [[ "$num" =~ ^[0-9]+$ ]] && (( num > last )); then
last=$num
fi
done
shopt -u nullglob
next=$((last + 1))
outfile="snapshots/data-$next.md"
listfile=""
cleanup() {
[[ -n "$listfile" && -f "$listfile" ]] && rm -f "$listfile"
}
trap cleanup exit
listfile="$(mktemp)"
#
# ─── filter helpers ────────────────────────────────────────────────────────────
#
path_is_in_kept_dir() {
local rel="$1"
local dir
if (( ${#keep_dirs[@]} == 0 )); then
return 0
fi
for dir in "${keep_dirs[@]}"; do
dir="$(normalize_dir "$dir")"
if [[ -z "$dir" || "$dir" == "." ]]; then
return 0
fi
if [[ "$rel" == "$dir" || "$rel" == "$dir"/* ]]; then
return 0
fi
done
return 1
}
matches_kept_extension() {
local rel="$1"
local rel_lower
local ext
local ext_lower
if (( ${#keep_extensions[@]} == 0 )); then
return 0
fi
rel_lower="$(lower "$rel")"
for ext in "${keep_extensions[@]}"; do
ext="${ext#.}"
ext_lower="$(lower "$ext")"
if [[ "$rel_lower" == *."$ext_lower" ]]; then
return 0
fi
done
return 1
}
matches_skip_pattern() {
local rel="$1"
local base="${rel##*/}"
local pat
for pat in "${skip_files[@]}"; do
if [[ "$base" == $pat || "$rel" == $pat ]]; then
return 0
fi
done
if [[ "$base" == "$self_script" ]]; then
return 0
fi
return 1
}
should_include_file() {
local rel="$1"
path_is_in_kept_dir "$rel" || return 1
matches_kept_extension "$rel" || return 1
matches_skip_pattern "$rel" && return 1
return 0
}
#
# ─── build find command (with directory pruning) ──────────────────────────────
#
find_cmd=(find .)
valid_skip_dir_count=0
for d in "${skip_dirs[@]}"; do
d="$(normalize_dir "$d")"
[[ -n "$d" && "$d" != "." ]] && ((valid_skip_dir_count += 1))
done
if (( valid_skip_dir_count > 0 )); then
find_cmd+=( \( -type d \( )
first=1
for d in "${skip_dirs[@]}"; do
d="$(normalize_dir "$d")"
[[ -z "$d" || "$d" == "." ]] && continue
if (( first == 0 )); then
find_cmd+=( -o )
fi
if [[ "$d" == */* ]]; then
find_cmd+=( -path "./$d" )
else
find_cmd+=( -name "$d" )
fi
first=0
done
find_cmd+=( \) -prune \) -o )
fi
find_cmd+=( -type f -print0 )
#
# ─── collect filtered file list ────────────────────────────────────────────────
#
while ifs= read -r -d '' file; do
rel="${file#./}"
should_include_file "$rel" || continue
printf '%s\n' "$rel" >> "$listfile"
done < <("${find_cmd[@]}")
lc_all=c sort -o "$listfile" "$listfile"
#
# ─── dump snapshot ─────────────────────────────────────────────────────────────
#
{
echo '```'
if [[ -s "$listfile" ]]; then
cat "$listfile"
else
echo "(no matching files)"
fi
echo '```'
echo
while ifs= read -r rel; do
[[ -z "$rel" ]] && continue
echo "## $rel"
cat "./$rel"
echo
done < "$listfile"
} > "$outfile"
echo "✅ saved snapshot to $outfile"
#
# ─── copy to clipboard ─────────────────────────────────────────────────────────
#
copy_osc52() {
local b64
b64="$(base64 < "$1" | tr -d '\n')"
printf '\e]52;c;%s\a' "$b64"
}
if [[ "$clip_mode" == "pbcopy-only" ]]; then
if command -v pbcopy >/dev/null 2>&1; then
pbcopy < "$outfile" && echo "copied via pbcopy."
else
echo "⚠️ pbcopy not found; copy $outfile manually."
fi
else
if command -v pbcopy >/dev/null 2>&1; then
pbcopy < "$outfile" && echo "copied via pbcopy."
elif command -v xclip >/dev/null 2>&1; then
if xclip -selection clipboard < "$outfile"; then
echo "copied via xclip."
else
echo "🔄 xclip failed; using osc52."
copy_osc52 "$outfile" && echo "copied via osc52."
fi
elif command -v xsel >/dev/null 2>&1; then
if xsel --clipboard --input < "$outfile"; then
echo "copied via xsel."
else
echo "🔄 xsel failed; using osc52."
copy_osc52 "$outfile" && echo "copied via osc52."
fi
elif [[ -n "${ssh_connection:-}" && -t 1 ]]; then
copy_osc52 "$outfile" && echo "copied via osc52 over ssh."
else
echo "⚠️ no clipboard tool found—install pbcopy, xclip, or xsel."
fi
fi