One day, my colleague told me that powershell is actually decent and that convinced me to try it out. The main motivation for trying out comes from my wanting to do automation on my personal laptop which runs Windows. At my day job, I work with Linux servers so my expertise is with working with Linux shells like bash or zsh, often writing scripts for mundane tasks. When I want to do any automation on Windows, I go through the awkardness of running a virtual machine (VM) like Windows Subsystem for Linux (WSL). Running something like powershell means that I can cut away the performance impact from doing things through the intermediate VM. In this post, I want to share my first impressions of powershell, and finish off with an example where I used powershell to extract xml data revealing some people’s clipboard data.

Bash vs powershell commands

As a regular Linux user, I often find myself paralyzed when I have to use the Windows command prompt (CMD). This was one of my concerns when trying out powershell. Surprizingly, powershell comes with several aliases from Linux defined which makes it easier to switch over.

cat <file> -> get-content <file> 
cp -> copy-item
cd -> set-location
mkdir -> new-item
history -> get-history
ls -> get-childitem
mv -> move-item
ps -> get-process
pwd -> get-location
rm -> remove-item
kill -> stop-process
man -> help

To view the full list, use ls alias:\. Something interesting to note here is that aliases can be viewed like files in a drive. c.f. ls C:\.

However, some other common commands require some translation.

wget/curl <url> -> invoke-restmethod
find . -type f -name foo -> get-childitem . -name foo -file (or ls for alias)
grep <text> -> select-string <text> (or sls for alias)

Profiles and rc (run comands)

Shells have the ability to run a set of commands on start up. On Linux, these are usually placed in a rc file that is dependent on the shell used. e.g. .bashrc for bash or .zshrc for zsh.

For powershell, the location of this is slightly awkward to type manually, thankfully, there is an inbuilt variable called $profile.

code $profile will start vscode to modify the profile file which is read on each start.

Functions

Any seasoned shell user will have their own workflow for doing things, and the best way to set up a workflow, IMHO, is to add functions into the profile or rc files.

# I have these defined in my .zshrc
alias k="cd /mnt/d/sync/knowledge" # Frequently accessed folders
alias es="vim ~/.localdlowrc" # For convenient modifications
alias rs="source ~/.localdlowrc" # For convenient reloading

After porting the above to powershell, they become:

function k {
    Set-Location -Path D:\sync\knowledge
}

function es {
			    vim $profile
}

function rs {
			    . $profile
}

n.b. There is a set-alias command but set-alias -name k -value "set-location -path D:\sync\knowledge" does not do the same thing.

Structured objects

Most things in linux shell are strings but all things in powershell are objects. A corollary of this is the ability to parse and traverse xml/json without using tools like jq.

> $x = echo '{"a":1}' | ConvertFrom-Json # now x is a JSON object
> $x.a
1

I once wanted to parse a 30MB unformatted json file, opening it in vim and vscode was extremely slow and was also unreadable. Using powershell, I was able to quickly traverse it. That said, Python could have been the other option too but the benefit of doing it in powershell is that you can integrate with other cmdlets.

For better or for worse, we need to learn the new syntax for traversing these objects. For example, let’s try to traverse all the alias and find out what gci means.

PS C:\Users\daniel> (ls alias:\) | Where-Object { $_.Name -eq 'gci' }

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           gci -> Get-ChildItem

n.b. get-alias does the same thing.

Environment

Powershell environment can be viewed, like aliases, in the env drive: ls env:. Alternatively, $env:<key> can also be used.

PS C:\Users\daniel> ls env:\PATH

Name                           Value
----                           -----
Path                           D:\App\vim;C:\Program Files (x86)\Vim\vim82\;...

To search for something in the path, we can extract the string value, split by ; and then search for the given program. e.g. to search for where Python binary is used:

PS C:\Users\daniel> $env:PATH.Split(";") | sls Python

C:\Users\daniel\AppData\Local\Programs\Python\Python38\Scripts\
C:\Users\daniel\AppData\Local\Programs\Python\Python38\
C:\Users\daniel\AppData\Local\Programs\Python\Launcher\
C:\Users\daniel\AppData\Local\Programs\Python\Python37\Scripts\
C:\Users\daniel\AppData\Local\Programs\Python\Python37\

Adding something to the path is simply reassigning that value:

$env:PATH = "C:\users\daniel\bin;$env:PATH"

XML example

I recently read an article about how secrets are leaked unintentionally through Intellij vim plugin. The rough idea is that the plugin stores clipboard history in a file vim_settings.xml. If someone has copied some secrets into the clipboard and then unintentionally uploads the vim_settings.xml onto Github, the secret is now on the internet. I decided to put my powershell-fu to the test to try to retrieve this hypothetical secret if I had access to such a file.

  1. First I search for some vim_settings.xml files on Github and store the link to the raw file in a variable. Here I retrieved it from my clipboard to save a few keystrokes.
$target = get-clipboard
  1. Next, I download the xml and save it into a file in.xml.
Invoke-RestMethod $target -OutFile .\in.xml

If I peek into the contents, I can see that the registers node looks like this.

    <registers>
      <register name="&quot;" type="2">
        <text encoding="base64">Cg==</text>
      </register>
      <register name="-" type="4">
        <text>333</text>
      </register>
      <register name="/" type="4">
        <text>main</text>
  1. Then I use select-xml to navigate to the relevant section and print out the content. Noticed that some nodes have base64 encoding, but I can decode it using the base64 program. I cheated a bit here because base64 is actually from WSL and not from powershell.
> Select-Xml -Path .\in.xml -XPath "//register/text[not(@encoding)]" | 
	foreach {$_.Node.InnerText}
# output truncated
> Select-Xml -Path .\in.xml -XPath "//register/text[@encoding='base64']" | 
	foreach {$_.Node.InnerText | base64.exe -d}
# output truncated

But there we have it, finding some random vim_settings.xml on the internet, downloading it, traversing the XML document and extracting some clipboard history data, all using powershell.

First impressions

Here are some of my impressions of powershell.

  • Definitely more usable than CMD. Might just be me, but my muscle memory is very resistant to CMD’s equivalent e.g. dir.
  • I am ambivalent about the verbosity of commands. I think Invoke-RestMethod is just as obscure as wget or curl.
  • Vim is a bit awkward to use now because I can’t send it into the background in powershell.
  • Consistency is definitely a nice change. e.g. all options are single-hyphened.
  • Commands on powershell are slightly verbose but follows a very predictable pattern of <verb>-<noun>(not all the time though. e.g. foreach-object). Thankfully it is very easy to customize and come up with aliases for people like me who cannot stand typing more than a few characters.

I’ll definitely be trying to port more of my unix-y workflow over to powershell since that’s where I spent most of my time in.