InitHelpers.psm1
$PSDefaultParameterValues['*:Encoding'] = 'utf8' function Get-CurrentScriptFullPath { <# .SYNOPSIS Returns the full path of the file that defines the current command. If that defining file is located under any $env:PSModulePath root, the function will attempt to return the first caller script path that is outside module roots. .DESCRIPTION The function searches for the "defining file" using the following priority: 1. $PSCommandPath 2. $MyInvocation.MyCommand.Path 3. $MyInvocation.MyCommand.Definition If none of the above yields a path, it falls back to the current process executable path. If the resulting path is under a PSModulePath entry, the call stack (Get-PSCallStack) is scanned and the first script frame whose path is not under any PSModulePath root is returned (caller script). If no suitable caller is found, the module file path is returned (backward-compatible behavior). .PARAMETER ThrowIfMissing If specified, throw an exception when no path can be determined. Otherwise return $null. .INPUTS None. .OUTPUTS System.String - full file path, or $null if unable to determine. .EXAMPLE # Typical usage after importing a module installed into PSModulePath: Import-Module MyModule Write-Host (Get-CurrentScriptFullPath) .EXAMPLE # When the function is placed directly in a script and the script runs: # The function returns the script path. .NOTES PowerShell 5.1 compatible. Uses Resolve-Path + GetFullPath for normalization. #> param([switch]$ThrowIfMissing,[switch]$InModule) function SafeFullPath($p) { if (-not $p) { return $null } try { $rp = Resolve-Path -LiteralPath $p -ErrorAction Stop return [IO.Path]::GetFullPath($rp.ProviderPath) } catch { try { return [IO.Path]::GetFullPath($p) } catch { return $null } } } # Candidate sources in priority order $candidates = @() if ($PSCommandPath) { $candidates += $PSCommandPath } if ($MyInvocation -and $MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) { $candidates += $MyInvocation.MyCommand.Path } if ($MyInvocation -and $MyInvocation.MyCommand -and $MyInvocation.MyCommand.Definition) { $candidates += $MyInvocation.MyCommand.Definition } $found = $null foreach ($c in $candidates) { $fp = SafeFullPath $c if ($fp) { $found = $fp; break } } if (-not $found) { try { $found = SafeFullPath ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName) } catch {} } if (-not $found) { if ($ThrowIfMissing) { throw "CAN'T DETERMINE CURRENT SCRIPT FULL PATH!" } return $null } # Build PSModulePath roots (normalized full paths with trailing directory separator) $sep = [IO.Path]::PathSeparator $dirSep = [IO.Path]::DirectorySeparatorChar $modRoots = @() foreach ($entry in ($env:PSModulePath -split $sep)) { $r = SafeFullPath $entry if ($r) { if ($r[-1] -ne $dirSep) { $r = $r + $dirSep } $modRoots += $r } } if($InModule){ return $found } $isInModule = $false foreach ($r in $modRoots) { if ($found.StartsWith($r, [System.StringComparison]::OrdinalIgnoreCase)) { $isInModule = $true; break } } if ($isInModule) { if (Get-Command Get-PSCallStack -ErrorAction SilentlyContinue) { foreach ($frame in Get-PSCallStack) { $s = $frame.ScriptName if ($s -and (Test-Path $s)) { $sFull = SafeFullPath $s if (-not $sFull) { continue } $inside = $false foreach ($r in $modRoots) { if ($sFull.StartsWith($r, [System.StringComparison]::OrdinalIgnoreCase)) { $inside = $true; break } } if (-not $inside) { return $sFull } # first caller outside module roots } } } # If no suitable caller was found, fall back to $found (module file) } return $found } function Get-CurrentScriptDirectory { <# .SYNOPSIS Returns the directory for the file returned by Get-CurrentScriptFullPath. .DESCRIPTION Calls Get-CurrentScriptFullPath and returns its parent directory (Split-Path -Parent). Behaves the same as Get-CurrentScriptFullPath regarding module vs caller resolution. .PARAMETER ThrowIfMissing If specified, throw an exception when no path can be determined. Otherwise return $null. .EXAMPLE Write-Host (Get-CurrentScriptDirectory) .NOTES PowerShell 5.1 compatible. #> param([switch]$ThrowIfMissing) $p = Get-CurrentScriptFullPath -ThrowIfMissing:$ThrowIfMissing if ($p) { return Split-Path -Path $p -Parent } return $null } function Get-ScriptPathInitialized { <# .SYNOPSIS Initializes script path and related subfolder/file variables. .DESCRIPTION Sets script-wide variables: - $global:ps_script_path - $global:ps_script_file Creates subfolder directories and file path variables based on input. .PARAMETER SubFolders Array of subfolder names to create under the script path. .PARAMETER FilesWithExtensions Array of file extensions to generate (default: log, toml). .EXAMPLE Get-ScriptPathInitialized -SubFolders @("logs","data") -FilesWithExtensions @("log","txt") #> param( [string[]]$SubFolders, [string[]]$FilesWithExtensions = @("log") ) $global:ps_script_path = Get-CurrentScriptDirectory $global:ps_script_file = Get-CurrentScriptFullPath if ($SubFolders -and $SubFolders.Count -gt 0) { foreach ($private:subfolder in $SubFolders) { $private:potential_path = Join-Path -Path $global:ps_script_path -ChildPath $private:subfolder $private:folder_var=(($private:subfolder.ToLower() -Replace "[\W|_]+","_") -replace "^(\d)","_`$1")+"_path" Set-Variable -Name $private:folder_var -Value $private:potential_path -Scope Global -Force New-Item -ItemType Directory -Path $private:potential_path -Force | Out-Null } } if ($FilesWithExtensions -and $FilesWithExtensions.Count -gt 0) { foreach ($private:ext in $FilesWithExtensions) { $private:ext=$private:ext.ToLower() -replace "[^a-z]+","" $private:file_var="{0}_file" -f $private:ext $private:potential_path = "{0}.{1}" -f (Join-Path -Path $global:ps_script_path -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension($global:ps_script_file))), $private:ext Set-Variable -Name $private:file_var -Value $private:potential_path -Scope Global -Force } } return } function Get-StringSha256Hash { <# .SYNOPSIS Computes the SHA256 hash of a string. .PARAMETER InputString The string to compute the hash for (mandatory). .EXAMPLE Get-StringSha256Hash -InputString "Hello World" #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$InputString ) $private:sha256 = [System.Security.Cryptography.SHA256Managed]::new() $private:bytes = [System.Text.Encoding]::UTF8.GetBytes($InputString) $private:hashBytes = $private:sha256.ComputeHash($private:bytes) $private:hash = -join ($private:hashBytes | ForEach-Object { $_.ToString("X2") }) return $private:hash } function Write-Log { <# .SYNOPSIS Writes a log message to console and log file. .PARAMETER Message The message to log (mandatory). .PARAMETER Level Log level: Info, Warning, Error, Success, Debug (default: Info). .PARAMETER MuteForDebug Do not display debug message. .PARAMETER NoNewLine Do not add a newline after output. .PARAMETER StartNewLine Start the log message on a new line. .EXAMPLE Write-Log -message "Operation completed" -level Info #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Message, [ValidateSet("Info", "Warning", "Error", "Success", "Debug")] [string]$Level = "Info", [switch]$MuteForDebug, [switch]$NoNewLine, [switch]$StartNewLine ) $private:timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $private:log_entry = "[$private:timestamp] [$Level] $Message" if ($StartNewLine) { $private:log_entry = "`n$private:log_entry" Write-Host "" } # Write to log file try { Add-Content -Path $global:log_file -Value $private:log_entry -ErrorAction Stop } catch { Write-Host "Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Red } # Write to console with appropriate color $private:foreground_color = switch ($Level.ToLower()) { "info" { "White" } "warning" { "Yellow" } "error" { "Red" } "success" { "Green" } "debug" { "Cyan" } default { "White" } } if($Level -eq "Debug" -and $MuteForDebug) { return } Write-Host -NoNewline "[$private:timestamp]" -ForegroundColor "DarkGray" Write-Host " [$Level] $Message" -ForegroundColor $private:foreground_color -NoNewline if (-not $NoNewLine) { Write-Host "" } } function Get-TempFileName() { <# .SYNOPSIS Generates a temporary file name. .PARAMETER ext File extension (default: tmp). .PARAMETER prefix Optional file name prefix. .PARAMETER suffix Optional file name suffix. .EXAMPLE Get-TempFileName -ext "txt" -prefix "log" -suffix "2025" #> [CmdletBinding()] param( [string]$ext = "tmp", [string]$prefix = "", [string]$suffix = "" ) return Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ((@([System.IO.Path]::GetRandomFileName().Replace(".", ""), $prefix, $suffix, $ext) | Where-Object { -not [string]::IsNullOrEmpty($_) }) -join ".").TrimEnd(".") } function Get-TypeByName() { <# .SYNOPSIS Gets the Type object by type name. .PARAMETER TypeName Full name of the type. .PARAMETER Mute Suppresses exception if type is not found. .EXAMPLE Get-TypeByName -type_name "System.Text.StringBuilder" #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$TypeName, [switch]$Mute ) $private:type_by_name = [Type]::GetType($TypeName) if (-not $private:type_by_name) { # fallback: search loaded assemblies for the type: $private:full_name = $TypeName.split(',')[0].Trim() $private:type_by_name = [AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetTypes() } | Where-Object { $_.FullName -eq $private:full_name } | Select-Object -First 1 } if (-not $private:type_by_name -and -not $Mute) { Write-Log -Message ("Failed to Find Type '{0}' in Loaded Assemblies." -f $TypeName) -Level "Error" throw ("Failed to Find Type '{0}' in Loaded Assemblies." -f $TypeName) } return $private:type_by_name } function Get-InstanceByTypeName() { <# .SYNOPSIS Creates an instance of a type by name. .PARAMETER TypeName Full name of the type. .PARAMETER Mute Suppresses exception if instance creation fails. .EXAMPLE Get-InstanceByTypeName -type_name "System.Text.StringBuilder" #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$TypeName, [switch]$Mute ) $private:type_by_name = Get-TypeByName -type_name $TypeName -mute:$Mute $private:instance = [Activator]::CreateInstance($private:type_by_name) if (-not $private:instance -and -not $Mute) { Write-Log -Message ("Failed to Create Instance of Type '{0}'." -f $TypeName) -Level "Error" throw ("Failed to Create Instance of Type '{0}'." -f $TypeName) } return $private:instance } function Install-NuGet { <# .SYNOPSIS Installs the NuGet command-line tool. .DESCRIPTION Downloads and adds nuget.exe to PATH if not already installed. .EXAMPLE Install-NuGet #> if (-not (Get-Command nuget -ErrorAction SilentlyContinue)) { $private:nuget_path = Join-Path -Path (Join-Path -Path $env:LOCALAPPDATA -ChildPath "Programs") -ChildPath "NuGet" if (-not (Test-Path -Path $private:nuget_path)) { New-Item -ItemType Directory -Path $private:nuget_path -Force | Out-Null } $private:nuget_path = Join-Path -Path $private:nuget_path -ChildPath "nuget.exe" if (-not (Test-Path -Path $private:nuget_path)) { try { Invoke-WebRequest -Uri "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -OutFile $private:nuget_path -UseBasicParsing -ErrorAction Stop } catch { Write-Log -Message ("Failed to download nuget.exe: {0}" -f $_.Exception.Message) -Level "Error" throw ("Failed to download nuget.exe: {0}" -f $_.Exception.Message) } } $env:PATH = "{0};{1}" -f ($env:PATH), ([System.IO.Path]::GetDirectoryName($private:nuget_path)) [System.Environment]::SetEnvironmentVariable("PATH", $env:PATH, [System.EnvironmentVariableTarget]::Process) if (-not (Get-Command nuget -ErrorAction SilentlyContinue)) { Write-Log -Message "NuGet installation failed or nuget.exe not found in PATH." -Level "Error" throw "NuGet installation failed or nuget.exe not found in PATH." } Write-Log -Message ("NuGet installed at path: {0}" -f $private:nuget_path) -Level "Debug" } return $true } function Install-Library() { <# .SYNOPSIS Installs a library using NuGet. .PARAMETER LibName Library name (mandatory). .PARAMETER Version Optional version to install. .EXAMPLE Install-Library -lib_name "Newtonsoft.Json" -version "13.0.1" #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$LibName, [string]$Version = "" ) if ([string]::IsNullOrEmpty($LibName)) { Write-Log -Message "Library name is required for Install-Library." -Level "Error" throw "Library name is required for Install-Library." } if (-not (test-Path -Path $global:libs_path)) { New-Item -ItemType Directory -Path $global:libs_path -Force | Out-Null } Install-NuGet|Out-Null try { if ([string]::IsNullOrEmpty($Version)) { nuget install $LibName -OutputDirectory $global:libs_path -ExcludeVersion -NonInteractive -Source "https://api.nuget.org/v3/index.json" | Where-Object { -not [string]::IsNullOrEmpty($_) } | ForEach-Object { Write-Log -Message $_ -Level "Debug" } } else { nuget install $LibName -Version $Version -OutputDirectory $global:libs_path -NonInteractive -Source "https://api.nuget.org/v3/index.json" | Where-Object { -not [string]::IsNullOrEmpty($_) } | ForEach-Object { Write-Log -Message $_ -Level "Debug" } } } catch { Write-Log -Message ("Failed to Install Library '{0}': {1}" -f $LibName, $_.Exception.Message) -Level "Error" throw ("Failed to Install Library '{0}': {1}" -f $LibName, $_.Exception.Message) } return $true } function Install-Assembly() { <# .SYNOPSIS Loads assembly DLLs from the library path. .PARAMETER LibName Optional library name filter. .PARAMETER version Library version folder (default: net4). .EXAMPLE Install-Assembly -lib_name "Newtonsoft.Json" #> [CmdletBinding()] param( [string]$LibName = $null, [string]$Version = "net4" ) if (-not (test-Path -Path $global:libs_path)) { New-Item -ItemType Directory -Path $global:libs_path -Force | Out-Null } $private:lib_dlls = (Get-ChildItem -Path $global:libs_path -Filter "*.dll" -Recurse | Where-Object { $_.FullName -match ('\\lib\\{0}' -f $Version) -and ($null -eq $LibName -or [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -match $LibName) }).FullName if ($private:lib_dlls) { foreach ($private:lib_dll in $private:lib_dlls) { [Reflection.Assembly]::LoadFile($private:lib_dll) | out-null Write-Log -Message ("Loaded Assembly: {0}" -f ([System.IO.Path]::GetFileNameWithoutExtension($private:lib_dll))) -Level "Debug" } } else { if (Install-Library -lib_name $LibName) { Install-Assembly -lib_name $LibName -version $Version return } Write-Log -Message ("No Assemblies Found in Path: {0}" -f $global:libs_path) -Level "Error" throw ("No Assemblies Found in Path: {0}" -f $global:libs_path) } } function Get-ModulesInstalled { <# .SYNOPSIS Gets or installs PowerShell modules. .PARAMETER name Array of module names. .PARAMETER force Force reinstall even if module exists. .PARAMETER detailed Output detailed logging. .EXAMPLE Get-ModulesInstalled -name @("PSReadLine") -force -detailed #> [CmdletBinding()] Param( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]]$name, [switch]$force, [switch]$detailed ) Begin { $private:results = @() } Process { if ($null -eq $name) { return } foreach ($private:module_name in $name) { $private:error_msg = $null if ($detailed) { Write-Host "Processing module '$private:module_name'..." } $private:existing_module = Get-Module -Name $private:module_name -ListAvailable -ErrorAction SilentlyContinue if ($null -eq $private:existing_module) { # Module not installed if ($detailed) { Write-Log -Message "Module '$private:module_name' is not installed. Installing..." -Level "Debug" } try { $null = Install-Module -Name $private:module_name -Scope CurrentUser -Force:$force -ErrorAction Stop $private:status = 'Installed' if ($detailed) { Write-Log -Message "Module '$private:module_name' installed successfully." -Level "Success" } } catch { $private:status = 'Failed' $private:error_msg = $_.Exception.Message Write-Log -Message "Failed to install module '$private:module_name': $private:error_msg" -Level "Error" throw "Failed to install module '$private:module_name': $private:error_msg" } } elseif ($force) { # Module is installed but Force is specified if ($detailed) { Write-Log -Message "Module '$private:module_name' is already installed. Reinstalling (Force)..." -Level "Debug" } try { $null = Install-Module -Name $private:module_name -Scope CurrentUser -Force -ErrorAction Stop $private:status = 'Reinstalled' if ($detailed) { Write-Log -Message "Module '$private:module_name' reinstalled successfully." -Level "Success" } } catch { $private:status = 'Failed' $private:error_msg = $_.Exception.Message Write-Log -Message "Failed to reinstall module '$private:module_name': $private:error_msg" -Level "Error" throw "Failed to reinstall module '$private:module_name': $private:error_msg" } } else { # Module is already installed and no Force flag $private:status = 'AlreadyInstalled' if ($detailed) { Write-Log -Message "Module '$private:module_name' is already installed." -Level "Debug" } } $private:results += [PsCustomObject]@{ Name = $private:module_name Status = $private:status Error = $private:error_msg } } } End { return $private:results } } |