SystemEssentials.psm1

function Get-WindowsVersion {
    Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
 
public static class WinBrand {
    [DllImport("winbrand.dll", CharSet = CharSet.Unicode)]
    public static extern IntPtr BrandingFormatString(string fmt, string win);
 
    public static string Brand(string fmt, string win) {
        return Marshal.PtrToStringUni(BrandingFormatString(fmt, win));
    }
}
'@


    $r = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\'

    [PSCustomObject]@{
        ProductName    = [WinBrand]::Brand("%WINDOWS_LONG%", "%WINDOWS_LONG%")
        DisplayVersion = $r.DisplayVersion
        EditionID      = $r.EditionID
        ReleaseID      = $r.ReleaseID
        Build          = "$($r.CurrentBuild).$($r.UBR)"
        BuildBranch    = $r.BuildBranch
        InstalledUTC   = ([datetime]"1/1/1601").AddTicks($r.InstallTime)
        Computername   = $env:ComputerName
    }
}

function Get-WindowsVersionString {
    Get-WindowsVersion | ForEach {
        "{3} {0} Version {1} (OS Build {2})" -f $_.ProductName, $_.EditionID, $_.Build, $_.Computername
    }
}

function Get-PowershellVersionString {
    "{0}" -f $PsVersionTable.PsVersion
}

function Get-AliasTable {
    Get-Alias | ForEach-Object {
        [PSCustomObject]@{
            Alias    = $_.Name
            Function = $_.Definition
            Source   = $_.Source
        }
    } | Format-Table -AutoSize
}

Set-Alias  winver   Get-WindowsVersion
Set-Alias  about    Get-WindowsVersionString
Set-Alias  psver    Get-PowershellVersionString
Set-Alias  aliases  Get-AliasTable

<#
.SYNOPSIS
    Writes a detailed message string.
 
.DESCRIPTION
    This command is designed to be used within your functions and scripts
    to make it easier to write a detailed message that you can use
    as verbose output. The assumption is that you are using an advanced
    function with Begin, Process, and End scriptblocks. You can create a
    detailed message to indicate what part of the code is being executed.
    The output can include a full-time stamp, or a time string which
    includes a millisecond value.
 
.NOTES
    In a script you might use it like this in a Begin block:
 
    $pfx = "BEGIN"
    Write-Detail "Starting $($MyInvocation.MyCommand)" -Prefix $pfx | Write-Verbose
    Write-Detail "PS $($PSVersionTable.PSVersion)" -Prefix $pfx | Write-Verbose
 
    If you don't specify a prefix, it will default to PROCESS.
 
.PARAMETER Message
    The message to display after the time stamp and prefix.
 
.PARAMETER Prefix
    Indicate whether you are in the BEGIN, PROCESS, or END script block.
    Although you can specify any text. It will be displayed in upper case.
 
.PARAMETER Date
    Display a date value like 9/15/2024 11:36:41.
 
.PARAMETER Time
    Display a time value with milliseconds like 11:37:01:4029.
 
.EXAMPLE
    PS C:\> Write-Detail "Getting file information" -Prefix Process
    [PROCESS] Getting file information
 
    Description:
    Normally you would use this command in a function, but here is an example
    from the console so that you can see what to expect.
 
.EXAMPLE
    PS C:\> Write-Detail -message "Initializing variables" -Prefix Begin -time
    01:25:29:8700 [BEGIN] Initializing variables
#>

function Write-Detail {
    [CmdletBinding(DefaultParameterSetName = "Default")]
    Param(
        [Parameter(Position = 0, Mandatory)]
        [Parameter(ParameterSetName = "Default")]
        [Parameter(ParameterSetName = "Date")]
        [Parameter(ParameterSetName = "Time")]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(ParameterSetName = "Default")]
        [Parameter(ParameterSetName = "Date")]
        [Parameter(ParameterSetName = "Time")]
        [string]$Prefix = "PROCESS",

        [Parameter(ParameterSetName = "Date")]
        [switch]$Date,
        [Parameter(ParameterSetName = "Time")]
        [Switch]$Time
    )

    if ($time) {
        $dt = (Get-Date -Format "hh:mm:ss:ffff")
    } elseif ($Date) {
        $dt = "{0} {1}" -f (Get-Date).ToShortDateString(), (Get-Date -Format "hh:mm:ss")
    }

    if ($PSCmdlet.ParameterSetName -eq 'Default') {
        $Text = "[$($prefix.ToUpper())] $Message"
    } else {
        $Text = "$dt [$($prefix.ToUpper())] $Message"
    }

    return $Text
}

<#
.SYNOPSIS
    Prints a message to the console and writes it to a file.
 
.DESCRIPTION
    This command is designed for simple messages logging.
    It doesn't requires multiple parameters per each call, only the message.
 
    If a path to a file to write to was not specified, it will be created
    automatically with the current date as a name. All subsequent
    calls to Write-Log will write messages to this file.
 
.NOTES
    To fully control the output use Write-Detail and Out-VerboseTee.
 
.PARAMETER Message
    The message to display after the time stamp and prefix.
 
.PARAMETER Level
    Prefix that will be displayed before the message.
    You can specify any text. It will be displayed in upper case.
 
.PARAMETER LogFullPath
    Display a date value like 9/15/2024 11:36:41.
 
.EXAMPLE
    Write-Log 'Tracing script'
    12:13:00 [INFO] Tracing script
 
.EXAMPLE
    Write-Log 'Tracing script' 'Debug' 'trace.log'
    12:21:41 [DEBUG] Tracing script
 
.EXAMPLE
    Get-WindowsVersion | ConvertTo-Json | Write-Log
 
    Description:
    Dumps information about current system to the generated log.
 
.EXAMPLE
    (&"C:\Program Files\7-Zip\7z.exe" l z.zip) | Out-String | Write-Log
 
    Description:
    List files inside z.zip, print them to console and write to the log.
 
.EXAMPLE
    Write-Log 'Tracing script' 'Info' 'info.log'
    Write-Log 'Init variables' 'Info' <-- writes to info.log
    Write-Log 'Create objects' 'Info' <-- writes to info.log
    Write-Log 'Wrong files' 'Warning' 'warns.log'
    Write-Log 'Locked file' 'Warning' <-- writes to warns.log
     
    Description:
    You can override the file for all subsequent calls
 
.LINK
    Out-VerboseTee
.LINK
    Write-Detail
.LINK
    ConvertTo-Json
#>

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position=0, ValueFromPipeline)]
        [string]$message,

        [Parameter(Position=1)]
        [string]$level = 'Info',

        [Parameter(Position=2)]
        [string]$logFullPath
    )

    begin {
        if($PSBoundParameters.ContainsKey('logFullPath')) {
            $script:logPath = $logFullPath
            Write-Debug "Recievied path to log: `"$($script:logPath)`""
        } elseif (-not (Test-Path variable:logPath)) {
            New-Variable logPath ("{0}.log" -f (Get-Date -format 'yyyy-MM-dd HH.mm.ss')) `
                -Scope Script -Visibility Private

            Write-Debug "Generated log: `"$($script:logPath)`""
        }
    }

    process {
        switch ($level) {
        'Verbose' {
            $isOutput = ($VerbosePreference -ne 'SilentlyContinue')
            $color = 'Cyan'
        }
        'Debug'   {
            $isOutput = ($DebugPreference   -ne 'SilentlyContinue')
            $color = 'DarkGray'
        }
        'Warning' {
            $isOutput = ($WarningPreference -ne 'SilentlyContinue')
            $color = 'Yellow'
        }
        'Error'   {
            $isOutput = ($ErrorActionPreference -ne 'SilentlyContinue')
            $color = 'Red'
        }
        default   {
            $isOutput = !$silent
            $color = 'White'
        }
        }

        $level = '[{0}]' -f $level.ToUpper()
        $date  = Get-Date -Format 'hh:mm:ss'

        if ($isOutput) {
            Write-Host   $date      -f DarkGray -n
            Write-Host " $level "   -f $color   -n
            Write-Host " $message"  -f $color
        }
    }

    end {
        ('{0} {1} {2}' -f $date, $level, $message) | Out-File -Append -FilePath $script:logPath
    }
}

<#
.SYNOPSIS
    Writes to the Verbose stream and a file.
 
.DESCRIPTION
    This command is intended to let you see your verbose output and write the
    verbose messages to a log file. It will only work if the verbose pipeline
    is enabled, usually when your command is run with -Verbose. This function
    is designed to be used within your scripts and functions. You either have
    to hard code a file name or find some other way to define it in your
    function or control script. You could pass a value as a parameter or set
    it as a PSDefaultParameterValue.
 
    When the command is run with -Verbose you will see the verbose output and
    it will be saved to the specified log file.
 
.PARAMETER Append
    Append to the specified text file.
 
.PARAMETER Encoding
    Specify a file encoding.
 
.PARAMETER Path
    The path for the output file.
 
.PARAMETER Value
    The message to be displayed as a verbose message and saved to the file.
 
.EXAMPLE
    PS C:\> $VerbosePreference= "continue"
    PS C:\> $log = New-CustomFileName ".\VerboseLog_%time.txt"
    PS C:\> Write-Detail "This is a verbose log test" | Out-VerboseTee -Path $log
    PS C:\> Get-Content $log
    11/29/2024 08:21:31:0704 [PROCESS] This is a verbose log test
    PS C:\> $verbosePreference = "SilentlyContinue"
    Normally you would use this command inside a function or script,
    but you can run it from the console if you want to understand how it works.
#>

function Out-VerboseTee {
    [CmdletBinding()]
    [Alias("log", "stdout")]

    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]$Value,
        [Parameter(Position = 0, Mandatory)]
        [string]$Path,
        [System.Text.Encoding]$Encoding,
        [switch]$Append
    )
    
    begin {
        # turn on verbose pipeline since if you are running this command you intend for it to be on
        $VerbosePreference = "continue"
    }
    
    process {
        # only run if Verbose is turned on
        if ($VerbosePreference -eq "continue") {
            $Value | Out-String | Write-Verbose
            [void]$PSBoundParameters.Remove("Append")

            if ($Append) {
                Add-Content @PSBoundParameters
            } else {
                Set-Content @PSBoundParameters
            }
        }
    }
    
    end {
        $VerbosePreference = "SilentlyContinue"
    }
}

<#
.SYNOPSIS
    Sends "pages" of objects to the pipeline.
 
.DESCRIPTION
    This function is designed to display groups or "pages" of objects to the PowerShell
    pipeline. It is modeled after the legacy `more` command-line utility. By default,
    the command will write objects out to the pipeline in groups of 50. You will be
    prompted after each grouping.
 
    Pressing M or Enter will get the next group. Pressing A will stop paging and display
    all of the remaining objects. Pressing N will display the next object. Press Q to
    stop writing anything else to the pipeline.
 
.NOTES
    Note that you may encounter an error message when quitting prematurely, especially
    on non-Windows platforms. You can ignore these errors.
 
.PARAMETER InputObject
    Specifies the objects to be sent to the pipeline.
 
.PARAMETER Count
    The number of objects to group as a page.
 
.PARAMETER ClearScreen
    Clear the screen before writing data to the pipeline.
 
    Type: SwitchParameter
    Parameter Sets: (All)
    Aliases: cls
 
    Required: False
    Position: Named
    Default value: False
    Accept pipeline input: False
    Accept wildcard characters: False
 
.EXAMPLE
    PS C:\> Get-Process | Out-More -count 10
    Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id SI ProcessName
    ------- ------ ----- ----- ----- ------ -- -- -----------
        103 9 1448 4220 67 0.02 1632 0 BtwRSupportService
         80 9 3008 8588 ...27 21.00 5192 1 conhost
         40 5 752 2780 ...82 0.00 5248 0 conhost
         53 7 972 3808 ...07 0.02 6876 1 conhost
        482 17 1932 3692 56 0.91 708 0 csrss
        520 30 2488 134628 180 31.67 784 1 csrss
        408 18 6496 12436 ...35 0.56 1684 0 dasHost
        180 14 3348 6748 66 0.50 4688 0 devmonsrv
    [M]ore [A]ll [N]ext [Q]uit
    Display processes in groups of 10.
 
.EXAMPLE
    PS C:\> dir c:\work -file -Recurse | more -ClearScreen | log -Variable work
    List all files in C:\Work and page them to Out-More using the default count,
    but after clearing the screen first. The results are then piped to Tee-Object
    which saves them to a variable.
#>

function Out-More {
    [CmdletBinding()]
    [Alias("more", "detailed")]

    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object[]]$InputObject,
        [ValidateRange(1, 1000)]
        [Alias("i")]
        [int]$Count = 50,
        [Alias("cls")]
        [Switch]$ClearScreen
    )

    begin {
        if ($ClearScreen) {
            Clear-Host
        }

        #initialize an array to hold objects
        $data = @()

        #initialize some variables to control flow
        $ShowAll = $False
        $ShowNext = $False
        $Ready = $False
        $Quit = $False

    }

    process {
        if ($Quit) {
            Write-Verbose "Quitting"
            break
        } elseif ($ShowAll) {
            $InputObject
        } elseif ($ShowNext) {
            Write-Verbose "Show Next"
            $ShowNext = $False
            $Ready = $True
            $data = , $InputObject
        } elseif ($data.count -lt $count) {
            Write-Verbose "Adding data"
            $data += $InputObject
        } else {
            #write the data to the pipeline
            $data
            #reset data
            $data  = , $InputObject
            $Ready = $True
        }

        if ($Ready) {
            #pause
            do {
                Write-Host "[M]ore [A]ll [N]ext [Q]uit " -ForegroundColor Green -NoNewline
                $r = Read-Host
                if ($r.Length -eq 0 -or $r -match "^m") {
                    #don't really do anything
                    $Asked = $True
                } else {
                    switch -regex ($r) {
                        "^n" {
                            $ShowNext = $True
                            $InputObject
                            $Asked = $True
                        }
                        "^a" {
                            $InputObject
                            $Asked = $True
                            $ShowAll = $True
                        }
                        "^q" {
                            #bail out
                            $Asked = $True
                            $Quit = $True
                        }
                        default {
                            $Asked = $False
                        }
                    }

                }
            } until ($Asked)

            $Ready = $false
            $Asked = $false
        }
    }

    end {
        # test if data is from a get-help command in
        # which case it will be a single string that needs
        # to be broken apart

        if ([regex]::Matches($data, "`n").count -gt 1) {
            [void]$PSBoundParameters.remove("InputObject")
            $data.split("`n") | Out-More @PSBoundParameters
        } elseif ($data[0].PSObject.TypeNames -contains "MamlCommandHelpInfo") {
            [void]$PSBoundParameters.remove("InputObject")
            ($data | Out-String).split("`n") | Out-More @PSBoundParameters
        }

        #display whatever is left in $data
        if ($data -and -not $ShowAll) {
            Write-Verbose "Displaying remaining data"
            return $data
        }
    }
}

<#
.SYNOPSIS
    Escapes arguments.
 
.DESCRIPTION
    Converts list of arguments (array of strings) to properly escaped string of arguments.
 
.PARAMETER Argument
    Array of stings with arguments values
#>

function ConvertTo-EscapedArguments {
    [CmdletBinding()]
    [Alias('escape')]
    
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('arg')]
        [string[]]$argument
    )
    
    begin {
        $escapedArgs = [System.Collections.Generic.List[string]]@()
    }

    process {
        foreach ($arg in $argument) {
            <#
            stdlib Rules:
            2N backslashes + " ==> N backslashes and begin/end quote
            2N+1 backslashes + " ==> N backslashes + literal "
            N backslashes ==> N backslashes
             
            https://docs.microsoft.com/en-gb/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw
            https://stackoverflow.com/questions/21314633/how-do-i-pass-a-literal-double-quote-from-powershell-to-a-native-command
            #>

            # escaping double quotes as we use them to indicate start/end of argument
            $arg = $arg.Replace('"', '\"')

            # double all the slashes followed by \"
            # (no more org before quote, so quote is never end/begin arg char)
            $arg = $arg -replace '(\\+)\\\"', '$1$1\"'

            # double all the slashes at the end of the argument
            $arg = $arg -replace '(\\+)$', '$1$1'

            # wrapping argument
            $escapedArgs.Add("`"$arg`"")
        }
        return ($escapedArgs -join ' ')
    }
}

<#
.SYNOPSIS
    Retrieves help information for PowerShell commands or external applications.
 
.DESCRIPTION
    For built-in PowerShell commands (Cmdlet, Function, Alias), it uses the standard Get-Help function.
    For external applications, it automatically tries various help parameter variants
    (--help, -h, /?, etc.), eliminating the need to remember application-specific help syntax.
 
.PARAMETER command
    The name of the Cmdlet, Function, Alias, full or relative path to the application
    or app name from PATH enviromental variable.
 
.PARAMETER arguments
    Arguments or sub-commands to pass to the command. If command is PowerShell command
    (Cmdlet, Function, Alias), pass arguments for Get-Help command that will be used to
    retrieve help message or specific details about this command.
 
.INPUTS
    System.String
    You can pipe file name or path to this function.
 
.OUTPUTS
    System.String
    Returns the help text for the command or application.
 
.EXAMPLE
    hlp Get-Process
 
    Description:
    Retrieves help for the built-in PowerShell command Get-Process.
    Equivalent to running Get-Help Get-Process.
 
.EXAMPLE
    hlp pip
 
    Description:
    Retrieves commands for the Python package manager.
 
.EXAMPLE
    hlp pip install
 
    Description:
    Retrieves help for the 'install' sub-command of Python package manager.
     
.EXAMPLE
    hlp "C:\Program Files\Git\cmd\git.exe"
 
    Description:
    Retrieves help information for the git VCS.
 
.EXAMPLE
    hlp Stop-HiddenScript -Parameter launcher
 
    Description:
    Retrieves detailed information about 'launcher' param.
 
.LINK
    Get-Help
 
.LINK
    Get-Command
#>

function Get-HelpMessage {
    [CmdletBinding()]
    [Alias('hlp', 'h')]

    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$command,

        [Parameter(ValueFromRemainingArguments)]
        [string[]]$arguments = @()
    )

    begin {
        [string[]]$helpParams = @('--help', '-help', 'help', '-h', '--h', '-?', '/help', '/h', '/?')
        # [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
    }

    process {
        $commandObj = Get-Command $command -CommandType Application -ErrorAction Ignore |
            Select-Object -First 1
        
        if ($commandObj -or (Test-Path $command)) {
            # Pass help arguments one by one.
            # Pass additional args if needed.
            $params = $arguments.count ? (ConvertTo-EscapedArguments $arguments) : ''
            
            # Prepare process launch
            $proc = New-Object System.Diagnostics.Process
            $proc.StartInfo = [System.Diagnostics.ProcessStartInfo]@{
                FileName = $commandObj.source   # Full path to the binary
                # StandardOutputEncoding = [System.Text.Encoding]::UTF8
                # StandardErrorEncoding = [System.Text.Encoding]::UTF8
                RedirectStandardError  = $true
                RedirectStandardOutput = $true
                UseShellExecute        = $false  # no visible shell
                WorkingDirectory       = (Get-Location).path 
            }
            
            ForEach ($help in $helpParams) {
                $proc.StartInfo.Arguments = "$params $help"
                $proc.Start() | Out-Null
                
                $output = $proc.StandardOutput.ReadToEnd()
                $proc.WaitForExit()

                if (!$proc.ExitCode) {
                    return $output
                }
            }
            
            Write-Error ($proc.StandardError.ReadToEnd())
            return
        }
        
        if (!$arguments.Count) {
            return (Get-Help $command)
        }

        # Parse arguments for Get-Help
        $params = @{}
        $i = 0;
        while ($i -lt $arguments.Count) {
            $key = $arguments[$i].Trim('-/" ')
            $val = $arguments[$i + 1]

            if ($val -and $val -notmatch '^[/-]') {
                # Next value is not a switch or named param
                $params[$key] = $val
                $i += 2
            } else {
                # No value provided, assume it's a switch
                $params[$key] = $true
                $i++
            }
        }

        Get-Help $command @params
    }
}

<#
.SYNOPSIS
    Extracts syntax for PowerShell commands or external applications.
 
.DESCRIPTION
    Extracts and pretty-prints syntax for PowerShell commands
    (Cmdlet, Function, Alias) and external applications.
 
.PARAMETER command
    The name of the Cmdlet, Function, Alias, full or relative path to
    the application or app name from PATH enviromental variable.
 
.PARAMETER arguments
    Additional arguments to pass to external application for retrieving
    raw help message with syntax.
 
.INPUTS
    System.String
    Command name or path.
 
.OUTPUTS
    System.String
    Returns extracted syntax.
 
.NOTES
    Parameter notation in PowerShell syntax:
 
    [[-name] <val>] - optional parameter, can be used by position or name
    [-name <val>] - optional parameter, can only be used by name
    [-name] <val> - required parameter, can be used by position or name
    -name <val> - required parameter, can only be used by name
    [-name] - switch parameter (always optional, only by name)
 
.EXAMPLE
    usage Get-Command
 
    Description:
    Displays all ParameterSets for Get-Command with formatted parameters.
 
.EXAMPLE
    usage ls
 
    Description:
    Resolves the 'ls' Alias to Get-ChildItem and shows its syntax.
 
.EXAMPLE
    usage pip
 
    Description:
    Extracts 'usage' section from Python package manager help output.
 
.EXAMPLE
    usage pip install
 
    Description:
    Extracts syntax for the 'install' sub-command of Python package manager.
 
.EXAMPLE
    usage "C:\Program Files\Git\cmd\git.exe"
 
    Description:
    Extracts syntax for the git.exe.
 
.LINK
    Get-Command
 
.LINK
    Get-Help
 
.LINK
    Get-HelpMessage
 
.LINK
    about_Command_Syntax
#>

function Get-HelpSyntax {
    [CmdletBinding()]
    [Alias('?!', 'syntax', 'usage')]

    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$command,

        [Parameter(ValueFromRemainingArguments)]
        [string[]]$arguments = @()
    )

    # Extract from external app
    if ((Test-Path $command) -or
        (Get-Command $command -CommandType Application -ErrorAction Ignore)) {
        $help = Get-HelpMessage $command $arguments
        $command = [System.IO.Path]::GetFileNameWithoutExtension($command)

        if ($help -match "(?msix) (^ (usage (?: :|\r?\n) | $command \s+) .+?) (?: \r?\n \r?\n [a-z] | \z)") {
            $matches[1].Trim() -split "`n" | ForEach {
                if ($_.StartsWith('usage')) {
                    $_.replace("usage:\s*", '')
                } else {
                    $_
                }
            }
        }
        return
    }

    # Extract from PowerShell command.
    # Resolve Alias
    if (Get-Command $command -CommandType Alias -ErrorAction Ignore) {
        $syntax = (Get-Command -syntax (Get-Command $command).ResolvedCommandName)
    } else {
        $syntax = (Get-Command -syntax $command)
    }

    $parameterSets = @((Get-Command $command).parameterSets.Name)
    $syntax = ($syntax -split ' (?=(\[{1,2}[-<]|-))' -replace '^[\[-]', ' $0').where{$_ -match '.*\w'}
    if ($parameterSets[0] -eq '__AllParameterSets') {
        return $syntax
    }

    # Get multiple parameters sets
    $syntax = (($syntax -join "`r`n").Trim() -split "($command)").where{$_.Trim() -ne ''}

    $i = 0
    $syntax | ForEach {
        if ($_.Trim() -eq $command){
            "`nParameterSet`: $($parameterSets[$i])"
            $_
            $i++
        } else {
            $_.TrimEnd()
        }
    }
}

<#
.SYNOPSIS
    Extracts examples for PowerShell commands or external applications.
 
.DESCRIPTION
    Extracts and pretty-prints examples for PowerShell commands
    (Cmdlet, Function, Alias) and external applications.
 
.PARAMETER command
    The name of the Cmdlet, Function, Alias, full or relative path
    to the application or app name from PATH enviromental variable.
 
.PARAMETER arguments
    Additional arguments to pass to external application for
    retrieving raw help message with examples.
 
.INPUTS
    System.String
    Command name or path.
 
.OUTPUTS
    System.String
    Returns extracted examples.
 
.EXAMPLE
    Get-HelpExample Stop-HiddenScript
 
    Description:
    Displays all examples for the function.
 
.EXAMPLE
    Get-HelpExample kills
 
    Description:
    Resolves the 'kills' alias to Stop-HiddenScript and shows it's examples.
 
.EXAMPLE
    Get-HelpExample gh
 
    Description:
    Extracts syntax for the gh.exe (GitHub).
 
.EXAMPLE
    Get-HelpExample gh search
 
    Description:
    Extracts example for the 'search' sub-command of GitHub command.
 
.EXAMPLE
    Get-HelpExample "C:\Windows\system32\taskkill.exe"
 
    Description:
    Extracts examples for the taskkill.exe.
 
.LINK
    Get-Command
 
.LINK
    Get-Help
 
.LINK
    Get-HelpMessage
 
.LINK
    about_Command_Syntax
#>

function Get-HelpExample {
    [CmdletBinding()]

    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$command,

        [Parameter(ValueFromRemainingArguments)]
        [string[]]$arguments = @()
    )
    
    if ((Test-Path $command) -or 
        (Get-Command $command -CommandType Application -ErrorAction Ignore)) {
        # Application
        $help = Get-HelpMessage $command $arguments
        if ($help -match '(?msix) Examples? (?: :|\r?\n) (.+?) (?: \r?\n \r?\n [A-Z] | \z)') {
            $matches[1].Trim() -split "`n" | ForEach {
                if ($_) {
                    $_
                }
            }
        }
    } else {
        # Function
        $help = Get-Help $command -full
        if ($help.examples.example) {
            $help.examples.example | ForEach {
                $_.Code.Split("`n") | ForEach {
                    if ($_.StartsWith('PS C:\>')) {
                        $_.replace('PS C:\>', '')
                    } else {
                        $_
                    }
                }
            }
        }
    }
}

<#
.SYNOPSIS
    Retrieves error information based on the code.
 
.DESCRIPTION
    This function uses the certutil command to obtain error
    information based on the provided code.
 
.PARAMETER code
    The error code for which to retrieve information.
 
.PARAMETER UI
    Displays a dialog box with the error description and code.
 
.INPUTS
    System.String
    You can pipe error code to this function.
 
.OUTPUTS
    None
 
    Outputs error message for this code.
 
.EXAMPLE
    Get-Error -code 0xc0000602
    Outputs the description:
    "Exception handlers will not be invoked, the process will be terminated immediately."
 
.EXAMPLE
    Get-Error -code 0xc0000602 -UI
    Displays a window "An error occurred" with its number and description.
#>

function Get-Error {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$code,
        [switch]$UI
    )
    if ($UI) {
        slui 0x2a $code
    } else {
        certutil -error $code
    }
}

<#
.SYNOPSIS
    Counts the total number of lines in files within a specified path.
 
.DESCRIPTION
    Recursively searches for files by filter in the specified directory
    and counts the total lines across all files.
    Displays individual file line counts and a total summary.
 
.PARAMETER Path
    The root path to search for. Defaults to current directory.
 
.PARAMETER Filter
    File filter pattern. Defaults to *.ahk.
 
.EXAMPLE
    Get-LineCount -Path "C:\Scripts"
 
.EXAMPLE
    Get-LineCount -Path "Build.ahk" -Recurse
 
.OUTPUTS
    PSCustomObject with properties: FullName, LineCount
#>

function Get-LineCount {
    [CmdletBinding()]
    [Alias('lines')]

    param (
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0)]
        [string]
        $Path = ".",

        [Parameter(Mandatory = $false)]
        [string]
        $Filter = "*.ahk"
    )

    begin {
        $totalLines = 0
        $fileCount  = 0
        $results    = [System.Collections.Generic.List[PSCustomObject]]@()
        
        $ErrorAction = if ($PSBoundParameters.ContainsKey('ErrorAction')) {
            $PSBoundParameters['ErrorAction']
        } else {
            'Ignore'
        }
    }

    process {
        $files = Get-ChildItem `
            -Path $Path -Filter $Filter `
            -ErrorAction:$ErrorAction `
            -Recurse

        if (-not $files) {
            Write-Warning "No $Filter files found in `"$Path`""
            return
        }

        foreach ($file in $files) {
            $content = Get-Content -Path $file.FullName -ErrorAction:$ErrorAction

            if ($null -eq $content) {
                $lineCount = 0
            } elseif ($content -is [string]) {
                $lineCount = 1
            } else {
                $lineCount = $content.Count
            }

            $totalLines += $lineCount
            $fileCount++

            $results.Add([PSCustomObject]@{
                FullName  = $file.FullName
                LineCount = $lineCount
            })
        }
    }

    end {
        $results | Format-Table -AutoSize

        Write-Output "Total Files: $fileCount"
        Write-Output "Total Lines: $totalLines"

        # [PSCustomObject]@{
            # TotalFiles = $fileCount
            # TotalLines = $totalLines
            # Files = $results
        # }
    }
}

<#
.SYNOPSIS
    Stops hidden running scripts by path or name.
 
.DESCRIPTION
    Stops running scripts by searching for hidden windows with
    specified script paths in the title.
 
    You can specify full or relative paths to the scripts or
    just filenames to find all matches using Everything search
    tool and terminate them.
 
.PARAMETER names
    Specifies the paths/names of the scripts to stop.
 
.PARAMETER launcher
    Specifies the owner/launcher of the script (e.g., "autohotkey", "python").
    This is used to filter processes by name pattern.
    If empty or missing, function terminates everything that was found by $names.
 
.PARAMETER force
    Stops the specified/found scripts without prompting for confirmation.
 
.PARAMETER confirm
    Prompts for confirmation before stopping each specified/found script.
 
.INPUTS
    System.String[]
    You can pipe script names to this function.
 
.OUTPUTS
    None
 
.EXAMPLE
    Stop-HiddenScript "C:\Script1.ahk", "C:\Script2.py"
 
    Description:
    Stops multiple scripts at once.
 
.EXAMPLE
    Stop-Script "Script1.ahk", "Script2.py" -confirm
 
    Description:
    Searches for scripts and prompts for confirmation before terminating each found script.
 
.EXAMPLE
    Stop-HiddenScript -names "Script1.py", "Script2.py" -launcher "python" -force
 
    Description:
    Searches for Python scripts and terminates them if they are launched using "python" process.
 
.NOTES
    Requirements:
    - es.exe (Everything search tool) for search method.
    - Administrative privileges may be required for some processes
 
    See Stop-Script for termination without search tool.
 
.LINK
    tasklist.exe
 
.LINK
    es.exe
#>

function Stop-HiddenScript {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$names,

        [Parameter()]
        [AllowEmptyString()]
        [string]$launcher,

        [Parameter()]
        [switch]$force,

        [Parameter()]
        [switch]$confirm
    )

    process {
        # Resolve passed names
        $scripts = @{}
        ForEach ($name in $names) {
            if (Test-Path $name) {
                $files = (Resolve-Path $name).Path
            } else {
                try {
                    $files = es.exe file:nopath:"$name" 2>$null
                } catch [System.Management.Automation.CommandNotFoundException] {
                    Write-Warning "Unable to find '$name': Everything search tool (es.exe) is not installed."
                    continue
                }
            }

            if (-not $files) {
                Write-Warning "Unable to find '$name': change the name and try again."
                continue
            }


            # Find matching scripts and get their PID
            ForEach ($file in $files) {
                # tasklist.exe params
                $params = [System.Collections.Generic.List[string]]@(
                    '-nh',                              # No table header
                    '-fo', "csv",                       # Output in CSV style
                    '-fi', "WindowTitle eq $file*"      # Search for path in WindowTitle
                )

                if ($launcher) {
                    # Process name must contain launcher name
                    $params.Add('-fi')
                    $params.Add("ImageName eq $launcher*")
                }

                # Get PIDs
                (tasklist.exe @params) | ForEach {
                    $id = ($_.Split(',').Trim('"'))[1]

                    if ($id -match '^\d+$') {
                        $scripts.Add($id, $file)
                    }
                }
            }
        }

        if (-not $scripts.count) {
            return $false
        }

        :StopScripts ForEach ($s in $scripts.GetEnumerator()) {
            :AskConfirm while ($confirm) {
                Write-Host "Terminate '" -n
                Write-Host "$($s.Value)" -f DarkCyan -n
                Write-Host "'?"

                $response = Read-Host "[Y] Yes [A] Yes to All [N] No [L] No to All [P] Pause [?] Help (default is `"Y`")"

                :AnalyzeResponse switch ($response) {
                    'A' { $confirm = $false; break AskConfirm }
                    'Y' { break AskConfirm }
                    'N' { continue StopScripts }
                    'L' { return $false }
                    'P' { pause }

                    default {
                        Write-Host "Y - Terminate current process.`nA - Terminate all processes without prompting`nN - Skip current process.`nL - Skip all remaining processes.`nP - Pause current operation and return to the command prompt."
                        continue AskConfirm
                    }
                }
            }

            if ($force) {
                taskkill.exe -pid $s.Key -f 1>$null
            } else {
                taskkill.exe -pid $s.Key 1>$null
            }

            if ($LastExitCode -eq 0) {
                Write-Host "$launcher script '" -f DarkGray -n
                Write-Host "$($s.Value)"        -f DarkCyan -n
                Write-Host "' ($($s.Key)) is "  -f DarkGray -n
                Write-Host "terminated"         -f Green
            }
        }

        return $true
    }
}

<#
.SYNOPSIS
    Stops running scripts by name.
 
.DESCRIPTION
    Stops running scripts by searching for processes associated
    with the specified script names (processes with files in command line).
 
    The function tries multiple methods sequentially (including Stop-HiddenScript)
    and returns after the first successful termination method.
 
.PARAMETER names
    Specifies the names of the scripts to stop.
 
.PARAMETER launcher
    Specifies the owner/launcher of the script (e.g., "autohotkey", "python").
    This is used to filter processes by name pattern. Cannot be empty.
 
.PARAMETER force
    Stops the specified scripts without prompting for confirmation.
 
.PARAMETER confirm
    Prompts for confirmation before stopping each script.
 
.INPUTS
    System.String[]
    You can pipe script names to this function.
 
.OUTPUTS
    None
 
    The function outputs a formatted table of terminated processes showing Id and
    ProcessName, along with status messages indicating success or failure.
 
.EXAMPLE
    Stop-Script "Script1", "Script2"
 
    Description:
    Stops multiple AutoHotkey scripts at once.
 
.EXAMPLE
    Stop-Script "Script1", "Script2" -confirm
 
    Description:
    Prompts for confirmation before terminating each found script.
 
.EXAMPLE
    kills -names "Script1.py", "Script2.py" -launcher "python" -force
 
    Description:
    Stops a Python scripts by specifying launcher.
 
.LINK
    Stop-Process
 
.LINK
    Get-Process
 
.LINK
    Get-CimInstance
 
.LINK
    Stop-HiddenScript
#>

function Stop-Script {
    [CmdletBinding()]
    [Alias('kills', 'stop')]

    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$names,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$launcher = "autohotkey",

        [Parameter()]
        [switch]$force,

        [Parameter()]
        [switch]$confirm
    )

    begin {
        # for Stop-Process
        $params = @{
            PassThru = $true
            force    = $force
            confirm  = $confirm
        }

        function Try-Kill($id) {
            if (!$id) { return $false }

            $proc = Stop-Process -id $id @params
            if ($proc) {
                $proc | Format-Table Id, ProcessName | Out-Host

                Write-Host "$launcher script '" -f DarkGray -n
                Write-Host "$name"        -f DarkCyan -n
                Write-Host "' is "        -f DarkGray -n
                Write-Host "terminated"   -f Green

                return $true
            }

            return $false
        }

        # for getters using Where-Object
        $filter = {
            $_.Name -like "$launcher*" -and
            $_.CommandLine -like "*$name*"
        }
    }

    process {
        ForEach ($name in $names) {
            # Try different methods
            $proc = Get-Process | Where $filter
            if (Try-Kill $proc.Id) { continue }

            $proc = Get-CimInstance Win32_Process | Where $filter
            if (Try-Kill $proc.processId) { continue }

            $proc = Get-CimInstance Win32_Process -filter "name like '$launcher%' and CommandLine like '%$name%'"
            if (Try-Kill $proc.processId) { continue }

            # Fallback to Everything search
            if (Stop-HiddenScript $name $launcher -force:$force -confirm:$confirm) { continue }

            Write-Warning "Unable to find $launcher script '$name'"
        }
    }
}