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, finds 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

Leave a Reply
You must be logged in to post a comment.