JHauge's blog

Philosophy major turned web developer

11 Jul 2020

My Shell Setup

Once you’ve got Windows Terminal up and running you will start to yearn for a really nice shell to use from the Terminal. My preferred shell is Powershell - more specifically Powershell Core, which seems to be the version Microsoft is favoring today, where the Windows Powershell prompt on Windows 10, actually advertises Powershell Core.

I find that installing Powershell Core is most easily done using chocolatey

choco install powershell-core

Introduction

Customizing your powershell environment is mostly a question of finding nice extensions for your environment and configure extensions, environment and prompt during shell startup.

Extensions
In Powershell world extensions comes in form of modules. Modules are packages of functions that adds additional commands to the existing commands in Powershell, an example would be The Az module also known as Azure Powershell that adds a long list of commands that lets you connect to and work with your Azure Subscriptions, Resourcegroups and Resources.
Environment
Powershell runs in your computer environment and a powershell environment, these environments has a number of predefined “variables”, you can access and manipulate in scripts.
Prompt
The prompt is basically the only UI always available on the screen that will give you some indication about where you are and what your options are. From DOS you would be accustomed to the C:\> prompt, telling you where you are on your disk system, but it’s possible to get much more information from the prompt if you want.

So to get at the setup I have I:

  1. Added some modules from the Powershell gallery that helps me customize my prompt to show some extra bits of information like: If the last command errored, if I’m in an elevated command prompt, what time last command ended, which user am I running as on what machine, which part I am currently in and the status of the git repo, if the current path is inside a git repo.
  2. Added a little customization to the prompt myself that shows what Azure context I’m operating in, if I load the Azure Powershell module.
  3. Created and added a powershell module with some helper methods I find it useful to have in my daily work, like what is my current external IP-address, commands that bundles common command series I tend to perform in git, a command that gives my a guid and some more
  4. Added some paths to the path-environment variable that lets me use some tools I’ve installed without qualifying the path to the exe I’m using.

The Powershell profile(s)

All customization of you powershell environment is done through the Powershell profiles - and I write that in plural on purpose. A Powershell profile is a powershell script that runs before your powershell shell is ready for input, letting you customize the environment so that you don’t need to do it manually every time you start a powershell prompt. The profile scripts is the place to put commands that set up everything I outlined above.

If you start a powershell sell, and type the command $PROFILE will return a path to a script file, that the shell will look for and execute every time you start it - if it’s present on the system, powershell doesn’t report an error if it’s not present, it’s purely optional.

But this is a little deceiving, powershell actually looks for 4 different profile script, which is revealed if you execute the following powershell command that instructs powershell to tell you a bit more about the variable $PROFILE

>$PROFILE | Format-List * -Force

AllUsersAllHosts       : C:\Program Files\PowerShell\7\profile.ps1
AllUsersCurrentHost    : C:\Program Files\PowerShell\7\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts    : C:\Users\jespe\OneDrive\Documents\PowerShell\profile.ps1
CurrentUserCurrentHost : C:\Users\jespe\OneDrive\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
Length                 : 77

This shows that the $PROFILE variable actually has 4 hidden properties corresponding to the 4 scripts it will actually look for, when you just type $PROFILE in the prompt it will by default return the value from the CurrentUserCurrentHost property.

The reason for this is that Powershell installed on a machine can run under different circumstances: It can run under different users, and it can run in different hosts. The user part is easily understandable, you might have noticed that the profile scripts starting with “CurrentUser” both are found in somewhere below your home directory on your machine. Host is most easily illustrated by starting VS Code and run a this command inside a Powershell prompt in VS Code:

>$PROFILE | Format-List * -Force

AllUsersAllHosts       : C:\Program Files\PowerShell\7\profile.ps1
AllUsersCurrentHost    : C:\Program Files\PowerShell\7\Microsoft.VSCode_profile.ps1
CurrentUserAllHosts    : C:\Users\jespe\OneDrive\Documents\PowerShell\profile.ps1
CurrentUserCurrentHost : C:\Users\jespe\OneDrive\Documents\PowerShell\Microsoft.VSCode_profile.ps1
Length                 : 73

As you see here the “CurrentHost” profiles differ from the ones you see when running directly in Powershell, these scripts are bound to the host Powershell runs in. Other hosts that might be present on your machine could be Windows Powershell or Powershell ISE, which is an integrated development environment for Powershell you can spin up with the command Powershell_ISE.

If you’ve dabbled with your profile-script before, and just followed the usual recommendation to edit the file you find when you type $PROFILE, you might have experienced some confusion as to why your profile script changes are not available in VS Code or Windows Powershell. This is because $PROFILE returns the path to the current user and current host. In other words if you want your customizations to be available in all your hosts I would recommend you create a file at the path known as “CurrentUserAllHosts”. If you frequently login as another user on your current machine you might even consider the AllUsersAllHosts profile.

With that out of the way we can get on to modifying the scripts themselves - let’s start by adding some modules.

Import modules

Powershell modules are small packages of functionality currently I load 3 different modules by default when I start Powershell

  1. The Z module - a module that has some clever scripting that helps me autocomplete paths to my most used locations. It has logic that watches when I navigate to different paths and stores them in a local db, then it does regex matching on what I typed at the prompt and autocompletes when I press the tab-key. So to go to the place I keep my blog project I just need to type >z bl and press tab and z autocompletes to z 'C:\Users\jespe\Projects\IQ-Blog', then I just press enter to go to the path.
  2. The posh-git module - this module expands my prompt with information about the status of the git directory I’m in, whenever I’m actually in a git directory.
  3. The oh-my-posh module - this module depends on the posh-git module, and expands the capabilities for customizing the prompt, adding theming support and the use of Powerline fonts to create a really nice font.

In order to load a module into your shell you need to install the module and import it into the shell. Installing a module is a bit like installing a chocolatey or nuget package. But since all packages are scripts, you need to allow running of local script files in Powershell if you haven’t done so already. Open a terminal with Powershell as administrator and type

>Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

This instructs Powershell to remember that you want to allow running script files that are downloaded to your local machine. Remote scripts needs to be signed.

Now you can install the needed modules using the Install-Module command. To get the Z module and the nice git prompt use the following commands.

>Install-Module Z -Scope CurrentUser
>Install-Module posh-git -Scope CurrentUser -AllowPrerelease -Force
>Install-Module oh-my-posh -Scope CurrentUser

Next up is to import your modules during shell startup. I’m using my CurrentUserAllHosts profile for this, since I want these modules in all hosts (that is Powershell in Windows Terminal and VS Code). So I added the following lines to C:\Users\jespe\OneDrive\Documents\PowerShell\profile.ps1, which is the location of my CurrentUserAllHosts powershell profile.

# Z directory browsing
Import-Module z

# git prompt
Import-Module posh-git
Import-Module oh-my-posh

As soon as this is up and running you can experiment with the different prompt themes included with oh-my-posh, using the Set-Theme command - try running the command Get-Theme first, this will produce a list of available themes, next you can try them out with Set-Theme -name ThemeName, remember to try it out from a git-directory to really see the effect of the prompt changes.

When you find a theme you like, you can add a Set-Theme command to your profile script to load it every time you start your prompt.

# Z directory browsing
Import-Module z

# git prompt
Import-Module posh-git
Import-Module oh-my-posh
Set-Theme -name AgnosterPlus

Setting environment variables

For me setting environment variables is mostly handy for adding some my mostly used commandline tools paths to the path environment variable, so that I don’t need to run them from the installed location. In my profile script I added the following line:

$env:path += ";C:\Program Files\Microsoft SQL Server\140\DAC\bin;C:\Program Files (x86)\Microsoft SDKs\Azure\AzCopy\;~\AppData\Local\gmaster\bin\"

This gives me immediate access to sqlpackage.exe, azcopy.exe and gmaster.exe. You might have you own favorite tools to add in here.

Making your own prompt

oh-my-posh supports creating your own prompt script and load it through the Set-Theme command, I find the easiest way to get started is to start with the script for the oh-my-posh prompt you like best and customize that. To see the location of the existing themes, where the prompt is set up, run Get-Theme which shows the location of the various theme scripts, now you can copy the script the corresponds to your favorite theme and make your on prompt from that.

Besides looking for theme scripts in the module folder, oh-my-posh looks for user theme scripts in the location defined in the variable $ThemeSettings.MyThemeslocation, see the location by typing in the variable name in the powershell prompt, and you can make your own theme available by creating .psm1 files in this folder. I created a jhauge.psm1 script in this folder looking like this.

#requires -Version 2 -Modules posh-git

function Write-Theme {
    param(
        [bool]
        $lastCommandFailed,
        [string]
        $with
    )

    $prompt = Write-Prompt -Object "`n"

    $lastColor = $sl.Colors.PromptBackgroundColor
    $prompt += Write-Prompt -Object $sl.PromptSymbols.StartSymbol -ForegroundColor $sl.Colors.PromptForegroundColor -BackgroundColor $sl.Colors.SessionInfoBackgroundColor

    #check the last command state and indicate if failed
    If ($lastCommandFailed) {
        $prompt += Write-Prompt -Object "$($sl.PromptSymbols.FailedCommandSymbol) " -ForegroundColor $sl.Colors.CommandFailedIconForegroundColor -BackgroundColor $sl.Colors.SessionInfoBackgroundColor
    }

    #check for elevated prompt
    If (Test-Administrator) {
        $prompt += Write-Prompt -Object "$($sl.PromptSymbols.ElevatedSymbol) " -ForegroundColor $sl.Colors.AdminIconForegroundColor -BackgroundColor $sl.Colors.SessionInfoBackgroundColor
    }

    # Write the current Azure Powershell context if any
    if ((Test-Path "~/.Azure/AzureRmContext.json") -and ($(Get-Module | Select-Object Name) -like "*Az.Account*")) {
        $contextName = $((Get-Content "~/.Azure/AzureRmContext.json" | ConvertFrom-Json).DefaultContextKey)
        $prompt += Write-Prompt -Object "$($sl.PromptSymbols.CloudEnabledSymbol)$contextName :: " `
            -ForegroundColor $sl.Colors.PromptForegroundColor `
            -BackgroundColor $sl.Colors.SessionInfoBackgroundColor
    }

    $user = $sl.CurrentUser
    $computer = $sl.CurrentHostname
    $path = Get-FullPath -dir $pwd
    if (Test-NotDefaultUser($user)) {
        $prompt += Write-Prompt -Object "$user@$computer " -ForegroundColor $sl.Colors.SessionInfoForegroundColor -BackgroundColor $sl.Colors.SessionInfoBackgroundColor
    }

    if (Test-VirtualEnv) {
        $prompt += Write-Prompt -Object "$($sl.PromptSymbols.SegmentForwardSymbol) " -ForegroundColor $sl.Colors.SessionInfoBackgroundColor -BackgroundColor $sl.Colors.VirtualEnvBackgroundColor
        $prompt += Write-Prompt -Object "$($sl.PromptSymbols.VirtualEnvSymbol) $(Get-VirtualEnvName) " -ForegroundColor $sl.Colors.VirtualEnvForegroundColor -BackgroundColor $sl.Colors.VirtualEnvBackgroundColor
        $prompt += Write-Prompt -Object "$($sl.PromptSymbols.SegmentForwardSymbol) " -ForegroundColor $sl.Colors.VirtualEnvBackgroundColor -BackgroundColor $sl.Colors.PromptBackgroundColor
    }
    else {
        $prompt += Write-Prompt -Object "$($sl.PromptSymbols.SegmentForwardSymbol) " -ForegroundColor $sl.Colors.SessionInfoBackgroundColor -BackgroundColor $sl.Colors.PromptBackgroundColor
    }

    # Writes the drive portion
    $prompt += Write-Prompt -Object "$path " -ForegroundColor $sl.Colors.PromptForegroundColor -BackgroundColor $sl.Colors.PromptBackgroundColor

    $status = Get-VCSStatus
    if ($status) {
        $themeInfo = Get-VcsInfo -status ($status)
        $lastColor = $themeInfo.BackgroundColor
        $prompt += Write-Prompt -Object $($sl.PromptSymbols.SegmentForwardSymbol) -ForegroundColor $sl.Colors.PromptBackgroundColor -BackgroundColor $lastColor
        $prompt += Write-Prompt -Object " $($themeInfo.VcInfo) " -BackgroundColor $lastColor -ForegroundColor $sl.Colors.GitForegroundColor
    }

    # Writes the postfix to the prompt
    $prompt += Write-Prompt -Object $sl.PromptSymbols.SegmentForwardSymbol -ForegroundColor $lastColor

    $timeStamp = Get-Date -UFormat %R
    $timestamp = "[$timeStamp]"

    $prompt += Set-CursorForRightBlockWrite -textLength ($timestamp.Length + 1)
    $prompt += Write-Prompt $timeStamp -ForegroundColor $sl.Colors.PromptForegroundColor

    $prompt += Set-Newline

    if ($with) {
        $prompt += Write-Prompt -Object "$($with.ToUpper()) " -BackgroundColor $sl.Colors.WithBackgroundColor -ForegroundColor $sl.Colors.WithForegroundColor
    }
    $prompt += Write-Prompt -Object ($sl.PromptSymbols.PromptIndicator) -ForegroundColor $sl.Colors.PromptBackgroundColor
    $prompt += ' '
    $prompt
}

$sl = $global:ThemeSettings #local settings
$sl.PromptSymbols.StartSymbol = ''
$sl.PromptSymbols.FailedCommandSymbol = '🤢'
$sl.PromptSymbols.CloudEnabledSymbol = [char]::ConvertFromUtf32(0x26C5)
$sl.PromptSymbols.PromptIndicator = [char]::ConvertFromUtf32(0x276F)
$sl.PromptSymbols.SegmentForwardSymbol = [char]::ConvertFromUtf32(0xE0B0)
$sl.Colors.PromptForegroundColor = [ConsoleColor]::White
$sl.Colors.PromptSymbolColor = [ConsoleColor]::White
$sl.Colors.PromptHighlightColor = [ConsoleColor]::DarkBlue
$sl.Colors.GitForegroundColor = [ConsoleColor]::Black
$sl.Colors.WithForegroundColor = [ConsoleColor]::DarkRed
$sl.Colors.WithBackgroundColor = [ConsoleColor]::Magenta
$sl.Colors.VirtualEnvBackgroundColor = [System.ConsoleColor]::Red
$sl.Colors.VirtualEnvForegroundColor = [System.ConsoleColor]::White

Which is basically the Agnoster theme with a couple of additions: An empty line before the prompt ($prompt = Write-Prompt -Object “`n”), and an if statement that adds the currently selected Azure Powershell context name to the prompt when the Azure Powershell Module is loaded.

Adding the Azure powershell context to the powershell prompt

I did this by first adding a cloud symbol to the $sl.PromptSymbols variable, so that I can use it the prompt script above.

$sl.PromptSymbols.CloudEnabledSymbol = [char]::ConvertFromUtf32(0x26C5)

Then I added an if statement that checks if there’s a file called “AzureRmContext.json” in a folder called “/.Azure/” in my home directory and if I have loaded and Az.Account module into my Powershell prompt. Of both of these conditions are true, I should have an active Azure Powershell Context, and I can read the active context name from the file and add it to the prompt with a little cloud symbol in the front.

# Write the current Azure Powershell context if any
if ((Test-Path "~/.Azure/AzureRmContext.json") -and ($(Get-Module | Select-Object Name) -like "*Az.Account*")) {
    $contextName = $((Get-Content "~/.Azure/AzureRmContext.json" | ConvertFrom-Json).DefaultContextKey)
    $prompt += Write-Prompt -Object "$($sl.PromptSymbols.CloudEnabledSymbol)$contextName :: " `
        -ForegroundColor $sl.Colors.PromptForegroundColor `
        -BackgroundColor $sl.Colors.SessionInfoBackgroundColor
}

That’s more or less it, besides the stuff mentioned here I have a Powershell module of my own, with some extension commands I find handy, but that’s a topic for another blog post.

If you want to check out my profiles scripts in the entirety, you can find it all in my dotfiles repository on github.