Persistent PowerShell: The PowerShell Profile

You can mould PowerShell to the way you want to work, with all the settings and modules that you require, by using the profiles. Profiles are PowerShell scripts that run at startup, and once you have understood where they are and when each is used, they have a whole range of uses that make using PowerShell a lot more convenient.

While you work with a PowerShell session, you can modify its state (or environment) in any number of ways: You might import modules, create functions, specify aliases, or define variables, to name a few. But all of those are transitory: their effects disappear as soon as you close the PowerShell session. However, PowerShell provides a mechanism, called the PowerShell profile, which allows you to recreate any such environmental constructs and settings each time you start up a new PowerShell session. And if one profile is good, wouldn’t more be better? It turns out that any single PowerShell session may use any one (or all) of four different profiles, and different PowerShell hosts provide yet more profile choices: But it is actually much simpler to use than all this might appear, once you appreciate where all the moving parts fit. Let’s take a look.

The Shell in PowerShell

There is a distinction between the shell and the host in PowerShell. Don Jones, in his aptly titled post The Shell vs. The Host, explains that you-as a user typing at the keyboard-do not directly interact with PowerShell’s shell (or engine). Rather more accurately, you interact with a host application (like powershell.exe or powershell_ise.exe) that creates a runspace (an instance of the PowerShell engine). You can actually see the distinction by displaying the contents of the Name property of the $Host system variable alongside the $ShellId variable. You will see that, if you examine the values for the three most common PowerShell hosts, they all use the same engine (shell) while each presents a different user interface (host).

Host $Host.Name $ShellId
PowerShell ConsoleHost Microsoft.PowerShell
PowerShell ISE Windows PowerShell ISE Host Microsoft.PowerShell
Visual Studio Package Manager Console Package Manager Host Microsoft.PowerShell

Other PowerShell hosts could potentially have different $ShellId values (for example, some of the freely available PowerShell IDEs include PowerGUI, PowerShell Analyzer, and PowerShell Plus, but I’ve not checked their $ShellId values).

The PowerShell Profile

A PowerShell profile is nothing more than a fancy name for a script that runs when a PowerShell host starts. Quoting standard PowerShell help on about_profiles, “You can use the profile as a logon script to customize the environment. You can add commands, aliases, functions, variables, snap-ins, modules, and Windows PowerShell drives. You can also add other session-specific elements to your profile so they are available in every session without having to import or re-create them.”

Each PowerShell host actually supports two profiles, one is user-level, distinct for each user, and the other is system-level, common to all users. This should be a familiar paradigm that you will have seen with many Windows applications. PowerShell adds its own unique twist, though: it similarly distinguishes between host-level (one profile for each host) and system-level (one common profile for all hosts).

Thus, taking all the combinations of users and hosts, you could potentially use any (or all) of four different profiles:

  • AllUsersAllHosts
  • AllUsersCurrentHost
  • CurrentUserAllHosts
  • CurrentUserCurrentHost

These profiles all peaceably co-exist so you then need to be aware of the precedence-they are listed above in the order of execution. If you were to define the same variable in all four profiles, the variable will, once you launch a PowerShell host and finally receive a prompt, have the value assigned by the last profile, CurrentUserCurrentHost, because each successively processed profile will overwrite that variable with its value. Another example, showing cooperation between profiles rather than contention, might be to increment a variable. First define and initialize it to a starting value (e.g. $someVar = 0) in AllUsersAllHosts, then increment it in each of the other profiles (e.g. $someVar++ or perhaps $someVar += 5 depending on what you want to do with it).

As to which of the four to use, that largely depends on your own needs: if you use a dedicated computer (i.e. not shared with anyone else) you don’t need to worry about the “all users” profiles. If you do use multiple hosts, you might want to differentiate some things between an “all hosts” profile and a specific host profile. I’ll be giving more details about this below in “How Many Profiles Do You Need?”

The $Profile Variable

To create or edit any of the profiles, of course, you need to know where to find them. PowerShell itself can easily tell you, but it can also just open one for editing without you having to explicitly bother with the path. To see the path, display the value of the $Profile variable. It reveals a single file path, which is the path to the CurrentUserCurrentHost profile. In a standard PowerShell host, mine shows this:

Note that this makes no claim about whether the file exists, only that that is the path it must have if it exists. To check existence, use Test-Path:

If the profile does not exist, you can easily create it:

And if you want to have that in a script, you can then combine the above, creating the file only if necessary:

Finally, to edit the profile, just invoke your favorite editor on the file, or you can always use the ubiquitous notepad editor:

All of the above examples are for the “default” (CurrentUserCurrentHost) profile, as mentioned. But you can apply all of the same commands to any of the four profiles by specific reference; either of these produce the same result:

Substitute any of the other three profile property names to access one of the non-default profiles.

Note that it can be tricky to create or save files in system directories (where the “all users” profiles are stored).

You can do so with certain Microsoft tools (e.g. notepad) but you cannot with certain non-Microsoft tools (e.g. my favorite editor, vim). Curiously, vim pretended to work for me but it actually did not: I could create a file, close the editor completely, then re-open the editor and recall the file-yet the file did not show up in Windows Explorer nor was it seen by PowerShell upon startup! (I do not quite know the root cause of this issue, but it is not due to lack of elevated privileges.)

On the other hand, notepad apparently knows the secret incantation, as that works as expected. Another workaround is to create your “all users” profile in your own user directory then just copy or move it into the appropriate system directory.

Profile File Names & Locations

The previous section explained how to edit any of your profiles without actually knowing where they are on the file system, but you are a developer; you have an innate compulsion to know where they are hiding! The table shows the path of each. ( HostId is a placeholder, explained in just a bit.)

Profile Location
AllUsersAllHosts $PsHome\profile.ps1
AllUsersCurrentHost $PsHome\HostId_profile.ps1
CurrentUserAllHosts $Home\Documents\WindowsPowerShell\profile.ps1
CurrentUserCurrentHost $Home\Documents\WindowsPowerShell\HostId_profile.ps1

An error that I have seen in a number of articles available on the web is that the “all users” profiles are under $env:WinDir\System32. That is incorrect ! $PsHome may coincidentally resolve to $env:WinDir\System32 for some hosts but not for all. As an example, on my system the Visual Studio Package Manager Console stores its “all users” profiles under $env:WinDir\SysWOW64. (This error appears even in articles from very reputable sources, like this MSDN article.)

Reviewing the locations, it is simple to understand the naming conventions. The system-level profiles-those for all users-are in the system directory pointed to by $PsHome. The user-level profiles are under each user’s home directory. The one point that needs explanation is the HostId shown for the host-specific profiles in the table. Unfortunately, this host id bears no direct resemblance to the host description nor to the $Host.Name property! The way to discover the HostId is simply to display the value of the $Profile variable, as it is part of the path. For convenience, here are the HostId values for the most common hosts:

Host HostId $Host.Name
PowerShell Microsoft.PowerShell ConsoleHost
PowerShell ISE Microsoft.PowerShellISE Windows PowerShell ISE Host
Visual Studio Package Manager Console NuGet Package Manager Host

Another error out there in the wild, though less common, is that HostId is equivalent to the $ShellId variable mentioned earlier. That is incorrect! As you have seen, all three of the common hosts shown just above have the same $ShellId, and that coincidentally matches the HostId only for the standard PowerShell host. (This error appears in, for example, the book Windows PowerShell Unleashed.)

How Many Profiles Do You Need?

Any standard Windows system has two profiles for the standard PowerShell host, two for the PowerShell ISE host, and two for all hosts-six in all as a minimum. Add in the VS package manager that I have shown, that’s two more. Add other PowerShell IDEs-two more for each. How do you manage so many profiles? 

Interpreting official MSDN doctrine ( about_profiles) consider a tripartite solution. 

  • First, put truly common things in AllUsersAllHost
  • Second, if there are some peculiarities in particular hosts, use AllUsersCurrentHost for those miscreant hosts. 
  • Finally, let each user manage his/her own preferences and settings in user-specific profiles. 

But again, there could be many different profiles for “current host”, both at the system level and at the user level. One good reference, as you are mulling over your choices here, is Ed Wilson’s (The Scripting Guy) post Deciding Between One or Multiple PowerShell Profiles. He includes in there a list of advantages and disadvantages for choosing between one profile and multiple profiles. But to me, the advantages of a single profile far outweigh the advantages of multiple profiles. It takes a little more work to set up, but you can still account for host-specific differences while having everything in one place, making it much simpler to maintain over time. 

Most of your profile stuff will probably be common to any host, so using a single profile means that any changes down the road will be done in exactly one file. For the stuff that differs amongst hosts, just include a section specific to each host you care about. Mine includes something like this:

Notice how I identify the current host with $Host.Name then selectively execute code. You see examples above for the standard PowerShell host as well as the PowerShell ISE host. For the former, it loads the PSReadline extension which only works in the PowerShell host. For the latter, it loads the PsISEProjectExplorer and the ISE Steroids module, both of which can only function in the ISE environment. 

Finally, if you use Git and the particular installer you used created a Git PowerShell host for you, note that it cannot be distinguished from the standard PowerShell host by the $Host.Name. Instead, I identified a uniquely defined variable, $env:github_shell, which is only present in the Git-flavored host.

What to Put in your Profile

The answer to this question depends on your individual needs so it has to be very abstract: put in your profile what you can use to be more productive. There is therefore no single answer to this, but I can certainly provide some suggestions to get your creative juices flowing. You have just seen above a few examples of importing third-party modules to add functionality to the given host. Besides module imports, the following sections provide just a few ideas to get you thinking about what you might want to put in there. Also, take a look at What’s in your PowerShell `profile.ps1` file? (on StackOverflow) for many more examples.

Aliases

Many built-in PowerShell cmdlets have aliases; some have multiple aliases. You can, for example use ls or dir or gci instead of typing Get-ChildItem. For those that you use regularly that do not provide aliases (or for your own custom functions and cmdlets) you can create your own aliases with Set-Alias. Set-Alias is appropriate if you just want to abbreviate the name of the cmdlet or function. But sometimes you might want an alias to include, for example, a cmdlet name plus a parameter that you tend to always use. For these, you can emulate an alias with a simple function. To illustrate, consider the cmdlet Import-Module. I use it frequently and I prefer all my frequent use cmdlets to just use the first letter of each component. This sets up the alias im to do that:

But being a developer, I also need to frequently use Import-Module with the -Force switch. So for that, I need to resort to a function. For my naming convention, I add the first letter of the switch, hence imf here:

I can then use, e.g. im foobar to do a vanilla import, or imf foobar to import with -Force applied.

Simple Functions

Functions were just mentioned as a means to creating pseudo-aliases, i.e. essentially to save you typing. But, of course, they are not limited to that purpose. You might want to include in your profile a variety of “one-liners” that both save typing and save having to remember details of cmdlets you use less often. Quick, how do you show the last 50 items in your command history? Not certain? The cmdlet to use is Get-History (it has a standard alias of just the letter h). Is it easy to remember Get-History -Count 50 or just h50? Here’s my definition for h50 (and an h10 thrown in just for good measure):

Here’s a more interesting function. How would you reveal the cmdlet behind an alias, the path to an executable given just the program name, the parameter sets available for a given cmdlet, or the content of a user-defined function? I use this one-liner to do all that (named after the unix/linux command that performs similarly):

Here are some results of using it:

Finally, another handy one-liner that again works like the unix/linux command, reporting your domain-qualified user name:

Complex Functions

It is likely that you will create some utility functions in the course of your work that are much more involved than a simple one-liner. You could, of course, just embed them in your profile but that tends to make your profile long and messy. If you have a collection of such functions you could always organize them into a true PowerShell module and then just add an Import-Module in your profile to bring them in. For something in-between those two extremes, consider this approach. In my profile I have this command sequence that brings all the scripts in my local profile_scripts directory into scope at startup:

$PSScriptRoot is a standard system variable that only exists within the context of a running script. It resolves to $Home\Documents\WindowsPowerShell\. Thus my profile_scripts directory is under that path. Any script (sans any test scripts) is dot-sourced, making it visible in your current scope as soon as you have a prompt after startup.

Perform Actions

The previous items in this section are all passive; they define things for you to use at some future time after startup. But you can also include active items that execute during startup. I highly recommend you use the “don’t let me shoot myself in the foot” cmdlet:

Languages that are weakly-typed (e.g. JavaScript, PowerShell) give you the option of working safely or not, whereas strongly-typed languages generally force you to work safely. Primarily, safely means not allowing you to use a variable before it is declared. That prevents inadvertent mistyping from causing you untold grief trying to figure out why your code is not working. (Presumably, the weakly-typed language folks think some people might find operating safely burdensome, so they make it an option.) Just put the Set-PSDebug in your profile. Be safe. Please.

Other types of actions you might put in your profile are things like displaying some statistics, e.g. up time, disk space, or PowerShell execution policy. If you administer multiple machines you might want to see details about the machine you have ‘remoted’ into, to make sure you’re on the box you think you are (domain name, computer name, IP address, etc.).

Securing Your Profile

When dealing with computers, security must always be a consideration. You use a firewall and an antivirus to try to keep your system safe. Similarly you need to consider PowerShell scripts that you run- including your own profiles. PowerShell has good support for security, starting with its default setting of not letting you run any scripts; out of the box you can only use PowerShell commands interactively. You need to open your system just enough to enable you to accomplish whatever work you need to do by setting your computer’s execution policy (see Set-ExecutionPolicy).

But as soon as you allow running scripts, there is a chance that you might inadvertently be running a compromised script. This is nothing unique to PowerShell scripts per se- anything on your computer could be compromised-it is just that PowerShell helps you mitigate the situation. And it does this by allowing you to set the execution policy to different security levels per your own needs. You can require that every script must be authenticated, or that just any scripts that you download must be authenticated, among other options. Authentication, in this case, refers to signing scripts with a digital signature (see Set-AuthenticodeSignature) so that, if a file is modified (maliciously or not), the digital signature would detect the tampering and prevent the script from running.

Managing security for your PowerShell scripts (including your profiles), however, is not a trivial endeavor. (It would more than double the length of this article!) But a lot of good information is already out there to guide you. I would recommend starting with another article here on Simple-Talk, Nicolas Prigent’s PowerShell Day-to-Day SysAdmin Tasks: Securing Scripts. There are also several good references in PowerShell’s own documentation: about_signing gives a good introduction to the topic; New-SelfSignedCertificate lets you create your own self-signed certificates, and Get-ChildItem for Certificate reveals the little-known differences in Get-ChildItem when referencing your certificate store. Microsoft provides an old but still useful reference on Code-Signing Best Practices. And Geoff Bard’s Signing PowerShell Scripts is worth a look as well.

Get That Profile Out of the Way!

Now you know how to set up your profile, why it is useful, what to do with it, and how to safeguard it. But like any super power, you have to be cognizant of its dark side. Well, not so much a dark side per se, but that there are times you simply do not want your profile(s) to get in your way. Or, more poignantly, other people’s profiles.

There are a variety of situations where you might want to actually execute powershell.exe either with a command literal or a script file to run. Here is just one example: say you have created a PowerShell script that you want to share with a colleague. Unlike batch files, you cannot just double-click a PowerShell script to execute it; that’s part of PowerShell’s security modus operandi to keep your system safe. But that is easy to circumvent (not that I am recommending it!) by creating a standard Windows shortcut targeting powershell.exe with your PowerShell script file as a parameter.

Another, perhaps more legitimate use, would be to run a PowerShell script or command within a build file. Since MSBuild does not innately know how to run PowerShell scripts, you would typically execute a script by supplying it as an argument to powershell.exe.

Anytime you run powershell.exe, though, you are opening a new PowerShell host. And what happens when you open a host? It runs any (or all) of your four profiles! But almost any time you are opening a host by directly invoking powershell.exe you do not want your profiles to run, neither for the overhead nor for the possible conflicts that might ensue. Keep in mind, if someone else is running a build where you have introduced a command to run powershell.exe, it is their profile that will be run on their machine, and you have no notion of what might be lurking there. Further still, you do not want to depend on something in a profile because the first time someone runs your build who does not know of the dependency, it will (possibly mysteriously) fail. So it is safest all around to simply adopt the best practice of always ignoring profiles when you invoke powershell.exe. (I don’t mean you should ignore them, rather that you should tell PowerShell to ignore them, of course!

So after all that suspenseful buildup, the denouement may be a bit anticlimactic: simply add a -NoProfile as a parameter to powershell.exe.

Conclusion

The PowerShell profile is your friend. With the roadmap set out in this article, you have seen the types of profiles available to you and can select the ones that will work for you. Or you can choose to use a single profile, distinguishing any host-specific items as needed. The profile is a simple yet powerful tool available to you, not at all complicated to use, and its uses are limited only by your imagination. About the only downside is all the time you are now going to spend searching for handy and clever things to add to your profile. (Did I mention you should look at including the Go-Shell module…?)