src/productivity.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function', Target = 'ConvertFrom-FolderStructure')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function', Target = 'Invoke-Pack')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function', Target = 'Out-Tree')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function', Target = 'Rename-FileExtension')]
Param()

function ConvertFrom-FolderStructure {
    <#
    .SYNOPSIS
    Convert Get-ChildItem output to nested hashtable
    .PARAMETER IncludeHidden
    Include hidden files (adds -Force to Get-ChildItem calls)
    .PARAMETER RemoveExtensions
    Do not include file extensions in output key names (use BaseName instead of Name)
    .EXAMPLE
    $Data = ConvertFrom-FolderStructure
    .EXAMPLE
    $Data = 'some/directory' | ConvertFrom-FolderStructure -RemoveExtensions
    .EXAMPLE
    ConvertFrom-FolderStructure -IncludeHidden | Out-Tree
    #>

    [CmdletBinding()]
    [OutputType([Object])]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $True)]
        [ValidateScript({ (Test-Path $_) })]
        [String] $Path = (Get-Location).Path,
        [Switch] $IncludeHidden,
        [Switch] $RemoveExtensions
    )
    Begin {
        function Test-Branch {
            Param(
                [Parameter(Mandatory = $True, Position = 0)]
                $Value
            )
            $Value.GetType().Name -eq 'DirectoryInfo'
        }
        function Invoke-Iterate {
            Param(
                [Parameter(Mandatory = $True, Position = 0)]
                [String] $Path
            )
            $Output = @{}
            $Parameters = @{
                IncludeHidden = $IncludeHidden
                RemoveExtensions = $RemoveExtensions
            }
            $Items = Get-ChildItem $Path -Force:$IncludeHidden
            foreach ($Item in $Items) {
                $Attribute = if ($RemoveExtensions) { 'BaseName' } else { 'Name' }
                $Output[$Item.$Attribute] = if (Test-Branch $Item) {
                    $Item | Get-StringPath | ConvertFrom-FolderStructure @Parameters
                } else {
                    $Item.Name
                }
            }
            $Output
        }
    }
    Process {
        Invoke-Iterate $Path
    }
}
function ConvertTo-AbstractSyntaxTree {
    <#
    .SYNOPSIS
    Convert string or file to abstract syntax tree object
    .EXAMPLE
    '$Answer = 42' | ConvertTo-AbstractSyntaxTree
    .EXAMPLE
    ConvertTo-AbstractSyntaxTree '.\path\to\script.ps1'
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.Language.ScriptBlockAst])]
    Param(
        [Parameter(Position = 0)]
        [String] $File,
        [Parameter(ValueFromPipeline = $True)]
        [String] $String
    )
    Process {
        if ($File) {
            $Path = Get-StringPath $File
        }
        if ($Path -and (Test-Path $Path)) {
            [System.Management.Automation.Language.Parser]::ParseFile($Path, [Ref]$Null, [Ref]$Null)
        } elseif ($String.Length -gt 0) {
            [System.Management.Automation.Language.Parser]::ParseInput($String, [Ref]$Null, [Ref]$Null)
        }
    }
}
function ConvertTo-ParameterString {
    <#
    .SYNOPSIS
    Convert hashtable to parameter string
    .EXAMPLE
    $Parameters = @{
        Hostname = 'MARIO'
        Age = 42
    }
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [PSObject] $InputObject
    )
    Process {
        $Result = New-Object 'System.Collections.ArrayList'
        foreach ($Item in $InputObject.GetEnumerator()) {
            $Name = $Item.Name
            $Key = if ($Name.Length -eq 1) { "-${Name}" } else { "--$($Name.ToLower())" }
            $Value = $Item.Value
            $Result.Add("${Key} ${Value}") | Out-Null
        }
        $Result -join ' '
    }
}
function ConvertTo-PlainText {
    <#
    .SYNOPSIS
    Convert SecureString value to human-readable plain text
    #>

    [CmdletBinding()]
    [Alias('plain')]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [SecureString] $Value
    )
    Process {
        try {
            $BinaryString = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Value);
            $PlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BinaryString);
        } finally {
            if ($BinaryString -ne [IntPtr]::Zero) {
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BinaryString);
            }
        }
        $PlainText
    }
}
function Find-Duplicate {
    <#
    .SYNOPSIS
    Helper function that calculates file hash values to find duplicate files recursively
    .EXAMPLE
    Find-Duplicate 'path/to/folder'
    .EXAMPLE
    Get-Location | Find-Duplicate
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [String] $Path,
        [Switch] $AsJob
    )
    $Path = Get-Item $Path
    "==> Finding duplicate files in `"$Path`"" | Write-Verbose
    if ($AsJob) {
        $ModulePath = Join-Path $PSScriptRoot 'productivity.ps1'
        $Job = if (Test-Command -Name 'Start-ThreadJob' -Silent) {
            Start-ThreadJob -Name 'Find-Duplicate' -ScriptBlock {
                . $Using:ModulePath
                Find-Duplicate -Path $Using:Path
            }
        } else {
            Start-Job -Name 'Find-Duplicate' -ScriptBlock {
                . $Using:ModulePath
                Find-Duplicate -Path $Using:Path
            }
        }
        "==> Started job (Id=$($Job.Id)) to find duplicate files" | Write-Verbose
        "==> To get results, use `"`$Files = Receive-Job $($Job.Name)`"" | Write-Verbose
    } else {
        $Path |
            Get-ChildItem -Recurse |
            Get-FileHash |
            Group-Object -Property Hash |
            Where-Object Count -GT 1 |
            ForEach-Object { $_.Group | Select-Object Path, Hash } |
            Sort-Object -Property Hash
    }
}
function Find-FirstTrueVariable {
    <#
    .SYNOPSIS
    Given list of variable names, returns string name of first variable that returns $True
    .EXAMPLE
    $Foo = $False
    $Bar = $True
    $Baz = $False
 
    Find-FirstTrueVariable 'Foo','Bar','Baz'
    # 'Bar'
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, Position = 0)]
        [Array] $VariableNames,
        [Int] $DefaultIndex = 0,
        $DefaultValue = $Null
    )
    $Index = $VariableNames | Get-Variable -ValueOnly | Find-FirstIndex
    if ($Index -ge 0) {
        $VariableNames[$Index]
    } else {
        if ($Null -ne $DefaultValue) {
            $DefaultValue
        } else {
            $VariableNames[$DefaultIndex]
        }
    }
}
function Get-ParameterList {
    <#
    .SYNOPSIS
    Get parameter names and types for a given piece of PowerShell code
    .EXAMPLE
    '{ Param($A, $B, $C) $A + $B + $C }' | Get-ParameterList
    .EXAMPLE
    'Get-Maximum' | Get-ParameterList
    .EXAMPLE
    Get-ParameterList -Path 'path/to/Some-Function.ps1'
    #>

    [CmdletBinding()]
    [OutputType([System.Object])]
    Param(
        [Parameter(Position = 0)]
        [String] $Path,
        [Parameter(ValueFromPipeline = $True)]
        [String] $String
    )
    $AlwaysTrue = { $True }
    $Lookup = @{
        Name = 'Name'
        Type = 'StaticType'
        Required = 'Extent'
    }
    $Reducer = {
        Param($Name, $Value)
        switch ($Name) {
            'Name' {
                $Value -replace '^\$', ''
            }
            'StaticType' {
                $Value.ToString()
            }
            'Extent' {
                $Value.Text -match '[Mm]andatory\s*=\s*\$[Tt]rue'
            }
            Default {
                $Value
            }
        }
    }
    $Code = if ($Path) {
        Get-Content $Path
    } else {
        if (Test-Command $String -Silent) {
            (Get-Item -Path function:$String).Definition
        } else {
            $String
        }
    }
    $Ast = $Code | ConvertTo-AbstractSyntaxTree
    $Ast.Findall($AlwaysTrue, $True) |
        ForEach-Object ParamBlock |
        Select-Object -First 1 |
        Deny-Null |
        ForEach-Object Parameters |
        Select-Object Name, StaticType, Extent |
        Invoke-PropertyTransform -Lookup $Lookup -Transform $Reducer |
        Sort-Object -Property Name -Unique
}
function Get-StringPath {
    <#
    .SYNOPSIS
    Converts directories and file information to strings.
    Converts string paths to absolute string paths.
    .EXAMPLE
    (Get-Location) | ConvertTo-String"
    # 'C:\full\path\to\current\directory'
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        $Value
    )
    Process {
        $Type = $Value.GetType().Name
        switch ($Type) {
            'DirectoryInfo' {
                $Value.FullName
            }
            'FileInfo' {
                $Value.FullName
            }
            'PathInfo' {
                $Value.Path
            }
            Default {
                if (Test-Path -Path $Value) {
                    (Resolve-Path -Path $Value).Path
                } else {
                    $Value
                }
            }
        }
    }
}
function Invoke-GoogleSearch {
    <#
    .SYNOPSIS
    Perform Google search within default web browser using Google search operators, available as cmdlet paramters
    .EXAMPLE
    'PowerShell Prelude' | google -Url 'pwsh'
    .EXAMPLE
    'Small-World Properties of Facebook Group Networks' | google -Type 'pdf' -Exact
    .EXAMPLE
    google -Site 'example.com' -Subdomain
 
    # Search subdomains for a given site
    #>

    [CmdletBinding()]
    [Alias('google')]
    [OutputType([String])]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $True)]
        [String[]] $Keyword = @(),
        [ValidateSet('OR', 'AND')]
        [Alias('OP')]
        [String] $BinaryOperation = 'OR',
        [Switch] $Exact,
        [String[]] $Exclude,
        [String[]] $Include,
        [Switch] $Private,
        [ValidateSet(
            'swf', 'pdf', 'ps', 'dwf', 'kml', 'kmz',
            'gpx', 'hwp', 'htm', 'html', 'xls', 'xlsx',
            'ppt', 'pptx', 'doc', 'docx', 'odp', 'ods',
            'odt', 'rtf', 'svg', 'tex', 'txt', 'text',
            'bas', 'c', 'cc', 'cpp', 'h', 'hpp', 'cs',
            'java', 'pl', 'py', 'wml', 'wap', 'xml'
        )]
        [String] $Type,
        [String] $Related,
        [String[]] $Site,
        [Switch] $Subdomain,
        [String] $Source,
        [String] $Text,
        [String] $Url,
        [String] $Custom,
        [Switch] $Encode,
        [Switch] $PassThru
    )
    Begin {
        Add-Type -AssemblyName System.Web
        $Root = if ($Private) { 'https://duckduckgo.com/?q=' } else { 'https://google.com/search?q=' }
        $Terms = @()
    }
    End {
        if ($Input.Count -gt 1) {
            $Keyword = $Input
        }
        if ($Exact) {
            $Keyword = $Keyword | ForEach-Object { "`"$_`"" }
        }
        if ($Include.Count -gt 0) {
            $Data = $Include | ForEach-Object { "+$_" }
            $Terms += ($Data -join ' ')
        }
        if ($Exclude.Count -gt 0) {
            $Data = $Exclude | ForEach-Object { "-$_" }
            $Terms += ($Data -join ' ')
        }
        if ($Related.Length -gt 0) {
            $Terms += "related:$Related"
        }
        if ($Site.Count -gt 0) {
            $Data = $Site | ForEach-Object { "site:$_" }
            $Terms += ($Data -join " $BinaryOperation ")
            if ($Subdomain) {
                $Terms += '-inurl:www'
            }
        }
        if ($Source.Length -gt 0) {
            $Terms += "source:$Source"
        }
        if ($Text.Length -gt 0) {
            $Terms += "intext:$Text"
        }
        if ($Url.Length -gt 0) {
            $Terms += "inurl:$Url"
        }
        if ($Type.Length -gt 0) {
            $Terms += "filetype:$Type"
        }
        if ($Custom.Length -gt 0) {
            $Terms += $Custom
        }
        $SearchString += ($Keyword -join " $BinaryOperation ")
        if ($Terms.Count -gt 0) {
            if ($SearchString.Length -gt 0) {
                $SearchString += ' '
            }
            $SearchString += "$($Terms -join ' ')"
        }
        if ($Encode) {
            $SearchString = [System.Web.HttpUtility]::UrlEncode($SearchString)
        }
        if ($PassThru) {
            return $SearchString
        } else {
            "${Root}${SearchString}" | Out-Browser -Default
        }
    }
}
function Invoke-Pack {
    <#
    .SYNOPSIS
    Function that will serialize one or more files into a single XML file. Use Invoke-Unpack to restore files.
    .PARAMETER Root
    Save paths relative to this path. Needed when packing folders/files not descendent from current location.
    .EXAMPLE
    ls some/folder | pack
    #>

    [CmdletBinding()]
    [Alias('pack')]
    [OutputType([String])]
    Param(
        [Parameter(ValueFromPipeline = $True)]
        [Array] $Items,
        [Parameter(Position = 0)]
        [ValidateScript({ (Test-Path $_) })]
        [String] $Root = (Get-Location).Path,
        [String] $Output = 'packed',
        [Switch] $Compress
    )
    Begin {
        function Get-PathFragment {
            Param(
                [Parameter(Position = 0)]
                [System.IO.FileInfo] $Item
            )
            ($Item | Get-StringPath).Replace($Root, '')
        }
        function ConvertTo-ItemList {
            Param(
                [Parameter(Position = 0)]
                [Array] $Values
            )
            foreach ($Value in $Values) {
                $Item = Get-Item -Path ($Value | Get-StringPath)
                switch ($Item.GetType().Name) {
                    'DirectoryInfo' {
                        Get-ChildItem -Path $Item -File -Recurse -Force
                    }
                    'FileInfo' {
                        Get-Item -Path $Item
                    }
                }
            }
        }
        function ConvertTo-ObjectList {
            Param(
                [Parameter(Position = 0)]
                [Array] $Items
            )
            foreach ($Item in $Items) {
                $Name = $Item.Name
                $Parameters = if ($Name.EndsWith('.dll')) {
                    @{
                        Raw = $True
                        Encoding = 'Byte'
                    }
                } else {
                    @{}
                }
                @{
                    Name = $Name
                    Path = Get-PathFragment $Item
                    Content = Get-Content $Item.FullName @Parameters
                }
            }
        }
    }
    End {
        $Values = if ($Input.Count -gt 0) { $Input } else { $Items }
        $OutputPath = Join-Path (Get-Location).Path "$Output.xml"
        ConvertTo-ObjectList (ConvertTo-ItemList $Values) | Export-Clixml $OutputPath -Force
        if ($Compress) {
            $CompressedOutputPath = Join-Path (Get-Location).Path "$Output.zip"
            Compress-Archive -Path $OutputPath -DestinationPath $CompressedOutputPath
            Remove-Item -Path $OutputPath
            return Get-StringPath $CompressedOutputPath
        }
        Get-StringPath $OutputPath
    }
}
function Invoke-Speak {
    <#
    .SYNOPSIS
    Use Windows Speech Synthesizer to speak input text
    .EXAMPLE
    Invoke-Speak 'hello world'
    .EXAMPLE
    'hello world' | Invoke-Speak -Verbose
    .EXAMPLE
    1, 2, 3 | %{ say $_ }
    .EXAMPLE
    Get-Content .\phrases.csv | Invoke-Speak
    #>

    [CmdletBinding()]
    [Alias('say')]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $True)]
        [String] $Text = '',
        [String] $InputType = 'text',
        [Int] $Rate = 0,
        [Switch] $Silent,
        [String] $Output = 'none'
    )
    Begin {
        Use-Speech
        $TotalText = ''
    }
    Process {
        if ($IsLinux -is [Bool] -and $IsLinux) {
            Write-Verbose '==> Invoke-Speak is only supported on Windows platform'
        } else {
            Write-Verbose '==> Creating speech synthesizer'
            $Synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer
            if (-not $Silent) {
                switch ($InputType) {
                    'ssml' {
                        Write-Verbose '==> Received SSML input'
                        $Synthesizer.SpeakSsml($Text)
                    }
                    Default {
                        Write-Verbose "==> Speaking: $Text"
                        $Synthesizer.Rate = $Rate
                        $Synthesizer.Speak($Text)
                    }
                }
            }
            $TotalText += "$Text "
        }
    }
    End {
        if ($IsLinux -is [Bool] -and $IsLinux) {
            Write-Verbose '==> Invoke-Speak was not executed, no output was created'
        } else {
            $TotalText = $TotalText.Trim()
            switch ($Output) {
                'file' {
                    Write-Verbose '==> [UNDER CONSTRUCTION] save as .WAV file'
                }
                'ssml' {
                    $Output = "
<speak version=`"1.0`" xmlns=`"http://www.w3.org/2001/10/synthesis`" xml:lang=`"en-US`">
    <voice xml:lang=`"en-US`">
        <prosody rate=`"$Rate`">
            <p>$TotalText</p>
        </prosody>
    </voice>
</speak>
"

                    $Output | Write-Output
                }
                'text' {
                    Write-Output $TotalText
                }
                Default {
                    Write-Verbose "==> $TotalText"
                }
            }
        }
    }
}
function Invoke-Unpack {
    <#
    .SYNOPSIS
    Function to restore folders/files serialized via Invoke-Pack.
    .EXAMPLE
    'path/to/packed.xml' | Invoke-Unpack
    .EXAMPLE
    ls 'some/folder' | %{ unpack -File $_ }
    #>

    [CmdletBinding()]
    [Alias('unpack')]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $True)]
        [ValidateScript({ (Test-Path $_) })]
        [String] $Path,
        [Parameter(ValueFromPipeline = $True)]
        [System.IO.FileInfo] $File
    )
    Process {
        $Value = if ($Path) { $Path } else { Get-Item $File }
        $Pack = Import-Clixml $Value
        $Base = Join-Path (Get-Location).Path (Get-Item $Value).BaseName
        foreach ($Item in $Pack) {
            $OutputPath = Join-Path $Base $Item.Path
            if ($Item.Path.EndsWith('.dll')) {
                New-Item -Path $OutputPath -Force | Out-Null
                $Item.Content | Set-Content -Path $OutputPath -Encoding 'Byte' -Force
            } else {
                New-Item -Path $OutputPath -Value ($Item.Content -join "`n") -Force | Out-Null
            }
        }
    }
}
function Measure-Performance {
    <#
    .SYNOPSIS
    Measure the execution of a scriptblock a certain number of times. Return analysis of results.
    .DESCRIPTION
    This function returns the results as an object with the following keys:
      - Min
      - Max
      - Range
      - Mean
      - TrimmedMean (mean trimmed 10% on both sides)
      - Median
      - StandardDeviation
      - Runs (the original results of each run - can be used for custom analysis beyond these results)
    .PARAMETER Milliseconds
    Output results in milliseconds instead of "ticks"
    .PARAMETER Sample
    Use ($Runs - 1) instead of $Runs when calculating the standard deviation
    .EXAMPLE
    { Get-Process } | Measure-Performance -Runs 500
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    [OutputType([Hashtable])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [ScriptBlock] $ScriptBlock,
        [Parameter(Position = 1)]
        [Int] $Runs = 100,
        [Switch] $Milliseconds,
        [Switch] $Sample
    )
    $Results = @()
    $Units = if ($Milliseconds) { 'TotalMilliseconds' } else { 'Ticks' }
    for ($Index = 0; $Index -lt $Runs; $Index++) {
        Write-Progress -Activity 'Measuring Performance' -CurrentOperation "Run #$($Index + 1) of ${Runs}" -PercentComplete ([Math]::Ceiling(($Index / $Runs) * 100))
        $Results += (Measure-Command -Expression $ScriptBlock).$Units
    }
    Write-Progress -Activity 'Analyzing performance data...'
    $Minimum = Get-Minimum $Results
    $Maximum = Get-Maximum $Results
    $Mean = Get-Mean $Results
    $TrimmedMean = Get-Mean $Results -Trim 0.1
    $Median = Get-Median $Results
    $StandardDeviation = [Math]::Sqrt((Get-Variance $Results -Sample:$Sample))
    "Results for $Runs run(s) (values in $Units):" | Write-Verbose
    "==> Mean = $Mean" | Write-Verbose
    "==> Mean (10% trimmed) = $TrimmedMean" | Write-Verbose
    "==> Median = $Median" | Write-Verbose
    "==> Standard Deviation = $StandardDeviation" | Write-Verbose
    Write-Progress -Activity 'Measuring Performance' -Completed
    @{
        Min = $Minimum
        Max = $Maximum
        Range = ($Maximum - $Minimum)
        Mean = $Mean
        TrimmedMean = $TrimmedMean
        Median = $Median
        StandardDeviation = $StandardDeviation
        Runs = $Results
    }
}
function New-File {
    <#
    .SYNOPSIS
    PowerShell equivalent of linux "touch" command (includes "touch" alias)
    .EXAMPLE
    New-File <file name>
    .EXAMPLE
    touch <file name>
    #>

    [CmdletBinding(SupportsShouldProcess = $True)]
    [Alias('touch')]
    [OutputType([Bool])]
    Param(
        [Parameter(Mandatory = $True)]
        [String] $Name,
        [Switch] $PassThru
    )
    $Result = $False
    if (Test-Path $Name) {
        if ($PSCmdlet.ShouldProcess($Name)) {
            (Get-ChildItem $Name).LastWriteTime = Get-Date
            "==> Updated `"last write time`" of $Name" | Write-Verbose
            $Result = $True
        } else {
            "==> Would have updated `"last write time`" of $Name" | Write-Color -DarkGray
        }
    } else {
        if ($PSCmdlet.ShouldProcess($Name)) {
            New-Item -Path . -Name $Name -ItemType 'file' -Value '' -Force
            "==> Created new file, $Name" | Write-Verbose
            $Result = $True
        } else {
            "==> Would have created new file, $Name" | Write-Color -DarkGray
        }
    }
    if ($PassThru) {
        $Result
    }
}
function Out-Tree {
    <#
    .SYNOPSIS
    Output a tree of the input array
    .PARAMETER Limit
    Limit the depth of the tree
    Caution: Performance will be affected by excessive file depth
    .EXAMPLE
    @{ Foo = 1; Bar = 2; Baz = 3 } | Out-Tree
    # ├─ Foo
    # ├─ Bar
    # └─ Baz
    .EXAMPLE
    @{ Foo = 1; Bar = 2; Baz = 3 } | Out-Tree -Property Key
    # ├─ Bar
    # ├─ Baz
    # └─ Foo
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        $Items,
        [String] $Prefix = '',
        [String] $Property = 'Value',
        [Int] $Level = 1,
        [Int] $Limit = 3
    )
    Begin {
        $Pipe = '│'
        $Initial = ''
        function Get-LineContent {
            Param(
                [Parameter(Position = 0)]
                [String] $Value,
                [Switch] $IsTerminal,
                [Switch] $IsDirectory
            )
            $Branch = '├'
            $EmHyphen = '─'
            $TerminalBranch = '└'
            $FolderMarker = if ($IsDirectory) { '/' } else { '' }
            if ($IsTerminal) {
                "${TerminalBranch}${EmHyphen} ${Value}${FolderMarker}`r`n"
            } else {
                "${Branch}${EmHyphen} ${Value}${FolderMarker}`r`n"
            }
        }
        function Out-TreeStructure {
            Param(
                [Parameter(Position = 0)]
                $Items
            )
            if ($Items.Count -gt 0) {
                $Ordered = $Items | ConvertTo-OrderedDictionary -Property $Property
                $LastIndex = $Ordered.Count - 1
                $Index = 0
                foreach ($Value in $Ordered.Keys) {
                    $IsTerminal = $Index -eq $LastIndex
                    $IsEnumerableValue = Test-Enumerable $Ordered.$Value
                    $Content = Get-LineContent $Value -IsTerminal:$IsTerminal -IsDirectory:$IsEnumerableValue
                    $Initial += "${Prefix}${Content}"
                    if ($IsEnumerableValue -and ($Level -lt $Limit)) {
                        $Augment = if (-not $IsTerminal) { "${Pipe} " } else { ' ' }
                        $Initial += Out-Tree $Ordered.$Value -Prefix "${Prefix}${Augment}" -Level ($Level + 1) -Limit $Limit
                    }
                    $Index += 1
                }
                $Initial
            }
        }
        Out-TreeStructure $Items
    }
    End {
        Out-TreeStructure $Input
    }
}
function Remove-DirectoryForce {
    <#
    .SYNOPSIS
    PowerShell equivalent of linux "rm -frd"
    .EXAMPLE
    rf ./path/to/folder
    #>

    [CmdletBinding(SupportsShouldProcess = $True)]
    [Alias('rf')]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [ValidateScript( { Test-Path $_ })]
        [String] $Path
    )
    Process {
        $AbsolutePath = Get-StringPath $Path
        if ($PSCmdlet.ShouldProcess($AbsolutePath)) {
            "==> Deleting $AbsolutePath" | Write-Verbose
            Remove-Item -Path $AbsolutePath -Recurse -Force
            "==> Deleted $AbsolutePath" | Write-Verbose
        } else {
            "==> Would have deleted $AbsolutePath" | Write-Color -DarkGray
        }
    }
}
function Rename-FileExtension {
    <#
    .SYNOPSIS
    Change the extension of one or more files
    .EXAMPLE
    'foo.bar' | Rename-FileExtension -To 'baz'
 
    # New name of file will be 'foo.baz'
    #>

    [CmdletBinding(SupportsShouldProcess = $True)]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [String] $Path,
        [String] $To,
        [Switch] $TXT,
        [Switch] $JPG,
        [Switch] $PNG,
        [Switch] $GIF,
        [Switch] $MD,
        [Switch] $PassThru
    )
    Process {
        $NewExtension = if ($To.Length -gt 0) {
            $To
        } else {
            Find-FirstTrueVariable 'TXT', 'JPG', 'PNG', 'GIF', 'MD'
        }
        $NewName = [System.IO.Path]::ChangeExtension($Path, $NewExtension.ToLower())
        if ($PSCmdlet.ShouldProcess($Path)) {
            Rename-Item -Path $Path -NewName $NewName
            "==> Renamed $Path to $NewName" | Write-Verbose
        } else {
            "==> Rename $Path to $NewName" | Write-Color -DarkGray
        }
        if ($PassThru) {
            $NewName
        }
    }
}
function Test-Admin {
    <#
    .SYNOPSIS
    Helper function that returns true if user is in the "built-in" "admin" group, false otherwise
    .EXAMPLE
    Test-Admin
    #>

    [CmdletBinding()]
    [OutputType([Bool])]
    Param()
    if ($IsLinux -is [Bool] -and $IsLinux) {
        (whoami) -eq 'root'
    } else {
        ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | Write-Output
    }
}
function Test-Command {
    <#
    .SYNOPSIS
    Helper function that returns true if the command is available in the current shell, false otherwise
    .DESCRIPTION
    This function does the work of Get-Command, but without the necessary error when the passed command is not found.
    .EXAMPLE
    Test-Command 'dir'
    #>

    [CmdletBinding()]
    [OutputType([Bool])]
    Param(
        [Parameter(Mandatory = $True, Position = 0)]
        [String] $Name,
        [Switch] $Silent
    )
    $Result = $False
    $OriginalPreference = $ErrorActionPreference
    $ErrorActionPreference = 'stop'
    try {
        if (Get-Command -Name $Name) {
            "==> [INFO] '$Name' is an available command" | Write-Verbose
            $Result = $True
        }
    } Catch {
        if (-not $Silent) {
            "==> [INFO] '$Name' is not available command" | Write-Verbose
        }
    } Finally {
        $ErrorActionPreference = $OriginalPreference
    }
    $Result
}
function Test-Empty {
    <#
    .SYNOPSIS
    Helper function that returns true if directory is empty, false otherwise
    .EXAMPLE
    echo someFolderName | Test-Empty
    .EXAMPLE
    dir . | %{Test-Empty $_.FullName}
    #>

    [CmdletBinding()]
    [ValidateNotNullorEmpty()]
    [OutputType([Bool])]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [String] $Name
    )
    Get-Item $Name | ForEach-Object { $_.psiscontainer -and $_.GetFileSystemInfos().Count -eq 0 } | Write-Output
}
function Test-Installed {
    <#
    .SYNOPSIS
    Return $True if module is installed, $False otherwise
    .EXAMPLE
    Test-Installed 'Prelude'
    #>

    [CmdletBinding()]
    [OutputType([Bool])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [String] $Name
    )
    if (Get-Module -ListAvailable -Name $Name) {
        $True
    } else {
        $False
    }
}
function Use-Speech {
    <#
    .SYNOPSIS
    Load System.Speech type if it is not already loaded.
    #>

    [CmdletBinding()]
    [OutputType([Bool])]
    Param(
        [Switch] $PassThru
    )
    $Result = $False
    if ($IsLinux -is [Bool] -and $IsLinux) {
        Write-Verbose '==> Speech synthesizer can only be used on Windows platform'
    } else {
        $SpeechSynthesizerTypeName = 'System.Speech.Synthesis.SpeechSynthesizer'
        if (-not ($SpeechSynthesizerTypeName -as [Type])) {
            '==> Adding System.Speech type' | Write-Verbose
            Add-Type -AssemblyName System.Speech
        } else {
            '==> System.Speech is already loaded' | Write-Verbose
        }
        $Result = $True
    }
    if ($PassThru) {
        $Result
    }
}