ProfileFever.psm1
# Use namespaces for PSReadLine extension using namespace System.Management.Automation using namespace System.Management.Automation.Language <# .SYNOPSIS Install all dependencies for the profile. .DESCRIPTION Insted of adding theses module dependencies into the module manifest, they are separated in this command. This is by design, to speed up the module load duration of ProfileFever. The module load time is essential for a fast profie script. #> function Install-Profile { [CmdletBinding()] param () ## ## MODULE DEPENDENCY ## $moduleNames = 'SecurityFever', 'Pester', 'posh-git', 'psake' if ($PSVersionTable.PSEdition -ne 'Core') { Install-PackageProvider -Name 'NuGet' -Scope 'CurrentUser' -MinimumVersion '2.8.5.201' -Force -ForceBootstrap -Verbose | Out-Null } # Only for Pester, update the built-in module with version 3.4.0 if ((Get-Module -Name 'Pester' -ListAvailable | Sort-Object -Property 'Version' -Descending | Select-Object -First 1).Version -eq '3.4.0') { Install-Module -Name 'Pester' -Repository 'PSGallery' -Scope 'CurrentUser' -Force -AllowClobber -SkipPublisherCheck -Verbose } foreach ($moduleName in $moduleNames) { if ($null -eq (Get-Module -Name $moduleName -ListAvailable)) { Install-Module -Name $moduleName -Repository 'PSGallery' -Scope 'CurrentUser' -Force -AllowClobber -SkipPublisherCheck -Verbose } else { Update-Module -Name $moduleName -Force -Verbose } } ## ## PROFILE SCRIPT ## $profilePaths = @() if ([System.Environment]::OSVersion.Platform -eq 'Win32NT') { $profilePaths += "$HOME\Documents\PowerShell" $profilePaths += "$HOME\Documents\WindowsPowerShell" } if ([System.Environment]::OSVersion.Platform -eq 'Unix') { $profilePaths += "$HOME/.config/powershell" } foreach ($profilePath in $profilePaths) { if (-not (Test-Path -Path $profilePath)) { New-Item -Path $profilePath -ItemType 'Directory' -Force | Out-Null } if (-not (Test-Path -Path "$profilePath\profile.ps1")) { Set-Content -Path "$profilePath\profile.ps1" -Value 'Start-Profile' } } } <# .SYNOPSIS Show the headline with information about the local system and current user. .DESCRIPTION Show the current PowerShell version, Operationg System details an the user session as profile headline. #> function Show-HostHeadline { # Only show the headline, if PowerShell was started with -NoLogo switch. The # test is more a workaround as checking the start parameter. Not found an # efficient way to test that without WMI. if ($Host.UI.RawUI.CursorPosition.Y -eq 0) { # Get the PowerShell version depending on the edition if ($PSVersionTable.PSEdition -eq 'Core') { $psInfo = 'PowerShell {0}' -f $PSVersionTable.PSVersion } else { $psInfo = 'Windows PowerShell {0}.{1}' -f $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor } # Get the operating system information, based on the operating system $osInfo = '' if ([System.Environment]::OSVersion.Platform -eq 'Win32NT') { # Get Windows version from registry. Update the object for non Windows 10 or # Windows Server 2016 systems to match the same keys. $osVersion = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' if ($null -eq $osVersion.ReleaseId) { $osVersion | Add-Member -MemberType NoteProperty -Name 'ReleaseId' -Value $osVersion.CurrentVersion } $osInfo = '{0}, Version {1}' -f $osVersion.ProductName, $osVersion.ReleaseId } if ([System.Environment]::OSVersion.Platform -eq 'Unix') { $osInfo = uname -a } # Get the info about the current logged on user, system and uptime $usrInfo = '' if ([System.Environment]::OSVersion.Platform -eq 'Win32NT') { $usrInfo = '{0}\{1} on {2}, Uptime {3:%d} day(s) {3:hh\:mm\:ss}' -f $Env:USERDOMAIN, $Env:USERNAME, $Env:COMPUTERNAME.ToUpper(), [System.TimeSpan]::FromMilliseconds([System.Environment]::TickCount) } if ([System.Environment]::OSVersion.Platform -eq 'Unix') { } # Show headline $Host.UI.WriteLine($psInfo) $Host.UI.WriteLine($osInfo) $Host.UI.WriteLine() $Host.UI.WriteLine($usrInfo) $Host.UI.WriteLine() } } <# .SYNOPSIS Initialize the PowerShell console profile. .DESCRIPTION This is the personal profile of Claudio Spizzi holding all commands to initialize the profile in the console. It's intended to be used on any PowerShell version and plattform. #> function Start-Profile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalAliases', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( # Optionally specify a path to the config file. [Parameter(Mandatory = $false)] [System.String] $ConfigPath ) ## ## PROFILE CONFIG ## # Guess the real config path for the all hosts current user if (-not $PSBoundParameters.ContainsKey('ConfigPath')) { $ConfigPath = $PROFILE.CurrentUserAllHosts -replace '\.ps1', '.json' } $config = Update-ProfileConfig -Path $ConfigPath -PassThru ## ## HEADLINE ## if ($config.Headline) { Show-HostHeadline } ## ## LOCATION ## if (Test-Path -Path $config.Location) { Set-Location -Path $config.Location } ## ## WORKSPACE ## if (Test-Path -Path $config.Workspace) { New-PSDrive -PSProvider 'FileSystem' -Scope 'Global' -Name 'Workspace' -Root $config.Workspace | Out-Null # Aliases to jump into the workspace named Workspace: and WS: Set-Item -Path 'Function:Global:Workspace:' -Value 'Set-Location -Path "Workspace:"' Set-Item -Path 'Function:Global:WS:' -Value 'Set-Location -Path "Workspace:"' # Specify the path to the workspace as environment variable [System.Environment]::SetEnvironmentVariable('Workspace', $Workspace, [System.EnvironmentVariableTarget]::Process) } ## ## PROMPT ## if ($config.Prompt) { Enable-Prompt } if ($config.PromptAlias) { Enable-PromptAlias } if ($config.PromptGit) { Enable-PromptGit } if ($config.PromptTimeSpan) { Enable-PromptTimeSpan } ## ## COMMAND NOT FOUND ## if ($config.CommandNotFound) { Enable-CommandNotFound } ## ## ALIASES ## $aliasKeys = $config.Aliases | Get-Member -MemberType 'NoteProperty' | Select-Object -ExpandProperty 'Name' foreach ($aliasKey in $aliasKeys) { New-Alias -Scope 'Global' -Name $aliasKey -Value $config.Aliases.$aliasKey } ## ## FUNCTIONS ## $functionKeys = $config.Functions | Get-Member -MemberType 'NoteProperty' | Select-Object -ExpandProperty 'Name' foreach ($functionKey in $functionKeys) { Set-Item -Path "Function:Global:$functionKey" -Value $config.Functions.$functionKey } ## ## SCRIPTS ## foreach ($script in $config.Scripts) { . $script } ## ## BINARIES ## foreach ($binary in $config.Binaries) { $Env:Path += ';' + $binary } ## ## PSREADLINE ## # History browser, history search and history save if ($config.ReadLineHistoryHelper) { Enable-PSReadLineHistoryHelper } # Enable smart insert/delete for ', ", [, ), { if ($config.ReadLineSmartInsertDelete) { Enable-PSReadLineSmartInsertDelete } # Enable F1 to show help if ($config.ReadLineCommandHelp) { Enable-PSReadLineCommandHelp } # Jump around in the file system if ($config.ReadLineLocationMark) { Enable-PSReadLineLocationMark } if ($config.ReadLinePSakeBuild) { # This will invoke the PSake build in the current directory Set-PSReadLineKeyHandler -Key 'Ctrl+B', 'Ctrl+b' -BriefDescription 'BuildCurrentDirectory' -LongDescription "Build the current directory" -ScriptBlock { [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() [Microsoft.PowerShell.PSConsoleReadLine]::Insert('Invoke-psake -buildFile ".\build.psake.ps1"') [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() } } if ($config.ReadLinePesterTest) { # This will invoke all Pester tests in the current directory Set-PSReadLineKeyHandler -Key 'Ctrl+T', 'Ctrl+t' -BriefDescription 'TestCurrentDirectory' -LongDescription "Test the current directory" -ScriptBlock { [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() [Microsoft.PowerShell.PSConsoleReadLine]::Insert('Invoke-Pester') [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() } } ## ## STRICT MODE ## if ($config.StrictMode) { Set-StrictMode -Version 'latest' } } <# .SYNOPSIS Create and update the profile configuration. .DESCRIPTION The profile configuration will be created if it does not exist. Every property will be initialized with a default value. #> function Update-ProfileConfig { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( # Path to the config file. [Parameter(Mandatory = $true)] [System.String] $Path, # Return the updated object [Parameter(Mandatory = $false)] [switch] $PassThru ) # Load the config file or specify an empty config object if (Test-Path -Path $Path) { $config = Get-Content -Path $Path -Encoding 'UTF8' | ConvertFrom-Json } else { $config = [PSCustomObject] @{} } # Initialize the configuration if not specified with default values if ($null -eq $config.Location) { $config | Add-Member -MemberType 'NoteProperty' -Name 'Location' -Value $(if ($IsLinux -or $IsMacOs) { '~' } else { "$Home\Desktop" }) } if ($null -eq $config.Workspace) { $config | Add-Member -MemberType 'NoteProperty' -Name 'Workspace' -Value $(if ($IsLinux -or $IsMacOs) { '~/workspace' } else { "$Home\Workspace" }) } if ($null -eq $config.Prompt) { $config | Add-Member -MemberType 'NoteProperty' -Name 'Prompt' -Value $true } if ($null -eq $config.PromptAlias) { $config | Add-Member -MemberType 'NoteProperty' -Name 'PromptAlias' -Value $true } if ($null -eq $config.PromptGit) { $config | Add-Member -MemberType 'NoteProperty' -Name 'PromptGit' -Value $false # Git client is not installed by default } if ($null -eq $config.PromptTimeSpan) { $config | Add-Member -MemberType 'NoteProperty' -Name 'PromptTimeSpan' -Value $true } if ($null -eq $config.ReadLineHistoryHelper) { $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLineHistoryHelper' -Value $true } if ($null -eq $config.ReadLineSmartInsertDelete) { $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLineSmartInsertDelete' -Value $true } if ($null -eq $config.ReadLineCommandHelp) { $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLineCommandHelp' -Value $true } if ($null -eq $config.ReadLineLocationMark) { $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLineLocationMark' -Value $true } if ($null -eq $config.ReadLinePSakeBuild) { $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLinePSakeBuild' -Value $true } if ($null -eq $config.ReadLinePesterTest) { $config | Add-Member -MemberType 'NoteProperty' -Name 'ReadLinePesterTest' -Value $true } if ($null -eq $config.StrictMode) { $config | Add-Member -MemberType 'NoteProperty' -Name 'StrictMode' -Value $false } if ($null -eq $config.CommandNotFound) { $config | Add-Member -MemberType 'NoteProperty' -Name 'CommandNotFound' -Value $false } if ($null -eq $config.Headline) { $config | Add-Member -MemberType 'NoteProperty' -Name 'Headline' -Value $true } if ($null -eq $config.Aliases) { $config | Add-Member -MemberType 'NoteProperty' -Name 'Aliases' -Value @{ # Baseline 'grep' = 'Select-String' # SecurityFever 'cred' = 'Use-VaultCredential' } } if ($null -eq $config.Functions) { $config | Add-Member -MemberType 'NoteProperty' -Name 'Functions' -Value @{ # Internet Search 'google' = 'Start-Process "https://www.google.com/search?q=$args"' 'dict' = 'Start-Process "https://www.dict.cc/?s=$args"' 'wiki' = 'Start-Process "https://en.wikipedia.org/wiki/Special:Search/$args"' 'stackoverflow' = 'Start-Process "https://stackoverflow.com/search?q=$args"' # PSake Build Module 'psake' = 'Invoke-psake -buildFile ".\build.psake.ps1"' 'psakedeploy' = 'Invoke-psake -buildFile ".\build.psake.ps1" -taskList "Deploy"' } } if ($null -eq $config.Scripts) { $config | Add-Member -MemberType 'NoteProperty' -Name 'Scripts' -Value @() } if ($null -eq $config.Binaries) { $config | Add-Member -MemberType 'NoteProperty' -Name 'Binaries' -Value @() } # Finally, store the config file on the disk $config | ConvertTo-Json | Set-Content -Path $Path -Encoding 'UTF8' if ($PassThru.IsPresent) { Write-Output $config } } <# .SYNOPSIS Add a command not found action to the list of actions. #> function Add-CommandNotFoundAction { [CmdletBinding()] param ( # Name of the command. [Parameter(Mandatory = $true)] [System.String] $CommandName, # For the remoting command, set the computer name of the target system. [Parameter(Mandatory = $true, ParameterSetName = 'RemotingWithCredential')] [Parameter(Mandatory = $true, ParameterSetName = 'RemotingWithVault')] [System.String] $ComputerName, # For the remoting command, set the credentials. [Parameter(Mandatory = $false, ParameterSetName = 'RemotingWithCredential')] [System.Management.Automation.PSCredential] $Credential, # For the remoting command, but only a pointer to the credential vault. [Parameter(Mandatory = $true, ParameterSetName = 'RemotingWithVault')] [System.String] $VaultTargetName, # Define a script block to execute for the command. [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')] [System.Management.Automation.ScriptBlock] $ScriptBlock ) $command = [PSCustomObject] @{ PSTypeName = 'ProfileFever.CommandNotFoundAction' CommandName = $CommandName CommandType = $null ComputerName = $null Credential = $null CredentialVault = $null ScriptBlock = $null } switch ($PSCmdlet.ParameterSetName) { 'RemotingWithCredential' { $command.CommandType = 'Remoting' $command.ComputerName = $ComputerName $command.Credential = $Credential } 'RemotingWithVault' { $command.CommandType = 'Remoting' $command.ComputerName = $ComputerName $command.CredentialVault = $VaultTargetName } 'ScriptBlock' { $command.CommandType = 'ScriptBlock' $command.ScriptBlock = $ScriptBlock } } $Script:CommandNotFoundAction[$CommandName] = $command } <# .SYNOPSIS Disable the command not found actions. #> function Disable-CommandNotFound { [CmdletBinding()] param () $Script:CommandNotFoundEnabled = $false } <# .SYNOPSIS Enable the command not found actions. #> function Enable-CommandNotFound { [CmdletBinding()] param () Register-CommandNotFound $Script:CommandNotFoundEnabled = $true } <# .SYNOPSIS Get the registered command not found actions. #> function Get-CommandNotFoundAction { [CmdletBinding()] param () $Script:CommandNotFoundAction.Values } <# .SYNOPSIS Register the command not found action callback. #> function Register-CommandNotFound { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')] param () $Global:ExecutionContext.InvokeCommand.CommandNotFoundAction = { param ($CommandName, $CommandLookupEventArgs) if ($Script:CommandNotFoundEnabled -and $Script:CommandNotFoundAction.ContainsKey($CommandName)) { $command = $Script:CommandNotFoundAction[$CommandName] $commandLine = (Get-PSCallStack)[1].Position.Text.Trim() switch ($command.CommandType) { 'Remoting' { $credentialSplat = @{} if ($command.Credential) { $credentialSplat['Credential'] = $command.Credential $credentialVerbose = " -Credential '{0}'" -f $command.Credential.UserName } if ($command.CredentialVault) { $credential = Use-VaultCredential -TargetName $command.CredentialVault $credentialSplat['Credential'] = $credential $credentialVerbose = " -Credential '{0}'" -f $credential.UserName } # Option 1: Enter Session # If no parameters were specified, just enter into a # remote session to the target system. if ($CommandName -eq $commandLine) { Write-Verbose ("Enter-PSSession -ComputerName '{0}'{1}" -f $command.ComputerName, $credentialVerbose) $CommandLookupEventArgs.StopSearch = $true $CommandLookupEventArgs.CommandScriptBlock = { $session = New-PSSession -ComputerName $command.ComputerName @credentialSplat -ErrorAction Stop if ($Host.Name -eq 'ConsoleHost') { Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock { Set-Location -Path "$Env:SystemDrive\" $PromptLabel = $Env:ComputerName.ToUpper() $PromptIndent = $using:session.ComputerName.Length + 4 function Global:prompt { Write-Host "[$PromptLabel]" -NoNewline -ForegroundColor Cyan; "$("`b `b" * $PromptIndent) $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) " } function profile { Install-Module -Name 'profile' Install-Profile Start-Profile } } } Enter-PSSession -Session $session -ErrorAction Stop }.GetNewClosure() } # Option 2: Open Session # If a variable is specified as output of the command, # a new remoting session will be opened and returned. $openSessionRegex = '^\$\S+ = {0}$' -f ([System.Text.RegularExpressions.Regex]::Escape($CommandName)) if ($commandLine -match $openSessionRegex) { Write-Verbose ("New-PSSession -ComputerName '{0}'{1}" -f $command.ComputerName, $credentialVerbose) $CommandLookupEventArgs.StopSearch = $true $CommandLookupEventArgs.CommandScriptBlock = { New-PSSession -ComputerName $command.ComputerName @credentialSplat -ErrorAction Stop }.GetNewClosure() } # Option 3: Invoke Command # If a script is appended to the command, execute that # script on the remote system. if ($commandline.StartsWith($CommandName) -and $commandLine.Length -gt $CommandName.Length) { $scriptBlock = [System.Management.Automation.ScriptBlock]::Create($commandLine.Substring($CommandName.Length).Trim()) Write-Verbose ("Invoke-Command -ComputerName '{0}'{1} -ScriptBlock {{ {2} }}" -f $command.ComputerName, $credentialVerbose, $scriptBlock.ToString()) $CommandLookupEventArgs.StopSearch = $true $CommandLookupEventArgs.CommandScriptBlock = { Invoke-Command -ComputerName $command.ComputerName @credentialSplat -ScriptBlock $scriptBlock -ErrorAction Stop }.GetNewClosure() } } 'ScriptBlock' { Write-Verbose ("& {{ {0} }}" -f $command.ScriptBlock) $CommandLookupEventArgs.StopSearch = $true $CommandLookupEventArgs.CommandScriptBlock = $command.ScriptBlock } } } } } <# .SYNOPSIS Unregister the command not found action callback. #> function Unregister-CommandNotFound { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')] param () $Global:ExecutionContext.InvokeCommand.CommandNotFoundAction = $null } <# .SYNOPSIS Disable the custom prompt and restore the default prompt. #> function Disable-Prompt { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')] param () Set-Item -Path 'Function:Global:prompt' -Value $Script:PromptDefault } <# .SYNOPSIS Disable the prompt alias recommendation output after each command. #> function Disable-PromptAlias { [CmdletBinding()] [Alias('dalias')] param () Remove-Variable -Scope Script -Name PromptAlias -ErrorAction SilentlyContinue -Force New-Variable -Scope Script -Option ReadOnly -Name PromptAlias -Value $false -Force } <# .SYNOPSIS Disable the git repository status in the prompt. #> function Disable-PromptGit { [CmdletBinding()] [Alias('dgit')] param () Remove-Variable -Scope Script -Name PromptGit -ErrorAction SilentlyContinue -Force New-Variable -Scope Script -Option ReadOnly -Name PromptGit -Value $false -Force } <# .SYNOPSIS Disable the prompt timestamp output. #> function Disable-PromptTimeSpan { [CmdletBinding()] [Alias('dtimespan')] param () Remove-Variable -Scope Script -Name PromptTimeSpan -ErrorAction SilentlyContinue -Force New-Variable -Scope Script -Option ReadOnly -Name PromptTimeSpan -Value $false -Force } <# .SYNOPSIS Enable the custom prompt by replacing the default prompt. #> function Enable-Prompt { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalFunctions', '')] param () function Global:Prompt { if ($Script:PromptHistory -ne $MyInvocation.HistoryId) { $Script:PromptHistory = $MyInvocation.HistoryId if ($Script:PromptAlias) { Show-PromptAliasSuggestion } if ($Script:PromptTimeSpan) { Show-PromptLastCommandDuration } } $Host.UI.Write($Script:PromptColor, $Host.UI.RawUI.BackgroundColor, $Script:PromptInfo) $Host.UI.Write(" $($ExecutionContext.SessionState.Path.CurrentLocation)") if ($Script:PromptGit) { Write-VcsStatus } return "`n$($MyInvocation.HistoryId.ToString().PadLeft(3, '0'))$('>' * ($NestedPromptLevel + 1)) " } } <# .SYNOPSIS Enable the prompt alias recommendation output after each command. #> function Enable-PromptAlias { [CmdletBinding()] [Alias('ealias')] param () Remove-Variable -Scope Script -Name PromptAlias -ErrorAction SilentlyContinue -Force New-Variable -Scope Script -Option ReadOnly -Name PromptAlias -Value $true -Force } <# .SYNOPSIS Enable the git repository status in the prompt. #> function Enable-PromptGit { [CmdletBinding()] [Alias('egit')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] param () if ($null -eq (Get-Module -Name posh-git)) { Import-Module -Name posh-git -Force $Global:GitPromptSettings.EnableWindowTitle = '{0} ~ ' -f $Host.UI.RawUI.WindowTitle } Remove-Variable -Scope Script -Name PromptGit -ErrorAction SilentlyContinue -Force New-Variable -Scope Script -Option ReadOnly -Name PromptGit -Value $true -Force } <# .SYNOPSIS Enable the prompt timestamp output. #> function Enable-PromptTimeSpan { [CmdletBinding()] [Alias('etimespan')] param () Remove-Variable -Scope Script -Name PromptTimeSpan -ErrorAction SilentlyContinue -Force New-Variable -Scope Script -Option ReadOnly -Name PromptTimeSpan -Value $true -Force } <# .SYNOPSIS Show the alias suggestion for the latest command. #> function Show-PromptAliasSuggestion { [CmdletBinding()] param () if ($MyInvocation.HistoryId -gt 1) { $history = Get-History -Id ($MyInvocation.HistoryId - 1) $reports = @() foreach ($alias in (Get-Alias)) { if ($history.CommandLine.IndexOf($alias.ResolvedCommandName) -ne -1) { $reports += $alias } } if ($reports.Count -gt 0) { $report = $reports | Group-Object -Property 'ResolvedCommandName' | ForEach-Object { ' ' + $_.Name + ' => ' + ($_.Group -join ', ') } $Host.UI.WriteLine('Magenta', $Host.UI.RawUI.BackgroundColor, "Alias suggestions:`n" + ($report -join "`n")) } } } <# .SYNOPSIS Show the during of the last executed command. #> function Show-PromptLastCommandDuration { [CmdletBinding()] param () if ($MyInvocation.HistoryId -gt 1 -and $Host.UI.RawUI.CursorPosition.Y -gt 0) { $history = Get-History -Id ($MyInvocation.HistoryId - 1) $duration = "{0:0.000}s" -f ($history.EndExecutionTime - $history.StartExecutionTime).TotalSeconds # Move cursor one up and to the right to show the execution time $position = $Host.UI.RawUI.CursorPosition $position.Y = $position.Y - 1 $position.X = $Host.UI.RawUI.WindowSize.Width - $duration.Length - 1 $Host.UI.RawUI.CursorPosition = $position $Host.UI.WriteLine('Gray', $Host.UI.RawUI.BackgroundColor, $duration) } } <# .SYNOPSIS Enable command help. .DESCRIPTION Type F1 for help off the current command line. .LINK https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1 #> function Enable-PSReadLineCommandHelp { # Show a grid view output if ($PSVersionTable.PSEdition -ne 'Core') { $commandHelpSplat = @{ Key = 'F1' BriefDescription = 'CommandHelp' LongDescription = 'Open the help window for the current command' ScriptBlock = { param($key, $arg) $ast = $null $tokens = $null $errors = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$errors, [ref]$cursor) $commandAst = $ast.FindAll( { $node = $args[0] $node -is [CommandAst] -and $node.Extent.StartOffset -le $cursor -and $node.Extent.EndOffset -ge $cursor }, $true) | Select-Object -Last 1 if ($null -ne $commandAst) { $commandName = $commandAst.GetCommandName() if ($null -ne $commandName) { $command = $ExecutionContext.InvokeCommand.GetCommand($commandName, 'All') if ($command -is [AliasInfo]) { $commandName = $command.ResolvedCommandName } if ($null -ne $commandName) { Get-Help $commandName -ShowWindow } } } } } Set-PSReadLineKeyHandler @commandHelpSplat } } <# .SYNOPSIS Enable the history browser, basic history search and history save. .DESCRIPTION On Windows PowerShell, use the F7 key to show a grid view with the last commands. A command can be selected and inserted to the current cmdline position. With the up and down arrows, search the history by the currently typed characters on the command line. Sometimes you enter a command but realize you forgot to do something else first. This binding will let you save that command in the history so you can recall it, but it doesn't actually execute. It also clears the line with RevertLine so the undo stack is reset - though redo will still reconstruct the command line. .LINK https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1 #> function Enable-PSReadLineHistoryHelper { [CmdletBinding()] param () # Basic history searching Set-PSReadLineOption -HistorySearchCursorMovesToEnd Set-PSReadLineKeyHandler -Key 'UpArrow' -Function 'HistorySearchBackward' Set-PSReadLineKeyHandler -Key 'DownArrow' -Function 'HistorySearchForward' # Save current command line to history $saveInHistorySplat = @{ Key = 'Alt+w' BriefDescription = 'SaveInHistory' LongDescription = 'Save current line in history but do not execute' ScriptBlock = { param($key, $arg) $line = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$null) [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($line) [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() } } Set-PSReadLineKeyHandler @saveInHistorySplat # Show a grid view output if ($PSVersionTable.PSEdition -ne 'Core') { $historySplat = @{ Key = 'F7' BriefDescription = 'History' LongDescription = 'Show command history' ScriptBlock = { $pattern = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$pattern, [ref]$null) if ($pattern) { $pattern = [regex]::Escape($pattern) } $history = [System.Collections.ArrayList] @( $last = '' $lines = '' foreach ($line in [System.IO.File]::ReadLines((Get-PSReadLineOption).HistorySavePath)) { if ($line.EndsWith('`')) { $line = $line.Substring(0, $line.Length - 1) $lines = if ($lines) { "$lines`n$line" } else { $line } continue } if ($lines) { $line = "$lines`n$line" $lines = '' } if (($line -cne $last) -and (!$pattern -or ($line -match $pattern))) { $last = $line $line } } ) $history.Reverse() $command = $history | Out-GridView -Title 'History' -PassThru if ($command) { [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine() [Microsoft.PowerShell.PSConsoleReadLine]::Insert(($command -join "`n")) } } } Set-PSReadLineKeyHandler @historySplat } } <# .SYNOPSIS Use this helper function to easy jump around in the shell. .DESCRIPTION Use Ctrl+Shift+J with a marker key to save the current directory in the marker list. Afterwards, with Ctrl+J, jump to the saved directory. To show all saved markers, use Alt+J. .LINK https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1 #> function Enable-PSReadLineLocationMark { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] param () $Global:PSReadLineMarks = @{} $markDirectorySplat = @{ Key = 'Ctrl+Shift+j' BriefDescription = 'MarkDirectory' LongDescription = 'Mark the current directory' ScriptBlock = { param($key, $arg) $key = [Console]::ReadKey($true) $Global:PSReadLineMarks[$key.KeyChar] = $pwd } } Set-PSReadLineKeyHandler @markDirectorySplat $jumpDirectorySplat = @{ Key = 'Ctrl+j' BriefDescription = 'JumpDirectory' LongDescription = 'Goto the marked directory' ScriptBlock = { param($key, $arg) $key = [Console]::ReadKey() $dir = $Global:PSReadLineMarks[$key.KeyChar] if ($dir) { Set-Location $dir [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } } } Set-PSReadLineKeyHandler @jumpDirectorySplat $showDirectoryMarks = @{ Key = 'Alt+j' BriefDescription = 'ShowDirectoryMarks' LongDescription = 'Show the currently marked directories' ScriptBlock = { param($key, $arg) $Global:PSReadLineMarks.GetEnumerator() | ForEach-Object { [PSCustomObject]@{Key = $_.Key; Dir = $_.Value} } | Format-Table -AutoSize | Out-Host [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } } Set-PSReadLineKeyHandler @showDirectoryMarks } <# .SYNOPSIS Enable the smart insert/delete. .DESCRIPTION The next four key handlers are designed to make entering matched quotes parens, and braces a nicer experience. I'd like to include functions in the module that do this, but this implementation still isn't as smart as ReSharper, so I'm just providing it as a sample. .LINK https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1 #> function Enable-PSReadLineSmartInsertDelete { [CmdletBinding()] param () $smartInsertQuoteSplat = @{ Key = '"',"'" BriefDescription = 'SmartInsertQuote' LongDescription = 'Insert paired quotes if not already on a quote' ScriptBlock = { param($key, $arg) $quote = $key.KeyChar $selectionStart = $null $selectionLength = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength) $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) # If text is selected, just quote it without any smarts if ($selectionStart -ne -1) { [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $quote + $line.SubString($selectionStart, $selectionLength) + $quote) [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2) return } $ast = $null $tokens = $null $parseErrors = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$parseErrors, [ref]$null) function FindToken { param($tokens, $cursor) foreach ($token in $tokens) { if ($cursor -lt $token.Extent.StartOffset) { continue } if ($cursor -lt $token.Extent.EndOffset) { $result = $token $token = $token -as [StringExpandableToken] if ($token) { $nested = FindToken $token.NestedTokens $cursor if ($nested) { $result = $nested } } return $result } } return $null } $token = FindToken $tokens $cursor # If we're on or inside a **quoted** string token (so not generic), we need to be smarter if ($token -is [StringToken] -and $token.Kind -ne [TokenKind]::Generic) { # If we're at the start of the string, assume we're inserting a new string if ($token.Extent.StartOffset -eq $cursor) { [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote ") [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) return } # If we're at the end of the string, move over the closing quote if present. if ($token.Extent.EndOffset -eq ($cursor + 1) -and $line[$cursor] -eq $quote) { [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) return } } if ($null -eq $token) { if ($line[0..$cursor].Where{$_ -eq $quote}.Count % 2 -eq 1) { # Odd number of quotes before the cursor, insert a single quote [Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote) } else { # Insert matching quotes, move cursor to be in between the quotes [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote") [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) } return } if ($token.Extent.StartOffset -eq $cursor) { if ($token.Kind -eq [TokenKind]::Generic -or $token.Kind -eq [TokenKind]::Identifier) { $end = $token.Extent.EndOffset $len = $end - $cursor [Microsoft.PowerShell.PSConsoleReadLine]::Replace($cursor, $len, $quote + $line.SubString($cursor, $len) + $quote) [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($end + 2) } return } # We failed to be smart, so just insert a single quote [Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote) } } Set-PSReadLineKeyHandler @smartInsertQuoteSplat $insertPairedBracesSplat = @{ Key = '(','{','[' BriefDescription = 'InsertPairedBraces' LongDescription = 'Insert matching braces' ScriptBlock = { param($key, $arg) $closeChar = switch ($key.KeyChar) { <#case#> '(' { [char]')'; break } <#case#> '{' { [char]'}'; break } <#case#> '[' { [char]']'; break } } [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)$closeChar") $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor - 1) } } Set-PSReadLineKeyHandler @insertPairedBracesSplat $smartCloseBracesSplat = @{ Key = ')',']','}' BriefDescription = 'SmartCloseBraces' LongDescription = 'Insert closing brace or skip' ScriptBlock = { param($key, $arg) $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) if ($line[$cursor] -eq $key.KeyChar) { [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) } else { [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)") } } } Set-PSReadLineKeyHandler @smartCloseBracesSplat $backspaceSplat = @{ Key = 'Backspace' BriefDescription = 'SmartBackspace' LongDescription = 'Delete previous character or matching quotes/parens/braces' ScriptBlock = { param($key, $arg) $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) if ($cursor -gt 0) { $toMatch = $null if ($cursor -lt $line.Length) { switch ($line[$cursor]) { <#case#> '"' { $toMatch = '"'; break } <#case#> "'" { $toMatch = "'"; break } <#case#> ')' { $toMatch = '('; break } <#case#> ']' { $toMatch = '['; break } <#case#> '}' { $toMatch = '{'; break } } } if ($null -ne $toMatch -and $line[$cursor-1] -eq $toMatch) { [Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor - 1, 2) } else { [Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteChar($key, $arg) } } } } Set-PSReadLineKeyHandler @backspaceSplat } <# .SYNOPSIS Disable the information output stream for the global shell. #> function Disable-Information { [CmdletBinding()] [Alias('di')] param () Set-Variable -Scope Global -Name InformationPreference -Value 'SilentlyContinue' } <# .SYNOPSIS Disable the verbose output stream for the global shell. #> function Disable-Verbose { [CmdletBinding()] [Alias('dv')] param () Set-Variable -Scope Global -Name VerbosePreference -Value 'SilentlyContinue' } <# .SYNOPSIS Enable the information output stream for the global shell. #> function Enable-Information { [CmdletBinding()] [Alias('ei')] param () Set-Variable -Scope Global -Name InformationPreference -Value 'Continue' } <# .SYNOPSIS Enable the verbose output stream for the global shell. #> function Enable-Verbose { [CmdletBinding()] [Alias('ev')] param () Set-Variable -Scope Global -Name VerbosePreference -Value 'Continue' } <# .SYNOPSIS Update the workspace configuration for Visual Studio Code which is used by the extension vscode-open-project. .LINK https://marketplace.visualstudio.com/items?itemName=svetlozarangelov.vscode-open-project #> function Update-Workspace { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $false)] [ValidateScript({Test-Path -Path $_})] [System.String] $Path = $Env:Worksapce, [Parameter(Mandatory = $false)] [System.String] $ProjectListPath = "$Env:AppData\Code\User\projectlist.json" ) $projectList = @{ projects = [Ordered] @{} } foreach ($workspace in (Get-ChildItem -Path $Path -Filter '*.code-workspace' -File)) { $projectList.projects.Add(('Workspace {0}' -f $workspace.BaseName), $workspace.FullName) } foreach ($group in (Get-ChildItem -Path $Path -Directory)) { foreach ($repo in (Get-ChildItem -Path $group.FullName -Directory)) { $key = '{0} \ {1}' -f $group.Name, $repo.Name $projectList.projects.Add($key, $repo.FullName) } } if ($PSCmdlet.ShouldProcess($ProjectListPath, 'Update Project List')) { $projectList | ConvertTo-Json | Set-Content -Path $ProjectListPath } } # Prompt configuration and variables $Script:PromptHistory = 0 $Script:PromptColor = 'Yellow' $Script:PromptInfo = '[PS {0}.{1}]' -f $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor $Script:PromptAlias = $false $Script:PromptTimeSpan = $false $Script:PromptGit = $false $Script:PromptDefault = Get-Command -Name 'prompt' | Select-Object -ExpandProperty 'Definition' # Enumerate the prompt color based on the operating system if ([System.Environment]::OSVersion.Platform -eq 'Win32NT') { $Script:PromptColor = $(if(([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'Red' } else { 'DarkCyan' }) } if ([System.Environment]::OSVersion.Platform -eq 'Unix') { $Script:PromptColor = $(if((whoami) -eq 'root') { 'Red' } else { 'DarkCyan' }) } # Module command not found action variables $Script:CommandNotFoundEnabled = $false $Script:CommandNotFoundAction = @{} |