PSProfile.psm1
[CmdletBinding()] Param ( [parameter(Position = 0)] [bool] $ShowLoadTime = $true ) $env:ShowPSProfileLoadTime = $ShowLoadTime enum PSProfileLogLevel { Information Warning Error Debug Verbose Quiet } enum PSProfileSecretType { PSCredential SecureString } class PSProfileEvent { hidden [datetime] $Time [timespan] $Total [timespan] $Last [PSProfileLogLevel] $LogLevel [string] $Section [string] $Message PSProfileEvent( [datetime] $time, [timespan] $last, [timespan] $total, [PSProfileLogLevel] $logLevel, [string] $section, [string] $message ) { $this.Time = $time $this.Last = $last $this.Total = $total $this.Section = $section $this.Message = $message $this.LogLevel = $logLevel } } class PSProfileSecret { [PSProfileSecretType] $Type hidden [pscredential] $PSCredential hidden [securestring] $SecureString PSProfileSecret([string]$userName, [securestring]$password) { $this.Type = [PSProfileSecretType]::PSCredential $this.PSCredential = [PSCredential]::new($userName,$password) } PSProfileSecret([pscredential]$psCredential) { $this.Type = [PSProfileSecretType]::PSCredential $this.PSCredential = $psCredential } PSProfileSecret([SecureString]$secureString) { $this.Type = [PSProfileSecretType]::SecureString $this.SecureString = $secureString } } class PSProfileVault : Hashtable { [hashtable] $_secrets PSProfileVault() { $this._secrets = @{ } } [void] SetSecret([string]$name, [string]$userName, [securestring]$password) { $this._secrets[$name] = [PSCredential]::new( $userName, $password ) } [void] SetSecret([pscredential]$psCredential) { $this._secrets[$psCredential.UserName] = $psCredential } [void] SetSecret([string]$name, [pscredential]$psCredential) { $this._secrets[$name] = $psCredential } [void] SetSecret([string]$name, [securestring]$secureString) { $this._secrets[$name] = $secureString } [pscredential] GetSecret() { if ($env:USERNAME) { return $this._secrets[$env:USERNAME] } elseif ($env:USER) { return $this._secrets[$env:USER] } else { return $null } } [object] GetSecret([string]$name) { return $this._secrets[$name] } [void] RemoveSecret([string]$name) { $this._secrets.Remove($name) } } class PSProfile { hidden [System.Collections.Generic.List[PSProfileEvent]] $Log [hashtable] $_internal [hashtable] $Settings [datetime] $LastRefresh [datetime] $LastSave [string] $RefreshFrequency [hashtable] $GitPathMap [hashtable] $PSBuildPathMap [object[]] $ModulesToImport [object[]] $ModulesToInstall [hashtable] $PathAliases [hashtable] $CommandAliases [hashtable[]] $Plugins [string[]] $PluginPaths [string[]] $ProjectPaths [hashtable] $Prompts [hashtable] $InitScripts [string[]] $ScriptPaths [hashtable] $SymbolicLinks [hashtable] $Variables [PSProfileVault] $Vault PSProfile() { $this.Log = [System.Collections.Generic.List[PSProfileEvent]]::new() $this.Vault = [PSProfileVault]::new() $this._internal = @{ } $this.GitPathMap = @{ } $this.PSBuildPathMap = @{ } $this.SymbolicLinks = @{ } $this.Prompts = @{ } $this.Variables = @{ Environment = @{ Home = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile) UserName = [System.Environment]::UserName ComputerName = [System.Environment]::MachineName } Global = @{ PathAliasDirectorySeparator = "$([System.IO.Path]::DirectorySeparatorChar)" AltPathAliasDirectorySeparator = "$([char]0xe0b1)" } } $this.Settings = @{ DefaultPrompt = $null PSVersionStringLength = 3 ConfigurationPath = (Join-Path (Get-ConfigurationPath -CompanyName 'SCRT HQ' -Name PSProfile) 'Configuration.psd1') FontType = 'Default' PromptCharacters = @{ GitRepo = @{ NerdFonts = "$([char]0xf418)" PowerLine = "$([char]0xe0a0)" Default = "@" } AWS = @{ NerdFonts = "$([char]0xf270)" PowerLine = "$([char]0xf0e7)" Default = "AWS: " } } PSReadline = @{ Options = @{} KeyHandlers = @{} } } $this.RefreshFrequency = (New-TimeSpan -Hours 1).ToString() $this.LastRefresh = [datetime]::Now.AddHours(-2) $this.LastSave = [datetime]::Now $this.ProjectPaths = @() $this.PluginPaths = @() $this.InitScripts = @{} $this.ScriptPaths = @() $this.PathAliases = @{ '~' = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile) } $this.CommandAliases = @{ } $this.Plugins = @() } [void] Load() { $this._internal['ProfileLoadStart'] = [datetime]::Now $this._log( "SECTION START", "MAIN", "Debug" ) $this._loadConfiguration() $this.Prompts['SCRTHQ'] = '$lastStatus = $? $lastColor = if ($lastStatus -eq $true) { "Green" } else { "Red" } $isAdmin = $false $isDesktop = ($PSVersionTable.PSVersion.Major -eq 5) if ($isDesktop -or $IsWindows) { $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() $windowsPrincipal = New-Object "System.Security.Principal.WindowsPrincipal" $windowsIdentity $isAdmin = $windowsPrincipal.IsInRole("Administrators") -eq 1 } else { $isAdmin = ((& id -u) -eq 0) } if ($isAdmin) { $idColor = "Magenta" } else { $idColor = "Cyan" } Write-Host "[" -NoNewline Write-Host -ForegroundColor $idColor "#$($MyInvocation.HistoryId)" -NoNewline Write-Host "] [" -NoNewline $verColor = @{ ForegroundColor = if ($PSVersionTable.PSVersion.Major -eq 7) { "Yellow" } elseif ($PSVersionTable.PSVersion.Major -eq 6) { "Magenta" } else { "Cyan" } } Write-Host @verColor ("PS {0}" -f (Get-PSVersion)) -NoNewline Write-Host "] [" -NoNewline Write-Host -ForegroundColor $lastColor ("{0}" -f (Get-LastCommandDuration)) -NoNewline Write-Host "] [" -NoNewline Write-Host ("{0}" -f $(Get-PathAlias)) -NoNewline -ForegroundColor DarkYellow if ((Get-Location -Stack).Count -gt 0) { Write-Host (("+" * ((Get-Location -Stack).Count))) -NoNewLine -ForegroundColor Cyan } Write-Host "]" -NoNewline if ($PWD.Path -notlike "\\*" -and $env:DisablePoshGit -ne $true) { Write-VcsStatus $GitPromptSettings.EnableWindowTitle = "PS {0} @" -f (Get-PSVersion) } else { $Host.UI.RawUI.WindowTitle = "PS {0}" -f (Get-PSVersion) } if ($env:AWS_PROFILE) { Write-Host "`n[" -NoNewline $awsIcon = if ($global:PSProfile.Settings.ContainsKey("FontType")) { $global:PSProfile.Settings.PromptCharacters.AWS[$global:PSProfile.Settings.FontType] } else { "AWS:" } if ([String]::IsNullOrEmpty($awsIcon)) { $awsIcon = "AWS:" } Write-Host -ForegroundColor Yellow "$($awsIcon) $($env:AWS_PROFILE)$(if($env:AWS_DEFAULT_REGION){" @ $env:AWS_DEFAULT_REGION"})" -NoNewline Write-Host "]" -NoNewline } "`n>> "' $plugPaths = @((Join-Path $PSScriptRoot "Plugins")) $curVer = (Import-Metadata (Join-Path $PSScriptRoot "PSProfile.psd1")).ModuleVersion $this.PluginPaths | Where-Object {-not [string]::IsNullOrEmpty($_) -and ($_ -match "[\/\\](Modules|BuildOutput)[\/\\]PSProfile[\/\\]$curVer" -or $_ -notmatch "[\/\\](Modules|BuildOutput)[\/\\]PSProfile[\/\\]\d+\.\d+\.\d+") } | ForEach-Object { $plugPaths += $_ } $this.PluginPaths = $plugPaths | Select-Object -Unique $plugs = @() $this.Plugins | Where-Object { $_.Name -ne 'PSProfile.PowerTools' } | ForEach-Object { $plugs += $_ } if ($plugs.Count) { $this.Plugins = $plugs } if (([datetime]::Now - $this.LastRefresh) -gt [timespan]$this.RefreshFrequency) { $withRefresh = ' with refresh.' $this.Refresh() } else { $withRefresh = '.' $this._log( "Skipped full refresh! Frequency set to '$($this.RefreshFrequency)', but last refresh was: $($this.LastRefresh.ToString())", "MAIN", "Verbose" ) } $this._invokeInitScripts() $this._importModules() $this._loadPlugins() $this._invokeScripts() $this._setVariables() $this._setCommandAliases() $this._loadPrompt() $this._internal['ProfileLoadEnd'] = [datetime]::Now $this._internal['ProfileLoadDuration'] = $this._internal.ProfileLoadEnd - $this._internal.ProfileLoadStart $this._log( "SECTION END", "MAIN", "Debug" ) if ($env:ShowPSProfileLoadTime -ne $false) { Write-Host "Loading PSProfile alone took $([Math]::Round($this._internal.ProfileLoadDuration.TotalMilliseconds))ms$withRefresh" } } [void] Refresh() { $this._log( "Refreshing project map, checking for modules to install and creating symbolic links", "MAIN", "Verbose" ) $this._cleanConfig() $this._findProjects() $this._installModules() $this._createSymbolicLinks() $this._formatPrompts() $this._formatInitScripts() $this.LastRefresh = [datetime]::Now $this.Save() $this._log( "Refresh complete", "MAIN", "Verbose" ) } [void] Save() { $this._log( "Saving PSProfile configuration", "MAIN", "Debug" ) $out = @{ } $this.LastSave = [DateTime]::Now $this.PSObject.Properties.Name | Where-Object { $_ -ne '_internal' } | ForEach-Object { $out[$_] = $this.$_ } $out | Export-Configuration -Name PSProfile -CompanyName 'SCRT HQ' $this._log( "PSProfile configuration has been saved.", "MAIN", "Debug" ) } hidden [string] _globalize([string]$content) { $noScopePattern = 'function\s+(?<Name>[\w+_-]{1,})\s+\{' $globalScopePattern = 'function\s+global\:' $noScope = [RegEx]::Matches($content, $noScopePattern, "Multiline, IgnoreCase") $globalScope = [RegEx]::Matches($content,$globalScopePattern,"Multiline, IgnoreCase") if ($noScope.Count -ge $globalScope.Count) { foreach ($match in $noScope) { $fullValue = ($match.Groups | Where-Object { $_.Name -eq 0 }).Value $funcName = ($match.Groups | Where-Object { $_.Name -eq 'Name' }).Value $content = $content.Replace($fullValue, "function global:$funcName {") } } return $content } hidden [void] _cleanConfig() { $this._log( "SECTION START", "CleanConfig", "Debug" ) foreach ($section in @('ModulesToImport','ModulesToInstall')) { $this._log( "[$section] Cleaning section", "CleanConfig", "Verbose" ) [hashtable[]]$final = @() $this.$section | Where-Object {$_ -is [hashtable] -and $_.Name} | ForEach-Object { $final += $_ } $this.$section | Where-Object {$_ -is [string]} | ForEach-Object { $this._log( "[$section] Converting module string to hashtable: $_", "CleanConfig", "Verbose" ) $final += @{Name = $_} } $this.$section = $final } foreach ($section in @('ScriptPaths','PluginPaths','ProjectPaths')) { $this._log( "[$section] Cleaning section", "CleanConfig", "Verbose" ) [string[]]$final = @() $this.$section | Where-Object {-not [string]::IsNullOrEmpty($_)} | ForEach-Object { $final += $_ } $this.$section = $final } $this._log( "SECTION END", "CleanConfig", "Debug" ) } hidden [void] _loadPrompt() { $this._log( "SECTION START", "LoadPrompt", "Debug" ) if (-not [String]::IsNullOrEmpty($this.Settings.DefaultPrompt)) { $this._log( "Loading default prompt: $($this.Settings.DefaultPrompt)", "LoadPrompt", "Verbose" ) $function:prompt = $this.Prompts[$this.Settings.DefaultPrompt] } else { $this._log( "No default prompt name found on PSProfile. Retaining current prompt.", "LoadPrompt", "Verbose" ) } $this._log( "SECTION END", "LoadPrompt", "Debug" ) } hidden [void] _formatPrompts() { $this._log( "SECTION START", "FormatPrompts", "Debug" ) $final = @{ } $this.Prompts.GetEnumerator() | ForEach-Object { $this._log( "Formatting prompt '$($_.Key)'", "FormatPrompts", "Verbose" ) $updated = ($_.Value -split "[\r\n]" | Where-Object { $_ }).Trim() -join "`n" $final[$_.Key] = $updated } $this.Prompts = $final $this._log( "SECTION END", "FormatPrompts", "Debug" ) } hidden [void] _formatInitScripts() { $this._log( "SECTION START", "FormatInitScripts", "Debug" ) $final = $this.InitScripts $this.InitScripts.GetEnumerator() | ForEach-Object { $this._log( "Formatting InitScript '$($_.Key)'", "FormatInitScripts", "Verbose" ) $updated = ($_.Value.ScriptBlock -split "[\r\n]" | Where-Object { $_ }).Trim() -join "`n" $final[$_.Key]['ScriptBlock'] = $updated } $this.InitScripts = $final $this._log( "SECTION END", "FormatInitScripts", "Debug" ) } hidden [void] _loadAdditionalConfiguration([string]$configurationPath) { $this._log( "SECTION START", "AddlConfiguration", "Debug" ) $this._log( "Importing additional file: $configurationPath", "AddlConfiguration", "Verbose" ) $additional = Import-Metadata -Path $configurationPath $this._log( "Adding additional configuration to PSProfile object", "AddlConfiguration", "Verbose" ) $this | Update-Object $additional $this._log( "SECTION END", "AddlConfiguration", "Debug" ) } hidden [void] _loadConfiguration() { $this._log( "SECTION START", "Configuration", "Debug" ) $this._log( "Importing layered Configuration", "Configuration", "Verbose" ) $conf = Import-Configuration -Name PSProfile -CompanyName 'SCRT HQ' -DefaultPath (Join-Path $PSScriptRoot "Configuration.psd1") $this._log( "Adding layered configuration to PSProfile object", "Configuration", "Verbose" ) $this | Update-Object $conf $this._log( "SECTION END", "Configuration", "Debug" ) } hidden [void] _setCommandAliases() { $this._log( "SECTION START", 'SetCommandAliases', 'Debug' ) $this.CommandAliases.GetEnumerator() | ForEach-Object { try { $Name = $_.Key $Value = $_.Value if ($null -eq (Get-Alias "$Name*")) { New-Alias -Name $Name -Value $Value -Scope Global -Option AllScope -ErrorAction SilentlyContinue $this._log( "Set command alias: $Name > $Value", 'SetCommandAliases', 'Verbose' ) } else { $this._log( "Alias already in use, skipping: $Name", 'SetCommandAliases', 'Verbose' ) } } catch { $this._log( "Failed to set command alias: $Name > $Value :: $($_)", 'SetCommandAliases', 'Warning' ) } } $this._log( "SECTION END", 'SetCommandAliases', 'Debug' ) } hidden [void] _createSymbolicLinks() { $this._log( "SECTION START", 'CreateSymbolicLinks', 'Debug' ) if ($null -ne $this.SymbolicLinks.Keys) { $null = $this.SymbolicLinks.GetEnumerator() | Start-RSJob -Name { "_PSProfile_SymbolicLinks_" + $_.Key } -ScriptBlock { if (-not (Test-Path $_.Key) -or ((Get-Item $_.Key).LinkType -eq 'SymbolicLink' -and (Get-Item $_.Key).Target -ne $_.Value)) { New-Item -ItemType SymbolicLink -Path $_.Key -Value $_.Value -Force } } } else { $this._log( "No symbolic links specified!", 'CreateSymbolicLinks', 'Verbose' ) } $this._log( "SECTION END", 'CreateSymbolicLinks', 'Debug' ) } hidden [void] _setVariables() { $this._log( "SECTION START", 'SetVariables', 'Debug' ) if ($null -ne $this.Variables.Keys) { foreach ($varType in $this.Variables.Keys) { switch ($varType) { Environment { $this.Variables.Environment.GetEnumerator() | ForEach-Object { $this._log( "`$env:$($_.Key) = '$($_.Value)'", 'SetVariables', 'Verbose' ) Set-Item "Env:\$($_.Key)" -Value $_.Value -Force } } default { $this.Variables.Global.GetEnumerator() | ForEach-Object { $this._log( "`$global:$($_.Key) = '$($_.Value)'", 'SetVariables', 'Verbose' ) Set-Variable -Name $_.Key -Value $_.Value -Scope Global -Force } } } } } else { $this._log( "No variables key/value pairs provided!", 'SetVariables', 'Verbose' ) } $this._log( "SECTION END", 'SetVariables', 'Debug' ) } hidden [void] _findProjects() { $this._log( "SECTION START", 'FindProjects', 'Debug' ) if (-not [string]::IsNullOrEmpty((-join $this.ProjectPaths))) { $this.GitPathMap = @{ } $this.ProjectPaths | ForEach-Object { $p = $_ $cnt = 0 if (Test-Path $p) { $p = (Resolve-Path $p).Path $cnt++ $pInfo = [System.IO.DirectoryInfo]::new($p) $this.PathAliases["@$($pInfo.Name)"] = $pInfo.FullName $this._log( "Added path alias: @$($pInfo.Name) >> $($pInfo.FullName)", 'FindProjects', 'Verbose' ) $g = 0 $b = 0 $pInfo.EnumerateDirectories('.git',[System.IO.SearchOption]::AllDirectories) | ForEach-Object { $g++ $this._log( "Found git project @ $($_.Parent.FullName)", 'FindProjects', 'Verbose' ) $this.GitPathMap[$_.Parent.BaseName] = $_.Parent.FullName $bldPath = [System.IO.Path]::Combine($_.Parent.FullName,'build.ps1') if ([System.IO.File]::Exists($bldPath)) { $b++ $this._log( "Found build script @ $($_.FullName)", 'FindProjects', 'Verbose' ) $this.PSBuildPathMap[$_.Parent.BaseName] = $_.Parent.FullName } } $this._log( "$p :: Found: $g git | $b build", 'FindProjects', 'Verbose' ) } else { $this._log( "'$p' Unable to resolve path!", 'FindProjects', 'Verbose' ) } } } else { $this._log( "No project paths specified to search in!", 'FindProjects', 'Verbose' ) } $this._log( "SECTION END", 'FindProjects', 'Debug' ) } hidden [void] _invokeScripts() { $this._log( "SECTION START", 'InvokeScripts', 'Debug' ) if (-not [string]::IsNullOrEmpty((-join $this.ScriptPaths))) { $this.ScriptPaths | ForEach-Object { $p = $_ if (Test-Path $p) { $i = Get-Item $p $p = $i.FullName if ($p -match '\.ps1$') { try { $this._log( "'$($i.Name)' Invoking script", 'InvokeScripts', 'Verbose' ) $sb = [scriptblock]::Create($this._globalize(([System.IO.File]::ReadAllText($i.FullName)))) .$sb } catch { $e = $_ $this._log( "'$($i.Name)' Failed to invoke script! Error: $e", 'InvokeScripts', 'Warning' ) } } else { [System.IO.DirectoryInfo]::new($p).EnumerateFiles('*.ps1',[System.IO.SearchOption]::AllDirectories) | Where-Object { $_.BaseName -notmatch '^(profile|CONFIG|WIP)' } | ForEach-Object { $s = $_ try { $this._log( "'$($s.Name)' Invoking script", 'InvokeScripts', 'Verbose' ) $sb = [scriptblock]::Create($this._globalize(([System.IO.File]::ReadAllText($s.FullName)))) .$sb } catch { $e = $_ $this._log( "'$($s.Name)' Failed to invoke script! Error: $e", 'InvokeScripts', 'Warning' ) } } } } else { $this._log( "'$p' Unable to resolve path!", 'FindProjects', 'Verbose' ) } } } else { $this._log( "No script paths specified to invoke!", 'InvokeScripts', 'Verbose' ) } $this._log( "SECTION END", 'InvokeScripts', 'Debug' ) } hidden [void] _invokeInitScripts() { $this._log( "SECTION START", "InvokeInitScripts", "Debug" ) $this.InitScripts.GetEnumerator() | ForEach-Object { $s = $_ if ($_.Value.Enabled) { $this._log( "Invoking Init Script: $($s.Key)", "InvokeInitScripts", "Verbose" ) try { $sb = [scriptblock]::Create($this._globalize($s.Value.ScriptBlock)) .$sb } catch { $this._log( "Error while invoking InitScript '$($s.Key)': $($_.Exception.Message)", "InvokeInitScripts", "Warning" ) } } else { $this._log( "Skipping disabled Init Script: $($_.Key)", "InvokeInitScripts", "Verbose" ) } } $this._log( "SECTION END", "InvokeInitScripts", "Debug" ) } hidden [void] _installModules() { $this._log( "SECTION START", 'InstallModules', 'Debug' ) if (-not [string]::IsNullOrEmpty((-join $this.ModulesToInstall))) { $null = $this.ModulesToInstall | Where-Object { ($_ -is [hashtable] -and $_.Name) -or ($_ -is [string] -and -not [string]::IsNullOrEmpty($_.Trim())) } | Start-RSJob -Name { "_PSProfile_InstallModule_$($_)" } -VariablesToImport this -ScriptBlock { Param ( [parameter()] [object] $Module ) $params = if ($Module -is [string]) { @{Name = $Module } } elseif ($Module -is [hashtable]) { $Module } else { $null } $this._log( "Checking if module is installed already: $($params | ConvertTo-Json -Compress)", 'InstallModules', 'Verbose' ) if ($null -eq (Get-Module $params['Name'] -ListAvailable)) { $this._log( "Installing missing module to CurrentUser scope: $($params | ConvertTo-Json -Compress)", 'InstallModules', 'Verbose' ) Install-Module -Name @params -Scope CurrentUser -AllowClobber -SkipPublisherCheck } else { $this._log( "Module already installed, skipping: $($params | ConvertTo-Json -Compress)", 'InstallModules', 'Verbose' ) } } } else { $this._log( "No modules specified to install!", 'InstallModules', 'Verbose' ) } $this._log( "SECTION END", 'InstallModules', 'Debug' ) } hidden [void] _importModules() { $this._log( "SECTION START", 'ImportModules', 'Debug' ) if (-not [string]::IsNullOrEmpty((-join $this.ModulesToImport))) { $this.ModulesToImport | Where-Object { ($_ -is [hashtable] -and $_.Name) -or ($_ -is [string] -and -not [string]::IsNullOrEmpty($_.Trim())) } | ForEach-Object { try { $params = if ($_ -is [string]) { @{Name = $_ } } elseif ($_ -is [hashtable]) { $_ } else { $null } if ($null -ne $params) { @('ErrorAction','Verbose') | ForEach-Object { if ($params.ContainsKey($_)) { $params.Remove($_) } } if ($params.Name -ne 'EditorServicesCommandSuite') { Import-Module @params -Global -ErrorAction SilentlyContinue -Verbose:$false $this._log( "Module imported: $($params | ConvertTo-Json -Compress)", 'ImportModules', 'Verbose' ) } elseif ($params.Name -eq 'EditorServicesCommandSuite' -and $psEditor) { Import-Module EditorServicesCommandSuite -ErrorAction SilentlyContinue -Global -Force -Verbose:$false # Twice because: https://github.com/SeeminglyScience/EditorServicesCommandSuite/issues/40 Import-Module EditorServicesCommandSuite -ErrorAction SilentlyContinue -Global -Force -Verbose:$false Import-EditorCommand -Module EditorServicesCommandSuite -Force -Verbose:$false $this._log( "Module imported: $($params | ConvertTo-Json -Compress)", 'ImportModules', 'Verbose' ) } } else { $this._log( "Module must be either a string or a hashtable!", 'ImportModules', 'Verbose' ) } } catch { $this._log( "'$($params['Name'])' Error importing module: $($Error[0].Exception.Message)", "ImportModules", "Warning" ) } } } else { $this._log( "No modules specified to import!", 'ImportModules', 'Verbose' ) } $this._log( "SECTION END", 'ImportModules', 'Debug' ) } hidden [void] _loadPlugins() { $this._log( "SECTION START", 'LoadPlugins', 'Debug' ) if ($this.Plugins.Count) { $this.Plugins.ForEach( { if ($_.Name -ne 'PSProfile.PowerTools') { $plugin = $_ $this._log( "'$($plugin.Name)' Searching for plugin", 'LoadPlugins', 'Verbose' ) try { $found = $null $importParams = @{ ErrorAction = 'Stop' Global = $true } if ($plugin.ArgumentList) { $importParams['ArgumentList'] = $plugin.ArgumentList } [string[]]$pathsToSearch = @($this.PluginPaths) $env:PSModulePath.Split([System.IO.Path]::PathSeparator) | ForEach-Object { $pathsToSearch += $_ } foreach ($plugPath in $pathsToSearch) { $fullPath = [System.IO.Path]::Combine($plugPath,"$($plugin.Name).ps1") $this._log( "'$($plugin.Name)' Checking path: $fullPath", 'LoadPlugins', 'Debug' ) if (Test-Path $fullPath) { $sb = [scriptblock]::Create($this._globalize(([System.IO.File]::ReadAllText($fullPath)))) if ($plugin.ArgumentList) { .$sb($plugin.ArgumentList) } else { .$sb } $found = $fullPath break } } if ($null -ne $found) { $this._log( "'$($plugin.Name)' plugin loaded from path: $found", 'LoadPlugins', 'Verbose' ) } else { if ($null -ne $plugin.Name -and $null -ne (Get-Module $plugin.Name -ListAvailable -ErrorAction SilentlyContinue)) { Import-Module $plugin.Name @importParams $this._log( "'$($plugin.Name)' plugin loaded from PSModulePath!", 'LoadPlugins' ) } else { $this._log( "'$($plugin.Name)' plugin not found! To remove this plugin from your profile, run 'Remove-PSProfilePlugin $($plugin.Name)'", 'LoadPlugins', 'Warning' ) } } } catch { throw } } }) } else { $this._log( "No plugins specified to load!", 'LoadPlugins', 'Verbose' ) } $this._log( "SECTION END", 'LoadPlugins', 'Debug' ) } hidden [void] _log([string]$message,[string]$section,[PSProfileLogLevel]$logLevel) { $dt = Get-Date $shortMessage = "[$($dt.ToString('HH:mm:ss'))] $message" $lastCommand = if ($this.Log.Count) { $dt - $this.Log[-1].Time } else { New-TimeSpan } $this.Log.Add( [PSProfileEvent]::new( $dt, $lastCommand, ($dt - $this._internal.ProfileLoadStart), $logLevel, $section, $message ) ) switch ($logLevel) { Information { Write-Host $shortMessage } Verbose { Write-Verbose $shortMessage } Warning { Write-Warning $shortMessage } Error { Write-Error $shortMessage } Debug { Write-Debug $shortMessage } } } hidden [void] _log([string]$message,[string]$section) { $this._log($message,$section,'Quiet') } } function Get-DecryptedValue { param($Item) if ($Item -is [System.Security.SecureString]) { [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR( $Item ) ) } else { $Item } } function Add-PSProfileCommandAlias { <# .SYNOPSIS Adds a command alias to your PSProfile configuration to set during PSProfile import. .DESCRIPTION Adds a command alias to your PSProfile configuration to set during PSProfile import. .PARAMETER Alias The alias to set for the command. .PARAMETER Command The name of the command to set the alias for. .PARAMETER Force If the alias already exists in $PSProfile.CommandAliases, use -Force to overwrite the existing value. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfileCommandAlias -Alias code -Command Open-Code -Save Adds the command alias 'code' targeting the command 'Open-Code' and saves your PSProfile configuration. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [String] $Alias, [Parameter(Mandatory,Position = 1)] [String] $Command, [Parameter()] [Switch] $Force, [Parameter()] [Switch] $Save ) Process { if ($Force -or $Global:PSProfile.CommandAliases.Keys -notcontains $Alias) { New-Alias -Name $Alias -Value $Command -Option AllScope -Scope Global -Force Write-Verbose "Adding alias '$Alias' for command '$Command' to PSProfile" $Global:PSProfile.CommandAliases[$Alias] = $Command if ($Save) { Save-PSProfile } } else { Write-Error "Unable to add alias to `$PSProfile.CommandAliases as it already exists. Use -Force to overwrite the existing value if desired." } } } Register-ArgumentCompleter -CommandName Get-PSProfileCommandAlias -ParameterName Command -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) (Get-Command "$wordToComplete*").Name | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Add-PSProfileCommandAlias' function Get-PSProfileCommandAlias { <# .SYNOPSIS Gets an alias from $PSProfile.CommandAliases. .DESCRIPTION Gets an alias from $PSProfile.CommandAliases. .PARAMETER Alias The alias to get from $PSProfile.CommandAliases. .EXAMPLE Get-PSProfileCommandAlias -Alias code Gets the alias 'code' from $PSProfile.CommandAliases. .EXAMPLE Get-PSProfileCommandAlias Gets the list of command aliases from $PSProfile.CommandAliases. #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Alias ) Process { if ($PSBoundParameters.ContainsKey('Alias')) { Write-Verbose "Getting command alias '$Alias' from `$PSProfile.CommandAliases" $Global:PSProfile.CommandAliases.GetEnumerator() | Where-Object {$_.Key -in $Alias} } else { Write-Verbose "Getting all command aliases from `$PSProfile.CommandAliases" $Global:PSProfile.CommandAliases } } } Register-ArgumentCompleter -CommandName Get-PSProfileCommandAlias -ParameterName Alias -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.CommandAliases.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileCommandAlias' function Remove-PSProfileCommandAlias { <# .SYNOPSIS Removes an alias from $PSProfile.CommandAliases. .DESCRIPTION Removes an alias from $PSProfile.CommandAliases. .PARAMETER Alias The alias to remove from $PSProfile.CommandAliases. .PARAMETER Force If $true, also removes the alias itself from the session if it exists. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileCommandAlias -Alias code -Save Removes the alias 'code' from $PSProfile.CommandAliases then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String[]] $Alias, [Parameter()] [Switch] $Force, [Parameter()] [Switch] $Save ) Process { foreach ($al in $Alias) { if ($PSCmdlet.ShouldProcess("Removing '$al' from `$PSProfile.CommandAliases")) { Write-Verbose "Removing '$al' from `$PSProfile.CommandAliases" $Global:PSProfile.CommandAliases.Remove($al) if ($Force -and $null -ne (Get-Alias "$al*")) { Write-Verbose "Removing Alias: $al" Remove-Alias -Name $al -Force } } } if ($Save) { Save-PSProfile } } } Register-ArgumentCompleter -CommandName Remove-PSProfileCommandAlias -ParameterName Alias -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.CommandAliases.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileCommandAlias' function Export-PSProfileConfiguration { <# .SYNOPSIS Exports the PSProfile configuration as a PSD1 file to the desired path. .DESCRIPTION Exports the PSProfile configuration as a PSD1 file to the desired path. .PARAMETER Path The existing folder or file path with PSD1 extension to export the configuration to. If a folder path is provided, the configuration will be exported to the path with the file name 'PSProfile.Configuration.psd1'. .PARAMETER IncludeVault If $true, includes the Vault property as well so Secrets are also exported. .PARAMETER Force If $true and the resolved file path exists, overwrite it with the current configuration. .EXAMPLE Export-PSProfileConfiguration ~\MyPSProfileConfig.psd1 Exports the configuration to the specified path. .EXAMPLE Export-PSProfileConfiguration ~\MyScripts -Force Exports the configuration to the resolved path of '~\MyScripts\PSProfile.Configuration.psd1' and overwrites the file if it already exists. The exported configuration does *not* include the Vault to prevent secrets from being migrated with the portable configuration that is exported. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [ValidateScript({ if ($_ -like '*.psd1') { $true } elseif ((Test-Path $_) -and (Get-Item $_).PSIsContainer) { $true } else { throw "The path provided was not an existing folder path or a file path ending in a PSD1 extension. Please provide either an existing folder to export the PSProfile configuration to or an exact file path ending in a PSD1 extension to export the configuration to. Path provided: $_" } })] [String] $Path, [Parameter()] [Switch] $IncludeVault, [Parameter()] [Switch] $Force ) Process { if (Test-Path $Path) { $item = Get-Item $Path if ($item.PSIsContainer) { $finalPath = [System.IO.Path]::Combine($item.FullName,'PSProfile.Configuration.psd1') } else { if ($item.Extension -ne '.psd1') { Write-Error "Please provide either a file path for a psd1" } else { $finalPath = $item.FullName } } } else { $finalPath = $Path } if ((Test-Path $finalPath) -and -not $Force) { Write-Error "File path already exists: $finalPath. Use the -Force parameter to overwrite the contents with the current PSProfile configuration." } else { try { if (Test-Path $finalPath) { Write-Verbose "Force specified! Removing existing file: $finalPath" Remove-Item $finalPath -ErrorAction Stop } Write-Verbose "Importing metadata from path: $($Global:PSProfile.Settings.ConfigurationPath)" $metadata = Import-Metadata -Path $Global:PSProfile.Settings.ConfigurationPath -ErrorAction Stop if (-not $IncludeVault -and $metadata.ContainsKey('Vault')) { Write-Warning "Removing the Secrets Vault from the PSProfile configuration for safety. If you would like to export the configuration with the Vault included, use the -IncludeVault parameter with this function" $metadata.Remove('Vault') | Out-Null Write-Verbose "Exporting cleaned PSProfile configuration to path: $finalPath" } else { Write-Verbose "Exporting PSProfile configuration to path: $finalPath" } $metadata | Export-Metadata -Path $finalPath -ErrorAction Stop } catch { Write-Error $_ } } } } Export-ModuleMember -Function 'Export-PSProfileConfiguration' function Import-PSProfile { <# .SYNOPSIS Reloads your PSProfile by running $PSProfile.Load() .DESCRIPTION Reloads your PSProfile by running $PSProfile.Load() .EXAMPLE Import-PSProfile .EXAMPLE Load-PSProfile #> [CmdletBinding()] Param() Process { Write-Verbose "Loading PSProfile configuration!" $global:PSProfile._loadConfiguration() } } Export-ModuleMember -Function 'Import-PSProfile' function Import-PSProfileConfiguration { <# .SYNOPSIS Imports a Configuration.psd1 file from a specific path and overwrites differing values on the PSProfile, if any. .DESCRIPTION Imports a Configuration.psd1 file from a specific path and overwrites differing values on the PSProfile, if any. .PARAMETER Path The path to the PSD1 file you would like to import. .PARAMETER Save If $true, saves the updated PSProfile after importing. .EXAMPLE Import-PSProfileConfiguration -Path ~\MyProfile.psd1 -Save #> [CmdletBinding()] Param( [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [Alias('FullName')] [String] $Path, [Parameter()] [Switch] $Save ) Process { $Path = (Resolve-Path $Path).Path Write-Verbose "Loading PSProfile configuration from path: $Path" $Global:PSProfile._loadAdditionalConfiguration($Path) if ($Save) { Save-PSProfile } } } Export-ModuleMember -Function 'Import-PSProfileConfiguration' function Save-PSProfile { <# .SYNOPSIS Saves the current PSProfile configuration by calling the $PSProfile.Save() method. .DESCRIPTION Saves the current PSProfile configuration by calling the $PSProfile.Save() method. .EXAMPLE Save-PSProfile #> [CmdletBinding()] Param() Process { Write-Verbose "Saving PSProfile configuration!" $global:PSProfile.Save() } } Export-ModuleMember -Function 'Save-PSProfile' function Start-PSProfileConfigurationHelper { <# .SYNOPSIS Starts the PSProfile Configuration Helper. .DESCRIPTION Starts the PSProfile Configuration Helper. .EXAMPLE Start-PSProfileConfigurationHelper #> [CmdletBinding()] Param () Process { Write-Verbose "Starting PSProfile Configuration Helper..." $color = @{ Tip = "Green" Command = "Cyan" Warning = "Yellow" Current = "Magenta" } $header = { param([string]$title) @( "----------------------------------" "| $($title.ToUpper())" "----------------------------------" ) -join "`n" } $changes = [System.Collections.Generic.List[string]]::new() $changeHash = @{ } $tip = { param([string]$text) "TIP: $text" | Write-Host -ForegroundColor $color['Tip'] } $command = { param([string]$text) "COMMAND: $text" | Write-Host -ForegroundColor $color['Command'] } $warning = { param([string]$text) Write-Warning -Message $text } $current = { param([string]$item) "EXISTING : $item" | Write-Host -ForegroundColor $color['Current'] } $multi = { .$tip("This accepts multiple answers as comma-separated values, e.g. 1,2,5") } $exit = { "" | Write-Host .$header("Changes made to configuration") if ($changes.Count) { $changes | Write-Host "" | Write-Host "Would you like to save your updated PSProfile configuration?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" switch -Regex ($decision) { "[Yy]" { Save-PSProfile -Verbose } } } else { "None!" | Write-Host } "`nExiting Configuration Helper`n" | Write-Host -ForegroundColor Yellow return } $legend = { "" | Write-Host .$header("Legend") | Write-Host .$tip("$($color['Tip']) - Helpful tips and tricks") .$command("$($color['Command']) - Commands to run to replicate the configuration update made") .$warning("$($color['Warning']) - Any warnings to be aware of") .$current("$($color['Current']) - Any existing configuration values for this section") "" | Write-Host } $menu = { "" | Write-Host .$header("Menu") | Write-Host $options = @( "Choose a PSProfile concept below to learn more and optionally update" "the configuration for it as well:" "" "[1] Command Aliases" "[2] Modules to Import" "[3] Modules to Install" "[4] Path Aliases" "[5] Plugin Paths" "[6] Plugins" "[7] Project Paths" "[8] Prompts" "[9] Script Paths" "[10] Init Scripts" "[11] Secrets" "[12] Symbolic Links" "[13] Variables" "" "[14] Power Tools" "[15] Configuration" "[16] Helpers" "[17] Meta" "" "[*] All concepts (Default)" "[H] Hide Help Topics" "" "[X] Exit" "" ) $options | Write-Host .$multi "" | Write-Host Read-Host -Prompt "Enter your choice(s) (Default: *)" } @( "" "Welcome to PSProfile! This Configuration Helper serves as a way to jump-start" "your PSProfile configuration and increase your workflow productivity quickly." "" "You'll be asked a few questions to help with setting up your PSProfile," "including being provided information around performing the same configuration" "tasks using the included functions after your initial setup is complete." "" "If you have any questions, comments or find any bugs, please open an issue" "on the PSProfile repo: https://github.com/scrthq/PSProfile/issues/new" ) | Write-Host .$legend $choices = .$menu if ([string]::IsNullOrEmpty($choices)) { $choices = '*' } $hideHelpTopics = if ($choices -match "[Hh]") { $true } else { $false } if ($choices -match "[Xx]") { .$exit return } else { "`nChoices:" | Write-Host if ($choices -match '\*') { $options | Select-String "^\[\*\]\s+" | Write-Host $resolved = @(1..17) if ($hideHelpTopics) { $resolved += 'H' } } else { $resolved = $choices.Split(',').Trim() | Where-Object { -not [string]::IsNullOrEmpty($_) } $resolved | ForEach-Object { $item = $_ $options | Select-String "^\[$item\]\s+" | Write-Host } } foreach ($choice in $resolved | Where-Object {$_ -ne 'H'}) { "" | Write-Host $topic = (($options | Select-String "^\[$choice\]\s+") -replace "^\[$choice\]\s+(.*$)",'$1').Trim() $helpTopic = 'about_PSProfile_' + ($topic -replace ' ','_') .$header($topic) if (-not $hideHelpTopics -or $choice -in 13..16) { Write-Verbose "Getting the HelpTopic for this concept: $helpTopic" Get-Help $helpTopic -Category HelpFile .$tip("To view this conceptual HelpTopic at any time, run the following command:") .$command("Get-Help $helpTopic") } "" | Write-Host switch ($choice) { 1 { if ($Global:PSProfile.CommandAliases.Keys.Count) { .$current("`n$(([PSCustomObject]$Global:PSProfile.CommandAliases | Format-List | Out-String).Trim())") } Write-Host "Would you like to add a Command Alias to your PSProfile?" $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the command you would like to add an alias for (ex: Test-Path)" $item2 = Read-Host "Please enter the alias that you would like to set for the command (ex: tp)" if ($null -eq (Get-PSProfileCommandAlias -Alias $item2)) { if (-not $changeHash.ContainsKey('Command Aliases')) { $changes.Add("Command Aliases:") $changeHash['Command Aliases'] = @{ } } .$command("Add-PSProfileCommandAlias -Command '$item1' -Alias '$item2'") Add-PSProfileCommandAlias -Command $item1 -Alias $item2 -Verbose $changes.Add(" - Command: $item1") $changes.Add(" Alias: $item2") $changeHash['Command Aliases'][$item1] = $item2 } else { .$warning("Command Alias '$item2' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileCommandAlias -Command '$item1' -Alias '$item2' -Force") } "`nWould you like to add another Command Alias to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 2 { if ($Global:PSProfile.ModulesToImport.Count) { .$current("`n- $((($Global:PSProfile.ModulesToImport|ForEach-Object{if($_ -is [hashtable]){$_.Name}else{$_}} | Sort-Object) -join "`n- "))") } Write-Host "Would you like to add a Module to Import to your PSProfile?" $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the name of the module you would like to import during PSProfile load" if ($null -eq (Get-PSProfileModuleToImport -Name $item1)) { if (-not $changeHash.ContainsKey('Modules to Import')) { $changes.Add("Modules to Import:") $changeHash['Modules to Import'] = @() } .$command("Add-PSProfileModuleToImport -Name '$item1'") Add-PSProfileModuleToImport -Name $item1 -Verbose $changes.Add(" - $item1") $changeHash['Modules to Import'] += $item1 } else { .$warning("Module to Import '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileModuleToImport -Name '$item1' -Force") } "`nWould you like to add another Module to Import to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 3 { if ($Global:PSProfile.ModulesToInstall.Count) { .$current("`n- $((($Global:PSProfile.ModulesToInstall|ForEach-Object{if($_ -is [hashtable]){$_.Name}else{$_}} | Sort-Object) -join "`n- "))") } Write-Host "Would you like to add a Module to Install to your PSProfile?" $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the name of the module you would like to install via background job during PSProfile load" if ($null -eq (Get-PSProfileModuleToInstall -Name $item1)) { if (-not $changeHash.ContainsKey('Modules to Install')) { $changes.Add("Modules to Install:") $changeHash['Modules to Install'] = @() } .$command("Add-PSProfileModuleToInstall -Name '$item1'") Add-PSProfileModuleToInstall -Name $item1 -Verbose $changes.Add(" - $item1") $changeHash['Modules to Install'] += $item1 } else { .$warning("Module to Install '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileModuleToInstall -Name '$item1' -Force") } "`nWould you like to add another Module to Install to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 4 { if ($Global:PSProfile.PathAliases.Keys.Count) { .$current("`n$(([PSCustomObject]$Global:PSProfile.PathAliases | Format-List | Out-String).Trim())") } Write-Host "Would you like to add a Path Alias to your PSProfile?" $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the path you would like to add an alias for (ex: C:\Users\$env:USERNAME)" $item2 = Read-Host "Please enter the alias that you would like to set for the path (ex: ~)" if ($null -eq (Get-PSProfilePathAlias -Alias $item2)) { if (-not $changeHash.ContainsKey('Path Aliases')) { $changes.Add("Path Aliases:") $changeHash['Path Aliases'] = @{ } } .$command("Add-PSProfilePathAlias -Path '$item1' -Alias '$item2'") Add-PSProfilePathAlias -Path $item1 -Alias $item2 -Verbose $changes.Add(" - Path: $item1") $changes.Add(" Alias: $item2") $changeHash['Path Aliases'][$item1] = $item2 } else { .$warning("Path Alias '$item2' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfilePathAlias -Path '$item1' -Alias '$item2' -Force") } "`nWould you like to add another Path Alias to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 5 { if ($Global:PSProfile.PluginPaths.Count) { .$current("`n- $(($Global:PSProfile.PluginPaths | Sort-Object) -join "`n- ")") } Write-Host "Would you like to add an additional Plugin Path to your PSProfile?" .$tip("This is only needed if you have PSProfile plugins in a path outside of your normal PSModulePath") $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the path to the additional plugin folder" if ($null -eq (Get-PSProfilePluginPath -Path $item1)) { if (-not $changeHash.ContainsKey('Plugin Paths')) { $changes.Add("Plugin Paths:") $changeHash['Plugin Paths'] = @() } .$command("Add-PSProfilePluginPath -Path '$item1'") Add-PSProfilePluginPath -Path $item1 -NoRefresh -Verbose $changes.Add(" - $item1") $changeHash['Plugin Paths'] += $item1 } else { .$warning("Plugin Path '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfilePluginPath -Path '$item1' -Force") } "`nWould you like to add another Plugin Path to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 6 { if ($Global:PSProfile.Plugins.Count) { .$current("`n- $(($Global:PSProfile.Plugins.Name | Sort-Object) -join "`n- ")") } Write-Host "Would you like to add a Plugin to your PSProfile?" .$tip("Plugins can be either scripts or modules.") .$tip("Use AD cmdlets? Try adding the plugin 'PSProfile.ADCompleters' to get tab-completion for the Properties parameter on Get-ADUser, Get-ADGroup, and Get-ADComputer!") $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the name of the plugin you would like to add (ex: PSProfile.ADCompleters)" if ($null -eq (Get-PSProfilePlugin -Name $item1)) { if (-not $changeHash.ContainsKey('Plugins')) { $changes.Add("Plugins:") $changeHash['Plugins'] = @() } .$command("Add-PSProfilePlugin -Name '$item1'") Add-PSProfilePlugin -Name $item1 -NoRefresh -Verbose $changes.Add(" - $item1") $changeHash['Plugins'] += $item1 } else { .$warning("Plugin '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfilePlugin -Name '$item1' -Force") } "`nWould you like to add another Plugin to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 7 { if ($Global:PSProfile.ProjectPaths.Count) { .$current("`n- $(($Global:PSProfile.ProjectPaths | Sort-Object) -join "`n- ")") } Write-Host "Would you like to add a Project Path to your PSProfile?" $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the path to the additional project folder" if ($null -eq (Get-PSProfileProjectPath -Path $item1)) { if (-not $changeHash.ContainsKey('Project Paths')) { $changes.Add("Project Paths:") $changeHash['Project Paths'] = @() } .$command("Add-PSProfileProjectPath -Path '$item1'") Add-PSProfileProjectPath -Path $item1 -NoRefresh -Verbose $changes.Add(" - $item1") $changeHash['Project Paths'] += $item1 } else { .$warning("Project Path '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileProjectPath -Path '$item1' -Force") } "`nWould you like to add another Project Path to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 8 { if ($Global:PSProfile.Prompts.Count) { .$current("`n- $(($Global:PSProfile.Prompts.Keys | Sort-Object) -join "`n- ")") } if ($function:prompt -notmatch [Regex]::Escape('# https://go.microsoft.com/fwlink/?LinkID=225750')) { "It looks like you are using a custom prompt! Would you like to save it to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" $promptSaved = $false do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter a friendly name to save this prompt as (ex: MyPrompt)" if ($null -eq (Get-PSProfilePrompt -Name $item1)) { if (-not $changeHash.ContainsKey('Prompts')) { $changes.Add("Prompts:") $changeHash['Prompts'] = @() } .$command("Add-PSProfilePrompt -Name '$item1'") Add-PSProfilePrompt -Name $item1 -Verbose $changes.Add(" - $item1") $changeHash['Prompts'] += $item1 $promptSaved = $true } else { .$warning("Prompt '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfilePrompt -Name '$item1' -Force") "`nWould you like to save your prompt as a different name?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } } "[Xx]" { .$exit return } } } until ($promptSaved -or $decision -notmatch "[Yy]") } else { "It looks like you are using the default prompt! Skipping prompt configuration for now." | Write-Host } } 9 { if ($Global:PSProfile.ScriptPaths.Count) { .$current("`n- $(($Global:PSProfile.ScriptPaths | Sort-Object) -join "`n- ")") } Write-Host "Would you like to add a Script Path to your PSProfile?" .$tip("Script paths are invoked during PSProfile load. If you are running any external scripts from your current profile script, this is where you would add them.") $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the path of the additional script to add" if ($null -eq (Get-PSProfileScriptPath -Path $item1)) { if (-not $changeHash.ContainsKey('Script Paths')) { $changes.Add("Script Paths:") $changeHash['Script Paths'] = @() } .$command("Add-PSProfileScriptPath -Path '$item1'") Add-PSProfileScriptPath -Path $item1 -Verbose $changes.Add(" - $item1") $changeHash['Script Paths'] += $item1 } else { .$warning("Script Path '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileScriptPath -Path '$item1' -Force") } "`nWould you like to add another Script Path to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 10 { if ($Global:PSProfile.ScriptPaths.Count) { .$current("`n- $(($Global:PSProfile.ScriptPaths | Sort-Object) -join "`n- ")") } Write-Host "Would you like to add an external script as an Init Script on your PSProfile?" .$tip("Init Scripts are also invoked during PSProfile load. These differ from Script Paths in that the full script is stored on the PSProfile configuration itself. Init Scripts can also be disabled without being removed from PSProfile.") .$tip("During this Configuration Helper, you are limited to providing a path to a script file to import as an Init Script. While using Add-PSProfileInitScript, however, you can provide a ScriptBlock or Strings of code directly if preferred.") $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the path of the script to import as Init Script" if ($null -eq (Get-PSProfileInitScript -Name (Get-Item $item1).BaseName)) { if (-not $changeHash.ContainsKey('Init Scripts')) { $changes.Add("Init Scripts:") $changeHash['Init Scripts'] = @() } .$command("Add-PSProfileInitScript -Path '$item1'") Add-PSProfileInitScript -Path $item1 -Verbose $changes.Add(" - $item1") $changeHash['Init Scripts'] += $item1 } else { .$warning("Init Script '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileInitScript -Path '$item1' -Force") } "`nWould you like to import another Init Script to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 11 { if (($Global:PSProfile.Vault._secrets.GetEnumerator() | Where-Object {$_.Key -ne 'GitCredentials'}).Count) { .$current("`n- $((($Global:PSProfile.Vault._secrets.GetEnumerator() | Where-Object {$_.Key -ne 'GitCredentials'}).Key | Sort-Object) -join "`n- ")") } Write-Host "Would you like to add a Secret to your PSProfile Vault?" .$tip("Vault Secrets can be either a PSCredential object or a SecureString such as an API key stored with a friendly name to recall them with.") .$warning("Vault Secrets are encrypted using the Data Protection API, which is not as secure on non-Windows machines and unsupported in PowerShell 6.0-6.1. For more details, please see the Pull Request covering the topic where support was re-added in PowerShell Core: https://github.com/PowerShell/PowerShell/pull/9199") $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { do { "What type of Secret would you like to add?" | Write-Host $secretType = Read-Host "[P] PSCredential [S] SecureString [C] Cancel" } until ($secretType -match "[Pp|Ss|Cc]") if ($secretType -match "[Pp]") { "Please enter the credentials you would like to store in the Secrets Vault" | Write-Host $creds = Get-Credential if ($null -eq (Get-PSProfileSecret -Name $creds.UserName)) { if (-not $changeHash.ContainsKey('Secrets')) { $changes.Add("Secrets:") $changeHash['Secrets'] = @() } .$command("Add-PSProfileSecret -Credential (Get-Credential)") Add-PSProfileSecret -Credential $creds -Verbose $changes.Add(" - $($creds.UserName)") $changeHash['Secrets'] += $creds.UserName } else { .$warning("Secret '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileSecret -Credential (Get-Credential) -Force") } "`nWould you like to add another Secret to your PSProfile Fault?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } elseif ($secretType -match "[Ss]") { $item1 = Read-Host "Please enter the name you would like to store the secret as (ex: SecretAPIKey)" $item2 = Read-Host -AsSecureString "Please enter the secret to store as a SecureString" if ($null -eq (Get-PSProfileSecret -Name $item1)) { if (-not $changeHash.ContainsKey('Secrets')) { $changes.Add("Secrets:") $changeHash['Secrets'] = @() } .$command("Add-PSProfileSecret -Name '$item1' -SecureString (Read-Host -AsSecureString 'Enter SecureString')") Add-PSProfileSecret -Name $item1 -SecureString $item2 -Verbose $changes.Add(" - $item1") $changeHash['Secrets'] += $item1 } else { .$warning("Secret '$item1' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileSecret -Name '$item1' -SecureString (Read-Host -AsSecureString 'Enter SecureString') -Force") } "`nWould you like to add another Secret to your PSProfile Fault?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } else { $decision = $secretType } } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 12 { if ($Global:PSProfile.SymbolicLinks.Keys.Count) { .$current("`n$(($Global:PSProfile.SymbolicLinks | Out-String).Trim())") } Write-Host "Would you like to add a Symbolic Link to your PSProfile?" $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { $item1 = Read-Host "Please enter the path you would like to add an symbolic link for (ex: C:\Users\$env:USERNAME)" $item2 = Read-Host "Please enter the path of the symbolic link you would like to target the previous path with (ex: C:\Home)" if ($null -eq (Get-PSProfileSymbolicLink -LinkPath $item2)) { if (-not $changeHash.ContainsKey('Symbolic Links')) { $changes.Add("Symbolic Links:") $changeHash['Symbolic Links'] = @{ } } .$command("Add-PSProfileSymbolicLink -ActualPath '$item1' -LinkPath '$item2'") Add-PSProfileSymbolicLink -ActualPath $item1 -LinkPath $item2 -Verbose $changes.Add(" - ActualPath: $item1") $changes.Add(" LinkPath: $item2") $changeHash['Symbolic Links'][$item1] = $item2 } else { .$warning("Symbolic Link '$item2' already exists on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileSymbolicLink -ActualPath '$item1' -LinkPath '$item2' -Force") } "`nWould you like to add another Symbolic Link to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 13 { if ($Global:PSProfile.Variables.Environment.Keys.Count -or $Global:PSProfile.Variables.Global.Keys.Count) { .$current("`n`n~~ ENVIRONMENT ~~`n$(($Global:PSProfile.Variables.Environment | Out-String).Trim())`n`n~~ GLOBAL ~~`n$(($Global:PSProfile.Variables.Global | Out-String).Trim())") } Write-Host "Would you like to add a Variable to your PSProfile?" $decision = Read-Host "[Y] Yes [N] No [X] Exit" do { switch -Regex ($decision) { "[Yy]" { "Please enter the scope of the Variable to add" | Write-Host $s = Read-Host "[E] Environment [G] Global" $scope = switch -RegEx ($s) { "[Ee]" { 'Environment' } "[Gg]" { 'Global' } } $item1 = Read-Host "Please enter the name of the variable to add (ex: AWS_PROFILE)" $item2 = Read-Host "Please enter the value to set for variable '$item1' (ex: production)" if ($null -eq (Get-PSProfileVariable -Scope $scope -Name $item1)) { if (-not $changeHash.ContainsKey('Variables')) { $changes.Add("Variables:") $changeHash['Variables'] = @{ Environment = @{} Global = @{} } } .$command("Add-PSProfileVariable -Scope '$scope' -Name '$item1' -Value '$item2'") Add-PSProfileVariable -Scope $scope -Name $item1 -Value $item2 -Verbose $changes.Add(" - Scope: $scope") $changes.Add(" Name: $item1") $changes.Add(" Value: $item2") $changeHash['Variables'][$scope][$item1] = $item2 } else { .$warning("Variable '$item1' already exists at scope '$scope' on your PSProfile configuration! If you would like to overwrite it, run the following command:") .$command("Add-PSProfileVariable -Scope '$scope' -Name '$item1' -Value '$item2' -Force") } "`nWould you like to add another Variable to your PSProfile?" | Write-Host $decision = Read-Host "[Y] Yes [N] No [X] Exit" } "[Xx]" { .$exit return } } } until ($decision -notmatch "[Yy]") } 14 { "Power Tools functions do not alter the PSProfile configuration, so there is nothing to configure with this Helper! Please see the HelpTopic '$helpTopic' for more info:" | Write-Host .$command("Get-Help $helpTopic") "" | Write-Host Read-Host "Press [Enter] to continue" } 15 { "Configuration functions are meant to interact with the PSProfile configuration directly, so there is nothing to configure with this Helper! Please see the HelpTopic '$helpTopic' for more info:" | Write-Host .$command("Get-Help $helpTopic") "" | Write-Host Read-Host "Press [Enter] to continue" } 16 { "Helper functions are meant to interact for use within prompts or add Log Events to PSProfile, so there is nothing to configure with this Helper! Please see the HelpTopic '$helpTopic' for more info:" | Write-Host .$command("Get-Help $helpTopic") "" | Write-Host Read-Host "Press [Enter] to continue" } 17 { "Meta functions are meant to provide information about PSProfile itself, so there is nothing to configure with this Helper! Please see the HelpTopic '$helpTopic' for more info:" | Write-Host .$command("Get-Help $helpTopic") "" | Write-Host Read-Host "Press [Enter] to continue" } } } .$exit } } } Export-ModuleMember -Function 'Start-PSProfileConfigurationHelper' function Update-PSProfileConfig { <# .SYNOPSIS Force refreshes the current PSProfile configuration by calling the $PSProfile.Refresh() method. .DESCRIPTION Force refreshes the current PSProfile configuration by calling the $PSProfile.Refresh() method. This will update the GitPathMap with any new projects found and other tasks that don't run on every PSProfile load. .EXAMPLE Update-PSProfileConfig .EXAMPLE Refresh-PSProfile Uses the shorter alias command instead of the long command. #> [CmdletBinding()] Param() Process { Write-Verbose "Refreshing PSProfile config!" $global:PSProfile.Refresh() } } Export-ModuleMember -Function 'Update-PSProfileConfig' function Update-PSProfileRefreshFrequency { <# .SYNOPSIS Sets the Refresh Frequency for PSProfile. The $PSProfile.Refresh() runs tasks that aren't run during every profile load, i.e. SymbolicLink creation, Git project path discovery, module installation, etc. .DESCRIPTION Sets the Refresh Frequency for PSProfile. The $PSProfile.Refresh() runs tasks that aren't run during every profile load, i.e. SymbolicLink creation, Git project path discovery, module installation, etc. .PARAMETER Timespan The frequency that you would like to refresh your PSProfile configuration. Refresh will occur during the profile load after the time since last refresh has surpassed the desired refresh frequency. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Update-PSProfileRefreshFrequency -Timespan '03:00:00' -Save Updates the RefreshFrequency to 3 hours and saves the PSProfile configuration after updating. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [timespan] $Timespan, [Parameter()] [Switch] $Save ) Process { Write-Verbose "Updating PSProfile RefreshFrequency to '$($Timespan.ToString())'" $Global:PSProfile.RefreshFrequency = $Timespan.ToString() if ($Save) { Save-PSProfile } } } Export-ModuleMember -Function 'Update-PSProfileRefreshFrequency' function Update-PSProfileSetting { <# .SYNOPSIS Update a PSProfile property's value by tab-completing the available keys. .DESCRIPTION Update a PSProfile property's value by tab-completing the available keys. .PARAMETER Path The property path you would like to update, e.g. Settings.PSVersionStringLength .PARAMETER Value The value you would like to update for the specified setting path. .PARAMETER Add If $true, adds the value to the specified PSProfile setting value array instead of overwriting the current value. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Update-PSProfileSetting -Path Settings.PSVersionStringLength -Value 3 -Save Updates the PSVersionStringLength setting to 3 and saves the configuration. .EXAMPLE Update-PSProfileSetting -Path ScriptPaths -Value ~\ProfileLoad.ps1 -Add -Save *Adds* the 'ProfileLoad.ps1' script to the $PSProfile.ScriptPaths array of scripts to invoke during profile load, then saves the configuration. #> [CmdletBinding()] Param( [Parameter(Mandatory,Position = 0)] [String] $Path, [Parameter(Mandatory,Position = 1)] [object] $Value, [Parameter()] [switch] $Add, [Parameter()] [Switch] $Save ) Process { Write-Verbose "Updating PSProfile.$Path with value '$Value'" $split = $Path.Split('.') switch ($split.Count) { 5 { if ($Add) { $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])"."$($split[4])" += $Value } else { $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])"."$($split[4])" = $Value } } 4 { if ($Add) { $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])" += $Value } else{ $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])" = $Value } } 3 { if ($Add) { $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])" += $Value } else{ $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])" = $Value } } 2 { if ($Add) { $Global:PSProfile."$($split[0])"."$($split[1])" += $Value } else{ $Global:PSProfile."$($split[0])"."$($split[1])" = $Value } } 1 { if ($Add) { $Global:PSProfile.$Path += $Value } else{ $Global:PSProfile.$Path = $Value } } } if ($Save) { Save-PSProfile } } } Register-ArgumentCompleter -CommandName Update-PSProfileSetting -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments @PSBoundParameters } Register-ArgumentCompleter -CommandName Update-PSProfileSetting -ParameterName Value -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) if ($fakeBoundParameter.Path -eq 'Settings.FontType') { @('Default','NerdFonts','PowerLine') | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } } Export-ModuleMember -Function 'Update-PSProfileSetting' function Add-PSProfileToProfile { <# .SYNOPSIS Adds `Import-Module PSProfile` to the desired PowerShell profile file if not already present. .DESCRIPTION Adds `Import-Module PSProfile` to the desired PowerShell profile file if not already present. .PARAMETER Scope The profile scope to add the module import to. Defaults to CurrentUserCurrentHost (same as bare $profile). .PARAMETER DisableLoadTimeMessage If $true, adds `-ArgumentList $false` to the Import-Module call to hide the Module Load Time message. .EXAMPLE Add-PSProfileToProfile -Scope CurrentUserAllHosts Adds `Import-Module PSProfile` to the $profile.CurrentUserAllHosts file. Creates the parent folder if missing. .EXAMPLE Add-PSProfileToProfile -DisableLoadTimeMessage Adds `Import-Module PSProfile -ArgumentList $false` to the $profile file. Creates the parent folder if missing. #> [CmdletBinding()] Param ( [Parameter(Position = 0)] [ValidateSet('AllUsersAllHosts','AllUsersCurrentHost','CurrentUserAllHosts','CurrentUserCurrentHost')] [String] $Scope = 'CurrentUserCurrentHost', [Parameter()] [Switch] $DisableLoadTimeMessage ) Process { $exists = $false foreach ($s in @('AllUsersAllHosts','AllUsersCurrentHost','CurrentUserAllHosts','CurrentUserCurrentHost')) { $sPath = $profile | Select-Object -ExpandProperty $s if ((Test-Path $sPath) -and (Select-String -Path $sPath -Pattern ([Regex]::Escape('Import-Module PSProfile')))) { Write-Warning "'Import-Module PSProfile' already exists @ profile scope '$s' ($sPath)! Skipping addition @ scope '$Scope' to prevent duplicate module imports." $exists = $true } } if (-not $exists) { $profilePath = $profile | Select-Object -ExpandProperty $Scope $profileFolder = Split-Path $profilePath if (-not (Test-Path $profileFolder)) { Write-Verbose "Creating parent folder: $profileFolder" New-Item $profileFolder -ItemType Directory -Force } if (-not (Test-Path $profilePath)) { Write-Verbose "Creating profile file: $profilePath" New-Item $profilePath -ItemType File -Force } $string = 'Import-Module PSProfile' if ($DisableLoadTimeMessage) { $string += ' -ArgumentList $false' } Write-Verbose "Adding line to profile @ scope '$Scope': $string" Add-Content -Path $profilePath -Value $string } } } Export-ModuleMember -Function 'Add-PSProfileToProfile' function Copy-Parameters { <# .SYNOPSIS Copies parameters from a file or function and returns a RuntimeDefinedParameterDictionary with the copied parameters. Used in DynamicParam blocks. .DESCRIPTION Copies parameters from a file or function and returns a RuntimeDefinedParameterDictionary with the copied parameters. Used in DynamicParam blocks. .PARAMETER From The file or function to copy parameters from. .PARAMETER Exclude The parameter or list of parameters to exclude from replicating into the returned Dictionary. .EXAMPLE function Start-Build { [CmdletBinding()] Param () DynamicParam { Copy-Parameters -From ".\build.ps1" } Process { #Function logic } } Replicates the parameters from the build.ps1 script into the Start-Build function. #> [OutputType('System.Management.Automation.RuntimeDefinedParameterDictionary')] [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [Alias('File','Function')] [String] $From, [Parameter()] [Alias('ExcludeParameter')] [String[]] $Exclude = @() ) try { $targetCmd = Get-Command $From $params = @($targetCmd.Parameters.GetEnumerator() | Where-Object { $_.Key -notin $Exclude }) if ($params.Length -gt 0) { $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() foreach ($param in $params) { try { if (-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Key)) { Write-Verbose "Copying parameter: $($param.Key)" $paramVal = $param.Value $dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new( $paramVal.Name, $paramVal.ParameterType, $paramVal.Attributes ) $paramDictionary.Add($paramVal.Name, $dynParam) } } catch { $Global:Error.Remove($Global:Error[0]) } } return $paramDictionary } } catch { $Global:Error.Remove($Global:Error[0]) } } Register-ArgumentCompleter -CommandName Copy-Parameters -ParameterName Exclude -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $set = if (-not [String]::IsNullOrEmpty($fakeBoundParameter.From)) { ([System.Management.Automation.Language.Parser]::ParseInput( (Get-Command $fakeBoundParameter.From).Definition, [ref]$null, [ref]$null )).ParamBlock.Parameters.Name.VariablePath.UserPath } else { @() } $set | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Copy-Parameters' function Get-LastCommandDuration { <# .SYNOPSIS Gets the elapsed time of the last command via Get-History. Intended to be used in prompts. .DESCRIPTION Gets the elapsed time of the last command via Get-History. Intended to be used in prompts. .PARAMETER Id The Id of the command to get from the history. .PARAMETER Format The format string for the resulting timestamp. .EXAMPLE Get-LastCommandDuration #> [CmdletBinding()] param( [Parameter()] [int] $Id, [Parameter()] [string] $Format = "{0:h\:mm\:ss\.ffff}" ) $null = $PSBoundParameters.Remove("Format") $LastCommand = Get-History -Count 1 @PSBoundParameters if (!$LastCommand) { return "0:00:00.0000" } elseif ($null -ne $LastCommand.Duration) { $Format -f $LastCommand.Duration } else { $Duration = $LastCommand.EndExecutionTime - $LastCommand.StartExecutionTime $Format -f $Duration } } Export-ModuleMember -Function 'Get-LastCommandDuration' function Get-PathAlias { <# .SYNOPSIS Gets the Path alias using either the short name from $PSProfile.GitPathMap or a path alias stored in $PSProfile.PathAliases, falls back to using a shortened version of the root drive + current directory. .DESCRIPTION Gets the Path alias using either the short name from $PSProfile.GitPathMap or a path alias stored in $PSProfile.PathAliases, falls back to using a shortened version of the root drive + current directory. .PARAMETER Path The full path to get the PathAlias for. Defaults to $PWD.Path .PARAMETER DirectorySeparator The desired DirectorySeparator character. Defaults to $global:PathAliasDirectorySeparator if present, falls back to [System.IO.Path]::DirectorySeparatorChar if not. .EXAMPLE Get-PathAlias #> [CmdletBinding()] Param ( [parameter(Position = 0)] [string] $Path = $PWD.Path, [parameter(Position = 1)] [string] $DirectorySeparator = $(if ($null -ne $global:PathAliasDirectorySeparator) { $global:PathAliasDirectorySeparator } else { [System.IO.Path]::DirectorySeparatorChar }) ) Begin { try { $origPath = $Path if ($null -eq $global:PSProfile) { $global:PSProfile = @{ Settings = @{ PSVersionStringLength = 3 } PathAliasMap = @{ '~' = $env:USERPROFILE } } } elseif ($null -eq $global:PSProfile._internal) { $global:PSProfile._internal = @{ PathAliasMap = @{ '~' = $env:USERPROFILE } } } elseif ($null -eq $global:PSProfile._internal.PathAliasMap) { $global:PSProfile._internal.PathAliasMap = @{ '~' = $env:USERPROFILE } } if ($gitRepo = Test-IfGit) { $gitIcon = if ($global:PSProfile.Settings.ContainsKey('FontType')) { $global:PSProfile.Settings.PromptCharacters.GitRepo[$global:PSProfile.Settings.FontType] } else { '@' } if ([String]::IsNullOrEmpty($gitIcon)) { $gitIcon = '@' } $key = $gitIcon + $gitRepo.Repo if (-not $global:PSProfile._internal.PathAliasMap.ContainsKey($key)) { $global:PSProfile._internal.PathAliasMap[$key] = $gitRepo.TopLevel } } $leaf = Split-Path $Path -Leaf if (-not $global:PSProfile._internal.PathAliasMap.ContainsKey('~')) { $global:PSProfile._internal.PathAliasMap['~'] = $env:USERPROFILE } Write-Verbose "Alias map => JSON: $($global:PSProfile._internal.PathAliasMap | ConvertTo-Json -Depth 5)" $aliasKey = $null $aliasValue = $null foreach ($hash in $global:PSProfile._internal.PathAliasMap.GetEnumerator() | Sort-Object { $_.Value.Length } -Descending) { if ($Path -like "$($hash.Value)*") { $Path = $Path.Replace($hash.Value,$hash.Key) $aliasKey = $hash.Key $aliasValue = $hash.Value Write-Verbose "AliasKey [$aliasKey] || AliasValue [$aliasValue]" break } } } catch { Write-Error $_ return $origPath } } Process { try { if ($null -ne $aliasKey -and $origPath -eq $aliasValue) { Write-Verbose "Matched original path! Returning alias base path" $finalPath = $Path } elseif ($null -ne $aliasKey) { Write-Verbose "Matched alias key [$aliasKey]! Returning path alias with leaf" $drive = "$($aliasKey)\" $finalPath = if ((Split-Path $origPath -Parent) -eq $aliasValue) { "$($drive)$($leaf)" } else { "$($drive)$([char]0x2026)\$($leaf)" } } else { $drive = (Get-Location).Drive.Name + ':\' Write-Verbose "Matched base drive [$drive]! Returning base path" $finalPath = if ($Path -eq $drive) { $drive } elseif ((Split-Path $Path -Parent) -eq $drive) { "$($drive)$($leaf)" } else { "$($drive)..\$($leaf)" } } if ($DirectorySeparator -notin @($null,([System.IO.Path]::DirectorySeparatorChar))) { $finalPath.Replace(([System.IO.Path]::DirectorySeparatorChar),$DirectorySeparator) } else { $finalPath } } catch { Write-Error $_ return $origPath } } } Export-ModuleMember -Function 'Get-PathAlias' function Get-PSProfileArguments { <# .SYNOPSIS Used for PSProfile Plugins to provide easy Argument Completers using PSProfile constructs. .DESCRIPTION Used for PSProfile Plugins to provide easy Argument Completers using PSProfile constructs. .PARAMETER FinalKeyOnly Returns only the final key of the completed argument to the list of completers. If $false, returns the full path. .PARAMETER WordToComplete The word to complete, typically passed in from the scriptblock arguments. .PARAMETER CommandName Here to allow passing @PSBoundParameters directly to this function from Register-ArgumentCompleter .PARAMETER ParameterName Here to allow passing @PSBoundParameters directly to this function from Register-ArgumentCompleter .PARAMETER CommandAst Here to allow passing @PSBoundParameters directly to this function from Register-ArgumentCompleter .PARAMETER FakeBoundParameter Here to allow passing @PSBoundParameters directly to this function from Register-ArgumentCompleter .EXAMPLE Get-PSProfileArguments -WordToComplete "Prompts.$wordToComplete" -FinalKeyOnly Gets the list of prompt names under the Prompts PSProfile primary key. .EXAMPLE Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly Gets the list of Git Path short names under the GitPathMap PSProfile primary key. #> [OutputType('System.Management.Automation.CompletionResult')] [CmdletBinding()] Param( [switch] $FinalKeyOnly, [string] $WordToComplete, [object] $CommandName, [object] $ParameterName, [object] $CommandAst, [object] $FakeBoundParameter ) Process { Write-Verbose "Getting PSProfile command argument completions" $split = $WordToComplete.Split('.') $setting = $null switch ($split.Count) { 5 { $setting = $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])" $base = "$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])" } 4 { $setting = $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])" $base = "$($split[0])"."$($split[1])"."$($split[2])" } 3 { $setting = $Global:PSProfile."$($split[0])"."$($split[1])" $base = "$($split[0])"."$($split[1])" } 2 { $setting = $Global:PSProfile."$($split[0])" $base = $split[0] } } if ($null -eq $setting) { $setting = $Global:PSProfile $base = $null $final = $WordToComplete } else { $final = $split | Select-Object -Last 1 } if ($setting.GetType() -notin @([string],[int],[long],[version],[timespan],[datetime],[bool])) { $props = if ($setting.PSTypeNames -match 'Hashtable') { $setting.Keys | Where-Object {$_ -ne '_internal' -and $_ -like "$final*"} | Sort-Object } else { ($setting | Get-Member -MemberType Property,NoteProperty).Name | Where-Object {$_ -notmatch '^_' -and $_ -like "$final*"} | Sort-Object } $props | ForEach-Object { $result = if (-not $FinalKeyOnly -and $null -ne $base) { @($base,$_) -join "." } else { $_ } [System.Management.Automation.CompletionResult]::new($result, $result, 'ParameterValue', $result) } } } } Register-ArgumentCompleter -CommandName Get-PSProfileArguments -ParameterName WordToComplete -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments @PSBoundParameters } Export-ModuleMember -Function 'Get-PSProfileArguments' function Get-PSVersion { <# .SYNOPSIS Gets the short formatted PSVersion string for use in a prompt or wherever else desired. .DESCRIPTION Gets the short formatted PSVersion string for use in a prompt or wherever else desired. .PARAMETER Places How many decimal places you would like the returned version string to be. Defaults to $PSProfile.Settings.PSVersionStringLength if present. .EXAMPLE Get-PSVersion -Places 2 Returns `6.2` when using PowerShell 6.2.2, or `5.1` when using Windows PowerShell 5.1.18362.10000 #> [OutputType('System.String')] [CmdletBinding()] Param ( [parameter(Position = 0)] [AllowNull()] [int] $Places = $global:PSProfile.Settings.PSVersionStringLength ) Process { $version = $PSVersionTable.PSVersion.ToString() if ($null -ne $Places) { $split = ($version -split '\.')[0..($Places - 1)] if ("$($split[-1])".Length -gt 1) { $split[-1] = "$($split[-1])".Substring(0,1) } $joined = $split -join '.' if ($version -match '[a-zA-Z]+') { $joined += "-$(($Matches[0]).Substring(0,1))" if ($version -match '\d+$') { $joined += $Matches[0] } } $joined } else { $version } } } Export-ModuleMember -Function 'Get-PSVersion' function Test-IfGit { <# .SYNOPSIS Tests if the current path is in a Git repo folder and returns the basic details as an object if so. Useful in prompts when determining current folder's Git status .DESCRIPTION Tests if the current path is in a Git repo folder and returns the basic details as an object if so. Useful in prompts when determining current folder's Git status .EXAMPLE Test-IfGit #> [CmdletBinding()] Param () Process { try { $topLevel = git rev-parse --show-toplevel *>&1 if ($topLevel -like 'fatal: *') { $Global:Error.Remove($Global:Error[0]) $false } else { $origin = git remote get-url origin $repo = Split-Path -Leaf $origin [PSCustomObject]@{ TopLevel = (Resolve-Path $topLevel).Path Origin = $origin Repo = $(if ($repo -notmatch '(\.git|\.ssh|\.tfs)$') { $repo } else { $repo.Substring(0,($repo.LastIndexOf('.'))) }) } } } catch { $false $Global:Error.Remove($Global:Error[0]) } } } Export-ModuleMember -Function 'Test-IfGit' function Write-PSProfileLog { <# .SYNOPSIS Adds a log entry to the current PSProfile Log. .DESCRIPTION Adds a log entry to the current PSProfile Log. Used for external plugins to hook into the existing log so items like Plugin load logging are contained in one place. .PARAMETER Message The message to log. .PARAMETER Section The name of the section you are logging for, e.g. the name of the plugin or overall what action is being done. .PARAMETER LogLevel The Level of the Log event. Defaults to Debug. .EXAMPLE Write-PSProfileLog -Message "Hunting for missing KBs" -Section 'KBUpdate' -LogLevel 'Verbose' #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [String] $Message, [Parameter(Mandatory,Position = 1)] [String] $Section, [Parameter(Position = 2)] [PSProfileLogLevel] $LogLevel = 'Debug' ) Process { $Global:PSProfile._log( $Message, $Section, $LogLevel ) } } Export-ModuleMember -Function 'Write-PSProfileLog' function Add-PSProfileInitScript { <# .SYNOPSIS Adds script contents to your PSProfile configuration directly. Contents will be invoked during PSProfile import. Useful for scripts that you want to include directly on your configuration for portability instead of calling as an external script via $PSProfile.ScriptPaths. .DESCRIPTION Adds script contents to your PSProfile configuration directly. Contents will be invoked during PSProfile import. Useful for scripts that you want to include directly on your configuration for portability instead of calling as an external script via $PSProfile.ScriptPaths. .PARAMETER Name The friendly name to reference the script block by. .PARAMETER Content The content of the script as a string, i.e. if using `Get-Content` against another file to pass as the value here. .PARAMETER ScriptBlock The content of the script as a scriptblock. Useful if you are adding as script manually .PARAMETER Path The path to an external PS1 file to import the contents to your $PSProfile.InitScripts directly. When using Path, the file's BaseName becomes the Name value. .PARAMETER State Whether the InitScript should be Enabled or Disabled. Defaults to Enabled. .PARAMETER RemoveDuplicateScriptPaths If a specified Path is also in $PSProfile.ScriptPaths, remove it from there to prevent duplicate scripts from being invoked during PSProfile import. .PARAMETER Force If the InitScript name already exists in $PSProfile.InitScripts, use -Force to overwrite the existing value. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Get-PSProfileScriptPath | Add-PSProfileInitScript #> [CmdletBinding(DefaultParameterSetName = 'Content')] Param( [Parameter(Mandatory,Position = 0,ParameterSetName = 'Content')] [Parameter(Mandatory,Position = 0,ParameterSetName = 'ScriptBlock')] [String] $Name, [Parameter(Mandatory,Position = 1,ParameterSetName = 'Content')] [String[]] $Content, [Parameter(Mandatory,Position = 1,ParameterSetName = 'ScriptBlock')] [ScriptBlock] $ScriptBlock, [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName = 'Path')] [Alias('FullName')] [String[]] $Path, [Parameter()] [ValidateSet('Enabled','Disabled')] [String] $State = 'Enabled', [Parameter(ParameterSetName = 'Path')] [Switch] $RemoveDuplicateScriptPaths, [Parameter()] [Switch] $Force, [Parameter()] [Switch] $Save ) Process { switch ($PSCmdlet.ParameterSetName) { Content { if (-not $Force -and $Global:PSProfile.InitScripts.Contains($Name)) { Write-Error "Unable to add Init Script '$Name' to `$PSProfile.InitScripts as it already exists. Use -Force to overwrite the existing value if desired." } else { Write-Verbose "Adding InitScript '$Name' from Contents to PSProfile configuration" $Global:PSProfile.InitScripts[$Name] = @{ Enabled = ($State -eq 'Enabled') ScriptBlock = ($Content -join "`n").Trim() } } } ScriptBlock { if (-not $Force -and $Global:PSProfile.InitScripts.Contains($Name)) { Write-Error "Unable to add Init Script '$Name' to `$PSProfile.InitScripts as it already exists. Use -Force to overwrite the existing value if desired." } else { Write-Verbose "Adding InitScript '$Name' from ScriptBlock to PSProfile configuration" $Global:PSProfile.InitScripts[$Name] = @{ Enabled = ($State -eq 'Enabled') ScriptBlock = $ScriptBlock.ToString().Trim() } } } Path { $Path | Where-Object {$_ -match '\.ps1$'} | ForEach-Object { $item = Get-Item $_ $N = $item.BaseName if (-not $Force -and $Global:PSProfile.InitScripts.Contains($N)) { Write-Error "Unable to add Init Script '$N' to `$PSProfile.InitScripts as it already exists. Use -Force to overwrite the existing value if desired." } else { Write-Verbose "Adding InitScript '$N' from Path '$($item.FullName)' to PSProfile configuration" $Global:PSProfile.InitScripts[$N] = @{ Enabled = ($State -eq 'Enabled') ScriptBlock = ((Get-Content $_) -join "`n").Trim() } if ($RemoveDuplicateScriptPaths -and (Get-PSProfileScriptPath) -contains $item.FullName) { Remove-PSProfileScriptPath -Path $item.FullName -Confirm:$false } } } } } } End { if ($Save) { Save-PSProfile } } } Export-ModuleMember -Function 'Add-PSProfileInitScript' function Disable-PSProfileInitScript { <# .SYNOPSIS Disables an enabled InitScript in $PSProfile.InitScripts. .DESCRIPTION Disables an enabled InitScript in $PSProfile.InitScripts. .PARAMETER Name The name of the InitScript to disable in $PSProfile.InitScripts. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Disable-PSProfileInitScript -Name PSReadlineSettings,DevOpsTools Disables the InitScripts 'PSReadlineSettings' and 'DevOpsTools' in $PSProfile.InitScripts. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String[]] $Name, [Parameter()] [Switch] $Save ) Process { foreach ($N in $Name) { Write-Verbose "Disabling InitScript '$N' in `$PSProfile.InitScripts" if ($Global:PSProfile.InitScripts.Contains($N)) { $Global:PSProfile.InitScripts[$N]['Enabled'] = $false } } if ($Save) { Save-PSProfile } } } Register-ArgumentCompleter -CommandName Disable-PSProfileInitScript -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.InitScripts.GetEnumerator() | Where-Object {$_.Value.Enabled -and $_.Key -like "$wordToComplete*"} | Select-Object -ExpandProperty Key | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Disable-PSProfileInitScript' function Edit-PSProfileInitScript { <# .SYNOPSIS Edit an InitScript from $PSProfile.InitScripts in Visual Studio Code. .DESCRIPTION Edit an InitScript from $PSProfile.InitScripts in Visual Studio Code. .PARAMETER Name The name of the InitScript to edit from $PSProfile.InitScripts. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileInitScript -Name PSReadlineSettings,DevOpsTools Removes the InitScripts 'PSReadlineSettings' and 'DevOpsTools' from $PSProfile.InitScripts. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [String[]] $Name, [Parameter()] [Switch] $Save ) Process { foreach ($initScript in $Name) { if ($Global:PSProfile.InitScripts.Contains($initScript)) { $in = @{ StdIn = $Global:PSProfile.InitScripts[$initScript].ScriptBlock TmpFile = [System.IO.Path]::Combine(([System.IO.Path]::GetTempPath()),"InitScript-$($initScript)-$(-join ((97..(97+25)|%{[char]$_}) | Get-Random -Count 3)).ps1") } $handler = { Param( [hashtable] $in ) try { $code = (Get-Command code -All | Where-Object { $_.CommandType -notin @('Function','Alias') })[0].Source $in.StdIn | Set-Content $in.TmpFile -Force & $code $in.TmpFile --wait } catch { throw } finally { if (Test-Path $in.TmpFile -ErrorAction SilentlyContinue) { [System.IO.File]::ReadAllText($in.TmpFile) Remove-Item $in.TmpFile -Force } } } Write-Verbose "Editing InitScript '$initScript' in Visual Studio Code, waiting for file to close" if ($updated = .$handler($in)) { $Global:PSProfile.InitScripts[$initScript].ScriptBlock = $updated } } } if ($Save) { Save-PSProfile } } } Register-ArgumentCompleter -CommandName Edit-PSProfileInitScript -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.InitScripts.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Edit-PSProfileInitScript' function Enable-PSProfileInitScript { <# .SYNOPSIS Enables a disabled InitScript in $PSProfile.InitScripts. .DESCRIPTION Enables a disabled InitScript in $PSProfile.InitScripts. .PARAMETER Name The name of the InitScript to enable in $PSProfile.InitScripts. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Enable-PSProfileInitScript -Name PSReadlineSettings,DevOpsTools Enables the InitScripts 'PSReadlineSettings' and 'DevOpsTools' in $PSProfile.InitScripts. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String[]] $Name, [Parameter()] [Switch] $Save ) Process { foreach ($N in $Name) { Write-Verbose "Enabling InitScript '$N' in `$PSProfile.InitScripts" if ($Global:PSProfile.InitScripts.Contains($N)) { $Global:PSProfile.InitScripts[$N]['Enabled'] = $true } } if ($Save) { Save-PSProfile } } } Register-ArgumentCompleter -CommandName Enable-PSProfileInitScript -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.InitScripts.GetEnumerator() | Where-Object {-not $_.Value.Enabled -and $_.Key -like "$wordToComplete*"} | Select-Object -ExpandProperty Key | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Enable-PSProfileInitScript' function Get-PSProfileInitScript { <# .SYNOPSIS Gets an InitScript from $PSProfile.InitScripts. .DESCRIPTION Gets an InitScript from $PSProfile.InitScripts. .PARAMETER Name The name of the InitScript to get from $PSProfile.InitScripts. .PARAMETER Full If $true, gets the compiled InitScript from $PSProfile.InitScripts. This only includes Enabled InitScripts. Each InitScript includes the name in a comment at the top of it for easy identification. .EXAMPLE Get-PSProfileInitScript -Name PSReadlineSettings,DevOpsTools Returns the information for the InitScripts 'PSReadlineSettings' and 'DevOpsTools' from $PSProfile.InitScripts .EXAMPLE Get-PSProfileInitScript -Full Gets the compiled InitScript from $PSProfile.InitScripts. This only includes Enabled InitScripts. #> [CmdletBinding(DefaultParameterSetName = 'Name')] Param ( [Parameter(Position = 0,ValueFromPipeline,ParameterSetName = 'Name')] [String[]] $Name, [Parameter(ParameterSetName = 'Full')] [Switch] $Full ) Process { switch ($PSCmdlet.ParameterSetName) { Name { if ($PSBoundParameters.ContainsKey('Name')) { Write-Verbose "Getting InitScript [ $($Name -join ', ') ] from `$PSProfile.InitScripts" $Global:PSProfile.InitScripts.GetEnumerator() | Where-Object {$_.Key -in $Name} | ForEach-Object { [PSCustomObject]@{ Name = $_.Key Enabled = $_.Value.Enabled ScriptBlock = ([scriptblock]::Create($_.Value.ScriptBlock)) } } } else { Write-Verbose "Getting all InitScripts from `$PSProfile.InitScripts" $Global:PSProfile.InitScripts.GetEnumerator() | ForEach-Object { [PSCustomObject]@{ Name = $_.Key Enabled = $_.Value.Enabled ScriptBlock = ([scriptblock]::Create($_.Value.ScriptBlock)) } } } } Full { Write-Verbose "Getting the full InitScript from `$PSProfile.InitScripts" $f = $Global:PSProfile.InitScripts.GetEnumerator() | Where-Object {$_.Value.Enabled} | ForEach-Object { "# From InitScript: $($_.Key)" $_.Value.ScriptBlock "" } if ($f) { [scriptblock]::Create(($f -join "`n").Trim()) } } } } } Register-ArgumentCompleter -CommandName Get-PSProfileInitScript -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.InitScripts.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileInitScript' function Remove-PSProfileInitScript { <# .SYNOPSIS Removes an InitScript from $PSProfile.InitScripts. .DESCRIPTION Removes an InitScript from $PSProfile.InitScripts. .PARAMETER Name The name of the InitScript to remove from $PSProfile.InitScripts. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileInitScript -Name PSReadlineSettings,DevOpsTools Removes the InitScripts 'PSReadlineSettings' and 'DevOpsTools' from $PSProfile.InitScripts. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [String[]] $Name, [Parameter()] [Switch] $Save ) Process { foreach ($initScript in $Name) { if ($Global:PSProfile.InitScripts.Contains($initScript)) { if ($PSCmdlet.ShouldProcess("Removing InitScript '$initScript' from `$PSProfile.InitScripts")) { Write-Verbose "Removing InitScript '$initScript' from `$PSProfile.InitScripts" $Global:PSProfile.InitScripts.Remove($initScript) } } } if ($Save) { Save-PSProfile } } } Register-ArgumentCompleter -CommandName Remove-PSProfileInitScript -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.InitScripts.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileInitScript' function Get-PSProfileCommand { <# .SYNOPSIS Gets the list of commands provided by PSProfile directly. .DESCRIPTION Gets the list of commands provided by PSProfile directly. .PARAMETER Command The command to get from the list of PSProfile commands. .EXAMPLE Get-PSProfileCommand Gets the full list of commands provided by PSProfile directly. #> [OutputType('System.Management.Automation.FunctionInfo')] [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Command ) Begin { $commands = Get-Command -Module PSProfile | Where-Object {$_.Name -in (Get-Module PSProfile).ExportedCommands.Keys} } Process { if ($PSBoundParameters.ContainsKey('Command')) { Write-Verbose "Getting PSProfile command '$Command'" $commands | Where-Object {$_.Name -in $Command} } else { Write-Verbose "Getting all commands provided by PSProfile directly" $commands } } } Register-ArgumentCompleter -CommandName Get-PSProfileCommand -ParameterName Command -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) (Get-Module PSProfile).ExportedCommands.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileCommand' function Get-PSProfileImportedCommand { <# .SYNOPSIS Gets the list of commands imported from scripts and plugins that are not part of PSProfile itself. .DESCRIPTION Gets the list of commands imported from scripts and plugins that are not part of PSProfile itself. .PARAMETER Command The command to get from the list of imported commands. .EXAMPLE Get-PSProfileImportedCommand Gets the full list of commands imported during PSProfile load. #> [OutputType('System.Management.Automation.FunctionInfo')] [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Command ) Begin { $commands = Get-Command -Module PSProfile | Where-Object {$_.Name -notin (Get-Module PSProfile).ExportedCommands.Keys} } Process { if ($PSBoundParameters.ContainsKey('Command')) { Write-Verbose "Getting imported command '$Command'" $commands | Where-Object {$_.Name -in $Command} } else { Write-Verbose "Getting commands imported during PSProfile load that are not part of PSProfile itself" $commands } } } Register-ArgumentCompleter -CommandName Get-PSProfileImportedCommand -ParameterName Command -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) (Get-Command -Module PSProfile | Where-Object {$_.Name -notin (Get-Module PSProfile).ExportedCommands.Keys} | Where-Object {$_ -like "$wordToComplete*"}).Name | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileImportedCommand' function Get-PSProfileLog { <# .SYNOPSIS Gets the PSProfile Log events. .DESCRIPTION Gets the PSProfile Log events. .PARAMETER Section Limit results to only a specific section. .PARAMETER LogLevel Limit results to only a specific LogLevel. .PARAMETER Summary Get a high-level summary of the PSProfile Log. .PARAMETER Raw Return the raw PSProfile Events. Returns the results via Format-Table for readability otherwise. .EXAMPLE Get-PSProfileLog Gets the current Log in full. .EXAMPLE Get-PSProfileLog -Summary Gets the Log summary. .EXAMPLE Get-PSProfileLog -Section InvokeScripts,LoadPlugins -Raw Gets the Log Events for only sections 'InvokeScripts' and 'LoadPlugins' and returns the raw Event objects. #> [CmdletBinding(DefaultParameterSetName = 'Full')] Param( [Parameter(Position = 0,ParameterSetName = 'Full')] [String[]] $Section, [Parameter(Position = 1,ParameterSetName = 'Full')] [PSProfileLogLevel[]] $LogLevel, [Parameter(ParameterSetName = 'Summary')] [Switch] $Summary, [Parameter(ParameterSetName = 'Full')] [Switch] $Raw ) Process { if ($Summary) { Write-Verbose "Getting PSProfile Log summary" $Global:PSProfile.Log | Group-Object Section | ForEach-Object { $sectName = $_.Name $Group = $_.Group $sectCaps = $Group | Where-Object {$_.Message -match '^SECTION (START|END)$'} [PSCustomObject]@{ Name = $sectName Start = $sectCaps[0].Time.ToString('HH:mm:ss.fff') SectionDuration = "$([Math]::Round(($sectCaps[-1].Time - $sectCaps[0].Time).TotalMilliseconds))ms" FullDuration = "$([Math]::Round(($Group[-1].Time - $Group[0].Time).TotalMilliseconds))ms" RunningJobs = Get-RSJob -State Running | Where-Object {$_.Name -match $sectName} | Select-Object -ExpandProperty Name } } | Sort-Object Start | Format-Table -AutoSize } else { Write-Verbose "Getting PSProfile Log" $items = if ($Section) { $Global:PSProfile.Log | Where-Object {$_.Section -in $Section} } else { $Global:PSProfile.Log } if ($LogLevel) { $items = $items | Where-Object {$_.LogLevel -in $LogLevel} } if (-not $Raw) { $items | Format-Table -AutoSize } else { $items } } } } Register-ArgumentCompleter -CommandName Get-PSProfileLog -ParameterName 'Section' -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Log.Section | Sort-Object -Unique | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileLog' function Add-PSProfileModuleToImport { <# .SYNOPSIS Adds a module to import during PSProfile import. .DESCRIPTION Adds a module to import during PSProfile import. .PARAMETER Name The name of the module to import. .PARAMETER Prefix Add the specified prefix to the nouns in the names of imported module members. .PARAMETER MinimumVersion Import only a version of the module that is greater than or equal to the specified value. If no version qualifies, Import-Module generates an error. .PARAMETER RequiredVersion Import only the specified version of the module. If the version is not installed, Import-Module generates an error. .PARAMETER ArgumentList Specifies arguments (parameter values) that are passed to a script module during the Import-Module command. Valid only when importing a script module. .PARAMETER Force If the module already exists in $PSProfile.ModulesToImport, use -Force to overwrite the existing value. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfileModuleToImport -Name posh-git -RequiredVersion '0.7.3' -Save Specifies to import posh-git version 0.7.3 during PSProfile import then saves the updated configuration. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String[]] $Name, [Parameter()] [String] $Prefix, [Parameter()] [String] $MinimumVersion, [Parameter()] [String] $RequiredVersion, [Parameter()] [Object[]] $ArgumentList, [Parameter()] [Switch] $Force, [Parameter()] [Switch] $Save ) Process { foreach ($mod in $Name) { if (-not $Force -and $null -ne ($Global:PSProfile.ModulesToImport | Where-Object {$_.Name -eq $mod})) { Write-Error "Unable to add module to `$PSProfile.ModulesToImport as it already exists. Use -Force to overwrite the existing value if desired." } else { $moduleParams = @{ Name = $mod } $PSBoundParameters.GetEnumerator() | Where-Object {$_.Key -in @('Prefix','MinimumVersion','RequiredVersion','ArgumentList')} | ForEach-Object { $moduleParams[$_.Key] = $_.Value } Write-Verbose "Adding '$mod' to `$PSProfile.ModulesToImport" [hashtable[]]$final = @($moduleParams) $Global:PSProfile.ModulesToImport | Where-Object {$_.Name -ne $mod} | ForEach-Object { $final += $_ } $Global:PSProfile.ModulesToImport = $final if ($Save) { Save-PSProfile } } } } } Register-ArgumentCompleter -CommandName Add-PSProfileModuleToImport -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-Module "$wordToComplete*" -ListAvailable | Select-Object -ExpandProperty Name | Sort-Object -Unique | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Add-PSProfileModuleToImport' function Get-PSProfileModuleToImport { <# .SYNOPSIS Gets a module from $PSProfile.ModulesToImport. .DESCRIPTION Gets a module from $PSProfile.ModulesToImport. .PARAMETER Name The name of the module to get from $PSProfile.ModulesToImport. .EXAMPLE Get-PSProfileModuleToImport -Name posh-git Gets posh-git from $PSProfile.ModulesToImport .EXAMPLE Get-PSProfileModuleToImport Gets the list of modules to import from $PSProfile.ModulesToImport #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Name ) Process { if ($PSBoundParameters.ContainsKey('Name')) { Write-Verbose "Getting ModuleToImport '$Name' from `$PSProfile.ModulesToImport" $Global:PSProfile.ModulesToImport | Where-Object {$_ -in $Name -or $_.Name -in $Name} } else { Write-Verbose "Getting all command aliases from `$PSProfile.ModulesToImport" $Global:PSProfile.ModulesToImport } } } Register-ArgumentCompleter -CommandName Get-PSProfileModuleToImport -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.ModulesToImport | ForEach-Object { if ($_ -is [hashtable]) { $_.Name } else { $_ } } | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileModuleToImport' function Remove-PSProfileModuleToImport { <# .SYNOPSIS Removes a module from $PSProfile.ModulesToImport. .DESCRIPTION Removes a module from $PSProfile.ModulesToImport. .PARAMETER Name The name of the module to remove from $PSProfile.ModulesToImport. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileModuleToImport -Name posh-git -Save Removes posh-git from $PSProfile.ModulesToImport then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String[]] $Name, [Parameter()] [Switch] $Save ) Process { foreach ($mod in $Name) { if ($PSCmdlet.ShouldProcess("Removing '$mod' from `$PSProfile.ModulesToImport")) { Write-Verbose "Removing '$mod' from `$PSProfile.ModulesToImport" $Global:PSProfile.ModulesToImport = $Global:PSProfile.ModulesToImport | Where-Object {($_ -is [hashtable] -and $_.Name -ne $mod) -or ($_ -is [string] -and $_ -ne $mod)} if ($Save) { Save-PSProfile } } } } } Register-ArgumentCompleter -CommandName Remove-PSProfileModuleToImport -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.ModulesToImport | ForEach-Object { if ($_ -is [hashtable]) { $_.Name } else { $_ } } | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileModuleToImport' function Add-PSProfileModuleToInstall { <# .SYNOPSIS Adds a module to ensure is installed in the CurrentUser scope. Module installations are handled via background job during PSProfile import. .DESCRIPTION Adds a module to ensure is installed in the CurrentUser scope. Module installations are handled via background job during PSProfile import. .PARAMETER Name The name of the module to install. .PARAMETER Repository The repository to install the module from. Defaults to the PowerShell Gallery. .PARAMETER MinimumVersion The minimum version of the module to install. .PARAMETER RequiredVersion The required version of the module to install. .PARAMETER AcceptLicense If $true, accepts the license for the module if necessary. .PARAMETER AllowPrerelease If $true, allows installation of prerelease versions of the module. .PARAMETER Force If the module already exists in $PSProfile.ModulesToInstall, use -Force to overwrite the existing value. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfileModuleToInstall -Name posh-git -RequiredVersion '0.7.3' -Save Specifies to install posh-git version 0.7.3 during PSProfile import if missing then saves the updated configuration. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String[]] $Name, [Parameter()] [String] $Repository, [Parameter()] [String] $MinimumVersion, [Parameter()] [String] $RequiredVersion, [Parameter()] [Switch] $AcceptLicense, [Parameter()] [Switch] $AllowPrerelease, [Parameter()] [Switch] $Force, [Parameter()] [Switch] $Save ) Process { foreach ($mod in $Name) { if (-not $Force -and $null -ne ($Global:PSProfile.ModulesToInstall | Where-Object {$_.Name -eq $mod})) { Write-Error "Unable to add module '$mod' to `$PSProfile.ModulesToInstall as it already exists. Use -Force to overwrite the existing value if desired." } else { $moduleParams = @{ Name = $mod } $PSBoundParameters.GetEnumerator() | Where-Object {$_.Key -in @('Repository','MinimumVersion','RequiredVersion','AcceptLicense','AllowPrerelease','Force')} | ForEach-Object { $moduleParams[$_.Key] = $_.Value } Write-Verbose "Adding '$mod' to `$PSProfile.ModulesToInstall" [hashtable[]]$final = @($moduleParams) $Global:PSProfile.ModulesToInstall | Where-Object {$_.Name -ne $mod} | ForEach-Object { $final += $_ } $Global:PSProfile.ModulesToInstall = $final if ($Save) { Save-PSProfile } } } } } Register-ArgumentCompleter -CommandName Add-PSProfileModuleToInstall -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-Module "$wordToComplete*" -ListAvailable | Select-Object -ExpandProperty Name | Sort-Object -Unique | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Add-PSProfileModuleToInstall' function Get-PSProfileModuleToInstall { <# .SYNOPSIS Gets a module from $PSProfile.ModulesToInstall. .DESCRIPTION Gets a module from $PSProfile.ModulesToInstall. .PARAMETER Name The name of the module to get from $PSProfile.ModulesToInstall. .EXAMPLE Get-PSProfileModuleToInstall -Name posh-git Gets posh-git from $PSProfile.ModulesToInstall .EXAMPLE Get-PSProfileModuleToInstall Gets the list of modules to install from $PSProfile.ModulesToInstall #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Name ) Process { if ($PSBoundParameters.ContainsKey('Name')) { Write-Verbose "Getting ModuleToImport '$Name' from `$PSProfile.ModulesToInstall" $Global:PSProfile.ModulesToInstall | Where-Object {$_ -in $Name -or $_.Name -in $Name} } else { Write-Verbose "Getting all command aliases from `$PSProfile.ModulesToInstall" $Global:PSProfile.ModulesToInstall } } } Register-ArgumentCompleter -CommandName Get-PSProfileModuleToInstall -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.ModulesToInstall | ForEach-Object { if ($_ -is [hashtable]) { $_.Name } else { $_ } } | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileModuleToInstall' function Remove-PSProfileModuleToInstall { <# .SYNOPSIS Removes a module from $PSProfile.ModulesToInstall. .DESCRIPTION Removes a module from $PSProfile.ModulesToInstall. .PARAMETER Name The name of the module to remove from $PSProfile.ModulesToInstall. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileModuleToInstall -Name posh-git -Save Removes posh-git from $PSProfile.ModulesToInstall then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String[]] $Name, [Parameter()] [Switch] $Save ) Process { foreach ($mod in $Name) { if ($PSCmdlet.ShouldProcess("Removing '$mod' from `$PSProfile.ModulesToInstall")) { Write-Verbose "Removing '$mod' from `$PSProfile.ModulesToInstall" $Global:PSProfile.ModulesToInstall = $Global:PSProfile.ModulesToInstall | Where-Object {($_ -is [hashtable] -and $_.Name -ne $mod) -or ($_ -is [string] -and $_ -ne $mod)} if ($Save) { Save-PSProfile } } } } } Register-ArgumentCompleter -CommandName Remove-PSProfileModuleToInstall -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.ModulesToInstall | ForEach-Object { if ($_ -is [hashtable]) { $_.Name } else { $_ } } | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileModuleToInstall' function Add-PSProfilePathAlias { <# .SYNOPSIS Adds a path alias to your PSProfile configuration. Path aliases are used for path shortening in prompts via Get-PathAlias. .DESCRIPTION Adds a path alias to your PSProfile configuration. Path aliases are used for path shortening in prompts via Get-PathAlias. .PARAMETER Alias The alias to substitute the full path for in prompts via Get-PathAlias. .PARAMETER Path The full path to be substituted. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfilePathAlias -Alias ~ -Path $env:USERPROFILE -Save Adds a path alias of ~ for the current UserProfile folder and saves your PSProfile configuration. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [String] $Alias, [Parameter(Mandatory,Position = 1)] [String] $Path, [Parameter()] [Switch] $Save ) Process { Write-Verbose "Adding alias '$Alias' to path '$Path' to PSProfile" $Global:PSProfile.PathAliases[$Alias] = $Path if ($Save) { Save-PSProfile } } } Export-ModuleMember -Function 'Add-PSProfilePathAlias' function Get-PSProfilePathAlias { <# .SYNOPSIS Gets a module from $PSProfile.PathAliases. .DESCRIPTION Gets a module from $PSProfile.PathAliases. .PARAMETER Alias The Alias to get from $PSProfile.PathAliases. .EXAMPLE Get-PSProfilePathAlias -Alias ~ Gets the alias '~' from $PSProfile.PathAliases .EXAMPLE Get-PSProfilePathAlias Gets the list of path aliases from $PSProfile.PathAliases #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Alias ) Process { if ($PSBoundParameters.ContainsKey('Alias')) { Write-Verbose "Getting Path Alias '$Alias' from `$PSProfile.PathAliases" $Global:PSProfile.PathAliases.GetEnumerator() | Where-Object {$_.Key -in $Alias} } else { Write-Verbose "Getting all command aliases from `$PSProfile.PathAliases" $Global:PSProfile.PathAliases } } } Register-ArgumentCompleter -CommandName Get-PSProfilePathAlias -ParameterName Alias -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.PathAliases.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfilePathAlias' function Remove-PSProfilePathAlias { <# .SYNOPSIS Removes an alias from $PSProfile.PathAliases. .DESCRIPTION Removes an alias from $PSProfile.PathAliases. .PARAMETER Alias The alias to remove from $PSProfile.PathAliases. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfilePathAlias -Alias Workplace -Save Removes the alias 'Workplace' from $PSProfile.PathAliases then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String] $Alias, [Parameter()] [Switch] $Save ) Process { if ($PSCmdlet.ShouldProcess("Removing '$Alias' from `$PSProfile.PathAliases")) { Write-Verbose "Removing '$Alias' from `$PSProfile.PathAliases" $Global:PSProfile.PathAliases.Remove($Alias) if ($Save) { Save-PSProfile } } } } Register-ArgumentCompleter -CommandName Remove-PSProfilePathAlias -ParameterName Alias -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.PathAliases.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfilePathAlias' function Add-PSProfilePluginPath { <# .SYNOPSIS Adds a PluginPath to your PSProfile to search for PSProfile plugins in during module load. .DESCRIPTION Adds a PluginPath to your PSProfile to search for PSProfile plugins in during module load. .PARAMETER Path The path of the folder to add to your $PSProfile.PluginPaths. This path should contain PSProfile.Plugins .PARAMETER NoRefresh If $true, skips reloading your PSProfile after updating. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfilePluginPath -Path ~\PSProfilePlugins -Save Adds the folder ~\PSProfilePlugins to $PSProfile.PluginPaths and saves the configuration after updating. .EXAMPLE Add-PSProfilePluginPath C:\PSProfilePlugins -Verbose Adds the path C:\PSProfilePlugins to your $PSProfile.PluginPaths, refreshes your PathDict but does not save. Call Save-PSProfile after if satisfied with the results. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({if ((Get-Item $_).PSIsContainer){$true}else{throw "$_ is not a folder! Please add only folders to this PSProfile property. If you would like to add a script, use Add-PSProfileScriptPath instead."}})] [Alias('FullName')] [String[]] $Path, [Parameter()] [Switch] $NoRefresh, [Parameter()] [Switch] $Save ) Process { foreach ($p in $Path) { $fP = (Resolve-Path $p).Path [string[]]$base = @() $Global:PSProfile.PluginPaths | Where-Object {-not [string]::IsNullOrEmpty($_)} | ForEach-Object { $base += $_ } if ($Global:PSProfile.PluginPaths -notcontains $fP) { Write-Verbose "Adding PluginPath to PSProfile: $fP" $base += $fP } else { Write-Verbose "PluginPath already in PSProfile: $fP" } $Global:PSProfile.PluginPaths = $base } if ($Save) { Save-PSProfile } if (-not $NoRefresh) { Import-PSProfile -Verbose:$false } } } Export-ModuleMember -Function 'Add-PSProfilePluginPath' function Get-PSProfilePluginPath { <# .SYNOPSIS Gets a plugin path from $PSProfile.PluginPaths. .DESCRIPTION Gets a plugin path from $PSProfile.PluginPaths. .PARAMETER Path The plugin path to get from $PSProfile.PluginPaths. .EXAMPLE Get-PSProfilePluginPath -Path E:\MyPSProfilePlugins Gets the path 'E:\MyPSProfilePlugins' from $PSProfile.PluginPaths .EXAMPLE Get-PSProfilePluginPath Gets the list of plugin paths from $PSProfile.PluginPaths #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Path ) Process { if ($PSBoundParameters.ContainsKey('Path')) { Write-Verbose "Getting plugin path '$Path' from `$PSProfile.PluginPaths" $Global:PSProfile.PluginPaths | Where-Object {$_ -in $Path} } else { Write-Verbose "Getting all plugin paths from `$PSProfile.PluginPaths" $Global:PSProfile.PluginPaths } } } Register-ArgumentCompleter -CommandName Get-PSProfilePluginPath -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.PluginPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfilePluginPath' function Remove-PSProfilePluginPath { <# .SYNOPSIS Removes a Plugin Path from $PSProfile.PluginPaths. .DESCRIPTION Removes a Plugin Path from $PSProfile.PluginPaths. .PARAMETER Path The path to remove from $PSProfile.PluginPaths. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfilePluginPath -Name E:\MyPluginPaths -Save Removes the path 'E:\MyPluginPaths' from $PSProfile.PluginPaths then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String] $Path, [Parameter()] [Switch] $Save ) Process { if ($PSCmdlet.ShouldProcess("Removing '$Path' from `$PSProfile.PluginPaths")) { Write-Verbose "Removing '$Path' from `$PSProfile.PluginPaths" $Global:PSProfile.PluginPaths = $Global:PSProfile.PluginPaths | Where-Object {$_ -notin @($Path,(Resolve-Path $Path).Path)} if ($Save) { Save-PSProfile } } } } Register-ArgumentCompleter -CommandName Remove-PSProfilePluginPath -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.PluginPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfilePluginPath' function Add-PSProfilePlugin { <# .SYNOPSIS Adds a PSProfile Plugin to the list of plugins. If the plugin already exists, it will overwrite it. Re-imports your PSProfile once done to load any newly added plugins. .DESCRIPTION Adds a PSProfile Plugin to the list of plugins. If the plugin already exists, it will overwrite it. Re-imports your PSProfile once done to load any newly added plugins. .PARAMETER Name The name of the Plugin to add, e.g. 'PSProfile.PowerTools' .PARAMETER ArgumentList Any arguments that need to be passed to the plugin on import, such as a hashtable to process. .PARAMETER NoRefresh If $true, skips reloading your PSProfile after updating. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfilePlugin -Name 'PSProfile.PowerTools' -Save Adds the included plugin 'PSProfile.PowerTools' to your PSProfile and saves it so it persists. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [String[]] $Name, [Parameter(Position = 1)] [Object] $ArgumentList, [Parameter()] [Switch] $NoRefresh, [Parameter()] [Switch] $Save ) Process { foreach ($pName in $Name) { Write-Verbose "Adding plugin '$pName' to `$PSProfile.Plugins" $plugin = @{ Name = $pName } if ($PSBoundParameters.ContainsKey('ArgumentList')) { $plugin['ArgumentList'] = $ArgumentList } $temp = @() $Global:PSProfile.Plugins | Where-Object {$_.Name -ne $pName} | ForEach-Object { $temp += $_ } $temp += $plugin $Global:PSProfile.Plugins = $temp } if ($Save) { Save-PSProfile } if (-not $NoRefresh) { Import-PSProfile -Verbose:$false } } } Register-ArgumentCompleter -CommandName Add-PSProfilePlugin -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.PluginPaths | Get-ChildItem | Select-Object -ExpandProperty BaseName -Unique | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Add-PSProfilePlugin' function Get-PSProfilePlugin { <# .SYNOPSIS Gets a Plugin from $PSProfile.Plugins. .DESCRIPTION Gets a Plugin from $PSProfile.Plugins. .PARAMETER Name The name of the Plugin to get from $PSProfile.Plugins. .EXAMPLE Get-PSProfilePlugin -Name PSProfile.Prompt Gets PSProfile.Prompt from $PSProfile.Plugins .EXAMPLE Get-PSProfilePlugin Gets the list of Plugins from $PSProfile.Plugins #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Name ) Process { if ($PSBoundParameters.ContainsKey('Name')) { Write-Verbose "Getting Plugin '$Name' from `$PSProfile.Plugins" $Global:PSProfile.Plugins | Where-Object {$_.Name -in $Name} } else { Write-Verbose "Getting all Plugins from `$PSProfile.Plugins" $Global:PSProfile.Plugins } } } Register-ArgumentCompleter -CommandName Get-PSProfilePlugin -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Plugins | ForEach-Object {$_.Name} | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfilePlugin' function Remove-PSProfilePlugin { <# .SYNOPSIS Removes a PSProfile Plugin from $PSProfile.Plugins. .DESCRIPTION Removes a PSProfile Plugin from $PSProfile.Plugins. .PARAMETER Name The name of the Plugin to remove from $PSProfile.Plugins. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfilePlugin -Name 'PSProfile.PowerTools' -Save Removes the Plugin 'PSProfile.PowerTools' from $PSProfile.Plugins then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String] $Name, [Parameter()] [Switch] $Save ) Process { if ($PSCmdlet.ShouldProcess("Removing '$Name' from `$PSProfile.Plugins")) { Write-Verbose "Removing '$Name' from `$PSProfile.Plugins" $Global:PSProfile.Plugins = $Global:PSProfile.Plugins | Where-Object {$_.Name -ne $Name} if ($Save) { Save-PSProfile } } } } Register-ArgumentCompleter -CommandName Remove-PSProfilePlugin -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Plugins.Name | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfilePlugin' function Confirm-ScriptIsValid { <# .SYNOPSIS Uses the PSParser to check for any errors in a script file. .DESCRIPTION Uses the PSParser to check for any errors in a script file. .PARAMETER Path The path of the script to check for errors. .EXAMPLE Confirm-ScriptIsValid MyScript.ps1 .EXAMPLE Get-ChildItem .\Scripts | Confirm-ScriptIsValid #> [CmdletBinding()] Param ( [parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [Alias("FullName")] [ValidateScript( { Test-Path $_ })] [String[]] $Path ) Begin { $errorColl = @() $analyzed = 0 $lenAnalyzed = 0 } Process { foreach ($p in $Path | Where-Object { $_ -like '*.ps1' }) { $analyzed++ $item = Get-Item $p $lenAnalyzed += $item.Length $contents = Get-Content -Path $item.FullName -ErrorAction Stop $errors = $null $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) $obj = [PSCustomObject][Ordered]@{ Name = $item.Name FullName = $item.FullName Length = $item.Length ErrorCount = $errors.Count Errors = $errors } $obj if ($errors.Count) { $errorColl += $obj } } } End { Write-Verbose "Total files analyzed: $analyzed" Write-Verbose "Total size of files analyzed: $lenAnalyzed ($([Math]::Round(($lenAnalyzed/1MB),2)) MB)" Write-Verbose "Files with errors:`n$($errorColl | Sort-Object FullName | Out-String)" } } Export-ModuleMember -Function 'Confirm-ScriptIsValid' function Enter-CleanEnvironment { <# .SYNOPSIS Enters a clean environment with -NoProfile and sets a couple helpers, e.g. a prompt to advise you are in a clean environment and some PSReadline helper settings for convenience. .DESCRIPTION Enters a clean environment with -NoProfile and sets a couple helpers, e.g. a prompt to advise you are in a clean environment and some PSReadline helper settings for convenience. .PARAMETER Engine The engine to open the clean environment with between powershell, pwsh, and pwsh-preview. Defaults to the current engine the clean environment is opened from. .PARAMETER ImportModule If $true, imports the module found in the BuildOutput folder if present. Useful for quickly testing compiled modules after building in a clean environment to avoid assembly locking and other gotchas. .EXAMPLE Enter-CleanEnvironment Opens a clean environment from the current path. .EXAMPLE cln Does the same as Example 1, but using the shorter alias 'cln'. .EXAMPLE cln -ipmo Enters the clean environment and imports the built module in the BuildOutput folder, if present. #> [CmdletBinding()] Param ( [parameter(Position = 0)] [ValidateSet('powershell','pwsh','pwsh-preview')] [Alias('E')] [String] $Engine = $(if ($PSVersionTable.PSVersion.ToString() -match 'preview') { 'pwsh-preview' } elseif ($PSVersionTable.PSVersion.Major -ge 6) { 'pwsh' } else { 'powershell' }), [Parameter()] [Alias('ipmo','Import')] [Switch] $ImportModule ) Begin { $parsedEngine = if ($Engine -eq 'pwsh-preview' -and ($PSVersionTable.PSVersion.Major -le 5 -or $IsWindows)) { "& '{0}'" -f (Resolve-Path ([System.IO.Path]::Combine((Split-Path (Get-Command pwsh-preview).Source -Parent),'..','pwsh.exe'))).Path } else { $Engine } } Process { $verboseMessage = "Creating clean environment...`n Engine : $Engine" $command = "$parsedEngine -NoProfile -NoExit -C `"```$global:CleanNumber = 0;if (```$null -ne (Get-Module PSReadline)) {Set-PSReadLineKeyHandler -Chord Tab -Function MenuComplete;Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward;Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward;Set-PSReadLineKeyHandler -Chord 'Ctrl+W' -Function BackwardKillWord;Set-PSReadLineKeyHandler -Chord 'Ctrl+z' -Function MenuComplete;Set-PSReadLineKeyHandler -Chord 'Ctrl+D' -Function KillWord};" if ($ImportModule) { if (($modName = (Get-ChildItem .\BuildOutput -Directory).BaseName)) { $modPath = '.\BuildOutput\' + $modName $verboseMessage += "`n Module : $modName" $command += "Import-Module '$modPath' -Verbose:```$false;Get-Module $modName;" } } $newline = '`n' $command += "function global:prompt {```$global:CleanNumber++;'[CLN#' + ```$global:CleanNumber + '] [' + [Math]::Round((Get-History -Count 1).Duration.TotalMilliseconds,0) + 'ms] ' + (Get-Location).Path.Replace(```$env:Home,'~') + '$newline[PS ' + ```$PSVersionTable.PSVersion.ToString() + ']>> '}`"" $verboseMessage += "`n Command : $command" Write-Verbose $verboseMessage Invoke-Expression $command } } Export-ModuleMember -Function 'Enter-CleanEnvironment' function Format-Syntax { <# .SYNOPSIS Formats a command's syntax in an easy-to-read view. .DESCRIPTION Formats a command's syntax in an easy-to-read view. .PARAMETER Command The command to get the syntax of. .EXAMPLE Format-Syntax Get-Process Gets the formatted syntax by parameter set for Get-Process .EXAMPLE syntax Get-Process Same as Example 1, but uses the alias 'syntax' instead. #> [CmdletBinding()] param ( [Parameter(Mandatory,Position = 0)] [String[]] $Command ) Process { foreach ($comm in $Command) { Write-Verbose "Getting formatted syntax for command: $comm" $check = Get-Command -Name $comm $params = @{ Name = if ($check.CommandType -eq 'Alias') { Get-Command -Name $check.Definition } else { $comm } Syntax = $true } (Get-Command @params) -replace '(\s(?=\[)|\s(?=-))', "`r`n " } } } Register-ArgumentCompleter -CommandName Format-Syntax -ParameterName Command -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) (Get-Command "$wordToComplete*").Name | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Format-Syntax' function Get-Definition { <# .SYNOPSIS Convenience function to easily get the defition of a function .DESCRIPTION Convenience function to easily get the defition of a function .PARAMETER Command The command or function to get the definition for .EXAMPLE Get-Definition Open-Code .EXAMPLE def Open-Code Uses the shorter alias to get the definition of the Open-Code function #> [CmdletBinding()] Param( [parameter(Mandatory,Position = 0)] [String] $Command ) Process { try { Write-Verbose "Getting definition for command: $Command" $Definition = (Get-Command $Command -ErrorAction Stop).Definition "function $Command {$Definition}" } catch { throw } } } Register-ArgumentCompleter -CommandName Get-Definition -ParameterName Command -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) (Get-Command "$wordToComplete*").Name | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-Definition' function Get-Gist { <# .SYNOPSIS Gets a GitHub Gist's contents using the public API .DESCRIPTION Gets a GitHub Gist's contents using the public API .PARAMETER Id The ID of the Gist to get .PARAMETER File The specific file from the Gist to get. If excluded, gets all of the files as an array of objects. .PARAMETER Sha The SHA of the specific Gist to get, if desired. .PARAMETER Metadata Any additional metadata you want to include on the resulting object, e.g. for identifying what the Gist is, add notes, etc. .PARAMETER Invoke If $true, invokes the Gist contents. If the Gist contains any PowerShell functions, it will adjust the scope to Global before invoking so the function remains available in the session after Get-Gist finishes. Useful for loading functions directly from a Gist. .EXAMPLE Get-Gist -Id f784228937183a1cf8105351872d2f8a -Invoke Gets the Update-Release and Test-GetGist functions from the following Gist URL and loads them into the current session for subsequent use: https://gist.github.com/scrthq/f784228937183a1cf8105351872d2f8a #> [CmdletBinding()] Param ( [parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,Position = 0)] [String] $Id, [parameter(ValueFromPipelineByPropertyName)] [Alias('Files')] [String[]] $File, [parameter(ValueFromPipelineByPropertyName)] [String] $Sha, [parameter(ValueFromPipelineByPropertyName)] [Object] $Metadata, [parameter()] [Switch] $Invoke ) Process { $Uri = [System.Collections.Generic.List[string]]@( 'https://api.github.com' '/gists/' $PSBoundParameters['Id'] ) if ($PSBoundParameters.ContainsKey('Sha')) { $Uri.Add("/$($PSBoundParameters['Sha'])") Write-Verbose "[$($PSBoundParameters['Id'])] Getting gist info @ SHA '$($PSBoundParameters['Sha'])'" } else { Write-Verbose "[$($PSBoundParameters['Id'])] Getting gist info" } $gistInfo = Invoke-RestMethod -Uri ([Uri](-join $Uri)) -Verbose:$false $fileNames = if ($PSBoundParameters.ContainsKey('File')) { $PSBoundParameters['File'] } else { $gistInfo.files.PSObject.Properties.Name } foreach ($fileName in $fileNames) { Write-Verbose "[$fileName] Getting gist file content" $fileInfo = $gistInfo.files.$fileName $content = if ($fileInfo.truncated) { (Invoke-WebRequest -Uri ([Uri]$fileInfo.raw_url)).Content } else { $fileInfo.content } $lines = ($content -split "`n").Count if ($Invoke) { Write-Verbose "[$fileName] Parsing gist file content ($lines lines)" $noScopePattern = '^function\s+(?<Name>[\w+_-]{1,})\s+\{' $globalScopePattern = '^function\s+global\:' $noScope = [RegEx]::Matches($content, $noScopePattern, "Multiline, IgnoreCase") $globalScope = [RegEx]::Matches($content,$globalScopePattern,"Multiline, IgnoreCase") if ($noScope.Count -ge $globalScope.Count) { foreach ($match in $noScope) { $fullValue = ($match.Groups | Where-Object { $_.Name -eq 0 }).Value $funcName = ($match.Groups | Where-Object { $_.Name -eq 'Name' }).Value Write-Verbose "[$fileName::$funcName] Updating function to global scope to ensure it imports correctly." $content = $content.Replace($fullValue, "function global:$funcName {") } } Write-Verbose "[$fileName] Invoking gist file content" $ExecutionContext.InvokeCommand.InvokeScript( $false, ([scriptblock]::Create($content)), $null, $null ) } [PSCustomObject]@{ File = $fileName Sha = $Sha Lines = $lines Metadata = $Metadata Content = $content -join "`n" } } } } Export-ModuleMember -Function 'Get-Gist' function Get-LongPath { <# .SYNOPSIS Expands a short-alias from the GitPathMap to the full path .DESCRIPTION Expands a short-alias from the GitPathMap to the full path .PARAMETER Path The short path to expand .PARAMETER Subpaths Any subpaths to join to the main path before resolving. .EXAMPLE Get-LongPath MyWorkRepo Gets the full path to MyWorkRepo .EXAMPLE path MyWorkRepo Same as Example 1, but uses the short-alias 'path' instead. #> [CmdletBinding(DefaultParameterSetName = 'Path')] Param ( [parameter(Position = 0,ParameterSetName = 'Path')] [String] $Path = $PWD.Path, [parameter(ValueFromRemainingArguments,Position = 1,ParameterSetName = 'Path')] [String[]] $Subpaths ) DynamicParam { if ($global:PSProfile.GitPathMap.ContainsKey('chef-repo')) { $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute $ParamAttrib.Mandatory = $true $ParamAttrib.ParameterSetName = 'Cookbook' $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $AttribColl.Add($ParamAttrib) $set = (Get-ChildItem (Join-Path $global:PSProfile.GitPathMap['chef-repo'] 'cookbooks') -Directory).Name $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($set))) $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute('c'))) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Cookbook', [string], $AttribColl) $RuntimeParamDic.Add('Cookbook', $RuntimeParam) } return $RuntimeParamDic } Begin { if (-not $PSBoundParameters.ContainsKey('Path')) { $PSBoundParameters['Path'] = $PWD.Path } } Process { $target = switch ($PSCmdlet.ParameterSetName) { Path { if ($PSBoundParameters['Path'] -eq '.') { $PWD.Path } elseif ($null -ne $global:PSProfile.GitPathMap.Keys) { if ($global:PSProfile.GitPathMap.ContainsKey($PSBoundParameters['Path'])) { $global:PSProfile.GitPathMap[$PSBoundParameters['Path']] } else { (Resolve-Path $PSBoundParameters['Path']).Path } } else { (Resolve-Path $PSBoundParameters['Path']).Path } } Cookbook { [System.IO.Path]::Combine($global:PSProfile.GitPathMap['chef-repo'],'cookbooks',$PSBoundParameters['Cookbook']) } } if ($Subpaths) { $target = Join-Path $target ($Subpaths -join [System.IO.Path]::DirectorySeparatorChar) } Write-Verbose "Resolved long path: $target" $target } } Register-ArgumentCompleter -CommandName Get-LongPath -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly } Export-ModuleMember -Function 'Get-LongPath' function Install-LatestModule { <# .SYNOPSIS A helper function to uninstall any existing versions of the target module before installing the latest one. .DESCRIPTION A helper function to uninstall any existing versions of the target module before installing the latest one. Defaults to CurrentUser scope when installing the latest module version from the desired repository. .PARAMETER Name The name of the module to install the latest version of .PARAMETER Repository The PowerShell repository to install the latest module from. Defaults to the PowerShell Gallery. .PARAMETER ConfirmNotImported If $true, safeguards module removal if the module you are trying to update is currently imported by throwing a terminating error. .EXAMPLE Install-LatestModule PSProfile #> [CmdletBinding()] Param( [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [String[]] $Name, [Parameter()] [String] $Repository = 'PSGallery', [Parameter()] [Switch] $ConfirmNotImported ) Process { foreach ($module in $Name) { if ($ConfirmNotImported -and (Get-Module $module)) { throw "$module cannot be loaded if trying to install!" } else { try { Write-Verbose "Uninstalling all version of module: $module" Get-Module $module -ListAvailable | Uninstall-Module Write-Verbose "Installing latest module version from PowerShell Gallery" Install-Module $module -Repository $Repository -Scope CurrentUser -AllowClobber -SkipPublisherCheck -AcceptLicense } catch { throw } } } } } Register-ArgumentCompleter -CommandName Install-LatestModule -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) (Get-Module "$wordToComplete*" -ListAvailable).Name | Sort-Object | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Install-LatestModule' function Open-Code { <# .SYNOPSIS A drop-in replacement for the Visual Studio Code CLI `code`. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. .DESCRIPTION A drop-in replacement for the Visual Studio Code CLI `code`. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. .PARAMETER Path The path of the file or folder to open with Code. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. .PARAMETER Cookbook If you are using Chef and have your chef-repo folder in your GitPaths, this will allow you to specify a cookbook path to open from the Cookbooks subfolder. .PARAMETER AddToWorkspace If $true, adds the folder to the current Code workspace. .PARAMETER InputObject Pipeline input to display as a temporary file in Code. Temp files are automatically cleaned up after the file is closed in Code. No need to add the `-` after `code` to specify that pipeline input is expected. .PARAMETER Language The language or extension of the temporary file created from the pipeline input. This allows specifying a file type like 'powershell' or 'csv' or an extension like 'ps1', enabling opening of the temp file with the editor file language already set correctly. .PARAMETER Wait If $true, waits for the file to be closed in Code before returning to the prompt. If $false, opens the file using a background job to allow immediately returning to the prompt. Defaults to $false. .PARAMETER ArgumentList Any additional arguments to be passed directly to the Code CLI command, e.g. `Open-Code --list-extensions` or `code --list-extensions` will still work the same as expected. .EXAMPLE Get-Process | ConvertTo-Csv | Open-Code -Language csv Gets the current running processes, converts to CSV format and opens it in Code via background job as a CSV. Easy Out-GridView! .EXAMPLE def Update-PSProfileSetting | code -l ps1 Using shorter aliases, gets the current function definition of the Update-PSProfileSetting function and opens it in Code as a PowerShell file to take advantage of immediate syntax highlighting. #> [CmdletBinding(DefaultParameterSetName = 'Path')] Param ( [parameter(Mandatory,Position = 0,ParameterSetName = 'Path')] [parameter(Position = 0,ParameterSetName = 'InputObject')] [AllowEmptyString()] [AllowNull()] [String] $Path, [parameter(ParameterSetName = 'Path')] [parameter(ParameterSetName = 'Cookbook')] [Alias('add','a')] [Switch] $AddToWorkspace, [parameter(ValueFromPipeline,ParameterSetName = 'InputObject')] [Object] $InputObject, [parameter(ParameterSetName = 'InputObject')] [Alias('l','lang','Extension')] [String] $Language = 'txt', [parameter(ParameterSetName = 'InputObject')] [Alias('w')] [Switch] $Wait, [parameter(ValueFromRemainingArguments)] [String[]] $ArgumentList ) DynamicParam { if ($global:PSProfile.GitPathMap.ContainsKey('chef-repo')) { $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute $ParamAttrib.Mandatory = $true $ParamAttrib.ParameterSetName = 'Cookbook' $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $AttribColl.Add($ParamAttrib) $set = (Get-ChildItem (Join-Path $global:PSProfile.GitPathMap['chef-repo'] 'cookbooks') -Directory).Name $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($set))) $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute('c'))) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Cookbook', [string], $AttribColl) $RuntimeParamDic.Add('Cookbook', $RuntimeParam) } return $RuntimeParamDic } Begin { if ($PSCmdlet.ParameterSetName -eq 'InputObject') { $collection = New-Object System.Collections.Generic.List[object] $codeArgs = New-Object System.Collections.Generic.List[string] $extDict = @{ txt = 'txt' powershell = 'ps1' csv = 'csv' sql = 'sql' xml = 'xml' json = 'json' yml = 'yml' csharp = 'cs' fsharp = 'fs' ruby = 'rb' html = 'html' css = 'css' go = 'go' jsonc = 'jsonc' javascript = 'js' typescript = 'ts' less = 'less' log = 'log' python = 'py' razor = 'cshtml' markdown = 'md' } } } Process { $code = (Get-Command code -All | Where-Object { $_.CommandType -notin @('Function','Alias') })[0].Source if ($PSCmdlet.ParameterSetName -eq 'InputObject') { $collection.Add($InputObject) if ($PSBoundParameters.ContainsKey('Path')) { $codeArgs.Add($PSBoundParameters['Path']) } if ($PSBoundParameters.ContainsKey('ArgumentList')) { $PSBoundParameters['ArgumentList'] | ForEach-Object { $codeArgs.Add($_) } } } else { $target = switch ($PSCmdlet.ParameterSetName) { Path { if ([String]::IsNullOrEmpty($PSBoundParameters['Path']) -or [String]::IsNullOrWhiteSpace($PSBoundParameters['Path'])) { $null } elseif ($PSBoundParameters['Path'] -eq '.') { $PWD.Path } elseif ($null -ne $global:PSProfile.GitPathMap.Keys) { if ($global:PSProfile.GitPathMap.ContainsKey($PSBoundParameters['Path'])) { $global:PSProfile.GitPathMap[$PSBoundParameters['Path']] } else { $PSBoundParameters['Path'] } } else { $PSBoundParameters['Path'] } } Cookbook { [System.IO.Path]::Combine($global:PSProfile.GitPathMap['chef-repo'],'cookbooks',$PSBoundParameters['Cookbook']) } } <# if ($AddToWorkspace) { Write-Verbose "Running command: code --add $($PSBoundParameters[$PSCmdlet.ParameterSetName]) $ArgumentList" & $code --add $target $ArgumentList } else { Write-Verbose "Running command: code $($PSBoundParameters[$PSCmdlet.ParameterSetName]) $ArgumentList" & $code $target $ArgumentList } #> $cmd = @() if ($AddToWorkspace) { $cmd += '--add' } if ($target) { $cmd += $target } if ($ArgumentList) { $ArgumentList | ForEach-Object { $cmd += $_ } } Write-Verbose "Running command: code $cmd" & $code $cmd } } End { if ($PSCmdlet.ParameterSetName -eq 'InputObject') { $ext = if ($extDict.ContainsKey($Language)) { $extDict[$Language] } else { $Language } $in = @{ StdIn = $collection Wait = $Wait -or $Path -eq '--wait' -or ($ArgumentList -join ' ') -match '(\-\-wait|\-w)' CodeArgs = $codeArgs TmpFile = [System.IO.Path]::Combine(([System.IO.Path]::GetTempPath()),"code-stdin-$(-join ((97..(97+25) | ForEach-Object {[char]$_}) | Get-Random -Count 3)).$ext") } $handler = { Param( [hashtable] $in ) try { $code = (Get-Command code -All | Where-Object { $_.CommandType -notin @('Function','Alias') })[0].Source $in.StdIn | Set-Content $in.TmpFile -Force & $code $in.TmpFile --wait $(($in.CodeArgs | Where-Object {$_ -ne '--wait'}) -join ' ') } catch { throw } finally { if (Test-Path $in.TmpFile -ErrorAction SilentlyContinue) { Remove-Item $in.TmpFile -Force } } } if (-not $in.Wait) { Write-Verbose "Piping input to Code as a Runspace job: `$in | Start-RSJob {code -}" $ind = [int]((Get-RSJob).Count) + 1 $null = Start-RSJob -Name "_PSProfile_OpenCode_$ind" -ScriptBlock $handler -ArgumentList $in } else { Write-Verbose "Piping input to Code and waiting for file to exit: `$in | code -" .$handler($in) } } } } Register-ArgumentCompleter -CommandName Open-Code -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly } Register-ArgumentCompleter -CommandName Open-Code -ParameterName Language -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) @('txt','powershell','csv','sql','xml','json','yml','csharp','fsharp','ruby','html','css','go','jsonc','javascript','typescript','less','log','python','razor','markdown') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Register-ArgumentCompleter -CommandName Open-Code -ParameterName ArgumentList -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) @('--help','--version','--new-window','--reuse-window','--goto','--diff','--wait','--locale','--install-extension','--uninstall-extension','--disable-extensions','--list-extensions','--show-versions','--enable-proposed-api','--extensions-dir','--user-data-dir','--status','--performance','--disable-gpu','--verbose','--prof-startup','--upload-logs','--add') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Open-Code' function Open-Item { <# .SYNOPSIS Opens the item specified using Invoke-Item. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. .DESCRIPTION Opens the item specified using Invoke-Item. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. .PARAMETER Path The path you would like to open. Supports anything that Invoke-Item normally supports, i.e. files, folders, URIs. .PARAMETER Cookbook If you are using Chef and have your chef-repo folder in your GitPaths, this will allow you to specify a cookbook path to open from the Cookbooks subfolder. .EXAMPLE Open-Item Opens the current path in Explorer/Finder/etc. .EXAMPLE open Uses the shorter alias to open the current path .EXAMPLE open MyWorkRepo Opens the folder for the Git Repo 'MyWorkRepo' in Explorer/Finder/etc. #> [CmdletBinding(DefaultParameterSetName = 'Path')] Param ( [parameter(Position = 0,ParameterSetName = 'Path')] [String] $Path = $PWD.Path ) DynamicParam { if ($global:PSProfile.GitPathMap.ContainsKey('chef-repo')) { $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute $ParamAttrib.Mandatory = $true $ParamAttrib.ParameterSetName = 'Cookbook' $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $AttribColl.Add($ParamAttrib) $set = (Get-ChildItem (Join-Path $global:PSProfile.GitPathMap['chef-repo'] 'cookbooks') -Directory).Name $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($set))) $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute('c'))) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Cookbook', [string], $AttribColl) $RuntimeParamDic.Add('Cookbook', $RuntimeParam) } return $RuntimeParamDic } Begin { if (-not $PSBoundParameters.ContainsKey('Path')) { $PSBoundParameters['Path'] = $PWD.Path } } Process { $target = switch ($PSCmdlet.ParameterSetName) { Path { if ($PSBoundParameters['Path'] -eq '.') { $PWD.Path } elseif ($null -ne $global:PSProfile.GitPathMap.Keys) { if ($global:PSProfile.GitPathMap.ContainsKey($PSBoundParameters['Path'])) { $global:PSProfile.GitPathMap[$PSBoundParameters['Path']] } else { $PSBoundParameters['Path'] } } else { $PSBoundParameters['Path'] } } Cookbook { [System.IO.Path]::Combine($global:PSProfile.GitPathMap['chef-repo'],'cookbooks',$PSBoundParameters['Cookbook']) } } Write-Verbose "Running command: Invoke-Item $($PSBoundParameters[$PSCmdlet.ParameterSetName])" Invoke-Item $target } } Register-ArgumentCompleter -CommandName Open-Item -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly } Export-ModuleMember -Function 'Open-Item' function Pop-Path { <# .SYNOPSIS Pops your location back the path you Push-Path'd from. .DESCRIPTION Pops your location back the path you Push-Path'd from. .EXAMPLE Pop-Path #> [CmdletBinding()] Param () Process { Write-Verbose "Popping back to previous location" Pop-Location } } Export-ModuleMember -Function 'Pop-Path' function Push-Path { <# .SYNOPSIS Pushes your current location to the path specified. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. Use Pop-Path to return to the location pushed from, as locations pushed from this function are within the module scope. .DESCRIPTION Pushes your current location to the path specified. Allows tab-completion of GitPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. Use Pop-Path to return to the location pushed from, as locations pushed from this function are within the module scope. .PARAMETER Path The path you would like to push your location to. .PARAMETER Cookbook If you are using Chef and have your chef-repo folder in your GitPaths, this will allow you to specify a cookbook path to push your location to from the Cookbooks subfolder. .EXAMPLE Push-Path MyWorkRepo Changes your current directory to your Git Repo named 'MyWorkRepo'. .EXAMPLE push MyWorkRepo Same as the first example but using the shorter alias. #> [CmdletBinding()] Param( [parameter(Mandatory,Position = 0,ParameterSetName = 'Path')] [String] $Path ) DynamicParam { $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary if ($global:PSProfile.GitPathMap.ContainsKey('chef-repo')) { $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute $ParamAttrib.Mandatory = $true $ParamAttrib.ParameterSetName = 'Cookbook' $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $AttribColl.Add($ParamAttrib) $set = (Get-ChildItem (Join-Path $global:PSProfile.GitPathMap['chef-repo'] 'cookbooks') -Directory).Name $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($set))) $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute('c'))) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Cookbook', [string], $AttribColl) $RuntimeParamDic.Add('Cookbook', $RuntimeParam) } return $RuntimeParamDic } Process { $target = switch ($PSCmdlet.ParameterSetName) { Path { if ($global:PSProfile.GitPathMap.ContainsKey($PSBoundParameters['Path'])) { $global:PSProfile.GitPathMap[$PSBoundParameters['Path']] } else { $PSBoundParameters['Path'] } } Cookbook { [System.IO.Path]::Combine($global:PSProfile.GitPathMap['chef-repo'],'cookbooks',$PSBoundParameters['Cookbook']) } } Write-Verbose "Pushing location to: $($target.Replace($env:HOME,'~'))" Push-Location $target } } Register-ArgumentCompleter -CommandName Push-Path -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly } Export-ModuleMember -Function 'Push-Path' function Start-BuildScript { <# .SYNOPSIS For those using the typical build.ps1 build scripts for PowerShell projects, this will allow invoking the build script quickly from wherever folder you are currently in using a child process. .DESCRIPTION For those using the typical build.ps1 build scripts for PowerShell projects, this will allow invoking the build script quickly from wherever folder you are currently in using a child process. Any projects in the ProjectPaths list that were discovered during PSProfile load and have a build.ps1 file will be able to be tab-completed for convenience. Temporarily sets the path to the build folder, invokes the build.ps1 file, then returns to the original path that it was invoked from. .PARAMETER Project The path of the project to build. Allows tab-completion of PSBuildPath aliases if ProjectPaths are filled out with PSProfile that expand to the full path when invoked. You can also pass the path to a folder containing a build.ps1 script, or a full path to another script entirely. .PARAMETER Task The list of Tasks to specify to the Build script. .PARAMETER Engine The engine to open the clean environment with between powershell, pwsh, and pwsh-preview. Defaults to the current engine the clean environment is opened from. .PARAMETER NoExit If $true, does not exit the child process once build.ps1 has completed and imports the built module in BuildOutput (if present to allow testing of the built project in a clean environment. .EXAMPLE Start-BuildScript MyModule -NoExit Changes directories to the repo root of MyModule, invokes build.ps1, imports the compiled module in a clean child process and stops before exiting to allow testing of the newly compiled module. .EXAMPLE bld MyModule -ne Same experience as Example 1 but uses the shorter alias 'bld' to call. Also uses the parameter alias `-ne` instead of `-NoExit` #> [CmdletBinding(PositionalBinding = $false)] Param ( [Parameter(Position = 0)] [Alias('p')] [String] $Project, [Parameter(Position = 1)] [Alias('t')] [String[]] $Task, [Parameter(Position = 2)] [ValidateSet('powershell','pwsh','pwsh-preview')] [Alias('e')] [String] $Engine = $(if ($PSVersionTable.PSVersion.ToString() -match 'preview') { 'pwsh-preview' } elseif ($PSVersionTable.PSVersion.Major -ge 6) { 'pwsh' } else { 'powershell' }), [parameter()] [Alias('ne','noe')] [Switch] $NoExit ) DynamicParam { $bldFolder = if ([String]::IsNullOrEmpty($PSBoundParameters['Project']) -or $PSBoundParameters['Project'] -eq '.') { $PWD.Path } elseif ($Global:PSProfile.PSBuildPathMap.ContainsKey($PSBoundParameters['Project'])) { Get-LongPath -Path $PSBoundParameters['Project'] } else { (Resolve-Path $PSBoundParameters['Project']).Path } $bldFile = if ($bldFolder -like '*.ps1') { $bldFolder } else { Join-Path $bldFolder "build.ps1" } Copy-Parameters -From $bldFile -Exclude Project,Task,Engine,NoExit } Process { if (-not $PSBoundParameters.ContainsKey('Project')) { $PSBoundParameters['Project'] = '.' } $parent = switch ($PSBoundParameters['Project']) { '.' { $PWD.Path } default { $global:PSProfile.PSBuildPathMap[$PSBoundParameters['Project']] } } $command = "$Engine -NoProfile -C `"" $command += "Set-Location '$parent'; . .\build.ps1" $PSBoundParameters.Keys | Where-Object {$_ -notin @('Project','Engine','NoExit','Debug','ErrorAction','ErrorVariable','InformationAction','InformationVariable','OutBuffer','OutVariable','PipelineVariable','WarningAction','WarningVariable','Verbose','Confirm','WhatIf')} | ForEach-Object { if ($PSBoundParameters[$_].ToString() -in @('True','False')) { $command += " -$($_):```$$($PSBoundParameters[$_].ToString())" } else { $command += " -$($_) '$($PSBoundParameters[$_] -join "','")'" } } $command += '"' Write-Verbose "Invoking expression: $command" Invoke-Expression $command if ($NoExit) { Push-Location $parent Enter-CleanEnvironment -Engine $Engine -ImportModule Pop-Location } } } Register-ArgumentCompleter -CommandName Start-BuildScript -ParameterName Project -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments -WordToComplete "PSBuildPathMap.$wordToComplete" -FinalKeyOnly } Register-ArgumentCompleter -CommandName Start-BuildScript -ParameterName Task -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $bldFolder = if ([String]::IsNullOrEmpty($fakeBoundParameter.Project) -or $fakeBoundParameter.Project -eq '.') { $PWD.Path } elseif ($Global:PSProfile.PSBuildPathMap.ContainsKey($fakeBoundParameter.Project)) { Get-LongPath -Path $fakeBoundParameter.Project } else { (Resolve-Path $fakeBoundParameter.Project).Path } $bldFile = if ($bldFolder -like '*.ps1') { $bldFolder } else { Join-Path $bldFolder "build.ps1" } $set = if (Test-Path $bldFile) { ((([System.Management.Automation.Language.Parser]::ParseFile( $bldFile, [ref]$null, [ref]$null )).ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'Task' }).Attributes | Where-Object { $_.TypeName.Name -eq 'ValidateSet' }).PositionalArguments.Value } else { @('Clean','Build','Import','Test','Deploy') } $set | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Start-BuildScript' function Test-RegEx { <# .SYNOPSIS Tests a RegEx pattern against a string and returns the results. .DESCRIPTION Tests a RegEx pattern against a string and returns the results. .PARAMETER Pattern The RegEx pattern to test against the string. .PARAMETER String The string to test. .EXAMPLE Test-RegEx -Pattern '^\w+' -String 'no spaces',' spaces in front' Matched Pattern Matches String ------- ------- ------- ------ True ^\w+ {no} no spaces False ^\w+ spaces in front #> [CmdletBinding()] Param ( [parameter(Mandatory,Position = 0)] [RegEx] $Pattern, [parameter(Mandatory,Position = 1,ValueFromPipeline)] [String[]] $String ) Process { foreach ($S in $String) { Write-Verbose "Testing RegEx pattern '$Pattern' against string '$S'" $Matches = $null [PSCustomObject][Ordered]@{ Matched = $($S -match $Pattern) Pattern = $Pattern Matches = $Matches.Values String = $S } } } } Export-ModuleMember -Function 'Test-RegEx' function Add-PSProfileProjectPath { <# .SYNOPSIS Adds a ProjectPath to your PSProfile to find Git project folders under during PSProfile refresh. These will be available via tab-completion .DESCRIPTION Adds a ProjectPath to your PSProfile to find Git project folders under during PSProfile refresh. .PARAMETER Path The path of the folder to add to your $PSProfile.ProjectPaths. This path should contain Git repo folders underneath it. .PARAMETER NoRefresh If $true, skips refreshing your PSProfile after updating. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfileProjectPath -Path ~\GitRepos -Save Adds the folder ~\GitRepos to $PSProfile.ProjectPaths and saves the configuration after updating. .EXAMPLE Add-PSProfileProjectPath C:\Git -Verbose Adds the path C:\Git to your $PSProfile.ProjectPaths, refreshes your PathDict but does not save. Call Save-PSProfile after if satisfied with the results. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({if ((Get-Item $_).PSIsContainer){$true}else{throw "$_ is not a folder! Please add only folders to this PSProfile property. If you would like to add a script, use Add-PSProfileScriptPath instead."}})] [Alias('FullName')] [String[]] $Path, [Parameter()] [Switch] $NoRefresh, [Parameter()] [Switch] $Save ) Process { foreach ($p in $Path) { $fP = (Resolve-Path $p).Path if ($Global:PSProfile.ProjectPaths -notcontains $fP) { Write-Verbose "Adding ProjectPath to PSProfile: $fP" $Global:PSProfile.ProjectPaths += $fP } else { Write-Verbose "ProjectPath already in PSProfile: $fP" } } if ($Save) { Save-PSProfile } if (-not $NoRefresh) { Update-PSProfileConfig } } } Export-ModuleMember -Function 'Add-PSProfileProjectPath' function Get-PSProfileProjectPath { <# .SYNOPSIS Gets a project path from $PSProfile.ProjectPaths. .DESCRIPTION Gets a project path from $PSProfile.ProjectPaths. .PARAMETER Path The project path to get from $PSProfile.ProjectPaths. .EXAMPLE Get-PSProfileProjectPath -Path E:\Git Gets the path 'E:\Git' from $PSProfile.ProjectPaths .EXAMPLE Get-PSProfileProjectPath Gets the list of project paths from $PSProfile.ProjectPaths #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Path ) Process { if ($PSBoundParameters.ContainsKey('Path')) { Write-Verbose "Getting project path '$Path' from `$PSProfile.ProjectPaths" $Global:PSProfile.ProjectPaths | Where-Object {$_ -in $Path} } else { Write-Verbose "Getting all project paths from `$PSProfile.ProjectPaths" $Global:PSProfile.ProjectPaths } } } Register-ArgumentCompleter -CommandName Get-PSProfileProjectPath -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.ProjectPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileProjectPath' function Remove-PSProfileProjectPath { <# .SYNOPSIS Removes a Project Path from $PSProfile.ProjectPaths. .DESCRIPTION Removes a Project Path from $PSProfile.ProjectPaths. .PARAMETER Path The path to remove from $PSProfile.ProjectPaths. .PARAMETER NoRefresh If $true, skips refreshing your PSProfile after updating project paths. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileProjectPath -Name E:\Git -Save Removes the path 'E:\Git' from $PSProfile.ProjectPaths then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String] $Path, [Parameter()] [Switch] $NoRefresh, [Parameter()] [Switch] $Save ) Process { if ($PSCmdlet.ShouldProcess("Removing '$Path' from `$PSProfile.ProjectPaths")) { Write-Verbose "Removing '$Path' from `$PSProfile.ProjectPaths" $Global:PSProfile.ProjectPaths = $Global:PSProfile.ProjectPaths | Where-Object {$_ -notin @($Path,(Resolve-Path $Path).Path)} if (-not $NoRefresh) { Update-PSProfileConfig } if ($Save) { Save-PSProfile } } } } Register-ArgumentCompleter -CommandName Remove-PSProfileProjectPath -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.ProjectPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileProjectPath' function Add-PSProfilePrompt { <# .SYNOPSIS Saves the Content to $PSProfile.Prompts as the Name provided for recall later. .DESCRIPTION Saves the Content to $PSProfile.Prompts as the Name provided for recall later. .PARAMETER Name The Name to save the prompt as. .PARAMETER Content The prompt content itself. .PARAMETER SetAsDefault If $true, sets the prompt as default by updated $PSProfile.Settings.DefaultPrompt. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfilePrompt -Name Demo -Content '"PS > "' Saves a prompt named 'Demo' with the provided content. #> [CmdletBinding()] Param( [Parameter(Position = 0)] [String] $Name = $global:PSProfile.Settings.DefaultPrompt, [Parameter()] [object] $Content, [Parameter()] [switch] $SetAsDefault, [Parameter()] [switch] $Save ) Process { if ($null -eq $Name) { throw "No value set for the Name parameter or resolved from PSProfile!" } else { Write-Verbose "Saving prompt '$Name' to `$PSProfile.Prompts" $tempContent = if ($Content) { $Content.ToString() } else { Get-PSProfilePrompt -Raw } $cleanContent = (($tempContent -split "[\r\n]" | Where-Object {$_}) -join "`n").Trim() $global:PSProfile.Prompts[$Name] = $cleanContent if ($SetAsDefault) { $global:PSProfile.Settings.DefaultPrompt = $Name } if ($Save) { Save-PSProfile } } } } Export-ModuleMember -Function 'Add-PSProfilePrompt' function Edit-PSProfilePrompt { <# .SYNOPSIS Enables editing the prompt from the desired editor. Once temporary file is saved, the prompt is updated in $PSProfile.Prompts. .DESCRIPTION Enables editing the prompt from the desired editor. Once temporary file is saved, the prompt is updated in $PSProfile.Prompts. .PARAMETER Save If $true, saves prompt back to your PSProfile after updating. .EXAMPLE Edit-PSProfilePrompt Opens the current prompt as a temporary file in Visual Studio Code to edit. Once the file is saved and closed, the active prompt is updated with the changes. #> [CmdletBinding()] Param( [Parameter()] [Switch] $Save ) Process { $in = @{ StdIn = Get-PSProfilePrompt -Global TmpFile = [System.IO.Path]::Combine(([System.IO.Path]::GetTempPath()),"ps-prompt-$(-join ((97..(97+25)|%{[char]$_}) | Get-Random -Count 3)).ps1") } $handler = { Param( [hashtable] $in ) try { $code = (Get-Command code -All | Where-Object { $_.CommandType -notin @('Function','Alias') })[0].Source $in.StdIn | Set-Content $in.TmpFile -Force & $code $in.TmpFile --wait } catch { throw } finally { if (Test-Path $in.TmpFile -ErrorAction SilentlyContinue) { Invoke-Expression ([System.IO.File]::ReadAllText($in.TmpFile)) Remove-Item $in.TmpFile -Force } } } Write-Verbose "Opening prompt in VS Code" .$handler($in) if ($Save) { Add-PSProfilePrompt -Save } } } Export-ModuleMember -Function 'Edit-PSProfilePrompt' function Get-PSProfilePrompt { <# .SYNOPSIS Gets the current prompt's definition as a string. Useful for inspection of the prompt in use. If PSScriptAnalyzer is installed, formats the prompt for readability before returning the prompt function string. .DESCRIPTION Gets the current prompt's definition as a string. Useful for inspection of the prompt in use. If PSScriptAnalyzer is installed, formats the prompt for readability before returning the prompt function string. .PARAMETER Name The Name of the prompt from $PSProfile.Prompts to get. If excluded, gets the current prompt. .PARAMETER Global If $true, adds the global scope to the returned prompt, e.g. `function global:prompt` .PARAMETER NoPSSA If $true, does not use PowerShell Script Analyzer's Invoke-Formatter to format the resulting prompt definition. .PARAMETER Raw If $true, returns only the prompt definition and does not add the `function prompt {...}` enclosure. .EXAMPLE Get-PSProfilePrompt #> [CmdletBinding()] Param( [Parameter(Position = 0)] [String] $Name, [Parameter()] [Switch] $Global, [Parameter()] [Switch] $NoPSSA, [Parameter()] [Switch] $Raw ) Process { $pContents = if ($PSBoundParameters.ContainsKey('Name')) { if ($Global:PSProfile.Prompts.ContainsKey($Name)) { $Global:PSProfile.Prompts[$Name] } else { return } } else { $function:prompt } $pssa = if ($NoPSSA -or $null -eq (Get-Module PSScriptAnalyzer* -ListAvailable)) { $false } else { $true Import-Module PSScriptAnalyzer -Verbose:$false } Write-Verbose "Getting current prompt" $i = 0 $lws = $null $g = if ($Global) { 'global:prompt' } else { 'prompt' } $header = if ($Raw) { '' } else { "function $g {`n" } $content = $pContents -split "`n" | ForEach-Object { if (-not [String]::IsNullOrWhiteSpace($_)) { if ($null -eq $lws) { $lws = if ($_ -match '^\s+') { $Matches.Values[0].Length } else { $null } } $_ -replace "^\s{0,$lws}",' ' "`n" } elseif ($i) { $_ "`n" } $i++ } $footer = if ($Raw) { '' } else { "}" } $p = ((@($header,(($content | Where-Object {"$_".Trim()}) -join "`n"),$footer) -split "[\r\n]") | Where-Object {"$_".Trim()}) -join "`n" if (-not $NoPSSA -and $pssa) { Write-Verbose "Formatting prompt with Invoke-Formatter" Invoke-Formatter $p -Verbose:$false } else { $p } } } Register-ArgumentCompleter -CommandName Get-PSProfilePrompt -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments -WordToComplete "Prompts.$wordToComplete" -FinalKeyOnly } Export-ModuleMember -Function 'Get-PSProfilePrompt' function Remove-PSProfilePrompt { <# .SYNOPSIS Removes a Prompt from $PSProfile.Prompts. .DESCRIPTION Removes a Prompt from $PSProfile.Prompts. .PARAMETER Name The name of the prompt to remove from $PSProfile.Prompts. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfilePrompt -Name Demo -Save Removes the Prompt named 'Demo' from $PSProfile.Prompts then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0)] [String] $Name, [Parameter()] [Switch] $Save ) Process { if ($PSCmdlet.ShouldProcess("Removing prompt '$Name' from `$PSProfile.Prompts")) { Write-Verbose "Removing prompt '$Name' from `$PSProfile.Prompts" if ($Global:PSProfile.Prompts.ContainsKey($Name)) { $Global:PSProfile.Prompts.Remove($Name) } if ($Save) { Save-PSProfile } } } } Register-ArgumentCompleter -CommandName Remove-PSProfilePrompt -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Prompts.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfilePrompt' function Switch-PSProfilePrompt { <# .SYNOPSIS Sets the prompt to the desired prompt by either the Name of the prompt as stored in $PSProfile.Prompts or the provided prompt content. .DESCRIPTION Sets the prompt to the desired prompt by either the Name of the prompt as stored in $PSProfile.Prompts or the provided prompt content. .PARAMETER Name The Name of the prompt to set as active from $PSProfile.Prompts. .PARAMETER Temporary If $true, does not update $PSProfile.Settings.DefaultPrompt with the selected prompt so that prompt selection does not persist after the current session. .PARAMETER Content If Content is provided as either a ScriptBlock or String, sets the current prompt to that. Equivalent to passing `function prompt {$Content}` .EXAMPLE Switch-PSProfilePrompt -Name Demo Sets the active prompt to the prompt named 'Demo' from $PSProfile.Prompts and saves it as the Default prompt for session persistence. #> [CmdletBinding(DefaultParameterSetName = 'Name')] Param( [Parameter(Mandatory,Position = 0,ParameterSetName = 'Name')] [String] $Name, [Parameter(ParameterSetName = 'Name')] [switch] $Temporary, [Parameter(Mandatory,ParameterSetName = 'Content')] [object] $Content ) Process { switch ($PSCmdlet.ParameterSetName) { Name { if ($global:PSProfile.Prompts.ContainsKey($Name)) { Write-Verbose "Setting active prompt to '$Name'" $function:prompt = $global:PSProfile.Prompts[$Name] if (-not $Temporary) { $global:PSProfile.Settings.DefaultPrompt = $Name Save-PSProfile } } else { Write-Warning "Falling back to default prompt -- '$Name' not found in Configuration prompts!" $function:prompt = ' "PS $($executionContext.SessionState.Path.CurrentLocation)$(''>'' * ($nestedPromptLevel + 1)) "; # .Link # https://go.microsoft.com/fwlink/?LinkID=225750 # .ExternalHelp System.Management.Automation.dll-help.xml ' } } Content { Write-Verbose "Setting active prompt to provided content directly" $function:prompt = $Content } } } } Register-ArgumentCompleter -CommandName Switch-PSProfilePrompt -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) Get-PSProfileArguments -WordToComplete "Prompts.$wordToComplete" -FinalKeyOnly } Export-ModuleMember -Function 'Switch-PSProfilePrompt' function Add-PSProfileScriptPath { <# .SYNOPSIS Adds a ScriptPath to your PSProfile to invoke during profile load. .DESCRIPTION Adds a ScriptPath to your PSProfile to invoke during profile load. .PARAMETER Path The path of the script to add to your $PSProfile.ScriptPaths. .PARAMETER Invoke If $true, invokes the script path after adding to $PSProfile.ScriptPaths to make it immediately available in the current session. .PARAMETER Invoke If $true, invokes the script at the path specified to load it into the current session. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfileScriptPath -Path ~\MyProfileScript.ps1 -Save Adds the script 'MyProfileScript.ps1' to $PSProfile.ScriptPaths and saves the configuration after updating. .EXAMPLE Get-ChildItem .\MyProfileScripts -Recurse -File | Add-PSProfileScriptPath -Verbose Adds all scripts under the MyProfileScripts folder to $PSProfile.ScriptPaths but does not save to allow inspection. Call Save-PSProfile after to save the results if satisfied. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [Alias('FullName')] [String[]] $Path, [Parameter()] [Switch] $Invoke, [Parameter()] [Switch] $Save ) Process { foreach ($p in $Path) { if ($p -match '\.ps1$') { $fP = (Resolve-Path $p).Path if ($Global:PSProfile.ScriptPaths -notcontains $fP) { Write-Verbose "Adding ScriptPath to PSProfile: $fP" $Global:PSProfile.ScriptPaths += $fP } else { Write-Verbose "ScriptPath already in PSProfile: $fP" } if ($Invoke) { . $fp } } else { Write-Verbose "Skipping non-ps1 file: $fP" } } if ($Save) { Save-PSProfile } } } Export-ModuleMember -Function 'Add-PSProfileScriptPath' function Get-PSProfileScriptPath { <# .SYNOPSIS Gets a script path from $PSProfile.ScriptPaths. .DESCRIPTION Gets a script path from $PSProfile.ScriptPaths. .PARAMETER Path The script path to get from $PSProfile.ScriptPaths. .EXAMPLE Get-PSProfileScriptPath -Path E:\Git\MyProfileScript.ps1 Gets the path 'E:\Git\MyProfileScript.ps1' from $PSProfile.ScriptPaths .EXAMPLE Get-PSProfileScriptPath Gets the list of script paths from $PSProfile.ScriptPaths #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $Path ) Process { if ($PSBoundParameters.ContainsKey('Path')) { Write-Verbose "Getting script path '$Path' from `$PSProfile.ScriptPaths" $Global:PSProfile.ScriptPaths | Where-Object {$_ -in $Path} } else { Write-Verbose "Getting all script paths from `$PSProfile.ScriptPaths" $Global:PSProfile.ScriptPaths } } } Register-ArgumentCompleter -CommandName Get-PSProfileScriptPath -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.ScriptPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileScriptPath' function Remove-PSProfileScriptPath { <# .SYNOPSIS Removes a Script Path from $PSProfile.ScriptPaths. .DESCRIPTION Removes a Script Path from $PSProfile.ScriptPaths. .PARAMETER Path The path to remove from $PSProfile.ScriptPaths. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileScriptPath -Name ~\Scripts\ProfileLoadScript.ps1 -Save Removes the path '~\Scripts\ProfileLoadScript.ps1' from $PSProfile.ScriptPaths then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0,ValueFromPipeline)] [String] $Path, [Parameter()] [Switch] $Save ) Process { if ($PSCmdlet.ShouldProcess("Removing '$Path' from `$PSProfile.ScriptPaths")) { Write-Verbose "Removing '$Path' from `$PSProfile.ScriptPaths" $Global:PSProfile.ScriptPaths = $Global:PSProfile.ScriptPaths | Where-Object {$_ -notin @($Path,(Resolve-Path $Path).Path)} if ($Save) { Save-PSProfile } } } } Register-ArgumentCompleter -CommandName Remove-PSProfileScriptPath -ParameterName Path -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.ScriptPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileScriptPath' function Add-PSProfileSecret { <# .SYNOPSIS Adds a PSCredential object or named SecureString to the PSProfile Vault then saves the current PSProfile. .DESCRIPTION Adds a PSCredential object or named SecureString to the PSProfile Vault then saves the current PSProfile. .PARAMETER Credential The PSCredential to add to the Vault. PSCredentials are recallable by the UserName from the stored PSCredential object via either `Get-MyCreds` or `Get-PSProfileSecret -UserName $UserName`. .PARAMETER Name For SecureString secrets, the friendly name to store them as for easy recall later via `Get-PSProfileSecret`. .PARAMETER SecureString The SecureString to store as the provided Name for recall later. .PARAMETER Force If $true and the PSCredential's UserName or SecureString's Name already exists, it overwrites it. Defaults to $false to prevent accidentally overwriting existing secrets in the $PSProfile.Vault. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfileSecret (Get-Credential) -Save Opens a Get-Credential window or prompt to enable entering credentials securely, then stores it in the Vault and saves your PSProfile configuration after updating. .EXAMPLE Add-PSProfileSecret -Name HomeApiKey -Value (ConvertTo-SecureString 1234567890xxx -AsPlainText -Force) -Save Stores the secret value '1234567890xxx' as the name 'HomeApiKey' in $PSProfile.Vault and saves your PSProfile configuration after updating. #> [CmdletBinding(DefaultParameterSetName = "PSCredential")] Param ( [Parameter(Mandatory,ValueFromPipeline,Position = 0,ParameterSetName = "PSCredential")] [pscredential] $Credential, [Parameter(Mandatory,ParameterSetName = "SecureString")] [string] $Name, [Parameter(Mandatory,ParameterSetName = "SecureString")] [securestring] $SecureString, [Parameter()] [Switch] $Force, [Parameter()] [Switch] $Save ) Process { switch ($PSCmdlet.ParameterSetName) { PSCredential { if ($Force -or $null -eq $Global:PSProfile.Vault.GetSecret($Credential.UserName)) { Write-Verbose "Adding PSCredential for user '$($Credential.UserName)' to `$PSProfile.Vault" $Global:PSProfile.Vault.SetSecret($Credential) } elseif (-not $Force -and $null -ne $Global:PSProfile.Vault.GetSecret($Credential.UserName)) { Write-Error "A secret with the name '$($Credential.UserName)' already exists! Include -Force to overwrite it." } } SecureString { if ($Force -or $null -eq $Global:PSProfile.Vault.GetSecret($Name)) { Write-Verbose "Adding SecureString secret with name '$Name' to `$PSProfile.Vault" $Global:PSProfile.Vault.SetSecret($Name,$SecureString) } elseif (-not $Force -and $null -ne $Global:PSProfile.Vault.GetSecret($Name)) { Write-Error "A secret with the name '$Name' already exists! Include -Force to overwrite it." } } } if ($Save) { Save-PSProfile } } } Export-ModuleMember -Function 'Add-PSProfileSecret' function Get-MyCreds { <# .SYNOPSIS Gets a credential object from the PSProfile Vault. Defaults to getting your current user's PSCredentials if stored in the Vault. .DESCRIPTION Gets a credential object from the PSProfile Vault. Defaults to getting your current user's PSCredentials if stored in the Vault. .PARAMETER Item The name of the Secret you would like to retrieve from the Vault. .PARAMETER IncludeDomain If $true, prepends the domain found in $env:USERDOMAIN to the Username on the PSCredential object before returning it. If not currently in a domain, prepends the MachineName instead. .EXAMPLE Get-MyCreds Gets the current user's PSCredentials from the Vault. .EXAMPLE Invoke-Command -ComputerName Server01 -Credential (Creds) Passes your current user credentials via the `Creds` alias to the Credential parameter of Invoke-Command to make a call against Server01 using your PSCredential .EXAMPLE Invoke-Command -ComputerName Server01 -Credential (Get-MyCreds SvcAcct07) Passes the credentials for account SvcAcct07 to the Credential parameter of Invoke-Command to make a call against Server01 using a different PSCredential than your own. #> [OutputType('PSCredential')] [CmdletBinding()] Param( [parameter(Mandatory = $false,Position = 0)] [String] $Item = $(if ($env:USERNAME) { $env:USERNAME } elseif ($env:USER) { $env:USER }), [parameter(Mandatory = $false)] [Alias('d','Domain')] [Switch] $IncludeDomain ) Process { if ($Item) { Write-Verbose "Checking Credential Vault for user '$Item'" if ($creds = $global:PSProfile.Vault.GetSecret($Item)) { Write-Verbose "Found item in CredStore" if (!$env:USERDOMAIN) { $env:USERDOMAIN = [System.Environment]::MachineName } if ($IncludeDomain -and $creds.UserName -notlike "$($env:USERDOMAIN)\*") { $creds = New-Object PSCredential "$($env:USERDOMAIN)\$($creds.UserName)",$creds.Password } return $creds } else { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( ([System.Management.Automation.ItemNotFoundException]"Could not find secret item '$Item' in the PSProfileVault"), 'PSProfile.Vault.SecretNotFound', [System.Management.Automation.ErrorCategory]::InvalidArgument, $global:PSProfile ) ) } } else { $global:PSProfile.Vault._secrets } } } Register-ArgumentCompleter -CommandName Get-MyCreds -ParameterName Item -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Vault._secrets.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-MyCreds' function Get-PSProfileSecret { <# .SYNOPSIS Gets a Secret from the $PSProfile.Vault. .DESCRIPTION Gets a Secret from the $PSProfile.Vault. .PARAMETER Name The name of the Secret you would like to retrieve from the Vault. .PARAMETER AsPlainText If $true and Confirm:$true, returns the decrypted password if the secret is a PSCredential object or the plain-text string if a SecureString. Requires confirmation. .PARAMETER Force If $true and AsPlainText is $true, bypasses Confirm prompt and returns the plain-text password or decrypted SecureString. .EXAMPLE Get-PSProfileSecret -Name MyApiKey Gets the Secret named 'MyApiKey' from the $PSProfile.Vault. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param( [parameter(Mandatory,Position = 0)] [String] $Name, [parameter()] [Switch] $AsPlainText, [parameter()] [Switch] $Force ) Process { Write-Verbose "Getting Secret '$Name' from `$PSProfile.Vault" $sec = $global:PSProfile.Vault.GetSecret($Name) if ($AsPlainText -and ($Force -or $PSCmdlet.ShouldProcess("Return plain-text value for Secret '$Name'"))) { if ($sec -is [pscredential]) { [PSCustomObject]@{ UserName = $sec.UserName Password = $sec.GetNetworkCredential().Password } } else { Get-DecryptedValue $sec } } else { $sec } } } Register-ArgumentCompleter -CommandName Get-PSProfileSecret -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Vault._secrets.Keys | Where-Object {$_ -notin @('GitCredentials','PSCredentials','SecureStrings') -and $_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileSecret' function Remove-PSProfileSecret { <# .SYNOPSIS Removes a Secret from $PSProfile.Vault. .DESCRIPTION Removes a Secret from $PSProfile.Vault. .PARAMETER Name The Secret's Name or UserName to remove from the Vault. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileSecret -Name $env:USERNAME -Save Removes the current user's stored credentials from the $PSProfile.Vault, then saves the configuration after updating. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0)] [Alias('UserName')] [String] $Name, [Parameter()] [Switch] $Save ) Process { if ($PSCmdlet.ShouldProcess("Removing '$Name' from `$PSProfile.Vault")) { if ($Global:PSProfile.Vault._secrets.ContainsKey($Name)) { Write-Verbose "Removing '$Name' from `$PSProfile.Vault" $Global:PSProfile.Vault.RemoveSecret($Name) } if ($Save) { Save-PSProfile } } } } Register-ArgumentCompleter -CommandName Remove-PSProfileSecret -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Vault._secrets.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileSecret' function Add-PSProfileSymbolicLink { <# .SYNOPSIS Adds a SymbolicLink to set if missing during profile load via background task. .DESCRIPTION Adds a SymbolicLink to set if missing during profile load via background task. .PARAMETER LinkPath The path of the symbolic link to create if missing. .PARAMETER ActualPath The actual target path of the symbolic link to set. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfileSymbolicLink -LinkPath C:\workstation -ActualPath E:\Git\workstation -Save Adds a symbolic link at path 'C:\workstation' targeting the actual path 'E:\Git\workstation' and saves your PSProfile configuration. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [Alias('Path','Name')] [String] $LinkPath, [Parameter(Mandatory,Position = 1)] [Alias('Target','Value')] [String] $ActualPath, [Parameter()] [Switch] $Save ) Process { Write-Verbose "Adding SymbolicLink '$LinkPath' pointing at ActualPath '$ActualPath'" $Global:PSProfile.SymbolicLinks[$LinkPath] = $ActualPath if ($Save) { Save-PSProfile } } } Export-ModuleMember -Function 'Add-PSProfileSymbolicLink' function Get-PSProfileSymbolicLink { <# .SYNOPSIS Gets a module from $PSProfile.SymbolicLinks. .DESCRIPTION Gets a module from $PSProfile.SymbolicLinks. .PARAMETER LinkPath The LinkPath to get from $PSProfile.SymbolicLinks. .EXAMPLE Get-PSProfileSymbolicLink -LinkPath C:\workstation Gets the LinkPath 'C:\workstation' from $PSProfile.SymbolicLinks .EXAMPLE Get-PSProfileSymbolicLink Gets the list of LinkPaths from $PSProfile.SymbolicLinks #> [CmdletBinding()] Param ( [Parameter(Position = 0,ValueFromPipeline)] [String[]] $LinkPath ) Process { if ($PSBoundParameters.ContainsKey('LinkPath')) { Write-Verbose "Getting Path LinkPath '$LinkPath' from `$PSProfile.SymbolicLinks" $Global:PSProfile.SymbolicLinks.GetEnumerator() | Where-Object {$_.Key -in $LinkPath} } else { Write-Verbose "Getting all command aliases from `$PSProfile.SymbolicLinks" $Global:PSProfile.SymbolicLinks } } } Register-ArgumentCompleter -CommandName Get-PSProfileSymbolicLink -ParameterName LinkPath -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.SymbolicLinks.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileSymbolicLink' function Remove-PSProfileSymbolicLink { <# .SYNOPSIS Removes a Symbolic Link from $PSProfile.SymbolicLinks. .DESCRIPTION Removes a PSProfile Plugin from $PSProfile.SymbolicLinks. .PARAMETER LinkPath The path of the symbolic link to remove from $PSProfile.SymbolicLinks. .PARAMETER Force If $true, also removes the SymbolicLink itself from the OS if it exists. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileSymbolicLink -LinkPath 'C:\workstation' -Force -Save Removes the SymbolicLink 'C:\workstation' from $PSProfile.SymbolicLinks, removes the then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory,Position = 0)] [String[]] $LinkPath, [Parameter()] [Switch] $Force, [Parameter()] [Switch] $Save ) Process { foreach ($path in $LinkPath) { if ($PSCmdlet.ShouldProcess("Removing '$path' from `$PSProfile.SymbolicLinks")) { Write-Verbose "Removing '$path' from `$PSProfile.SymbolicLinks" $paths = @($path) if (Test-Path $path) { $paths += (Resolve-Path $path).Path } $paths | Select-Object -Unique | ForEach-Object { if ($Global:PSProfile.SymbolicLinks.ContainsKey($_)) { $Global:PSProfile.SymbolicLinks.Remove($_) } } if ($Force -and (Test-Path $path)) { Write-Verbose "Removing SymbolicLink: $path" Remove-Item $path -Force } } } if ($Save) { Save-PSProfile } } } Register-ArgumentCompleter -CommandName Remove-PSProfileSymbolicLink -ParameterName LinkPath -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.SymbolicLinks.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileSymbolicLink' function Add-PSProfileVariable { <# .SYNOPSIS Adds a global or environment variable to your PSProfile configuration. Variables added to PSProfile will be set during profile load. .DESCRIPTION Adds a global or environment variable to your PSProfile configuration. Variables added to PSProfile will be set during profile load. .PARAMETER Name The name of the variable. .PARAMETER Value The value to set the variable to. .PARAMETER Scope The scope of the variable to set between Environment or Global. Defaults to Environment. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Add-PSProfileVariable -Name HomeBase -Value C:\HomeBase -Save Adds the environment variable named 'HomeBase' to be set to the path 'C:\HomeBase' during profile load and saves your PSProfile configuration. #> [CmdletBinding()] Param ( [Parameter(Mandatory,Position = 0)] [String] $Name, [Parameter(Mandatory,Position = 1)] [Object] $Value, [Parameter(Position = 2)] [ValidateSet('Environment','Global')] [String] $Scope = 'Environment', [Parameter()] [Switch] $Save ) Process { if (-not ($Global:PSProfile.Variables.ContainsKey($Scope))) { $Global:PSProfile.Variables[$Scope] = @{} } Write-Verbose "Adding $Scope variable '$Name' to PSProfile" $Global:PSProfile.Variables[$Scope][$Name] = $Value if ($Save) { Save-PSProfile } } } Export-ModuleMember -Function 'Add-PSProfileVariable' function Get-PSProfileVariable { <# .SYNOPSIS Gets a global or environment variable from your PSProfile configuration. .DESCRIPTION Gets a global or environment variable from your PSProfile configuration. .PARAMETER Scope The scope of the variable to get the variable from between Environment or Global. .PARAMETER Name The name of the variable to get. .EXAMPLE Get-PSProfileVariable -Name HomeBase Gets the environment variable named 'HomeBase' and its value from $PSProfile.Variables. .EXAMPLE Get-PSProfileVariable Gets the list of environment variables from $PSProfile.Variables. .EXAMPLE Get-PSProfileVariable -Scope Global Gets the list of Global variables from $PSProfile.Variables. #> [CmdletBinding()] Param ( [Parameter(Mandatory, Position = 0)] [ValidateSet('Environment','Global')] [String] $Scope, [Parameter(Position = 1)] [String] $Name ) Process { if ($Global:PSProfile.Variables.ContainsKey($Scope)) { if ($PSBoundParameters.ContainsKey('Name')) { Write-Verbose "Getting $Scope variable '$Name' from PSProfile" $Global:PSProfile.Variables[$Scope].GetEnumerator() | Where-Object {$_.Key -in $Name} } else { Write-Verbose "Getting $Scope variable list from PSProfile" $Global:PSProfile.Variables[$Scope] } } } } Register-ArgumentCompleter -CommandName Get-PSProfileVariable -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Variables[$fakeBoundParameter.Scope].Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Get-PSProfileVariable' function Remove-PSProfileVariable { <# .SYNOPSIS Removes a Variable from $PSProfile.Variables. .DESCRIPTION Removes a Variable from $PSProfile.Variables. .PARAMETER Name The name of the Variable to remove from $PSProfile.Variables. .PARAMETER Scope The scope of the Variable to remove between Environment or Global. .PARAMETER Force If $true, also removes the variable from the current session at the specified scope. .PARAMETER Save If $true, saves the updated PSProfile after updating. .EXAMPLE Remove-PSProfileVariable -Scope Environment -Name '~' -Save Removes the Environment variable '~' from $PSProfile.Variables then saves the updated configuration. #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] Param ( [Parameter(Mandatory, Position = 0)] [ValidateSet('Environment','Global')] [String] $Scope, [Parameter(Mandatory,Position = 1)] [String[]] $Name, [Parameter()] [Switch] $Force, [Parameter()] [Switch] $Save ) Process { foreach ($item in $Name) { if ($PSCmdlet.ShouldProcess("Removing $Scope variable '$item' from `$PSProfile.Variables")) { Write-Verbose "Removing $Scope variable '$item' from `$PSProfile.Variables" if ($Global:PSProfile.Variables[$Scope].ContainsKey($item)) { $Global:PSProfile.Variables[$Scope].Remove($item) } if ($Force) { switch ($Scope) { Environment { Remove-Item "Env:\$item" } Global { Remove-Variable -Name $item -Scope Global } } } } } if ($Save) { Save-PSProfile } } } Register-ArgumentCompleter -CommandName Remove-PSProfileVariable -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $Global:PSProfile.Variables[$fakeBoundParameter.Scope].Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } Export-ModuleMember -Function 'Remove-PSProfileVariable' New-Alias -Name 'push' -Value 'Push-Path' -Scope Global -Force Export-ModuleMember -Alias 'push' New-Alias -Name 'def' -Value 'Get-Definition' -Scope Global -Force Export-ModuleMember -Alias 'def' New-Alias -Name 'Remove-Prompt' -Value 'Remove-PSProfilePrompt' -Scope Global -Force Export-ModuleMember -Alias 'Remove-Prompt' New-Alias -Name 'syntax' -Value 'Format-Syntax' -Scope Global -Force Export-ModuleMember -Alias 'syntax' New-Alias -Name 'bld' -Value 'Start-BuildScript' -Scope Global -Force Export-ModuleMember -Alias 'bld' New-Alias -Name 'Get-Prompt' -Value 'Get-PSProfilePrompt' -Scope Global -Force Export-ModuleMember -Alias 'Get-Prompt' New-Alias -Name 'path' -Value 'Get-LongPath' -Scope Global -Force Export-ModuleMember -Alias 'path' New-Alias -Name 'pop' -Value 'Pop-Path' -Scope Global -Force Export-ModuleMember -Alias 'pop' New-Alias -Name 'Creds' -Value 'Get-MyCreds' -Scope Global -Force Export-ModuleMember -Alias 'Creds' New-Alias -Name 'Copy-DynamicParameters' -Value 'Copy-Parameters' -Scope Global -Force Export-ModuleMember -Alias 'Copy-DynamicParameters' New-Alias -Name 'Load-PSProfile' -Value 'Import-PSProfile' -Scope Global -Force Export-ModuleMember -Alias 'Load-PSProfile' New-Alias -Name 'Set-Prompt' -Value 'Switch-PSProfilePrompt' -Scope Global -Force Export-ModuleMember -Alias 'Set-Prompt' New-Alias -Name 'Refresh-PSProfile' -Value 'Update-PSProfileConfig' -Scope Global -Force Export-ModuleMember -Alias 'Refresh-PSProfile' New-Alias -Name 'Switch-Prompt' -Value 'Switch-PSProfilePrompt' -Scope Global -Force Export-ModuleMember -Alias 'Switch-Prompt' New-Alias -Name 'Edit-Prompt' -Value 'Edit-PSProfilePrompt' -Scope Global -Force Export-ModuleMember -Alias 'Edit-Prompt' New-Alias -Name 'open' -Value 'Open-Item' -Scope Global -Force Export-ModuleMember -Alias 'open' New-Alias -Name 'cln' -Value 'Enter-CleanEnvironment' -Scope Global -Force Export-ModuleMember -Alias 'cln' New-Alias -Name 'Save-Prompt' -Value 'Add-PSProfilePrompt' -Scope Global -Force Export-ModuleMember -Alias 'Save-Prompt' # If we're in an interactive shell, load the profile. if ([Environment]::UserInteractive -or ($null -eq [Environment]::UserInteractive -and $null -eq ([Environment]::GetCommandLineArgs() | Where-Object { $_ -like '-NonI*' }))) { $global:OriginalPrompt = $global:PSProfile = [PSProfile]::new() $global:PSProfile.Load() Export-ModuleMember -Variable PSProfile $global:PSProfileConfigurationWatcher = [System.IO.FileSystemWatcher]::new($(Split-Path $global:PSProfile.Settings.ConfigurationPath -Parent),'Configuration.psd1') $job = Register-ObjectEvent -InputObject $global:PSProfileConfigurationWatcher -EventName Changed -Action { [PSProfile]$conf = Import-Configuration -Name PSProfile -CompanyName 'SCRT HQ' -Verbose:$false $conf._internal = $global:PSProfile._internal $global:PSProfile = $conf } $PSProfile_OnRemoveScript = { try { $global:PSProfileConfigurationWatcher.Dispose() } finally { Remove-Variable PSProfile -Scope Global -Force Remove-Variable PSProfileConfigurationWatcher -Scope Global -Force } } $ExecutionContext.SessionState.Module.OnRemove += $PSProfile_OnRemoveScript Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action $PSProfile_OnRemoveScript } |