ptree.ps1

<#PSScriptInfo
 
.VERSION 1.0.0
 
.GUID c7dd965c-30f4-4d68-b592-7280489c3803
 
.AUTHOR theplantinthedesk
 
.COMPANYNAME N/A
 
.COPYRIGHT
 
.TAGS dir, tree, file, directory, powershell-script, directory-tree, directories, powershell-scripts, scripts, script
 
.LICENSEURI https://gitlab.com/treex/treex-cli/-/blob/main/LICENSE
 
.PROJECTURI https://gitlab.com/treex/treex-cli
 
.ICONURI https://gitlab.com/uploads/-/system/project/avatar/66207727/folder.png
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
 
.DESCRIPTION
 ptree is a PowerShell script designed to generate directory tree structures in a clear and organized format. It enables users to visualize the structure of directories and files on their system, optionally excluding specific directories, printing file contents, and saving the result to a markdown file or printing it to the console.
#>
 

param (
    [string]$path = ".",
    [string[]]$exclude = @(),
    [string]$outputFile = ""
)

function Write-ToFile {
    param (
        [string]$filePath,
        [string]$content
    )

    $utf8WithBom = New-Object System.Text.UTF8Encoding $true

    $streamWriter = New-Object System.IO.StreamWriter($filePath, $false, $utf8WithBom)
    $streamWriter.Write($content)
    $streamWriter.Close()
}

function Should-ExcludeItem {
    param (
        [string]$itemName,
        [string[]]$excludeList
    )

    return $excludeList -contains $itemName
}

function Process-FileContent {
    param (
        [string]$filePath,
        [ref]$contents
    )

    try {
        $fileContent = Get-Content -Path $filePath -Raw
        $contents.Value += "`r`n`r`n--- File: $filePath ---`r`n$fileContent`r`n"
    } catch {
        $contents.Value += "`r`n`r`n--- File: $filePath ---`r`n[Error: Could not read file contents]`r`n"
    }
}

function Print-Item {
    param (
        [string]$itemName,
        [int]$depth,
        [bool]$isLast,
        [bool]$isDirectory,
        [string]$outputFile = "",
        [ref]$structure
    )

    $indent = "`t" * $depth
    $branchSymbol = if ($isLast) { "└── " } else { "├── " }

    $line = if ($isDirectory) {
        "$indent$branchSymbol📁$itemName"
    } else {
        "$indent └── 📄$itemName"
    }

    if ($outputFile) {
        $structure.Value += $line + "`r`n"
    } else {
        Write-Host $line
    }
}

function Test-DirectoryPermissions {
    param(
        [string]$directoryPath
    )

    try {
        # Test read access by listing items (this will throw an exception if no read access)
        Get-ChildItem -Path $directoryPath -ErrorAction Stop | Out-Null
        return $true
    }
    catch {
        Write-Error "Insufficient permissions to access directory: $directoryPath"
        return $false
    }
}


function Get-DirectoryTree {
    param (
        [string]$directory,
        [int]$depth = 0,
        [bool]$isLast = $true,
        [string[]]$exclude = @(),
        [string]$outputFile = "",
        [ref]$structureOutput = "",
        [ref]$contentsOutput = ""
    )

    # Use Resolve-Path here to get the absolute path
    $absoluteDirectory = Resolve-Path -Path $directory

    $directoryName = Split-Path -Leaf $absoluteDirectory

    # Check if the current directory should be excluded
    if (Should-ExcludeItem -itemName $directoryName -excludeList $exclude) {
        return
    }

    Print-Item -itemName $directoryName -depth $depth -isLast $isLast -isDirectory $true -outputFile $outputFile -structure $structureOutput

    # Retrieve directories and files, excluding those specified
    $directories = Get-ChildItem -Path $absoluteDirectory -Directory | Where-Object { -not (Should-ExcludeItem -itemName $_.Name -excludeList $exclude) }
    $files = Get-ChildItem -Path $absoluteDirectory -File | Where-Object { -not (Should-ExcludeItem -itemName $_.Name -excludeList $exclude) }

    # Combine directories and files into a single array
    $allItems = @($directories; $files)

    for ($i = 0; $i -lt $allItems.Count; $i++) {
        $item = $allItems[$i]
        $isLastItem = $i -eq ($allItems.Count - 1)

        if ($item.PSIsContainer) {
            Get-DirectoryTree -directory $item.FullName -depth ($depth + 1) -isLast $isLastItem -exclude $exclude -outputFile $outputFile -structureOutput $structureOutput -contentsOutput $contentsOutput
        } else {
            Print-Item -itemName $item.Name -depth $depth -isLast $isLastItem -isDirectory $false -outputFile $outputFile -structure $structureOutput

            if ($outputFile) {
                Process-FileContent -filePath $item.FullName -contents $contentsOutput
            }
        }
    }
}

# --- Main Script ---

# Validate and sanitize path input
if ([string]::IsNullOrWhiteSpace($path)) {
    $sanitizedPath = "."  # Default to the current directory if $path is empty or whitespace
} else {
    $sanitizedPath = $path -replace "[^\w\-\:\\\.]", ""  # Sanitize
}

# Resolve the path to an absolute path
$resolvedPath = Resolve-Path -Path $sanitizedPath

if (-not (Test-Path -Path $resolvedPath -PathType Container)) {
    Write-Error "Invalid or inaccessible directory path: $resolvedPath"
    exit 1
}

# Check read permissions on the resolved path
if (-not (Test-DirectoryPermissions -directoryPath $resolvedPath)) {
    exit 1
}


# Sanitize exclude list (basic)
$sanitizedExclude = $exclude | ForEach-Object { $_ -replace "[^\w\-\.]", "" }

$structureOutput = ""
$contentsOutput = ""

Get-DirectoryTree -directory $resolvedPath -exclude $sanitizedExclude -outputFile $outputFile -structureOutput ([ref]$structureOutput) -contentsOutput ([ref]$contentsOutput)

if (-not [string]::IsNullOrWhiteSpace($outputFile)) {
    Write-ToFile -filePath $outputFile -content $structureOutput
    Write-ToFile -filePath "project-contents.md" -content $contentsOutput
}