|
Hi Rick,
Great Job.
I hope you don't mind but it inspired me to convert it to PowerShell.
Here is the PowerShell Version:
<
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
Did very little optimizations and code changes from original C
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"/>.
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 {
@(' <!Access Denied!>')
}
}
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))
}
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()
$null = $sbTree.AppendLine($startPath)
$null = $sbTree.AppendLine()
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"
}
}
<
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')
|
|
|
|
|
Wow, did you use some tool to convert the C# code?
|
|
|
|
|
Hi Rick,
I had that same thought "Wouldn't be nice to have a C# to Powershell converter". 1/2 way through converting your code manually, I thought if I should try writing one. It was just a thought.
If you do come across a good C# to PowerShell language converter, please let me know.
To write a PS to C# converter would require having a better understanding of the C# language syntax. I'd look at using the Abstract Syntax Tree (AST) Classes (System.Management.Automation.Language) but just haven't had time. Was easier to just manually code the conversion.
I've been using PowerShell almost exclusively for the past decade. I have on occasion, had to convert PowerShell WPF GUI based applications to .Net WPF C#. I never really liked C++ or C#.
My IT experiences started in 1981 with IBM 370, UNIX, CP/M, Cromix, HP-3000, Primos, OS/2, Windows, and Linux for OS, and BAL Assembler, PL/1, Z-80 Assembly, C, x-Base, ADA, Relate-3000, and then moved to Delphi (Object PASCAL), Java, PL/SQL & T/SQL and used C-Shell, Borne Shell, TCL/TK, REXX, DOS Batch, perl, and PowerShell for scripting languages. My expertise was in technology development, systems design, development, and integration, reverse engineering, API development, ETL, Oracle, Directory Services, IdAM, Document Management, and automation.
I enjoyed C & Delphi but most organizations don't know the Power and Productivity gains using Delphi, I had to embrace Visual Studio & C#. When I moved into a operations support role, I embraced PowerShell for everything due to its similarity to TCL and its flexibility and have never looked backed. Now I do Azure Security and Policy management using PowerShell & Azure RM, Graph, Office365 PS Modules & APIs.
I still love using the PowerShell_ISE to edit code which is PS Version 5.x because of the Intellisense. I would use VS Code to develop Powershell 7.x code but usually try to avoid it.
The new Powershell is version 7.2.x which supports .Net Core and has other advanced features such as the ability to run in Windows, Linux, and Mac OS. When writing Linux / OS Code, you always have to remember when it comes to file & folder paths, it's CASE Sensitive and requires '/' (Windows PS supports both \ and /) as the Path Separator.
One suggestion from the comments about your DirTree was to use $FSO.Directory.EnumerateFileSystemInfos() which is faster than using GetDirectories() but requires using the additional options for case & scope such as:
$startingPath = [System.IO.DirectoryInfo]'c:\'
$EnumerationOptions = [System.IO.EnumerationOptions]::new()
$EnumerationOptions.RecurseSubdirectories = $true
$EnumerationOptions.MatchCasing = [System.IO.MatchCasing]::CaseInsensitive
$searchFilter = $Filter
$Directories = $startingPath.EnumerateDirectories($searchFilter, $EnumerationOptions)
modified 4-Jul-22 16:30pm.
|
|
|
|
|
|
It may be more efficient to use Directory.EnumerateDirectories rather than Directory.GetDirectories and then employ a bit of Linq to process the enumerable using deferred execution. Something like
static string[] GetFoldersFiltered(string path, string filter)
{
var folders = Directory.EnumerateDirectories(path, filter);
return folders.Where(s => !s.Replace(path, "").StartsWith(".") && !excludeList.Contains(s.Replace(path, ""))).ToArray();
}
|
|
|
|
|
There is room for improvement, as mentioned this is a "quick and dirty" implementation. I did test it on a big VS solution with more than hundred projects and it was fast enough for me, might not be the case on older machines without an SSD however 
|
|
|
|
|
I forgot to add a +5 from me for the excellent way you built the tree structure. They are not easy to implement.
|
|
|
|
|
Thanks, that compensates for the negative downvote of 1, of course given without any motivation as usual 
|
|
|
|
|
It would be helpful to include an example of the output. I frequently use the Windows "Tree" command, I'm curious how this compares visually to the output from that command.
|
|
|
|
|
Good idea, added a screendump from the open-source Bug Tracker "Budoco" 
|
|
|
|
|
Rick, Thanks for the useful piece. Is the download link functional? It simply redirects my browser to this page.
|
|
|
|
|
The download link does not seem to work, but you can just make a Console App and paste the code in program.cs.
Good luck!
|
|
|
|
|
Yes, that is fine but, as a link was provided, I thought it best to download the solution to be sure that the program is compiled and executed using the same settings as your version. Thanks again, George.
|
|
|
|
|