PSUtil.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSUtil.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName PSUtil.Import.DoDotSource -Fallback $false
if ($PSUtil_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName PSUtil.Import.IndividualFiles -Fallback $false
if ($PSUtil_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PSUtil' -Language 'en-US'

Register-PSFConfigValidation -Name "PSUBrowseHistoryOptions" -ScriptBlock {
    param (
        $Value
    )
    
    $result = New-Object PSObject -Property @{
        Message  = ""
        Value    = $null
        Success  = $false
    }
    
    try { [PSUtil.Configuration.HistoryOption]$option = $Value }
    catch
    {
        $result.Message = "Could not convert $Value into a valid help option. Please specify either of these: Session | Global"
        return $result
    }
    
    $result.Success = $true
    $result.Value = $option
    
    return $result
}

Register-PSFConfigValidation -Name "PSUConsoleWidth" -ScriptBlock {
    param (
        $Value
    )
    
    $result = New-Object PSObject -Property @{
        Message  = ""
        Value    = $null
        Success  = $false
    }
    
    try { [int]$option = $Value }
    catch
    {
        $result.Message = "Could not convert $Value into a valid integer | $_"
        return $result
    }
    
    if ($option -le 0)
    {
        $result.Message = "Cannot specify a window width of 0 or less"
        return $result
    }
    
    if ($option -gt $Host.UI.RawUI.MaxWindowSize.Width)
    {
        $result.Message = "Cannot specify a window width larger than the maximum width: $option / $($Host.UI.RawUI.MaxWindowSize.Width)"
        return $result
    }
    
    $result.Success = $true
    $result.Value = $option
    
    return $result
}

Register-PSFConfigValidation -Name "PSUGetHelpOptions" -ScriptBlock {
    param (
        $Value
    )
    
    $result = New-Object PSObject -Property @{
        Message = ""
        Value = $null
        Success = $false
    }
    
    try { [PSUtil.Configuration.HelpOption]$option = $Value }
    catch
    {
        $result.Message = "Could not convert $Value into a valid help option. Please specify either of these: Short | Detailed | Examples | Full | Window | Online"
        return $result
    }
    
    $result.Success = $true
    $result.Value = $option
    
    return $result
}

# Configure the current window width
Set-PSFConfig -Module PSUtil -Name 'Console.Width' -Value ($Host.UI.RawUI.WindowSize.Width) -Initialize -Validation "PSUConsoleWidth" -Handler { Set-PSUShell -WindowWidth $args[0] } -Description "The width of the current console"

# Buffer Length
Set-PSFConfig -Module PSUtil -Name 'Console.Buffer' -Value ($Host.UI.RawUI.BufferSize.Height) -Initialize -Validation "integerpositive" -Handler { Set-PSUShell -BufferLength $args[0] } -Description "The length of the console screen history"

# Foreground Color
Set-PSFConfig -Module PSUtil -Name 'Console.ForegroundColor' -Value ($Host.ui.rawui.ForegroundColor) -Initialize -Validation "consolecolor" -Handler { Set-PSUShell -ForegroundColor $args[0] } -Description "The foreground color used in the PowerShell console"

# Background Color
Set-PSFConfig -Module PSUtil -Name 'Console.BackgroundColor' -Value ($Host.ui.rawui.BackgroundColor) -Initialize -Validation "consolecolor" -Handler { Set-PSUShell -BackgroundColor $args[0] } -Description "The background color used in the PowerShell console"

# Window Title
Set-PSFConfig -Module PSUtil -Name 'Console.WindowTitle' -Value ($Host.ui.rawui.Windowtitle) -Initialize -Validation "string" -Handler { Set-PSUShell -WindowTitle $args[0] } -Description "The background color used in the PowerShell console"

Set-PSFConfig -Module PSUtil -Name 'Import.Aliases.Grep' -Value $true -Initialize -Validation "bool" -Handler { } -Description "Whether the module will on import create an alias named 'grep' for Select-String"
Set-PSFConfig -Module PSUtil -Name 'Import.Keybindings' -Value $true -Initialize -Validation "bool" -Handler { } -Description "Whether the module will on import register keybindings in PSReadline"
Set-PSFConfig -Module PSUtil -Name 'Import.Alias.SystemOverride' -Value $false -Initialize -Validation "bool" -Description "Whether the module will on import pverwrite some default aliases with custom versions. Affects 'select' and 'gm'"

Set-PSFConfig -Module 'PSUtil' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'PSUtil' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

Set-PSFConfig -Module 'PSUtil' -Name 'Keybinding.GetHelp' -Value "F1" -Initialize -Description "The key chord(s) the open help functionality is bound to. Will provide help for the currently typed command."
Set-PSFConfig -Module 'PSUtil' -Name 'Keybinding.ExpandAlias' -Value "Alt+Q" -Initialize -Description "The key chord(s) the alias expansion functionality is bound to. Will expand all aliases in the current input."
Set-PSFConfig -Module 'PSUtil' -Name 'Keybinding.CopyAll' -Value "Ctrl+Shift+c" -Initialize -Description "The key chord(s) the copy all functionality is bound to. Will copy all lines of the current input to the clipboard."
Set-PSFConfig -Module 'PSUtil' -Name 'Keybinding.BrowseHistory' -Value "F7" -Initialize -Description "The key chord(s) the browse history functionality is bound to. Will open a gridview and allow you to pick from your input history, then insert the chosen line(s) as your current input."
Set-PSFConfig -Module 'PSUtil' -Name 'Keybinding.SendToHistory' -Value "Alt+w" -Initialize -Description "The key chord(s) the send to history functionality is bound to. Your current input will be sent to history without actually executing it. Access it by using the Arrow-UP key."

Set-PSFConfig -Module 'PSUtil' -Name 'Knowledge.LibraryPath' -Value (Join-Path (Get-PSFPath AppData) 'PSUtil\Knowledge') -Initialize -Validation 'string' -Description "The path where the PSUtil knowledge commands look for knowledge files"
Set-PSFConfig -Module 'PSUtil' -Name 'Knowledge.DefaultBook' -Value 'default' -Initialize -Validation 'string' -Description 'The default book of knowledge. Use when creating a new knowledge entry without specifying a book to write to'

$defaultHelpPreference = [PSUtil.Configuration.HelpOption]::Window
if (-not (Test-PSFPowerShell -OperatingSystem Windows))
{
    $defaultHelpPreference = [PSUtil.Configuration.HelpOption]::Detailed
}
Set-PSFConfig -Module PSUtil -Name 'Help.Preference' -Value $defaultHelpPreference -Initialize -Validation "PSUGetHelpOptions" -Handler { } -Description "The way in which help is shown when pressing the 'F1' key. Can be any of the following options: Short | Detailed | Examples | Full | Window | Online"
Set-PSFConfig -Module PSUtil -Name 'History.Preference' -Value ([PSUtil.Configuration.HistoryOption]::Session) -Initialize -Validation "PSUBrowseHistoryOptions" -Handler { } -Description "Where the system will retrieve input history when pressing the 'F7' key. Can be any of the following options: Session | Global. Session includes history since the process was started, Global will try to look up the history from PSReadline log-file instead"
Set-PSFConfig -Module PSUtil -Name 'History.Limit' -Value (-1) -Initialize -Validation 'integer' -Handler { } -Description "The maximum number of history entries to show when pressing the 'F7' key. Negative numbers disable limit"
Set-PSFConfig -Module PSUtil -Name 'Expand.DefaultProperties' -Value "Definition", "Guid", "DisinguishedName", "FullName", "Name", "Length" -Initialize -Validation stringarray -Description "The properties Expand-PSUObject (exp) picks from by default"

Set-PSFConfig -Module PSUtil -Name 'Path.Temp' -Value $([System.IO.Path]::GetTempPath()) -Initialize -Validation "string" -Handler { } -Description "The path to move to when calling Invoke-PSUTemp (temp)"
Set-PSFConfig -Module PSUtil -Name 'Path.BackupStepsDefault' -Value 1 -Initialize -Validation "integer" -Description "The number of levels you stup up when calling Backup-PSULocation (bu) without parameter"


function Import-PSUAlias
{
    <#
        .SYNOPSIS
            Internal command to set aliases in a user-controlled fashion.
         
        .DESCRIPTION
            Internal command to set aliases in a user-controlled fashion.
            - Can be blocked by setting a config "PSUtil.Import.Aliases.$Name" to $false.
            - Will not overwrite existing aliases
         
        .PARAMETER Name
            Name of the alias to set.
         
        .PARAMETER Command
            Name of the command to alias.
         
        .EXAMPLE
            PS C:\> Import-PSUAlias -Name grep -Command Select-String
     
            Sets the alias grep for the command Select-String
    #>

    
    [CmdletBinding()]
    Param (
        $Name,
        
        $Command
    )
    
    if (((-not (Test-Path alias:$name)) -or ($Name -eq "Select") -or ($Name -eq "gm")) -and (Get-PSFConfigValue -FullName PSUtil.Import.Aliases.$name -Fallback $true))
    {
        New-Alias -Name $Name -Value $Command -Force -Scope Global
    }
}

function Backup-PSULocation
{
<#
    .SYNOPSIS
        Sets the location n number of levels up.
     
    .DESCRIPTION
        You no longer have to cd ..\..\..\..\ to move back four levels. You can now
        just type bu 4
     
    .PARAMETER Levels
        Number of levels to move back.
     
    .EXAMPLE
        PS C:\Users\dlbm3\source\pullrequests\somePR\vsteam> bu 4
         
        PS C:\Users\dlbm3>
     
    .NOTES
        Author: Donovan Brown
        Source: http://donovanbrown.com/post/Why-cd-when-you-can-just-backup
     
        Thank you for sharing and granting permission to use this convenience :)
#>

    [CmdletBinding()]
    param (
        [int]
        $Levels = (Get-PSFConfigValue -FullName 'PSUtil.Path.BackupStepsDefault' -Fallback 1)
    )
    
    Set-Location -Path (,".." * $Levels | Join-String -With ([System.IO.Path]::DirectorySeparatorChar))
}

Import-PSUAlias -Name "bu" -Command "Backup-PSULocation"

function Invoke-PSUDesktop
{
    <#
        .SYNOPSIS
            Function that sets the current console path to the user desktop.
         
        .DESCRIPTION
            Function that sets the current console path to the user desktop.
            Uses the current user's desktop by default, but can be set to the desktop of any locally available profile.
         
        .PARAMETER User
            Alias: u
            Choose which user's desktop path to move to. Must be available as a local profile for things to work out.
     
        .PARAMETER Get
            Alias: g
            Returns the path, rather than changing the location
         
        .EXAMPLE
            PS C:\> Desktop
     
            Sets the current location to the desktop path of the current user.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]
        [Alias('u')]
        [string]
        $User = $env:USERNAME,
        
        [Alias('g')]
        [switch]
        $Get
    )
    
    # Default Path for current user
    $Path = "$env:SystemDrive\Users\$User\Desktop"
    
    if (-not (Test-Path $Path))
    {
        Stop-PSFFunction -Message "Path to Desktop not found: $Path" -Tag fail -Target $User -Category InvalidArgument
        return
    }
    
    if ($Get) { return $Path }
    else { Push-Location $Path }
}
Import-PSUAlias -Name "desktop" -Command "Invoke-PSUDesktop"

function Invoke-PSUExplorer
{
<#
    .SYNOPSIS
        Opens the windows explorer at the specified position.
     
    .DESCRIPTION
        Opens the windows explorer at the specified position.
     
    .PARAMETER Path
        Alias: FullName
        Default: (Get-Location).Path
        The folder to open in explorer. If a file was passed the containing folder will be opened instead.
     
    .PARAMETER Module
        The module, the base folder of which should be opened.
     
    .PARAMETER Duplicates
        Setting this switch will cause the function to open the same folder multiple times, if it was passed multiple times.
        By default, the function will not open the same folder multiple times (a dir of a directory with multiple files would otherwise cause multiple open windows).
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> dir | Explorer
         
        Opens each folder in the current directory in a separate explorer Window.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [string[]]
        $Path = (Get-Location).ProviderPath,
        
        [System.Management.Automation.PSModuleInfo[]]
        $Module,
        
        [switch]
        $Duplicates,
        
        [switch]
        $EnableException
    )
    
    Begin {
        # Contains previously opened folders
        $List = @()
    }
    Process {
        foreach ($item in $Path)
        {
            try { $resolvedPaths = Resolve-PSFPath -Path $item -Provider FileSystem }
            catch { Stop-PSFFunction -Message "Path not found: $item" -EnableException $EnableException -ErrorRecord $_ -Continue -Tag input -Target $item }
            
            foreach ($resolvedPath in $resolvedPaths)
            {
                
                $object = Get-Item $resolvedPath
                
                if ($object.PSIsContainer) { $finalPath = $object.FullName }
                else { $finalPath = $object.Directory.FullName }
                
                # If it was already opened once, skip it unless duplicates are enabled
                if ((-not $Duplicates) -and ($List -contains $finalPath))
                {
                    Write-PSFMessage -Level Verbose -Message "Skipping folder since it already was opened once: $finalPath" -Target $item -Tag skip
                    continue
                }
                
                Write-PSFMessage -Level Verbose -Message "Opening explorer at: $finalPath"
                explorer.exe $finalPath
                $List += $finalPath
            }
        }
        
        foreach ($item in $Module)
        {
            if ((-not $Duplicates) -and ($List -contains $item.ModuleBase))
            {
                Write-PSFMessage -Level Verbose -Message "Skipping folder since it already was opened once: $($item.ModuleBase)" -Target $item -Tag skip
                continue
            }
            Write-PSFMessage -Level Verbose -Message "Opening explorer at: $($item.ModuleBase)"
            explorer.exe $item.ModuleBase
            $List += $item.ModuleBase
        }
    }
}
Import-PSUAlias -Name "explorer" -Command "Invoke-PSUExplorer"

function Invoke-PSUTemp
{
    <#
        .SYNOPSIS
            Moves the current location to a temp directory.
         
        .DESCRIPTION
            Moves the current location to a temp directory.
     
            The path returned can be set by configuring the 'psutil.path.temp' configuration. E.g.:
            Set-PSFConfig "psutil.path.temp" "D:\temp\_Dump"
     
            If this configuration is not set, it will check the following locations and return the first one found:
            C:\Temp
            D:\Temp
            E:\Temp
            C:\Service
            $env:temp
         
        .PARAMETER Get
            Alias: g
            Rather than move to the directory, return its path.
         
        .EXAMPLE
            PS C:\> Invoke-PSUTemp
     
            Moves to the temporary directory.
    #>

    [CmdletBinding()]
    Param (
        [Alias('g')]
        [switch]
        $Get
    )
    
    if ($Get) { Get-PSFConfigValue -FullName 'PSUtil.Path.Temp' -Fallback $env:TEMP }
    else { Push-Location -Path (Get-PSFConfigValue -FullName 'PSUtil.Path.Temp' -Fallback $env:TEMP) }
}
Import-PSUAlias -Name "temp" -Command "Invoke-PSUTemp"

function New-PSUDirectory
{
<#
    .SYNOPSIS
        Creates a folder and moves the current path to it.
     
    .DESCRIPTION
        Creates a folder and moves the current path to it.
     
    .PARAMETER Path
        Name of the folder to create and move to.
     
    .EXAMPLE
        PS C:\> mcd Test
     
        creates folder C:\Test, then moves the current location to it.
     
    .NOTES
        Author: Donovan Brown
        Source: http://donovanbrown.com/post/How-to-create-and-navigate-a-directory-with-a-single-command
     
        Thank you for sharing and granting permission to use this convenience :)
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $Path
    )
    
    New-Item -Path $Path -ItemType Directory
    
    Set-Location -Path $Path
}

Import-PSUAlias -Name "mcd" -Command "New-PSUDirectory"

function Set-PSUDrive
{
<#
    .SYNOPSIS
        Creates a new psdrive, and moves location to it.
     
    .DESCRIPTION
        Will create a PSDrive, by default in the current path.
        This allows swiftly reducing path length.
        Then it will immediately change location to the new drive.
     
    .PARAMETER Name
        What to name the new PSDrive?
     
    .PARAMETER Root
        Default: .
        The root of the new drive.
     
    .EXAMPLE
        PS C:\> set-as pr
     
        Sets the current path as drive "pr" and sets it as the current location.
     
    .NOTES
        Author: Donovan Brown
        Source: http://donovanbrown.com/post/Shorten-your-PowerShell-directory-path
     
        Thank you for sharing and granting permission to use this convenience :)
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $Name,
        
        [string]
        $Root = "."
    )
    
    $path = Resolve-Path $Root
    $null = New-PSDrive -PSProvider $path.Provider -Name $Name -Root $Root -Scope Global
    Set-Location -LiteralPath "$($Name):"
}

Import-PSUAlias -Name "set-as" -Command "Set-PSUDrive"

function Read-PSUKnowledge
{
<#
    .SYNOPSIS
        Searches for knowledge.
     
    .DESCRIPTION
        Searches for knowledge.
        Generally, knowledge must first be generated using Write-PSUKnowledge.
        This allows these functions to server as a searchable notes section right within your console.
     
        However, there might be some other ways to seek knowledge ...
     
    .PARAMETER Name
        The name of the knowledge entry.
     
    .PARAMETER Tags
        Tags to search by. At least one of the specified tags must be contained.
     
    .PARAMETER Pattern
        Search Name and text of the page by using this regex pattern.
     
    .PARAMETER Book
        The book to search in.
        By default you only have one and don't need to worry about this.
     
    .PARAMETER Online
        Mysterious parameter. I wonder what it does ...
     
    .EXAMPLE
        PS C:\> Read-PSUKnowledge
     
        Lists all knowledge entries.
     
    .EXAMPLE
        PS C:\> Read-PSUKnowledge -Tags DNS
     
        Lists all knowledge entries with the tag "DNS"
     
    .EXAMPLE
        PS C:\> read -p ldap
     
        Lists all knowledge entries with the string "ldap" in name or text.
#>

    [CmdletBinding()]
    param (
        [parameter(Position = 0)]
        [Alias('Page')]
        [string]
        $Name = '*',
        
        [string[]]
        $Tags,
        
        [Alias('p','f','filter')]
        [string]
        $Pattern = '.',
        
        [string]
        $Book = '*',
        
        [switch]
        $Online
    )
    
    begin
    {
        $libraryPath = Get-PSFConfigValue -FullName 'PSUtil.Knowledge.LibraryPath'
    }
    process
    {
        # Gimmick: Search in wikipedia
        if ($Online)
        {
            $url = "https://en.wikipedia.org/wiki/Special:Search/$Name"
            Start-Process $url
            return
        }
        
        if (-not (Test-Path -Path $libraryPath)) { return }
        
        foreach ($bookFile in (Get-ChildItem -Path $libraryPath -Filter *.json))
        {
            $bookName = [System.Text.Encoding]::UTF8.GetString(([convert]::FromBase64String($bookFile.BaseName)))
            if ($bookName -notlike $Book) { continue }
            
            $data = Get-Content -Path $bookFile.FullName | ConvertFrom-Json
            
            foreach ($page in $data)
            {
                if ($page.Name -notlike $Name) { continue }
                if ($Tags -and -not ($Tags | Where-Object { $_ -in $page.Tags })) { continue }
                
                if ($Pattern -ne '.')
                {
                    $matched = $false
                    if ($page.Name -match $Pattern) { $matched = $true }
                    elseif ($page.Text -match $Pattern) { $matched = $true }
                    if (-not $matched) { continue }
                }
                
                $page | Select-PSFObject -KeepInputObject -TypeName 'PSUtil.Knowledge.Page'
            }
        }
    }
}
Import-PSUAlias -Name 'read' -Command Read-PSUKnowledge
Import-PSUAlias -Name 'rdk' -Command Read-PSUKnowledge
Import-PSUAlias -Name 'page' -Command Read-PSUKnowledge
Import-PSUAlias -Name 'learn' -Command Read-PSUKnowledge

function Remove-PSUKnowledge
{
<#
    .SYNOPSIS
        Removes pages from the books of your holy console library of knowledge.
     
    .DESCRIPTION
        Removes pages from the books of your holy console library of knowledge.
        Contribute new knowledge by using Write-PSUKnowledge or search it by using Read-PSUKnowlege.
     
    .PARAMETER Name
        The name of the page to rip out.
     
    .PARAMETER Book
        The book of knowledge to deface.
        Defaults to the book specified under the 'PSUtil.Knowledge.DefaultBook' configuration setting.
        It will look for books in your library path, which can be specified under 'PSUtil.Knowledge.LibraryPath'.
     
    .EXAMPLE
        PS C:\> Remove-PSUKnowledge -Name 'DNS for Dummies'
     
        Rips out the page 'DNS for Dummies' from your default book.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('page')]
        [string[]]
        $Name,
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $Book = (Get-PSFConfigValue -FullName 'PSUtil.Knowledge.DefaultBook')
    )
    
    begin
    {
        $libraryPath = Get-PSFConfigValue -FullName 'PSUtil.Knowledge.LibraryPath'
    }
    process
    {
        if (-not (Test-Path -Path $libraryPath)) { return }
        
        $bookName = '{0}.json' -f [System.Text.Encoding]::UTF8.GetBytes($Book)
        $bookPath = Join-Path $libraryPath $bookName
        
        if (-not (Test-Path -Path $bookPath)) { return }
        
        $pageEntries = Get-Content -Path $bookPath | ConvertFrom-Json
        $pageEntries = $pageEntries | Where-Object Name -notin $Name
        $pageEntries | ConvertTo-Json | Set-Content -Path $bookPath
    }
}

function Write-PSUKnowledge
{
<#
    .SYNOPSIS
        Writes a piece of knowledge into the designated book.
     
    .DESCRIPTION
        Writes a piece of knowledge into the designated book.
        This can be later retrieved using Read-PSUKnowledge.
         
        "Books" are an arbitrary label grouping one or multiple pieces of text ("pages").
        You could separate them by category (e.g. "Active Directory" or "Exchagne Online") or by computer (e.g. "Desktop", "Notebook", ...).
     
    .PARAMETER Name
        The name of the page to write.
     
    .PARAMETER Text
        The page / text to write.
     
    .PARAMETER Tags
        Additional tags to include.
        Tags help find content later on.
     
    .PARAMETER Book
        The book to write the content to.
        Defaults to the book specified under the 'PSUtil.Knowledge.DefaultBook' configuration setting.
        It will look for books in your library path, which can be specified under 'PSUtil.Knowledge.LibraryPath'.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Set-PSUKnowledge -Name 'DNS Queries' -Text $nslookupExplained -Tags dns, network, infrastructure
     
        Defines a new page named "DNS Queries" with the text contained in $nslookupExplained.
        It also adds a few tags to make searching for it later easier.
#>

    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name,
        
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Page')]
        [string]
        $Text,
        
        [parameter(ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Tags,
        
        [parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $Book = (Get-PSFConfigValue -FullName 'PSUtil.Knowledge.DefaultBook'),
        
        [switch]
        $EnableException
    )
    
    begin
    {
        $libraryPath = Get-PSFConfigValue -FullName 'PSUtil.Knowledge.LibraryPath'
        if (-not (Test-Path -Path $libraryPath))
        {
            try { $null = New-Item -Path $libraryPath -ItemType Directory -Force -ErrorAction Stop }
            catch
            {
                Stop-PSFFunction -String 'Set-PSUKnowledge.Library.Path.CreationFailed' -StringValues $libraryPath -ErrorRecord $_ -EnableException $EnableException
                return
            }
        }
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        # Fake loop to make interrupting easier in pipeline scenarios
        foreach ($nameItem in $Name)
        {
            $bookName = '{0}.json' -f [System.Convert]::ToBase64String(([System.Text.Encoding]::UTF8.GetBytes($Book)))
            $bookPath = Join-Path $libraryPath $bookName
            $bookData = @{ }
            
            if (Test-Path -Path $bookPath)
            {
                try { $pages = Get-Content -Path $bookPath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
                catch { Stop-PSFFunction -String 'Set-PSUKnowledge.Book.ImportFailed' -StringValues $Book, $bookPath -ErrorRecord $_ -EnableException $EnableException -Continue }
                
                foreach ($page in $pages)
                {
                    $bookdata[$page.Name] = $page
                }
            }
            
            $bookData[$Name] = [pscustomobject]@{
                Name = $Name
                Text = $Text
                Tags = $Tags
            }
            
            try { $bookData.Values | ConvertTo-Json -ErrorAction Stop | Set-Content -Path $bookPath -ErrorAction Stop }
            catch { Stop-PSFFunction -String 'Set-PSUKnowledge.Book.ExportFailed' -StringValues $Name, $Book, $bookPath -ErrorRecord $_ -EnableException $EnableException -Continue }
        }
    }
}

function Convert-PSUObject
{
<#
    .SYNOPSIS
        Converts objects from one data-type/-format to another.
     
    .DESCRIPTION
        Converts objects from one data-type/-format to another.
        For example can this be used to convert numbers from binary to hex.
     
        This function can be dynamically extended by registering conversion paths.
        Use Register-PSUObjectConversion to set up such a type conversion.
     
    .PARAMETER InputObject
        The object(s) to convert.
     
    .PARAMETER From
        The type/format that is assumed to be the input type.
     
    .PARAMETER To
        The type/format that the input is attempted to convert to.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> 100..110 | convert IntDec IntHex
     
        Converts the numbers 100 through 110 from decimal to hexadecimal.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $InputObject,
        
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $From,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [string]
        $To,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        if (-not ([PSUtil.Object.ObjectHost]::Conversions.ContainsKey("$($From):$($To)")))
        {
            Stop-PSFFunction -Message "No conversion path configured for $From --> $To" -EnableException $EnableException -Category NotImplemented -Tag 'fail', 'input', 'convert'
            return
        }
        
        $scriptBlock = [PSUtil.Object.ObjectHost]::Conversions["$($From):$($To)"].Script
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        foreach ($item in $InputObject)
        {
            [PSFramework.Utility.UtilityHost]::ImportScriptBlock($scriptBlock)
            try { $scriptBlock.Invoke($item) }
            catch
            {
                Stop-PSFFunction -Message "Failed to convert $item from $From to $To" -EnableException $EnableException -ErrorRecord $_ -Target $item -Tag 'fail','convert','item' -Continue
            }
        }
    }
}
Import-PSUAlias -Name convert -Command Convert-PSUObject

function Expand-PSUObject
{
    <#
        .SYNOPSIS
            A comfortable replacement for Select-Object -ExpandProperty.
         
        .DESCRIPTION
            A comfortable replacement for Select-Object -ExpandProperty.
            Allows extracting properties with less typing and more flexibility:
     
            Preferred Properties:
            By defining a list of property-names in $DefaultExpandedProperties the user can determine his own list of preferred properties to expand.
            This allows using this command without specifying a property at all.
            It will then check the first object for the property to use (starting from the first element of the list until it finds an exact case-insensitive match).
     
            Defined Property:
            The user can specify the exact property to extract. This is the same behavior as Select-Object -ExpandProperty, with less typing (dir | exp length).
     
            Like / Match comparison:
            Specifying either like or match allows extracting any number of matching properties from each object.
            Note that this is a somewhat more CPU-expensive operation (which shouldn't matter unless with gargantuan numbers of objects).
         
        .PARAMETER Name
            ParSet: Equals, Like, Match
            The name of the Property to expand.
         
        .PARAMETER Like
            ParSet: Like
            Expands all properties that match the -Name parameter using -like comparison.
         
        .PARAMETER Match
            ParSet: Match
            Expands all properties that match the -Name parameter using -match comparison.
         
        .PARAMETER InputObject
            The objects whose properties are to be expanded.
         
        .EXAMPLE
            PS C:\> dir | exp
     
            Expands the property whose name is the first on the defaults list ($DefaultExpandedProperties).
            By default, FullName would be expanded.
     
        .EXAMPLE
            PS C:\> dir | exp length
     
            Expands the length property of all objects returned by dir. Simply ignores those that do not have the property (folders).
     
        .EXAMPLE
            PS C:\> dir | exp name -match
     
            Expands all properties from all objects returned by dir that match the string "name" ("PSChildName", "FullName", "Name", "BaseName" for directories)
    #>

    [CmdletBinding(DefaultParameterSetName = "Equals")]
    Param (
        [Parameter(Position = 0, ParameterSetName = "Equals")]
        [Parameter(Position = 0, ParameterSetName = "Like", Mandatory = $true)]
        [Parameter(Position = 0, ParameterSetName = "Match", Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(ParameterSetName = "Like", Mandatory = $true)]
        [switch]
        $Like,
        
        [Parameter(ParameterSetName = "Match", Mandatory = $true)]
        [switch]
        $Match,
        
        [Parameter(ValueFromPipeline = $true)]
        [object]
        $InputObject
    )
    
    Begin
    {
        Write-PSFMessage -Level Debug -Message "Expanding Objects" -Tag start
        $ParSet = $PSCmdlet.ParameterSetName
        Write-PSFMessage -Level InternalComment -Message "Active Parameterset: $ParSet | Bound Parameters: $($PSBoundParameters.Keys -join ", ")" -Tag start
        
        # Null the local scoped variable (So later checks for existence don't return super-scoped variables)
        $n9ZPiBh8CI = $null
        [bool]$____found = $false
        
        # If a property was specified, set it and return it
        if (Test-PSFParameterBinding -ParameterName "Name")
        {
            $n9ZPiBh8CI = $Name
            $____found = $true
        }
        $DefaultExpandedProperties = Get-PSFConfigValue -FullName 'PSutil.Expand.DefaultProperties'
    }
    
    process
    {
        :main foreach ($Object in $InputObject)
        {
            if ($null -eq $Object) { continue }
            
            switch ($ParSet)
            {
                #region Equals
                "Equals"
                {
                    # If we didn't ask for a property in specific, and we have something prepared for this type: Run it
                    if ((Test-PSFParameterBinding -ParameterName "Name" -Not) -and ([PSUtil.Object.ObjectHost]::ExpandedTypes[$Object.GetType().FullName]))
                    {
                        [PSUtil.Object.ObjectHost]::ExpandedTypes[$Object.GetType()].Invoke($Object)
                        continue main
                    }
                    
                    # If we already have determined the property to use, return it
                    if ($____found)
                    {
                        if ($null -ne $Object.$n9ZPiBh8CI) { $Object.$n9ZPiBh8CI }
                        continue main
                    }
                    
                    # Otherwise, search through defaults and try to match
                    foreach ($Def in $DefaultExpandedProperties)
                    {
                        if (Get-Member -InputObject $Object -MemberType 'Properties' -Name $Def)
                        {
                            $n9ZPiBh8CI = $Def
                            $____found = $true
                            if ($null -ne $Object.$n9ZPiBh8CI) { $Object.$n9ZPiBh8CI }
                            
                            break
                        }
                    }
                    continue main
                }
                #endregion Equals
                
                #region Like
                "Like"
                {
                    # Return all properties whose name are similar
                    foreach ($prop in ($Object.PSObject.Properties | Where-Object Name -like $Name | Select-Object -ExpandProperty Name))
                    {
                        if ($null -ne $Object.$prop) { $Object.$prop }
                    }
                    continue
                }
                #endregion Like
                
                #region Match
                "Match"
                {
                    # Return all properties whose name match
                    foreach ($prop in ($Object.PSObject.Properties | Where-Object Name -Match $Name | Select-Object -ExpandProperty Name))
                    {
                        if ($null -ne $Object.$prop) { $Object.$prop }
                    }
                    continue main
                }
                #endregion Match
            }
        }
    }
    
    End
    {
        Write-PSFMessage -Level Debug -Message "Expanding Objects" -Tag end
    }
}
Import-PSUAlias -Name "exp" -Command "Expand-PSUObject"

function Register-PSUObjectConversion
{
<#
    .SYNOPSIS
        Registers an object conversion for Convert-PSUObject.
     
    .DESCRIPTION
        This command can be used to register an object conversion for Convert-PSUObject, allowing the user to extend the conversion utility as desired.
     
    .PARAMETER From
        The input type. Using a suitable shorthand is recommended ("int" rather than "System.Int32", etc.).
     
    .PARAMETER To
        The conversion target type. Using a suitable shorthand is recommended ("int" rather than "System.Int32", etc.).
     
    .PARAMETER ScriptBlock
        The scriptblock that will be invoked to convert.
        Receives a single argument: The input object to convert.
     
    .EXAMPLE
        PS C:\> Register-PSUObjectConversion -From 'dec' -To 'oct' -ScriptBlock $ScriptBlock
     
        Registers a conversion that is supposed to convert a decimal into an octal number.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $From,
        
        [Parameter(Mandatory = $true)]
        [string]
        $To,
        
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock
    )
    
    process
    {
        $conversion = New-Object PSUtil.Object.ObjectConversionMapping -Property @{
            From   = $From
            To     = $To
            Script = $ScriptBlock
        }
        
        [PSUtil.Object.ObjectHost]::Conversions["$($From):$($To)"] = $conversion
    }
}

function Register-PSUObjectExpansion
{
<#
    .SYNOPSIS
        Registers a custom scriptblock for a type when processed by Expand-PSUObject.
     
    .DESCRIPTION
        Registers a custom scriptblock for a type when processed by Expand-PSUObject.
     
        Expand-PSUObject enables accelerated object expansion,
        by shortening the "Select-Object -ExpandProperty" call to "exp".
        It further has a list of default properties to expand,
        but it also allows implementing custom expansion rules, based on input type.
         
        This commands sets up these custom expansion rules.
        Define a scriptblock, it receives a single parameter - the input object to expand.
        The scriptblock is then responsible for expanding it and producing the desired output.
     
    .PARAMETER TypeName
        The name of the type to custom-expand.
     
    .PARAMETER ScriptBlock
        The scriptblock performing the expansion.
     
    .EXAMPLE
        PS C:\> Register-PSUObjectExpansion -TypeName 'MyModule.MyClass' -ScriptBlock $ScriptBlock
     
        Sets up a custom expansion rule for the 'MyModule.MyClass' class.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $TypeName,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [scriptblock]
        $ScriptBlock
    )
    
    process
    {
        [PSUtil.Object.ObjectHost]::ExpandedTypes[$TypeName] = $ScriptBlock
    }
}

function Select-PSUObjectSample
{
    <#
        .SYNOPSIS
            Used to only pick a sample from the objects passed to the function.
         
        .DESCRIPTION
            Used to only pick a sample from the objects passed to the function.
         
        .PARAMETER InputObject
            The objects to pick a sample from.
         
        .PARAMETER Skip
            How many objects to skip.
         
        .PARAMETER Number
            How many objects to pick
            Use a negative number to pick the last X items instead.
         
        .EXAMPLE
            PS C:\> Get-ChildItem | Select-PSUObjectSample -Skip 1 -Number 3
     
            Scans the current directory, skips the first returned object, then passes through the next three objects and skips the rest.
         
        .EXAMPLE
            PS C:\> dir | s 3 1
     
            Same as the previous example, only this time using aliases and positional binding.
            Scans the current directory, skips the first returned object, then passes through the next three objects and skips the rest.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        $InputObject,
        
        [Parameter(Position = 1)]
        [int]
        $Skip = 0,
        
        [Parameter(Position = 0)]
        [int]
        $Number = 1
    )
    
    Begin
    {
        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Select-Object', [System.Management.Automation.CommandTypes]::Cmdlet)
        $splat = @{ }
        if ($Skip -gt 0) { $splat["Skip"] = $Skip }
        if ($Number -ge 0) { $splat["First"] = $Number + 1 }
        else { $splat["Last"] = $Number * -1 }
        $scriptCmd = { & $wrappedCmd @splat }
        $steppablePipeline = $scriptCmd.GetSteppablePipeline()
        $steppablePipeline.Begin($true)
    }
    
    Process
    {
        foreach ($o in $InputObject)
        {
            $steppablePipeline.Process($o)
        }
    }
    
    End
    {
        $steppablePipeline.End()
    }
}
Import-PSUAlias -Name "s" -Command "Select-PSUObjectSample"

function Set-PSUObjectType
{
    <#
        .SYNOPSIS
            Tries to convert an object from one type of another.
         
        .DESCRIPTION
            Tries to convert an object from one type of another.
         
        .PARAMETER InputObject
            The objects to convert.
         
        .PARAMETER Type
            ParSet: Type
            The type to cast to.
         
        .PARAMETER TypeName
            ParSet: String
            The type to cast to.
         
        .PARAMETER EnableException
            Replaces user friendly yellow warnings with bloody red exceptions of doom!
            Use this if you want the function to throw terminating errors you want to catch.
         
        .EXAMPLE
            PS C:\> "01", "02", "03", "42" | Set-PSUObjectType "int"
             
            Tries to convert strings with numeric values into pure integers (hint: This will probably succeede).
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = "String")]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,
        
        [Parameter(ParameterSetName = "Type")]
        [Type]
        $Type,
        
        [Parameter(ParameterSetName = "String", Position = 0)]
        [String]
        $TypeName,
        
        [switch]
        $EnableException
    )
    
    Begin
    {
        Write-PSFMessage -Level Debug -Message "Casting Objects to another type" -Tag start
        $ParSet = $PSCmdlet.ParameterSetName
        Write-PSFMessage -Level InternalComment -Message "Active Parameterset: $ParSet | Bound Parameters: $($PSBoundParameters.Keys -join ", ")" -Tag start
        
        switch ($ParSet)
        {
            "Type" { $name = $Type.FullName }
            "String" { $name = $TypeName }
        }
    }
    Process
    {
        foreach ($object in $InputObject)
        {
            $temp = $null
            $temp = $object -as $name
            if ($temp) { $temp }
            else { Stop-PSFFunction -Message "Failed to convert '$object' to '$name'" -EnableException $EnableException -Category InvalidData -Tag fail, cast -Target $object -Continue }
        }
    }
    End
    {
        Write-PSFMessage -Level Debug -Message "Casting Objects to another type" -Tag end
    }
}
Import-PSUAlias -Name "cast" -Command "Set-PSUObjectType"

function Out-PSUVariable
{
<#
    .SYNOPSIS
        Writes the input to a variable.
     
    .DESCRIPTION
        Writes the input to a variable.
        This allows doing variable assignments at the end of a pipeline, rather than just at the beginning.
     
        Previous contents will be overwritten.
     
    .PARAMETER Name
        The name of the variable to write to.
     
    .PARAMETER InputObject
        The objects to write.
     
    .EXAMPLE
        PS C:\> Get-ChildItem | Out-PSUVariable -Name 'files'
     
        Writes the files & folders in the current path into the variable $files
     
    .EXAMPLE
        PS C:\> dir | ov files
     
        Does the same thing as the first example, only this time in a convenient interactive commandline usage
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $Name,
        
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    
    begin
    {
        $list = New-Object System.Collections.Generic.List[Object]
    }
    process
    {
        foreach ($object in $InputObject)
        {
            $list.Add($object)
        }
    }
    end
    {
        # Write to caller scope
        $PSCmdlet.SessionState.PSVariable.Set($Name, ($list | Write-Output))
    }
}
Import-PSUAlias -Name ov -Command Out-PSUVariable

function Select-PSUFunctionCode
{
<#
    .SYNOPSIS
        Function that shows you the definition of a function and allows you to select lines to copy to your clipboard.
     
    .DESCRIPTION
        Function that shows you the definition of a function and allows you to select lines to copy to your clipboard.
        After running this command you will see a GridView pop up. Select as many lines of code as you would like and select
        ok to copy them to your clipboard.
     
    .PARAMETER Function
        A description of the Function parameter.
     
    .PARAMETER NoWait
        Shows function code in gridview and returns control without waiting for the window to close
     
    .PARAMETER PassThru
        Presents input command(s) in gridview, selected lines (if any) get returned as output
     
    .PARAMETER NoTrim
        If enabled, the white space will not be trimmed.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> Select-PSUFunctionCode -function 'Start-PSUTimer'
         
        This will open up the code for the function Start-PSUTimer in a GridView window.
     
    .EXAMPLE
        PS C:\> Get-Command timer | Select-PSUFunctionCode
         
        You can also pipe functions in.
     
    .NOTES
        Author: Andrew Pla
#>

    
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Function,
        
        [Alias('w')]
        [Parameter(ParameterSetName = 'NoWait')]
        [switch]
        $NoWait,
        
        [Alias('p')]
        [Parameter(ParameterSetName = 'PassThru')]
        [switch]
        $PassThru,
        
        [Alias('t')]
        [switch]
        $NoTrim,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param'
        
        if (Test-PSFPowerShell -PSMinVersion '6.0' -PSMaxVersion '6.9.9')
        {
            Stop-PSFFunction -Message "This command is not supported on PowerShell v6 due to lack of GUI elements!" -Category NotEnabled -Tag fail, ps6
            return
        }
        
        $FinalArray = [System.Collections.Arraylist]@()
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        #region Process Items
        :main foreach ($item in $Function)
        {
            #region Resolve Command
            try { $command = Get-Command $item -ErrorAction Stop }
            catch { Stop-PSFFunction -Message "Failed to resolve command $item" -Tag fail -ErrorRecord $_ -Target $item -Continue -ContinueLabel main -EnableException $EnableException }
            
            switch ($command.CommandType)
            {
                'Alias'
                {
                    if ($command.ResolvedCommand.CommandType -eq "Function")
                    {
                        $functionText = $command.ResolvedCommand | Expand-PSUObject | Split-String "`n"
                    }
                    else
                    {
                        Stop-PSFFunction -Message "$($command.ResolvedCommand.CommandType) not supported: The alias $item resolves to $($command.ResolvedCommand). Please supply a function or an alias for a function." -Tag fail -Target $item -Continue -ContinueLabel main -EnableException $EnableException
                    }
                }
                'Function'
                {
                    $functionText = $command | Expand-PSUObject | Split-String "`n"
                }
                default
                {
                    Stop-PSFFunction -Message "$($command.CommandType) not supported: $item. Please supply a function or an alias for a function." -Tag fail -Target $item -Continue -ContinueLabel main -EnableException $EnableException
                }
            }
            #endregion Resolve Command
            
            #region Process Definition content
            $count = 1
            foreach ($line in $functionText)
            {
                $Object = [PSCustomObject]@{
                    LineNumber       = $Count
                    Text           = $line
                    Function       = $item
                }
                
                $count++
                
                if ($NoTrim)
                {
                    $null = $FinalArray.add($Object)
                }
                else
                {
                    if (-not ([string]::IsNullOrWhiteSpace($line)))
                    {
                        $null = $FinalArray.add($Object)
                    }
                }
            }
            #endregion Process Definition content
        }
        #endregion Process Items
    }
    
    end
    {
        if (Test-PSFFunctionInterrupt) { return }
        
        # This is the default behavior with no params
        if (-not ($NoWait -or $PassThru))
        {
            $data = $FinalArray | Out-GridView -PassThru -Title 'Select-PSUFunctionCode' | Expand-PSUObject text
            if ($data) { $data | Set-Clipboard }
        }
        
        if ($NoWait)
        {
            $FinalArray | Out-GridView -Title 'Select-PSUFunctionCode'
        }
        if ($PassThru)
        {
            $FinalArray | Out-GridView -PassThru -Title 'Select-PSUFunctionCode'
        }
    }
}

Import-PSUAlias -Name "inspect" -Command "Select-PSUFunctionCode"

function Set-PSUPrompt
{
<#
    .SYNOPSIS
        Applies one of the pre-defined prompts.
     
    .DESCRIPTION
        Applies one of the pre-defined prompts.
     
    .PARAMETER Prompt
        The prompt to apply
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> Set-PSUPrompt -Prompt fred
     
        Applies the prompt fred uses.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Prompt,
        
        [switch]
        $EnableException
    )
    
    process
    {
        if (-not (Test-Path "$script:ModuleRoot\internal\prompts\$Prompt.prompt.ps1"))
        {
            Stop-PSFFunction -Message "Failed to find prompt: $Prompt" -Target $Prompt -EnableException $EnableException -Category ObjectNotFound
            return
        }
        & "$script:ModuleRoot\internal\prompts\$Prompt.prompt.ps1"
    }
}

function Set-PSUShell
{
    <#
        .SYNOPSIS
            Command that sets various console properties
         
        .DESCRIPTION
            Command that sets various console properties.
         
        .PARAMETER WindowWidth
            The width of the console window.
            Not much of a change on windows 10, more of a chore on older console hosts.
         
        .PARAMETER BackgroundColor
            The background color to use.
            Is PSReadline aware.
         
        .PARAMETER ForegroundColor
            The foreground color to use.
            Is PSReadline aware.
         
        .PARAMETER BufferLength
            How lengthy a memory the console screen keeps.
            The size of the stuff cls clears.
         
        .PARAMETER WindowTitle
            The title the window should have.
         
        .PARAMETER EnableException
            Replaces user friendly yellow warnings with bloody red exceptions of doom!
            Use this if you want the function to throw terminating errors you want to catch.
         
        .EXAMPLE
            PS C:\> Set-PSUShell -WindowWidth 140 -WindowTitle "The Foo Shell" -ForegroundColor DarkGreen -BackgroundColor Black
     
            Sets the current shell to ...
            - 140 pixel width
            - have a title of "The Foo Shell"
            - Use a foreground color of DarkGreen for all output, default prompt color and comment color (PSReadline syntax detection remains unaffected)
            - Use a background color of Black
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    Param (
        [int]
        $WindowWidth,
        
        [System.ConsoleColor]
        $BackgroundColor,
        
        [System.ConsoleColor]
        $ForegroundColor,
        
        [int]
        $BufferLength,
        
        [string]
        $WindowTitle,
        
        [switch]
        $EnableException
    )
    
    # Test whether the PSReadline Module is loaded
    $PSReadline = Get-Module PSReadline
    
    #region Utility Functions
    function Set-ShellWindowWidth
    {
        [CmdletBinding()]
        Param (
            $WindowWidth
        )
        
        $currentWindow = $host.ui.rawui.WindowSize
        $currentBuffer = $host.ui.rawui.Buffersize
        
        if ($currentBuffer.Width -gt $WindowWidth)
        {
            # Set Window
            $currentWindow.Width = $WindowWidth
            $host.ui.rawui.WindowSize = $currentWindow
            
            # Set Buffer
            $currentBuffer.Width = $WindowWidth
            $host.ui.rawui.Buffersize = $currentBuffer
        }
        else
        {
            # Set Buffer
            $currentBuffer.Width = $WindowWidth
            $host.ui.rawui.Buffersize = $currentBuffer
            
            # Set Window
            $currentWindow.Width = $WindowWidth
            $host.ui.rawui.WindowSize = $currentWindow
        }
    }
    #endregion Utility Functions
    
    #region Set Buffer
    if (Test-PSFParameterBinding -ParameterName "BufferLength")
    {
        $currentBuffer = $host.ui.rawui.Buffersize
        $currentBuffer.Height = $BufferLength
        $host.ui.rawui.Buffersize = $currentBuffer
    }
    #endregion Set Buffer
    
    #region Set Foreground Color
    if (Test-PSFParameterBinding -ParameterName "ForegroundColor")
    {
        $host.ui.rawui.ForegroundColor = $ForegroundColor
        
        if ($PSReadline.Version.Major -eq 1)
        {
            Set-PSReadlineOption -ContinuationPromptForegroundColor $ForegroundColor
            Set-PSReadlineOption -ForegroundColor $ForegroundColor -TokenKind 'Comment'
            Set-PSReadlineOption -ForegroundColor $ForegroundColor -TokenKind None
        }
        elseif ($PSReadline.Version.Major -gt 1)
        {
            Set-PSReadLineOption -Colors @{
                Default = $ForegroundColor
                Comment = $ForegroundColor
                ContinuationPrompt = $ForegroundColor
            }
        }
    }
    #endregion Set Foreground Color
    
    #region Set Background Color
    if (Test-PSFParameterBinding -ParameterName "BackgroundColor")
    {
        $host.ui.rawui.BackgroundColor = $BackgroundColor
        if ($PSReadline.Version.Major -eq 1)
        {
            Set-PSReadlineOption -ContinuationPromptBackgroundColor $BackgroundColor
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'None'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Comment'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Keyword'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'String'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Operator'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Variable'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Command'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Type'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Number'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Member'
            Set-PSReadlineOption -BackgroundColor $BackgroundColor -TokenKind 'Parameter'
            Set-PSReadlineOption -EmphasisBackgroundColor $BackgroundColor
            Set-PSReadlineOption -ErrorBackgroundColor $BackgroundColor
        }
    }
    #endregion Set Background Color
    
    #region Set Window Title
    if (Test-PSFParameterBinding -ParameterName "WindowTitle")
    {
        $host.ui.rawui.Windowtitle = $WindowTitle
    }
    #endregion Set Window Title
    
    #region Set Window Width
    if (Test-PSFParameterBinding -ParameterName "WindowWidth")
    {
        try { Set-ShellWindowWidth -WindowWidth $WindowWidth -ErrorAction Stop }
        catch
        {
            Stop-PSFFunction -Message "Failed to set window width to $WindowWidth" -EnableException $EnableException -ErrorRecord $_ -Tag 'fail', 'width', 'console', 'window'
            return
        }
    }
    #endregion Set Window Width
}

function Start-PSUTimer
{
<#
    .SYNOPSIS
        Creates a timer that will alarm the user after it has expired.
     
    .DESCRIPTION
        Creates a timer that will alarm the user after it has expired.
        Provides both visual and sound warnings.
        Also provides a progress bar with a time remaining display.
     
    .PARAMETER Duration
        The time to wait.
     
    .PARAMETER Message
        What to wait for.
     
    .PARAMETER AlarmCount
        How often to give warning.
     
    .PARAMETER NoProgress
        Disables progress bar.
     
    .PARAMETER AlarmInterval
        In what time interval to write warnings and send sound.
     
    .PARAMETER MinFrequency
        The minimum frequency of the beeps.
        Must be at least one lower than MaxFrequency.
        Increase delta to play random frequency sounds on each beep.
     
    .PARAMETER MaxFrequency
        The maximum frequency of the beeps.
        Must be at least one higher than MaxFrequency.
        Increase delta to play random frequency sounds on each beep.
     
    .EXAMPLE
        PS C:\> timer 170 Tea
         
        After 170 Duration give warning that the tea is ready.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [Alias('Seconds')]
        [PSFDateTime]
        $Duration,
        
        [Parameter(Position = 1, Mandatory = $true)]
        $Message,
        
        [Parameter(Position = 2)]
        [int]
        $AlarmCount = 25,
        
        [switch]
        $NoProgress,
        
        [int]
        $AlarmInterval = 250,
        
        [int]
        $MinFrequency = 2999,
        
        [int]
        $MaxFrequency = 3000
    )
    
    begin
    {
        $start = Get-Date
        $end = $Duration.Value
        
        function Get-FriendlyTime
        {
            [CmdletBinding()]
            param (
                [int]
                $Seconds
            )
            
            $tempSeconds = $Seconds
            $strings = @()
            if ($tempSeconds -gt 3599)
            {
                [int]$count = [math]::Floor(($tempSeconds / 3600))
                $strings += "{0}h" -f $count
                $tempSeconds = $tempSeconds - ($count * 3600)
            }
            
            if ($tempSeconds -gt 59)
            {
                [int]$count = [math]::Floor(($tempSeconds / 60))
                $strings += "{0}m" -f $count
                $tempSeconds = $tempSeconds - ($count * 60)
            }
            
            $strings += "{0}s" -f $tempSeconds
            
            $strings -join " "
        }
    }
    process
    {
        if (-not $NoProgress)
        {
            Write-Progress -Activity "Waiting for $Message" -Status "Starting" -PercentComplete 0
        }
        
        while ($end -gt (Get-Date))
        {
            Start-Sleep -Milliseconds 500
            
            if (-not $NoProgress)
            {
                $friendlyTime = Get-FriendlyTime -Seconds ($end - (Get-Date)).TotalSeconds
                [int]$percent = ((Get-Date) - $start).TotalSeconds / ($end - $start).TotalSeconds * 100
                Write-Progress -Activity "Waiting for $Message" -Status "Time remaining: $($friendlyTime)" -PercentComplete ([System.Math]::Min($percent, 100))
            }
        }
        
        if (-not $NoProgress)
        {
            Write-Progress -Activity "Waiting for $Message" -Completed
        }
        
        $countAlarm = 0
        while ($countAlarm -lt $AlarmCount)
        {
            Write-PSFHostColor -String "(<c='sub'>$countAlarm</c>) ### <c='em'>$($Message)</c> ###"
            [System.Console]::Beep((Get-Random -Minimum $MinFrequency -Maximum $MaxFrequency), $AlarmInterval)
            Start-Sleep -Milliseconds $AlarmInterval
            $countAlarm++
        }
    }
}
Import-PSUAlias -Name "timer" -Command "Start-PSUTimer"

function Get-PSUPathAlias
{
<#
    .SYNOPSIS
        Gets the PSUPathAlias configuration values.
 
    .DESCRIPTION
        Gets the PSUPathAlias configuration values from the PSFConfig system.
 
    .PARAMETER Alias
        This is the name of the alias that you want for Set-PSUPath. Wildcards accepted
 
        Default Value: *
 
    .EXAMPLE
        PS C:\> Get-PSUPathAlias
     
        Returns all aliases
#>

    [CmdletBinding()]
    param (
        [string]
        $Alias = '*'
    )
    
    $aliases = Get-PSFConfig -FullName psutil.pathalias.$Alias
    
    foreach ($currentAlias in $aliases)
    {
        [pscustomobject]@{
            Alias = ($currentAlias.fullname -replace '^psutil.pathalias.')
            Path  = $currentAlias.value
        }
    }
}


function Remove-PSUPathAlias
{
<#
    .SYNOPSIS
        Removes a path alias fromm the configuration system.
     
    .DESCRIPTION
        Removes a path alias from the configuration system using Unregister-PSFConfig.
        Note: This command has no effect on configuration setings currently in memory.
     
    .PARAMETER Alias
        The name of the Alias that you want to remove from the configuration system.
     
    .EXAMPLE
        PS C:\> Remove-PSUPathAlias -Alias work
     
        Removes the path alias named work from the configuration system.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(ValuefromPipelineByPropertyName = $true)]
        $Alias
    )
    
    process
    {
        Get-PSFConfig -FullName psutil.pathalias.$Alias | Unregister-PSFConfig
        Remove-PSFAlias -Name $Alias
    }
}

function Set-PSUPath
{
<#
    .SYNOPSIS
        Detects the alias that called it and sets the location to the corresponding path found in the configuration system.
 
    .DESCRIPTION
        Detects the alias that called it and sets the location to the corresponding path.
        This function will normally be called using an alias that gets set by using Set-PSUPathAlias.
 
    .PARAMETER Alias
        This is the name of the alias that called the command.
        Default Value is $MyInvocation.InvocationName and is detected automatically
 
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
 
    .EXAMPLE
        PS C:\> Software
        PS C:\Software>
 
        In this example 'Software' is an alias for Set-PSUPath that was created by using Set-PSUPathAlias.
        Set-PSUPath detected that 'Software' was the alias that called it and then sends it to the path.
        It receives the path from Get-PSUPathAlias 'software'
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [string]
        $Alias = $MyInvocation.InvocationName,
        
        [switch]
        $EnableException
    )
    
    begin
    {
        function Resolve-PsuPath
        {
            [OutputType([System.String])]
            [CmdletBinding()]
            param (
                [string]
                $Path
            )
            
            $environ = @{}
            foreach ($item in Get-ChildItem env:)
            {
                $environ[$item.Name] = $item.Value
            }
            $pattern = '%{0}%' -f ($environ.Keys -join '%|%')
            $replacement = {
                param (
                    [string]
                    $Match
                )
                $environ = @{ }
                foreach ($item in Get-ChildItem env:)
                {
                    $environ[$item.Name] = $item.Value
                }
                $environ[$Match.Trim('%')]
            }
            [regex]::Replace($Path, $pattern, $replacement, 'IgnoreCase')
        }
    }
    process
    {
        try
        {
            $psfConfigPath = Get-PSFConfigValue -FullName psutil.pathalias.$Alias -NotNull
        }
        
        catch
        {
            $paramStopPSFFunction = @{
                Message            = "Unable to find a path setting for the alias"
                Category        = 'InvalidOperation'
                Tag                = 'fail'
                ErrorRecord        = $_
                EnableException = $EnableException
            }
            
            Stop-PSFFunction @paramStopPSFFunction
            return
        }
        
        try
        {
            Set-Location (Resolve-PsuPath -Path $psfConfigPath) -ErrorAction Stop
        }
        catch
        {
            $psfFuncParams = @{
                EnableException = $EnableException
                Message            = "Unable to set location to $psfConfigPath"
                Category        = 'InvalidOperation'
                Tag                = 'fail'
                ErrorRecord        = $_
            }
            Stop-PSFFunction @psfFuncParams
            return
        }
    }
}

function Set-PSUPathAlias
{
<#
    .SYNOPSIS
        Used to create an an alias that sets your location to the path you specify.
     
    .DESCRIPTION
        A detailed description of the Set-PSUPathAlias function.
     
    .PARAMETER Alias
        Name of the Alias that will be created for Set-PSUPath.
        Set-PSU Path detects the alias that called it and then finds the corresponding PSFConfig entry for it.
     
    .PARAMETER Path
        This is the path that you want your location to change to when the alias is called.
     
    .PARAMETER Register
        Causes PSUtil to remember the alias across sessions.
        For more advanced options, see Register-PSFConfig.
     
    .PARAMETER EnableException
        Replaces user friendly yellow warnings with bloody red exceptions of doom!
        Use this if you want the function to throw terminating errors you want to catch.
     
    .EXAMPLE
        PS C:\> Set-PSUPathAlias -Alias 'work' -Path 'C:\work'
        Creates an alias to Set-PSUPath that will set the location to 'c:\work'
     
    .EXAMPLE
        PS C:\> Set-PSUPathAlias -Alias 'repos' -Path 'C:\repos' -Register
         
        Creates an alias for repos and registers the setting so that it will persist between sessions.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param
    (
        [Parameter(Position = 0, Mandatory)]
        [string]
        $Alias,
        
        [Parameter(Position = 1, Mandatory)]
        [string]
        $Path,
        
        [switch]
        $Register,
        
        [switch]
        $EnableException
    )
    
    try
    {
        Set-PSFConfig -FullName psutil.pathalias.$Alias -Value $Path -Description 'Sets an alias for Set-PSUPath that takes you to the path specified in the value.'
    }
    catch
    {
        $stopParams = @{
            Message            = 'Error encountered. Alias not set'
            Category        = 'InvalidOperation'
            Tag                = 'Fail'
            ErroRecord        = $_
            EnableException = $EnableException
        }
        Stop-PSFFunction @stopParams
        return
    }
    
    if ($Register)
    {
        Get-PSFConfig -FullName psutil.pathalias.$Alias | Register-PSFConfig
    }
    
    try
    {
        Import-PSUAlias -Name $Alias -Command Set-PSUPath
    }
    catch
    {
        $stopParams = @{
            Message            = 'Error. Alias not set'
            Category        = 'InvalidOperation'
            Tag                = 'Fail'
            ErroRecord        = $_
            EnableException = $EnableException
        }
        Stop-PSFFunction @stopParams
        return
    }
}


Register-PSFTeppScriptblock -Name psutil-convert-object-from -ScriptBlock {
    [PSUtil.Object.ObjectHost]::Conversions.Values.From | Select-Object -Unique
}

Register-PSFTeppScriptblock -Name psutil-convert-object-to -ScriptBlock {
    [PSUtil.Object.ObjectHost]::Conversions.Values | Where-Object From -EQ $fakeBoundParameter.From | Expand-PSUObject -Name To
}

Register-PSFTeppScriptblock -Name 'PSUtil.Knowledge.Book' -ScriptBlock {
    $libraryPath = Get-PSFConfigValue -FullName 'PSUtil.Knowledge.LibraryPath'
    if (-not (Test-Path $libraryPath)) { return }
    
    Get-ChildItem -Path $libraryPath -Filter *.json | ForEach-Object {
        [System.Text.Encoding]::UTF8.GetString(([convert]::FromBase64String($_.BaseName)))
    } | Sort-Object
}

Register-PSFTeppScriptblock -Name 'PSUtil.Knowledge.Page' -ScriptBlock {
    $book = '*'
    if ($fakeBoundParameter.Book) { $book = $fakeBoundParameter.Book }
    (Read-PSUKnowledge -Book $book).Name | Select-PSFObject -Unique | Sort-Object
}

Register-PSFTeppScriptblock -Name 'PSUtil.Knowledge.Tags' -ScriptBlock {
    $book = '*'
    if ($fakeBoundParameter.Book) { $book = $fakeBoundParameter.Book }
    (Read-PSUKnowledge -Book $book).Tags | Select-PSFObject -Unique | Sort-Object
}

Register-PSFTeppScriptBlock -Name 'PSUtil-Module-Installed' -ScriptBlock {
    (Get-PSFTaskEngineCache -Module PSUtil -Name Module).InstalledModules
}

Register-PSFTeppScriptBlock -Name 'PSUtil-Module-Total' -ScriptBlock {
    (Get-PSFTaskEngineCache -Module PSUtil -Name Module).AvailableModules
}

Register-PSFTeppScriptblock -Name 'PSUtil-Module-Repository' -ScriptBlock {
    (Get-PSFTaskEngineCache -Module PSUtil -Name Module).Repositories
}

Register-PSFTeppScriptblock -Name 'PSUtil-Module-PackageProvider' -ScriptBlock {
    (Get-PSFTaskEngineCache -Module PSUtil -Name Module).PackageProvider
}

Register-PSFTeppScriptblock -Name 'psutil.prompt' -ScriptBlock {
    $module = Get-Module PSUtil
    & $module {
        foreach ($item in (Get-ChildItem "$script:ModuleRoot\internal\prompts"))
        {
            $item.BaseName -replace '\.prompt$'
        }
    }
}

Register-PSFTeppScriptblock -Name psutil-userprofile -ScriptBlock {
    Get-ChildItem "$env:SystemDrive\Users" -Force | Where-Object PSIsContainer | Expand-PSUObject Name
}

Register-PSFTeppArgumentCompleter -Command Invoke-PSUDesktop -Parameter User -Name psutil-userprofile

Register-PSFTeppArgumentCompleter -Command Convert-PSUObject -Parameter From -Name psutil-convert-object-from
Register-PSFTeppArgumentCompleter -Command Convert-PSUObject -Parameter To -Name psutil-convert-object-to

Register-PSFTeppArgumentCompleter -Command Set-PSUPrompt -Parameter Prompt -Name 'psutil.prompt'

#region Module
Register-PSFTeppArgumentCompleter -Command Update-Module -Parameter Name -Name 'PSUtil-Module-Installed'
Register-PSFTeppArgumentCompleter -Command Uninstall-Module -Parameter Name -Name 'PSUtil-Module-Installed'
Register-PSFTeppArgumentCompleter -Command Get-InstalledModule -Parameter Name -Name 'PSUtil-Module-Installed'
Register-PSFTeppArgumentCompleter -Command Install-Module -Parameter Name -Name 'PSUtil-Module-Total'

Register-PSFTeppArgumentCompleter -Command Find-Command -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Find-DscResource -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Find-Module -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Find-RoleCapability -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Find-Script -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Install-Module -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Install-Script -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Publish-Module -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Publish-Script -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Save-Module -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Save-Script -Parameter Repository -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Get-PSRepository -Parameter Name -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Unregister-PSRepository -Parameter Name -Name 'PSUtil-Module-Repository'
Register-PSFTeppArgumentCompleter -Command Register-PSRepository -Parameter PackageManagementProvider -Name 'PSUtil-Module-PackageProvider'
#endregion Module

#region Input Object Property
Register-PSFTeppArgumentCompleter -Command Select-Object -Parameter Property -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Select-Object -Parameter ExpandProperty -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Select-Object -Parameter ExcludeProperty -Name PSFramework-Input-ObjectProperty

Register-PSFTeppArgumentCompleter -Command Expand-PSUObject -Parameter Name -Name PSFramework-Input-ObjectProperty

Register-PSFTeppArgumentCompleter -Command Get-Member -Parameter Name -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Format-Table -Parameter Property -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Format-List -Parameter Property -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Group-Object -Parameter Property -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Measure-Object -Parameter Property -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Sort-Object -Parameter Property -Name PSFramework-Input-ObjectProperty
Register-PSFTeppArgumentCompleter -Command Where-Object -Parameter Property -Name PSFramework-Input-ObjectProperty
#endregion Input Object Property

Register-PSFTeppArgumentCompleter -Command Read-PSUKnowledge -Parameter Book -Name 'PSUtil.Knowledge.Book'
Register-PSFTeppArgumentCompleter -Command Read-PSUKnowledge -Parameter Name -Name 'PSUtil.Knowledge.Page'
Register-PSFTeppArgumentCompleter -Command Read-PSUKnowledge -Parameter Tags -Name 'PSUtil.Knowledge.Tags'
Register-PSFTeppArgumentCompleter -Command Remove-PSUKnowledge -Parameter Book -Name 'PSUtil.Knowledge.Book'
Register-PSFTeppArgumentCompleter -Command Remove-PSUKnowledge -Parameter Name -Name 'PSUtil.Knowledge.Page'
Register-PSFTeppArgumentCompleter -Command Write-PSUKnowledge -Parameter Book -Name 'PSUtil.Knowledge.Book'
Register-PSFTeppArgumentCompleter -Command Write-PSUKnowledge -Parameter Tags -Name 'PSUtil.Knowledge.Tags'

New-PSFLicense -Product 'PSUtil' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2018-12-16") -Text @"
Copyright (c) 2018 Friedrich Weinmann
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


<#
In this file, all default expansion definitions are stored.
Wrap into a region each, with corresponding region label.
#>


#region Microsoft.PowerShell.Commands.MatchInfo
Register-PSUObjectExpansion -TypeName "Microsoft.PowerShell.Commands.MatchInfo" -ScriptBlock {
    param (
        $Object
    )
    
    foreach ($item in $Object.Matches)
    {
        $item.Groups[1 .. ($item.Groups.Count - 1)].Value
    }
}
#endregion Microsoft.PowerShell.Commands.MatchInfo

#region Microsoft.PowerShell.Commands.MemberDefinition
Register-PSUObjectExpansion -TypeName "Microsoft.PowerShell.Commands.MemberDefinition" -ScriptBlock {
    param (
        $Object
    )
    
    $Object.Definition.Replace("), ", ")þ").Split("þ")
}
#endregion Microsoft.PowerShell.Commands.MemberDefinition

#region System.Management.Automation.FunctionInfo
Register-PSUObjectExpansion -TypeName "System.Management.Automation.FunctionInfo" -ScriptBlock {
    param (
        $Object
    )
    
    @"
function $($Object.Name)
{
$($Object.Definition)
}
"@

}
#endregion System.Management.Automation.FunctionInfo

#region System.Management.Automation.AliasInfo
Register-PSUObjectExpansion -TypeName "System.Management.Automation.AliasInfo" -ScriptBlock {
    param (
        $Object
    )
    
    if ($Object.ResolvedCommand.CommandType -eq "Function")
    {
        @"
function $($Object.ResolvedCommand.Name)
{
$($Object.ResolvedCommand.Definition)
}
"@

    }
    else
    {
        $Object.ResolvedCommand
    }
}
#endregion System.Management.Automation.AliasInfo

# The king of aliases
Import-PSUAlias -Name "grep" -Command "Select-String"

# Strings
Import-PSUAlias -Name add -Command Add-String
Import-PSUAlias -Name Add-PSUString -Command Add-String
Import-PSUAlias -Name wrap -Command Add-String
Import-PSUAlias -Name Add-PSUString -Command Add-String
Import-PSUAlias -Name format -Command Format-String
Import-PSUAlias -Name Format-PSUString -Command Format-String
Import-PSUAlias -Name trim -Command Get-SubString
Import-PSUAlias -Name Remove-PSUString -Command Get-SubString
Import-PSUAlias -Name join -Command Join-String
Import-PSUAlias -Name Join-PSUString -Command Join-String
Import-PSUAlias -Name replace -Command Set-String
Import-PSUAlias -Name Set-PSUString -Command Set-String
Import-PSUAlias -Name split -Command Split-String
Import-PSUAlias -Name Split-PSUString -Command Split-String

# Add simple aliases for everyday cmdlets
Import-PSUAlias -Name "a" -Command "Get-Alias"
Import-PSUAlias -Name "c" -Command "Get-Command"
Import-PSUAlias -Name "m" -Command "Measure-Object"
Import-PSUAlias -Name "v" -Command "Get-Variable"

# Add aliases for frequent export commands
Import-PSUAlias -Name "ix" -Command "Import-PSFClixml"
Import-PSUAlias -Name "ex" -Command "Export-PSFClixml"
Import-PSUAlias -Name "ic" -Command "Import-Csv"
Import-PSUAlias -Name "ec" -Command "Export-Csv"
Import-PSUAlias -Name "ctj" -Command "ConvertTo-Json"
Import-PSUAlias -Name "cfj" -Command "ConvertFrom-Json"
Import-PSUAlias -Name "ctx" -Command "ConvertTo-PSFClixml"
Import-PSUAlias -Name "cfx" -Command "ConvertFrom-PSFClixml"

# Add alias for easy clipboarding
Import-PSUAlias -Name "ocb" -Command "Set-Clipboard"

# Add alias for creating object
Import-PSUAlias -Name "new" -Command "New-Object"

# Add alias for the better select and to avoid breaking on old command
Import-PSUAlias -Name "spo" -Command "Select-PSFObject"
Import-PSUAlias -Name "Select-PSUObject" -Command "Select-PSFObject"

if (Get-PSFConfigValue -FullName 'PSUtil.Import.Alias.SystemOverride')
{
    Remove-PSFAlias -Name select -Force
    Remove-PSFAlias -Name gm -Force
    Import-PSUAlias -Name select -Command 'Select-PSFObject'
    Import-PSUAlias -Name gm -Command 'Get-PSMDMember'
}

Import-PSUAlias -Name "rmn" -Command "Remove-PSFNull"

if ((Get-Module psreadline).Version.Major -ge 2)
{
    Set-PSReadlineKeyHandler -Chord 'Shift+SpaceBar' -BriefDescription Whitespace -Description "Inserts a whitespace" -ScriptBlock {
        [Microsoft.Powershell.PSConsoleReadLine]::Insert(' ')
    }
}

if ((Get-PSFConfigValue -FullName "PSUtil.Import.Keybindings" -Fallback $true) -and (Get-Module PSReadline))
{
    foreach ($file in (Get-ChildItem -Path (Join-PSFPath $script:ModuleRoot 'internal' 'keybindings')))
    {
        . Import-ModuleFile -Path $file.FullName
    }
}


Register-PSUObjectConversion -From dec -To bin -ScriptBlock {
    Param (
        $InputObject
    )
    
    [System.Convert]::ToString($InputObject, 2)
}

Register-PSUObjectConversion -From bin -To dec -ScriptBlock {
    Param (
        $InputObject
    )
    
    [System.Convert]::ToInt32($InputObject, 2)
}

Register-PSUObjectConversion -From dec -To hex -ScriptBlock {
    Param (
        $InputObject
    )
    
    [System.Convert]::ToString($InputObject, 16)
}

Register-PSUObjectConversion -From hex -To dec -ScriptBlock {
    Param (
        $InputObject
    )
    
    [System.Convert]::ToInt32($InputObject, 16)
}


Register-PSUObjectConversion -From dec -To oct -ScriptBlock {
    Param (
        $InputObject
    )
    
    [System.Convert]::ToString($InputObject, 8)
}

Register-PSUObjectConversion -From oct -To dec -ScriptBlock {
    Param (
        $InputObject
    )
    
    [System.Convert]::ToInt32($InputObject, 8)
}


Register-PSUObjectConversion -From hex -To bin -ScriptBlock {
    Param (
        $InputObject
    )
    
    $temp = [System.Convert]::ToInt32($InputObject, 16)
    [System.Convert]::ToString($temp, 2)
}

Register-PSUObjectConversion -From bin -To hex -ScriptBlock {
    Param (
        $InputObject
    )
    
    $temp = [System.Convert]::ToInt32($InputObject, 2)
    [System.Convert]::ToString($temp, 16)
}


Register-PSUObjectConversion -From hex -To oct -ScriptBlock {
    Param (
        $InputObject
    )
    
    $temp = [System.Convert]::ToInt32($InputObject, 16)
    [System.Convert]::ToString($temp, 8)
}

Register-PSUObjectConversion -From oct -To hex -ScriptBlock {
    Param (
        $InputObject
    )
    
    $temp = [System.Convert]::ToInt32($InputObject, 8)
    [System.Convert]::ToString($temp, 16)
}


Register-PSUObjectConversion -From bin -To oct -ScriptBlock {
    Param (
        $InputObject
    )
    
    $temp = [System.Convert]::ToInt32($InputObject, 2)
    [System.Convert]::ToString($temp, 8)
}

Register-PSUObjectConversion -From oct -To bin -ScriptBlock {
    Param (
        $InputObject
    )
    
    $temp = [System.Convert]::ToInt32($InputObject, 8)
    [System.Convert]::ToString($temp, 2)
}


Register-PSUObjectConversion -From script -To encoded -ScriptBlock {
    Param (
        $InputObject
    )
    
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($InputObject)
    [Convert]::ToBase64String($bytes)
}

Register-PSUObjectConversion -From encoded -To script -ScriptBlock {
    Param (
        $InputObject
    )
    
    $bytes = [System.Convert]::FromBase64String($InputObject)
    [System.Text.Encoding]::Unicode.GetString($bytes)
}


Set-PSFTaskEngineCache -Module PSUtil -Name Module -Lifetime 5m -Collector {
    $paramRegisterPSFTaskEngineTask = @{
        Name        = 'PSUtil.ModuleCache'
        Description = 'Refreshes the data on locally available modules and repositories'
        Once        = $true
        ResetTask   = $true
        ScriptBlock = {
            $data = @{
                InstalledModules = ((Get-InstalledModule).Name | Select-Object -Unique)
                AvailableModules  = ((Get-Module -ListAvailable).Name | Select-Object -Unique)
                PackageProvider  = ((Get-PackageProvider).Name)
                Repositories = ((Get-PSRepository).Name)
            }
            Set-PSFTaskEngineCache -Module PSUtil -Name Module -Value $data
        }
    }
    
    Register-PSFTaskEngineTask @paramRegisterPSFTaskEngineTask
    
    while (-not [PSFramework.TaskEngine.TaskHost]::GetCacheItem('PSUtil', 'Module').Value)
    {
        Start-Sleep -Milliseconds 100
    }
    
    # Deliver expired data right away anyway
    [PSFramework.TaskEngine.TaskHost]::GetCacheItem('PSUtil', 'Module').Value
}
#endregion Load compiled code