InitHelpers.psm1
$PSDefaultParameterValues['*:Encoding'] = 'utf8' function Get-CurrentScriptFullPath { <# .SYNOPSIS Returns the full path of the current script or executable. .DESCRIPTION Determines the path with the following priority: 1. $PSCommandPath 2. $MyInvocation.MyCommand.Path 3. $MyInvocation.MyCommand.Definition 4. Current process MainModule file name (exe) If none is found, returns $null. .PARAMETER ThrowIfMissing Throws an exception if the path cannot be determined. .EXAMPLE Get-CurrentScriptFullPath -ThrowIfMissing #> param( [switch]$ThrowIfMissing # 如果找不到则抛错 ) # 1. PSCommandPath(PowerShell 3+,模块/脚本中最可靠) if ($PSCommandPath) { try { return (Resolve-Path -LiteralPath $PSCommandPath).ProviderPath } catch { return $PSCommandPath } } # 2. MyInvocation.MyCommand.Path(脚本、模块、dot-sourced 时通常有效) if ($MyInvocation -and $MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) { try { return (Resolve-Path -LiteralPath $MyInvocation.MyCommand.Path).ProviderPath } catch { return $MyInvocation.MyCommand.Path } } # 3. MyInvocation.MyCommand.Definition(某些情形下包含定义) if ($MyInvocation -and $MyInvocation.MyCommand -and $MyInvocation.MyCommand.Definition) { $def = $MyInvocation.MyCommand.Definition if ($def -and ($def -is [string]) -and (Test-Path $def)) { try { return (Resolve-Path -LiteralPath $def).ProviderPath } catch { return $def } } } # 4. 回退到当前进程的可执行文件(ps2exe 编译后通常想要这个) try { $exe = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName if ($exe) { try { return (Resolve-Path -LiteralPath $exe).ProviderPath } catch { return $exe } } } catch { # 忽略权限 / 平台异常 } if ($ThrowIfMissing) { throw "CAN'T DETERMINE CURRENT SCRIPT FULL PATH!" } return $null } function Get-CurrentScriptDirectory { <# .SYNOPSIS Returns the directory of the current script. .DESCRIPTION Uses Get-CurrentScriptFullPath to determine the script directory. .PARAMETER ThrowIfMissing Throws an exception if the path cannot be determined. .EXAMPLE Get-CurrentScriptDirectory -ThrowIfMissing #> param([switch]$ThrowIfMissing) $full = Get-CurrentScriptFullPath -ThrowIfMissing:$ThrowIfMissing if ($full) { return Split-Path -Path $full -Parent } return $null } function Get-ScriptPathInitialized { <# .SYNOPSIS Initializes script path and related subfolder/file variables. .DESCRIPTION Sets script-wide variables: - $script:ps_script_path - $script: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", "toml") ) $script:ps_script_path = Get-CurrentScriptDirectory $script:ps_script_file = Get-CurrentScriptFullPath if ($SubFolders -and $SubFolders.Count -gt 0) { foreach ($private:subfolder in $SubFolders) { $private:potential_path = Join-Path -Path $script: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 Script -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 $script:ps_script_path -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension($script:ps_script_file))), $private:ext Set-Variable -Name $private:file_var -Value $private:potential_path -Scope Script -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 $script: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 $script:libs_path)) { New-Item -ItemType Directory -Path $script:libs_path -Force | Out-Null } try { if ([string]::IsNullOrEmpty($Version)) { nuget install $LibName -OutputDirectory $script: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 $script: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 $script:libs_path)) { New-Item -ItemType Directory -Path $script:libs_path -Force | Out-Null } $private:lib_dlls = (Get-ChildItem -Path $script: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 $script:libs_path) -Level "Error" throw ("No Assemblies Found in Path: {0}" -f $script: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 } } |