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.

Revisions

  1. bulatovv revised this gist Sep 5, 2025. No changes.
  2. bulatovv revised this gist Sep 5, 2025. No changes.
  3. bulatovv revised this gist Sep 5, 2025. 1 changed file with 85 additions and 49 deletions.
    134 changes: 85 additions & 49 deletions treecat.sh
    Original file line number Diff line number Diff line change
    @@ -1,22 +1,23 @@
    #!/bin/sh

    # treecat - A script to display directory structure and file contents.
    # (Generated using Gemini Pro 2.5)
    # 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_DIR="."
    # TARGET_PATH is now handled by arguments

    # --- Helper Functions ---

    # Print usage information and exit.
    usage() {
    cat <<EOF
    Usage: $(basename "$0") [options] [directory]
    Usage: $(basename "$0") [options] [path1] [path2] ...
    Displays the directory structure and file contents in a tree format.
    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 '.').
    @@ -27,10 +28,9 @@ Options:
    EOF
    }

    # Check if a file is binary. A file is considered binary if its first 512
    # bytes contain any non-printable characters (excluding whitespace).
    # Returns 0 (true) if binary, 1 (false) otherwise.
    # 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
    @@ -41,8 +41,7 @@ is_binary() {
    # Traverse upwards from a given directory to find the root of a .git repository.
    find_git_root() {
    path="$1"
    # Resolve to an absolute path to handle `cd ..` safely
    path="$(cd "$path" 2>/dev/null && pwd)"
    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"
    @@ -82,48 +81,61 @@ while [ "$#" -gt 0 ]; do
    exit 1
    ;;
    *)
    TARGET_DIR="$1"
    shift
    # Not an option, so it must be a path. Stop option processing.
    break
    ;;
    esac
    done

    # --- Main Logic ---

    if ! [ -d "$TARGET_DIR" ]; then
    printf "Error: Directory not found: %s\n" "$TARGET_DIR" >&2
    exit 1
    # If no paths are left after parsing options, default to the current directory.
    if [ "$#" -eq 0 ]; then
    set -- "."
    fi

    # Determine the absolute path of the target for gitignore checks BEFORE changing directory.
    ABS_TARGET_DIR="$(cd "$TARGET_DIR" 2>/dev/null && pwd)"
    if [ -z "$ABS_TARGET_DIR" ]; then
    printf "Error: Could not access target directory: %s\n" "$TARGET_DIR" >&2
    exit 1
    fi

    # Find the git repository root if enabled.
    GIT_ROOT=""
    if [ "$USE_GITIGNORE" = true ] && command -v git >/dev/null 2>&1; then
    GIT_ROOT=$(find_git_root "$ABS_TARGET_DIR")
    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 main recursive function to process a directory.

    # 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.
    process_path() {
    # $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

    @@ -132,14 +144,12 @@ process_path() {
    basename=$(basename "$item")

    # Filter based on gitignore if applicable
    if [ -n "$GIT_ROOT" ]; then
    # Use -C to run git from the repo root, checking the path from find.
    # Since we chdir'd, "$item" is already the correct relative path.
    if git -C "$GIT_ROOT" check-ignore -q "$item"; then
    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} "
    @@ -152,18 +162,18 @@ process_path() {

    if [ -d "$item" ]; then
    printf "%s%s 📁 %s/\n" "$base_prefix" "$connector" "$basename"
    process_path "$item" "$next_base_prefix"
    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" "$base_prefix" "$size"
    printf "%s[file too large: %s bytes]\n" "$content_prefix" "$size"
    continue
    fi

    if is_binary "$item"; then
    printf "%s [binary file]\n" "$base_prefix"
    printf "%s[binary file]\n" "$content_prefix"
    continue
    fi

    @@ -174,14 +184,40 @@ process_path() {
    done
    }

    # --- Initial Call ---
    # --- Main Logic ---

    # Print the initial root directory name based on the original argument.
    printf "📁 %s/\n" "$(basename "$TARGET_DIR")"
    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

    # Change to the target directory to simplify path handling for `find` and `git`.
    cd "$TARGET_DIR" || exit 1
    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

    # Start the recursive processing from the current directory (".").
    process_path "." ""
    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
  4. bulatovv created this gist Sep 4, 2025.
    187 changes: 187 additions & 0 deletions treecat.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,187 @@
    #!/bin/sh

    # treecat - A script to display directory structure and file contents.
    # (Generated using Gemini Pro 2.5)

    # --- Default Configuration ---
    MAX_SIZE=102400 # 100 KB
    SHOW_HIDDEN=false
    USE_GITIGNORE=true
    TARGET_DIR="."

    # --- Helper Functions ---

    # Print usage information and exit.
    usage() {
    cat <<EOF
    Usage: $(basename "$0") [options] [directory]
    Displays the directory structure and file contents in a tree format.
    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. A file is considered binary if its first 512
    # bytes contain any non-printable characters (excluding whitespace).
    # Returns 0 (true) if binary, 1 (false) otherwise.
    is_binary() {
    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"
    # Resolve to an absolute path to handle `cd ..` safely
    path="$(cd "$path" 2>/dev/null && pwd)"
    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
    ;;
    *)
    TARGET_DIR="$1"
    shift
    ;;
    esac
    done

    # --- Main Logic ---

    if ! [ -d "$TARGET_DIR" ]; then
    printf "Error: Directory not found: %s\n" "$TARGET_DIR" >&2
    exit 1
    fi

    # Determine the absolute path of the target for gitignore checks BEFORE changing directory.
    ABS_TARGET_DIR="$(cd "$TARGET_DIR" 2>/dev/null && pwd)"
    if [ -z "$ABS_TARGET_DIR" ]; then
    printf "Error: Could not access target directory: %s\n" "$TARGET_DIR" >&2
    exit 1
    fi

    # Find the git repository root if enabled.
    GIT_ROOT=""
    if [ "$USE_GITIGNORE" = true ] && command -v git >/dev/null 2>&1; then
    GIT_ROOT=$(find_git_root "$ABS_TARGET_DIR")
    fi

    # The main recursive function to process a directory.
    # $1: The path to process (relative to the chdir'd location).
    # $2: The prefix for printing tree lines.
    process_path() {
    local path="$1"
    local base_prefix="$2"

    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
    # Use -C to run git from the repo root, checking the path from find.
    # Since we chdir'd, "$item" is already the correct relative path.
    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_path "$item" "$next_base_prefix"
    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" "$base_prefix" "$size"
    continue
    fi

    if is_binary "$item"; then
    printf "%s [binary file]\n" "$base_prefix"
    continue
    fi

    while IFS= read -r line || [ -n "$line" ]; do
    printf "%s%s\n" "$content_prefix" "$line"
    done < "$item"
    fi
    done
    }

    # --- Initial Call ---

    # Print the initial root directory name based on the original argument.
    printf "📁 %s/\n" "$(basename "$TARGET_DIR")"

    # Change to the target directory to simplify path handling for `find` and `git`.
    cd "$TARGET_DIR" || exit 1

    # Start the recursive processing from the current directory (".").
    process_path "." ""