Pete's Log: PowerShell Tips Part 1 - PowerShell Profile

Entry #1837, (Coding, Hacking, & CS stuff)
(posted when I was 42 years old.)

I've been using PowerShell a lot of late and have been learning and customizing things I'd like to remember, so... part 1 of maybe multiple parts is going to focus on the PowerShell profile file, a script that is run when you start a PowerShell session, similar to a .bashrc or .cshrc. The location of this file is stored in the $profile environment variable and probably looks something like

C:\Users\username\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1

It probably also doesn't exist unless you've created it yourself. The WindowsPowerShell directory may also not exist. So you'll need to create those yourself. The next bit of difficulty is that out-of-the-box, PowerShell probably won't like that file unless it's signed. I'll touch on that some more at the end. But first some more fun stuff.

Per-instance command history

Visual Studio Code has a nice built-in terminal feature, and by default that terminal will be a PowerShell terminal in Windows. It's not uncommon that I have several projects I'm working on simultaneously, and while it's great that PowerShell will save your command history across sessions, it annoys me to find commands from other sessions in my history when I'm working in the context of a specific project. Google didn't find me a quick answer, so I did it the old fashioned way and figured it out myself. Add the following to your PowerShell profile script:

$psLocalHistoryFile = ".pslocalhistory.txt"

function Use-LocalHistory {
	if (test-path $psLocalHistoryFile) {
		Set-PSReadlineOption -HistorySavePath (Resolve-Path $psLocalHistoryFile).Path
	}
}

function Start-LocalHistory {
	Add-Content "" -Path $psLocalHistoryFile
	Use-LocalHistory
}

Use-LocalHistory

This does two things:

  1. On Startup, PowerShell will check for the existence of a .pslocalhistory.txt file and if found will use that local file instead of the global file for its command history.
  2. Defines a Start-LocalHistory function you can use to create the file if it doesn't exist and start using it.

This has worked great so far for my use case in the Visual Studio Code integrated terminal. Just don't forget to add .pslocalhistory.txt to your git/svn ignore list. Also, this does not work as expected for the explorer context menu "Open PowerShell window here" option, because apparently that starts PowerShell inside C:\Windows\System32 and then changes directory to your directory only after running your profile. Annoying, but that's not my target use case so I haven't searched for a workaround.

Custom Prompt

My Linux shell prompt, for at least 20 years now has looked as follows, and I am pretty fond of the way it looks:

[14:17:44] prijks@esgeroth:pts/0 [/var/www/] (0)
prompt-fu =>

I really like having the time and current directory in there, and the username and hostname are often helpful too, especially when you want to be sure you're on the right server before running a command. Since all that information will often fill most of a line, I added a new line before the actual prompt so there'd be an almost full line for typing the command. And then I added "prompt-fu" because it looked weird if the line was too empty. So here's what that looks like in my .cshrc file:

set prompt="[%P] %B%n@%M%b:%l [%~] (%?)\nprompt-fu =%# "

PowerShell handles the prompt a little differently — instead of setting a special variable, you define a function called Prompt. PowerShell runs this function each time it needs to display your prompt and displays the return value of that function. Here's what my PowerShell prompt looks like, which is close to but not quite the same as the above:

function Prompt {
	$LastCommandResult = $?
	"[$(Get-Date -Format "HH:mm:ss")] $($env:UserName)@$($env:ComputerName) [$($executionContext.SessionState.Path.CurrentLocation)] ($LastCommandResult)`r`nprompt-fu =$('>' * ($nestedPromptLevel + 1))"
}

And here's what it looks like:

[14:04:15] prijks@esgeroth [C:\Users\prijks] (False)
prompt-fu =>

The main differences are

  1. It doesn't display the tty (%l in the tcsh prompt variable above) since that's not really a thing in Windows
  2. The username@hostname aren't bold since that's apparently non-trivial to do
  3. The $? will show True/False as to whether the previous command executed successfully instead of the exit code of the previous command, which is what %? in tcsh does. $? has to be captured in a variable before formatting the string, since if inlined in the string it will always show true. I assume that's because it's capturing the result of Get-Date or something like that.
  4. It will show multiple >s depending on the PowerShell nesting level. This isn't something I've really ever come across before, but it's what the default PowerShell prompt does, so I figured I'd emulate that behavior. That's the '>' * ($nestedPromptLevel + 1).

And for reference, you can see the current definition of your prompt function with this command:

(get-command Prompt).ScriptBlock

The default is

"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) ";

Convincing PowerShell to run your profile

Depending on your execution policy, PowerShell may refuse to run your profile script and instead show you something like this on startup:

. : File C:\Users\username\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded because running
scripts is disabled on this system. For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170.

There are a couple ways around this:

  1. Change the Execution Policy using Set-ExecutionPolicy. I still haven't made up my mind on how I feel about PowerShell execution policies, so I won't make a specific recommendation here other than to know what you're doing before you change this setting.
  2. Sign your profile. To do this, you'll need a code signing certificate. You can use a self-signed certificate or get a properly signed one. Again, I'm not going to recommend a specific approach or document how to get that certificate (it's easy enough to find instructions). Once you have that certificate, you can sign your profile using the command
    Set-AuthenticodeSignature -FilePath $profile -Certificate $cert
    
    Where $cert is obtained for example (if your code signing certificate is in your user's personal certificate store) with the command
    $cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert