Skip to content

Instantly share code, notes, and snippets.

@bulatovv
Last active September 5, 2025 05:33
Show Gist options
  • Select an option

  • Save bulatovv/0bf0c2d03a890a22c4857cc56b5f61c4 to your computer and use it in GitHub Desktop.

Select an option

Save bulatovv/0bf0c2d03a890a22c4857cc56b5f61c4 to your computer and use it in GitHub Desktop.
Treecat is a shell utility that displays directory structures and file contents in a tree-like format with options for hidden files, gitignore filtering, and file size limits
#!/bin/sh
# treecat - A script to display directory structure or file contents for multiple paths.
# (Generated with Gemini Pro 2.5)
# --- Default Configuration ---
MAX_SIZE=102400 # 100 KB
SHOW_HIDDEN=false
USE_GITIGNORE=true
# TARGET_PATH is now handled by arguments
# --- Helper Functions ---
# Print usage information and exit.
usage() {
cat <<EOF
Usage: $(basename "$0") [options] [path1] [path2] ...
Displays the directory structure and file contents for one or more paths.
If no path is given, it defaults to the current directory.
Options:
-a, --show-hidden Show hidden files and directories (those starting with '.').
--no-gitignore Ignore .gitignore files and show all files.
--max-size BYTES Set the maximum file size in bytes for displaying content.
(Default: ${MAX_SIZE})
-h, --help Show this help message.
EOF
}
# Check if a file is binary.
is_binary() {
# A file is considered binary if its first 512 bytes contain any non-printable characters.
if head -c 512 "$1" | LC_ALL=C grep -q '[^[:print:][:space:]]'; then
return 0 # Has non-text characters -> binary
else
return 1 # No non-text characters found -> text
fi
}
# Traverse upwards from a given directory to find the root of a .git repository.
find_git_root() {
path="$1"
path="$(cd "$path" 2>/dev/null && pwd)" # Resolve to absolute path
while [ -n "$path" ] && [ "$path" != "/" ]; do
if [ -d "$path/.git" ]; then
printf "%s\n" "$path"
return
fi
path=$(dirname "$path")
done
}
# --- Argument Parsing ---
while [ "$#" -gt 0 ]; do
case "$1" in
-a|--show-hidden)
SHOW_HIDDEN=true
shift
;;
--no-gitignore)
USE_GITIGNORE=false
shift
;;
--max-size)
if [ -z "$2" ] || ! [ "$2" -eq "$2" ] 2>/dev/null; then
printf "Error: --max-size requires a numeric argument.\n" >&2
exit 1
fi
MAX_SIZE="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
-*)
printf "Unknown option: %s\n" "$1" >&2
usage
exit 1
;;
*)
# Not an option, so it must be a path. Stop option processing.
break
;;
esac
done
# If no paths are left after parsing options, default to the current directory.
if [ "$#" -eq 0 ]; then
set -- "."
fi
# --- Processing Functions ---
# Processes and displays the content of a single file.
# $1: The file path to process.
process_file() {
local file_path="$1"
printf "πŸ“„ %s\n" "$(basename "$file_path")"
size=$(wc -c < "$file_path")
if [ "$size" -gt "$MAX_SIZE" ]; then
printf " [file too large: %s bytes]\n" "$size"
return
fi
if is_binary "$file_path"; then
printf " [binary file]\n"
return
fi
# Use sed to add a prefix to each line for consistent indentation.
sed 's/^/β”‚ /' "$file_path"
}
# The recursive function to process a directory.
# $1: The path to process (relative to the chdir'd location).
# $2: The prefix for printing tree lines.
# $3: The git repository root, if found.
process_directory() {
local path="$1"
local base_prefix="$2"
local git_root="$3"
items=$(find "$path" -mindepth 1 -maxdepth 1 | sort)
if [ "$SHOW_HIDDEN" = false ]; then
items=$(printf "%s\n" "$items" | grep -v '/\.')
fi
if [ -z "$items" ]; then
return
fi
item_count=$(printf "%s\n" "$items" | wc -l)
current_item=0
printf "%s\n" "$items" | while IFS= read -r item; do
current_item=$((current_item + 1))
basename=$(basename "$item")
# Filter based on gitignore if applicable
if [ -n "$git_root" ]; then
if git -C "$git_root" check-ignore -q "$item"; then
continue
fi
fi
if [ "$current_item" -eq "$item_count" ]; then
connector="└──"
content_prefix="${base_prefix} "
next_base_prefix="${base_prefix} "
else
connector="β”œβ”€β”€"
content_prefix="${base_prefix}β”‚ "
next_base_prefix="${base_prefix}β”‚ "
fi
if [ -d "$item" ]; then
printf "%s%s πŸ“ %s/\n" "$base_prefix" "$connector" "$basename"
process_directory "$item" "$next_base_prefix" "$git_root"
elif [ -f "$item" ]; then
printf "%s%s πŸ“„ %s\n" "$base_prefix" "$connector" "$basename"
size=$(wc -c < "$item")
if [ "$size" -gt "$MAX_SIZE" ]; then
printf "%s[file too large: %s bytes]\n" "$content_prefix" "$size"
continue
fi
if is_binary "$item"; then
printf "%s[binary file]\n" "$content_prefix"
continue
fi
while IFS= read -r line || [ -n "$line" ]; do
printf "%s%s\n" "$content_prefix" "$line"
done < "$item"
fi
done
}
# --- Main Logic ---
is_first_arg=true
for TARGET_PATH in "$@"; do
# Add a separator between outputs for multiple arguments
if [ "$is_first_arg" = true ]; then
is_first_arg=false
else
printf "\n---\n\n"
fi
if [ -d "$TARGET_PATH" ]; then
# Run in a subshell to prevent `cd` from affecting subsequent arguments.
(
ABS_TARGET_DIR="$(cd "$TARGET_PATH" 2>/dev/null && pwd)"
if [ -z "$ABS_TARGET_DIR" ]; then
printf "Error: Could not access target directory: %s\n" "$TARGET_PATH" >&2
exit 1
fi
GIT_ROOT=""
if [ "$USE_GITIGNORE" = true ] && command -v git >/dev/null 2>&1; then
GIT_ROOT=$(find_git_root "$ABS_TARGET_DIR")
fi
printf "πŸ“ %s/\n" "$(basename "$TARGET_PATH")"
cd "$TARGET_PATH" || exit 1
process_directory "." "" "$GIT_ROOT"
)
elif [ -f "$TARGET_PATH" ]; then
# --- It's a file, run the single-file logic ---
process_file "$TARGET_PATH"
else
# --- It's neither a file nor a directory ---
printf "Error: Path not found or is not a regular file/directory: %s\n" "$TARGET_PATH" >&2
fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment