Treegraph.sh a tool for generating pretty file structure graphs
Linux has a nice little tool in the repositories called tree
generates a nice console window-based directory map from the current location down. It’s great, but there’s not an equivalent in FreeBSD, though you can get the basic need met using find and some text processing pipes like
find . -type d -maxdepth 3 | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/"
. |-subdir0 | |-subsubdir1 | |-subsubdir0 |-subdir3 |-sub dir 2
However, this is enough to make me wish for something more visual. As usual, someone else already did it, but their version wasn’t quite what I wanted so I enlisted Claude.ai to help generate a little script that, like the above command, find
s it’s way through a directory structure and builds a .dot
file map as it goes, which can be converted into useful graphic formats with graphviz
.
The code (below or at gitlab) is pretty easy to use, it does the dotfile generation then just parse the dotfile like usual with graphviz, either using an installed version or online.
treegraph.sh -d /usr/home -f -s -m 2 > tree.dot dot -Tsvg tree.dot > tree.svg
The edges seem to overlap a bit more than I’d like, but that’s an issue with graphviz
and if you really want a presentation ready graph can be adjusted manually with invisible nodes or edges and manually set connection points to merge lines. It defaults to dot
, which makes sense for ranks, but looks cool with neato
or twopi
as well.
#!/usr/local/bin/bash # Help function Help() { echo "Usage: treegraph.sh [options]" echo echo "Options:" echo " -h Display this help message" echo " -f Enumerate both directories and files (default: directories only)" echo " -d DIR Specify the root directory to parse (default: current directory)" echo " -m DEPTH Specify the maximum depth of parsing (default: no limit)" echo " -s Compute the size of folders and files using 'du -sh' (default: off)" echo echo "Examples:" echo " treegraph.sh # Current directory, directories only" echo " treegraph.sh -d /usr/home -f # Parse /usr/home, include files" echo " treegraph.sh -d /usr/home -m 3 # Parse /usr/home with max depth 3" echo " treegraph.sh -s # Show sizes for current directory" echo " treegraph.sh -d /usr/home -f -s -m 2 # All options combined" echo echo "Example generating an SVG file (requires graphviz installed):" echo " treegraph.sh -d /usr/home -f -s -m 2 > tree.dot" echo " dot -Tsvg tree.dot > tree.svg" echo echo "When using -s, nodes are color-coded by size:" echo " B (bytes) - gray50" echo " K (KB) - black" echo " M (MB) - blue2" echo " G (GB) - darkorange3" echo " T (TB) - crimson" } # Initialize variables with defaults StartDir="." Type="d" MaxDepth="" MaxDepthArg="" ShowSizes=0 # Process options while getopts ":hfsd:m:" option; do case $option in h) # Display help Help exit;; f) # Include files Type="f";; s) # Show sizes ShowSizes=1;; d) # Directory StartDir="$OPTARG";; m) # Maximum depth MaxDepth="$OPTARG" MaxDepthArg="-maxdepth $MaxDepth";; \?) # Invalid option echo "Error: Invalid option" Help exit 1;; :) # Missing argument echo "Error: Option -$OPTARG requires an argument" Help exit 1;; esac done # Build find command based on Type if [ "$Type" = "f" ]; then TypeArg="" # No type filter for both files and directories else TypeArg="-type d" # Only directories (default) fi # Get absolute path of starting directory for proper path handling StartDirAbs=$(cd "$StartDir" 2>/dev/null && pwd) || StartDirAbs="$StartDir" # Extract base directory name for display BaseDir=$(basename "$StartDirAbs") # If showing sizes, get root directory size if [ $ShowSizes -eq 1 ]; then RootSizeInfo=$(du -sh "$StartDir" 2>/dev/null | awk '{print $1}') # Determine color based on size unit if [[ "$RootSizeInfo" =~ [0-9]+B ]]; then RootColor="gray50" elif [[ "$RootSizeInfo" =~ [0-9]+K ]]; then RootColor="black" elif [[ "$RootSizeInfo" =~ [0-9]+M ]]; then RootColor="blue2" elif [[ "$RootSizeInfo" =~ [0-9]+G ]]; then RootColor="darkorange3" elif [[ "$RootSizeInfo" =~ [0-9]+T ]]; then RootColor="crimson" else RootColor="black" # Default color fi SizeLabel=" ($RootSizeInfo)" ColorAttr=", fontcolor=\"$RootColor\"" else SizeLabel="" ColorAttr="" fi # Run find and generate graphviz dotfile ( echo "digraph directory_structure {" echo " node [shape=box,style=rounded,width=0.1,height=0.1,margin=\"0.07,0.01\"];" echo " edge [arrowhead=dot, arrowtail=dot, dir=both, arrowsize=0.3, color=gray];" echo " nodesep=0.15;" echo " rankdir=LR;" echo " concentrate=true;" echo " overlap=false;" echo " splines=true;" echo " searchsize=10000;" echo " \"$StartDir\" [label=\"$BaseDir$SizeLabel\"$ColorAttr];" # Process find output but skip the root directory itself find "$StartDir" $TypeArg $MaxDepthArg | grep -v "^$StartDir\$" | awk -v start_dir="$StartDir" -v type="$Type" -v show_sizes=$ShowSizes ' { current=$0; # Check if this is a file or directory is_file = 0; if (type == "f") { cmd = "test -f \"" current "\" && echo 1 || echo 0"; cmd | getline is_file; close(cmd); } # Get the parent path parent = current; sub(/\/[^\/]*$/, "", parent); # Remove last component for parent if (parent == "" || parent == ".") parent = start_dir; # Extract just the name of the current node (not the full path) split(current, path_parts, "/"); node_name = path_parts[length(path_parts)]; # Get size if requested size_info = ""; color_attr = ""; if (show_sizes == 1) { cmd = "du -sh \"" current "\" 2>/dev/null | awk '\''{print $1}'\''"; cmd | getline size_info; close(cmd); if (size_info != "") { # Determine color based on size unit if (size_info ~ /[0-9]+B/) { color = "gray50"; } else if (size_info ~ /[0-9]+K/) { color = "black"; } else if (size_info ~ /[0-9]+M/) { color = "blue2"; } else if (size_info ~ /[0-9]+G/) { color = "darkorange3"; } else if (size_info ~ /[0-9]+T/) { color = "crimson"; } else { color = "black"; # Default color } size_info = " (" size_info ")"; color_attr = ", fontcolor=\"" color "\""; } } # Create node with appropriate shape and just the name as label if (is_file) { printf " \"%s\" [shape=plain, width=0, margin=0, label=\"%s%s\"%s];\n", current, node_name, size_info, color_attr; } else { printf " \"%s\" [label=\"%s%s\"%s];\n", current, node_name, size_info, color_attr; } # Create node connection printf " \"%s\" -> \"%s\";\n", parent, current; } ' echo "}" ) # End of generated dotfile