Skip to content

scaffold_kit.tree

Generates a hierarchical tree representation of a directory.

This module scans a specified directory, builds a hierarchical structure of its contents, and renders a text-based tree diagram. It uses an IgnoreParser to exclude files and directories based on patterns found in a specified ignore file (e.g., ‘.gitignore’). The generated tree is written to a file and also printed to the console.

Usage

To run this script, navigate to your project’s root directory and execute it as a module:

Generate full project tree:#

$ uv run python -m scaffold_kit.tree

Generate partial tree from subdirectory:#

$ uv run python -m scaffold_kit.tree my_project/data

build_tree_structure(paths, ignore_matches) #

Builds a hierarchical tree structure from a list of file paths.

Parameters:

Name Type Description Default
paths list[str]

A list of file/directory paths.

required
ignore_matches Callable

A function to check if a path should be ignored.

required

Returns:

Type Description
dict

A nested dictionary representing the directory structure.

Source code in src/scaffold_kit/tree.py
def build_tree_structure(paths: list[str], ignore_matches: Callable) -> dict:
    """Builds a hierarchical tree structure from a list of file paths.

    Args:
        paths: A list of file/directory paths.
        ignore_matches: A function to check if a path should be ignored.

    Returns:
        A nested dictionary representing the directory structure.
    """
    tree = {}

    for path in paths:
        if ignore_matches(path):
            continue

        parts = Path(path).parts
        current = tree

        # Build the nested structure by iterating through path parts.
        for _, part in enumerate(parts):
            if part not in current:
                current[part] = {}
            current = current[part]

    return tree

generate_tree(root_dir='.', ignore_file='.gitignore', output_file='directory-tree.txt', output_dir=None) #

Generates a directory tree of files in the specified directory.

The function scans a directory, applies ignore patterns, and creates a text-based tree representation that is saved to a file and printed to the console.

Parameters:

Name Type Description Default
root_dir str

The root directory to scan (default: current directory).

'.'
ignore_file str

The name of the file containing ignore patterns.

'.gitignore'
output_file str

The name of the output file for the tree.

'directory-tree.txt'
output_dir Optional[str]

The directory where the output file will be saved. (default: current directory).

None

Raises:

Type Description
SystemExit

If the specified root_dir does not exist.

Source code in src/scaffold_kit/tree.py
def generate_tree(
    root_dir: str = ".",
    ignore_file: str = ".gitignore",
    output_file: str = "directory-tree.txt",
    output_dir: Optional[str] = None,
):
    """Generates a directory tree of files in the specified directory.

    The function scans a directory, applies ignore patterns, and creates
    a text-based tree representation that is saved to a file and printed
    to the console.

    Args:
        root_dir: The root directory to scan (default: current directory).
        ignore_file: The name of the file containing ignore patterns.
        output_file: The name of the output file for the tree.
        output_dir: The directory where the output file will be saved.
            (default: current directory).

    Raises:
        SystemExit: If the specified `root_dir` does not exist.
    """
    # 1. Validate that the root directory exists.
    if not os.path.isdir(root_dir):
        print(f"Error: Directory '{root_dir}' does not exist.")
        sys.exit(1)

    # 2. Generate the output filename, sanitizing for partial trees.
    if root_dir == ".":
        display_root = "."
    else:
        # Sanitize path for filename (replace slashes with dashes).
        sanitized_path = root_dir.replace("/", "-").replace("\\", "-")
        output_file_base = Path(output_file).stem
        output_file = f"{output_file_base}-{sanitized_path}.txt"
        # Use just the last directory name for display.
        display_root = Path(root_dir).name + "/"

    # 3. Always read ignore file from the original project root.
    parser = IgnoreParser.from_file(ignore_file)

    # 4. Change to the target directory for scanning.
    original_cwd = os.getcwd()
    os.chdir(root_dir)

    try:
        # 5. Get all paths using glob, and add directories explicitly.
        # pylint: disable=unexpected-keyword-arg
        all_paths = glob.glob("**/*", recursive=True, include_hidden=True)

        all_dirs = set()
        for path in all_paths:
            p = Path(path)
            # Add all parent directories.
            for parent in p.parents:
                if parent != Path("."):
                    all_dirs.add(str(parent))

        # Combine files and directories.
        all_items = list(set(all_paths + list(all_dirs)))

        # 6. Filter paths using the ignore rules.
        if root_dir != ".":
            # For partial trees, use full paths for ignore checking.
            full_paths_for_ignore = [
                os.path.join(original_cwd, root_dir, p) for p in all_items
            ]
            filtered_relative_paths = [
                rel_path
                for rel_path, full_path in zip(all_items, full_paths_for_ignore)
                if not parser.matches(full_path)
            ]
        else:
            # For full tree, paths are already correct for ignore checking.
            filtered_relative_paths = [
                p for p in all_items if not parser.matches(p)
            ]

        sorted_paths = sorted(filtered_relative_paths)

        # 7. Build and render the tree structure.
        tree = build_tree_structure(
            sorted_paths, lambda x: False
        )  # No additional filtering needed
        lines = [display_root]
        if tree:
            lines.extend(render_tree(tree))

        content = "\n".join(lines)

    finally:
        # 8. Always return to the original directory.
        os.chdir(original_cwd)

    # 9. Write the output file in the original or set directory.
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
        print(f"Directory '{output_dir}' created successfully!")

    # Sanitize and assemble the final output file path.
    output_filename = (
        f"{slugify(Path(output_file).stem)}{Path(output_file).suffix}"
    )
    output_path = Path(output_dir) / output_filename

    print(f"Writing {output_path}...")
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(content)

    print(content)
    print(f"\nSuccessfully wrote directory tree to {output_path}")

main() #

Main entry point to run the directory tree generation process.

Parses command-line arguments and runs the tree generation process.

Source code in src/scaffold_kit/tree.py
def main():
    """Main entry point to run the directory tree generation process.

    Parses command-line arguments and runs the tree generation process.
    """
    # 1. Create the argument parser.
    parser = argparse.ArgumentParser(
        description="Generate a directory tree of the project structure"
    )
    # 2. Add the positional argument for the root directory.
    parser.add_argument(
        "root_dir",
        nargs="?",
        default=".",
        help="Root directory to scan (default: current directory)",
    )
    # 3. Add the optional argument for the ignore file.
    parser.add_argument(
        "--ignore-file",
        default=IGNORE_FILE,
        help=f"Ignore file to use (default: {IGNORE_FILE})",
    )

    args = parser.parse_args()
    generate_tree(
        args.root_dir,
        args.ignore_file,
        output_file=TREE_FILE,
        output_dir=TREE_DIRECTORY,
    )

render_tree(tree, prefix='', current_path='') #

Recursively renders the tree structure with proper tree characters.

Parameters:

Name Type Description Default
tree dict

The nested dictionary representing the directory structure.

required
prefix str

The current line prefix for indentation.

''
current_path str

The full path to the current directory being processed.

''

Returns:

Type Description
list

A list of formatted lines representing the tree structure.

Source code in src/scaffold_kit/tree.py
def render_tree(tree: dict, prefix: str = "", current_path: str = "") -> list:
    """Recursively renders the tree structure with proper tree characters.

    Args:
        tree: The nested dictionary representing the directory structure.
        prefix: The current line prefix for indentation.
        current_path: The full path to the current directory being processed.

    Returns:
        A list of formatted lines representing the tree structure.
    """
    lines = []
    items = list(tree.items())
    sorted_items = sort_tree_items(items, current_path)

    for i, (name, subtree) in enumerate(sorted_items):
        is_last_item = i == len(sorted_items) - 1

        # Choose the appropriate tree character based on position.
        if is_last_item:
            current_prefix = prefix + "└── "
            next_prefix = prefix + "    "
        else:
            current_prefix = prefix + "├── "
            next_prefix = prefix + "│   "

        # Construct the full path to check if it's a directory.
        full_path = os.path.join(current_path, name) if current_path else name

        # Add directory indicator for directories.
        display_name = name + "/" if os.path.isdir(full_path) else name
        lines.append(current_prefix + display_name)

        # Recursively render subdirectories.
        if subtree:
            lines.extend(render_tree(subtree, next_prefix, full_path))

    return lines

sort_tree_items(items, current_path='') #

Sorts items with directories first, then files, both alphabetically.

Parameters:

Name Type Description Default
items list

A list of (name, subtree) tuples.

required
current_path str

The current path for checking if items are directories.

''

Returns:

Type Description
list

A sorted list of (name, subtree) tuples.

Source code in src/scaffold_kit/tree.py
def sort_tree_items(items: list, current_path: str = "") -> list:
    """Sorts items with directories first, then files, both alphabetically.

    Args:
        items: A list of `(name, subtree)` tuples.
        current_path: The current path for checking if items are directories.

    Returns:
        A sorted list of `(name, subtree)` tuples.
    """
    directories = []
    files = []

    # 1. Separate directories and files.
    for name, subtree in items:
        # Construct the full path to check if it's a directory.
        full_path = os.path.join(current_path, name) if current_path else name

        if os.path.isdir(full_path):
            directories.append((name, subtree))
        else:
            files.append((name, subtree))

    # 2. Sort each category alphabetically.
    directories.sort(key=lambda x: x[0].lower())
    files.sort(key=lambda x: x[0].lower())

    return directories + files