Last active
September 5, 2025 05:33
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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