Click here to Skip to main content
15,887,683 members
Articles / Programming Languages / C#
Tip/Trick

DirTree: A Simplified Directory Tree Tool

Rate me:
Please Sign up or sign in to vote.
3.90/5 (6 votes)
29 Jun 2022CPOL 8.6K   158   11   13
Console app to scan a VS directory and create a simplified directory tree
This is a quick and dirty Console app written in VS2019 and .NET 4.8 to scan a VS directory and create a simplified directory tree.

Introduction

This quick and dirty Console app can be used to produce a simplified tree representation similar to the DOS Tree command, but with an option to limited the tree depth.

Directories starting with '.' are excluded, and also other directories like bin and obj.

Below is an example tree from the open-source Budoco VS Code solution (Bug Tracker in ASP.NET Core available on GitHub).

Image 1

Using the Code

Command line usage:

DirTree [path] [depth]

If no path is given, the current directory is used.
Depth must be >= 0, default value is 2.

So without further ado, here is the source code:

C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace DirTree
{
    /// <summary>
    /// Console app to scan a VS directory and create a simplified directory tree.
    /// Command line usage: 
    ///   DirTree [path] [depth]
    /// If no path is given, the current directory is used.
    /// Depth must be >= 0, default value is 2.
    /// </summary>
    class Program
    {
        static string filter = "*.*";
        static string startPath;
        static List<string> excludeList = new List<string>();
        static int folderLevel;
        static int treeDepth = 2;
        static int folderLevelStart;
        static string lastFolder;
        static bool lastDirFlag;
        static int tabSize = 3;
        static StringBuilder sbTree;        // Optionally write to DirTree.txt

        static void Main(string[] args)
        {
            if (args.Length > 0)
            {
                startPath = args[0];

                if (args.Length > 1)
                {
                    treeDepth = int.Parse(args[1]);
                }
            }

            if (string.IsNullOrEmpty(startPath))
            {
                startPath = Environment.CurrentDirectory;
            }

            if (!startPath.EndsWith(@"\"))
            {
                startPath += @"\";
            }

            sbTree = new StringBuilder(50);
            sbTree.AppendLine(startPath);
            sbTree.AppendLine();

            // Add directories to exclude
            excludeList.Add("bin");
            excludeList.Add("obj");
            excludeList.Add("x64");
            excludeList.Add("x86");
            excludeList.Add("Properties");

            folderLevelStart = startPath.Count(x => x == '\\');
            var dirs = GetFoldersFiltered(startPath, filter);

            if (dirs.Length > 0)
            {
                lastFolder = dirs[dirs.Length - 1];
                ScanFolders(startPath);
                //File.WriteAllText("DirTree.txt", sbTree.ToString()); // Enable this 
                                                          // to write DirTree.txt
            }
            else
            {
                Console.WriteLine("No directories found.");
            }

            Console.ReadKey();
        }

        /// <summary>
        /// Get folders array, exclude folders with excludeList and 
        /// starting with '.' like .git .vs.
        /// </summary>
        /// <param name="path">The path</param>
        /// <param name="filter">Wildcard filter</param>
        /// <returns>A string array</returns>
        static string[] GetFoldersFiltered(string path, string filter)
        {
            var folders = Directory.GetDirectories(path, filter).ToList();

            for (int i = folders.Count - 1; i >= 0; i--)
            {
                string dirFullname = folders[i];
                string dirname = dirFullname.Replace(path, "");

                if (dirname.StartsWith(".") || excludeList.Contains(dirname))
                {
                    folders.RemoveAt(i);
                }
            }

            return folders.ToArray();
        }

        /// <summary>
        /// Recursively scan folders with a depth of <see cref="treeDepth"/>.
        /// </summary>
        /// <param name="path">The start path</param>
        /// <param name="continueLine">The folder level to continue vertical line
        /// </param>
        /// <param name="previous_indent">The previous indentation</param>
        static void ScanFolders(string path, int continueLine = -1, 
                                string previous_indent = "")
        {
            int i = 0;

            if (!path.EndsWith(@"\"))
            {
                path += @"\";
            }

            var dirs = GetFoldersFiltered(path, filter);
            int lastNode = dirs.Length - 1;

            while (i < dirs.Length)
            {
                string dirFullname = dirs[i];
                string dirname = dirFullname.Replace(path, "");
                string indent = "  ";
                string indent2 = string.Empty;

                if (dirFullname.StartsWith(lastFolder))
                {
                    lastDirFlag = true;
                }

                folderLevel = dirFullname.Count(x => x == '\\') - folderLevelStart;

                for (int j = 0; j < folderLevel; j++)
                {
                    if (lastDirFlag && continueLine >= j)
                    {
                        indent += " " + new string(' ', tabSize);
                    }
                    else
                    {
                        indent += "│" + new string(' ', tabSize);
                    }
                }

                if (previous_indent.Length > 0)
                {
                    indent = previous_indent + indent.Substring(previous_indent.Length);
                }

                if (i == lastNode)
                {
                    indent2 = "└" + new string('─', tabSize);
                    continueLine = folderLevel;
                }
                else
                {
                    indent2 = "├" + new string('─', tabSize);
                }

                Console.WriteLine($"{indent}{indent2}{dirname}");
                sbTree.AppendLine($"{indent}{indent2}{dirname}");

                if (folderLevel < treeDepth)
                {
                    if (i == lastNode)
                    {
                        indent += " " + new string(' ', tabSize);
                    }

                    // Recursive call
                    ScanFolders(dirFullname, continueLine, indent);
                }

                i++;
            }
        }
    }
}

History

  • 29th June, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Netherlands Netherlands
Software developer in the Netherlands, currently working on Video Surveillance applications.
Experience: C#, C++, VB, ASP, SQL Server, PostgreSQL, Gitea, TeamCity.
It all started with Black&White, no not the whiskey but the Sinclair ZX81 followed by several Atari's and PC's. The journey continues ...

Comments and Discussions

 
QuestionPowerShell Conversion Pin
Brooks Vaughn (US)2-Jul-22 7:36
Brooks Vaughn (US)2-Jul-22 7:36 
Hi Rick,

Great Job.

I hope you don't mind but it inspired me to convert it to PowerShell.

Here is the PowerShell Version:

PowerShell
<#
    DirTree.ps1 - DirTree: a simplified directory tree tool

    Converted from Rick Zeeland's Article on CodeProject from 29 Jun 2022
        https://www.codeproject.com/Tips/5336104/DirTree-a-simplified-directory-tree-tool

    A quick and dirty Console app written in VS2019 and .NET 4.8 to scan a VS directory and create a simplified directory tree.

    Console app to scan a VS directory and create a simplified directory tree.
    Command line usage: 
       DirTree [path] [depth]
    If no $path is given, the current directory is used.
    Depth must be >= 0, default value is 2.

    PowerShell Script Notes:
      Converted from Rick Zeeland's C# DirTree.cs program  by Brooks Vaughn (developers @ brooksvaughn . net)
      Did very little optimizations and code changes from original C# code
      Added several switches and options
      If a Folder cannot be accessed, <!Access Denied!> is appended to the Folder Name
    Parameters
      -Path <Starting Directory Path> Defaults to Current Folder
      -Depth <Max Nested Directory Levels> Defaults to 2
      -filter <Directory Search Pattern> Defaults to '*.*'
      -OutputFile <FilePath> Defaults to 'C:\Temp\DirTree\DirTree.txt'
      -TimeStampFmt = 'yyyy-MMdd-HHmm'
      -excludeList <Array List of Folder Names to exclude from output> Defaults to @('bin', 'obj', 'x64', 'x86', 'Properties')
    Switches
      -NoPrompt will not pause at end of script
      -SaveOutput will enable saving output to file
      -Quiet will enable -SaveOutput
      -TimeStamp will append the DateTime to OutputFile as -TimeStampFmt

Includes Converted Functions:
DirTree() - Main Function -- Console app to scan a VS directory and create a simplified directory tree
GetFoldersFiltered() - Get folders array, exclude folders with $excludeList and starting with '.' like .git .vs.
ScanFolders() - Recursively scan folders with a depth of <see cref="$treeDepth"/>.

#>

# <summary>
# Get folders array, exclude folders with $excludeList and starting with '.' like .git .vs.
# </summary>
# <param name="path">The path</param>
# <param name="$filter">Wildcard $filter</param>
# <returns>A [string]array</returns>
Function GetFoldersFiltered() {
    [CmdletBinding()]
    Param(
        [string]$path,
        [string]$filter,
        $excludeList = $excludeList
    )
    try {
        [System.Collections.Generic.List[string]]$folders = [System.IO.Directory]::GetDirectories($path, $filter)
        for ($i = $folders.Count - 1; $i -ge 0; $i--) {
            [string]$dirname = $folders[$i].Replace($path, '')
            if ($dirname.StartsWith('.') -or $excludeList.Contains($dirname)) {
                $folders.RemoveAt($i)
            }
        }
        return $folders.ToArray()
    } catch {
        # Write-Warning $_.Exception.Message
        @(' <!Access Denied!>')
    }
}

# <summary>
# Recursively scan folders with a depth of <see cref="$treeDepth"/>.
# </summary>
# <param name="path">The start path</param>
# <param name="$continueLine">The folder level to continue vertical line</param>
# <param name="$previous_indent">The previous $indentation</param>
Function ScanFolders() {
    [CmdletBinding()]
    Param(
        [string]$path,
        [int]$continueLine = -1,
        [string]$previous_indent = [String]::Empty
    )
    $i = 0
    if (-not $path.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
        $path += [System.IO.Path]::DirectorySeparatorChar
    }

    $dirs = GetFoldersFiltered -path $path -filter $filter
    If ($dirs -is [Array] -or $dirs -notlike '*<!*!>') {
        $lastNode = $dirs.Count - 1
        while ($i -lt $dirs.Count) {
            If ($dirs -is [array]) {
                $dirFullname = $dirs[$i]
            } Else {
                $dirFullname = $dirs
            }
            $dirname = $dirFullname.Replace($path, '')
            $indent = '  '
            $indent2 = [string]::Empty
            if ($dirFullname.StartsWith($lastFolder)) {
                $lastDirFlag = $true
            }
            $folderLevel = $dirFullname.Split([System.IO.Path]::DirectorySeparatorChar).Count - $folderLevelStart
            for ($j = 0; $j -lt $folderLevel; $j++) {
                if ($lastDirFlag -and $continueLine -ge $j) {
                    $indent += (' ' + (' ' * $tabSize))
                } else {
                    $indent += ('│' + (' ' * $tabSize))
                }
            }

            if ($previous_indent.Length -gt 0) {
                $indent = $previous_indent + $indent.Substring($previous_indent.Length)
            }

            if ($i -eq $lastNode) {
                $indent2 = ('└' + ('─' * $tabSize))
                $continueLine = $folderLevel
            } else {
                $indent2 = ('├' + ('─' * $tabSize))
            }

            $Line = "`n{0}{1}{2}" -f $indent, $indent2, $dirname
            if (-not $Quiet) {
                Write-Host $line -NoNewline
            }
            $null = $sbTree.Append($Line)

            if ($folderLevel -lt $treeDepth) {
                if ($i -eq $lastNode) {
                    $indent += (' ' + (' ' * $tabSize))
                }

                # Recursive call
                ScanFolders -path $dirFullname -continueLine $continueLine -previous_indent $indent
            }
            $i++
        }
    } Else {
        if (-not $Quiet) {
            Write-Host $dirs -NoNewline
        }
        $null = $sbTree.Append($dirs)
    }

}

Function DirTree () {
    [CmdletBinding()]
    Param (
        [string]$Path,
        [string]$filter = '*.*',
        [int]$Depth = 2,
        [switch]$NoPrompt,
        [switch]$Quiet,
        [switch]$TimeStamp,
        [switch]$SaveOutput,
        [System.IO.FileInfo]$OutputFile = [System.IO.FileInfo]'C:\Temp\DirTree\DirTree.txt',
        [string]$TimeStampFmt = 'yyyy-MMdd-HHmm',
        [System.Collections.Generic.List[string]]$excludeList
    )
    if ($Path) {
        $startpath = $Path
    } Else {
        $startpath = [Environment]::CurrentDirectory
    }

    If ($Quiet) {
        $SaveOutput = $true
    }

    If ($SaveOutput -and -not $OutputFile.Directory.Exists) {
        $OutputFile.Directory.Create()
    }
    $OutFilePath = $OutputFile.FullName
    If ($TimeStamp) {
        $OutFilePath = $OutputFile.FullName.Replace($OutputFile.Extension, "_$(Get-Date -Format $TimeStampFmt)$($OutputFile.Extension)")
    }

    if (-not $startPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
        $startpath += [System.IO.Path]::DirectorySeparatorChar
    }

    $timeStampString = [string]::Empty
    [int]$treeDepth = $Depth

    [int]$folderLevel = 0
    [int]$folderLevelStart = 0
    [string]$lastFolder = [String]::Empty
    [bool]$lastDirFlag = $false
    [int]$tabSize = 3
    $sbTree = [System.Text.StringBuilder]::new() # Optionally write to DirTree.txt
    $null = $sbTree.AppendLine($startPath)
    $null = $sbTree.AppendLine()

    # Add directories to exclude List
    If (-not $excludeList) {
        $excludeList.Add('bin')
        $excludeList.Add('obj')
        $excludeList.Add('x64')
        $excludeList.Add('x86')
        $excludeList.Add('Properties')
    }

    $folderLevelStart = $startPath.Split([System.IO.Path]::DirectorySeparatorChar).Count
    $dirs = GetFoldersFiltered -path $startPath -filter $filter
    if ($dirs -and $dirs.Count -gt 0) {
        $lastFolder = $dirs[-1]
        ScanFolders -path $startPath
        $null = $sbTree.AppendLine()
        If ($SaveOutput) {
            $sbTree.ToString() | Out-File -FilePath $OutFilePath -Encoding utf8 -Force
            Write-Host ("`nOutput Saved to File: ({0})" -f $OutFilePath) -ForegroundColor Cyan
        }
    } else {
        Write-Host('No directories found.')
    }
    If (-not $NoPrompt) {
        Read-Host -Prompt "`n`nCompleted. Press <Return> key to exit"
    }
}

<# Usage Examples: # >
# Notes:
#   If a Folder cannot be accessed, <!Access Denied!> is appended to the Folder Name
# Parameters
#   -Path <Starting Directory Path> Defaults to Current Folder
#   -Depth <Max Nested Directory Levels> Defaults to 2
#   -filter <Directory Search Pattern> Defaults to '*.*'
#   -OutputFile <FilePath> Defaults to 'C:\Temp\DirTree\DirTree.txt'
#   -TimeStampFmt = 'yyyy-MMdd-HHmm'
#   -excludeList <Array List of Folder Names to exclude from output> Defaults to @('bin', 'obj', 'x64', 'x86', 'Properties')
# Switches
#   -NoPrompt will not pause at end of script
#   -SaveOutput will enable saving output to file
#   -Quiet will enable -SaveOutput
#   -TimeStamp will append the DateTime to OutputFile as -TimeStampFmt
DirTree
DirTree -Path C:\ -Depth 1
DirTree -SaveOutput
DirTree -SaveOutput -OutputFile C:\temp\TreeList.txt
DirTree -SaveOutput -OutputFile C:\temp\TreeList.txt -TimeStamp
DirTree -Quiet -NoPrompt
DirTree -Quiet -OutputFile C:\temp\DirTree.txt -TimeStamp
DirTree -Depth 1 -excludeList @('Roaming', 'bin', 'obj', 'x64', 'x86', 'Properties')
#>

PraiseRe: PowerShell Conversion Pin
RickZeeland2-Jul-22 8:00
mveRickZeeland2-Jul-22 8:00 
GeneralRe: PowerShell Conversion Pin
Brooks Vaughn (US)3-Jul-22 4:52
Brooks Vaughn (US)3-Jul-22 4:52 
GeneralRe: PowerShell Conversion Pin
Andreas Saurwein4-Jul-22 1:07
Andreas Saurwein4-Jul-22 1:07 
QuestionDeferred Execution Pin
George Swan30-Jun-22 9:29
mveGeorge Swan30-Jun-22 9:29 
AnswerRe: Deferred Execution Pin
RickZeeland30-Jun-22 9:36
mveRickZeeland30-Jun-22 9:36 
GeneralRe: Deferred Execution Pin
George Swan30-Jun-22 9:46
mveGeorge Swan30-Jun-22 9:46 
GeneralRe: Deferred Execution Pin
RickZeeland30-Jun-22 21:00
mveRickZeeland30-Jun-22 21:00 
QuestionOutput Example? Pin
DSchopp29-Jun-22 6:56
professionalDSchopp29-Jun-22 6:56 
PraiseRe: Output Example? Pin
RickZeeland29-Jun-22 7:29
mveRickZeeland29-Jun-22 7:29 
QuestionDownload Pin
George Swan29-Jun-22 3:06
mveGeorge Swan29-Jun-22 3:06 
AnswerRe: Download Pin
RickZeeland29-Jun-22 3:29
mveRickZeeland29-Jun-22 3:29 
GeneralRe: Download Pin
George Swan29-Jun-22 6:10
mveGeorge Swan29-Jun-22 6:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.