Jump to page content

The PowerShell-Haters Handbook



PowerShell is the result of some crazed lunatics who thought that the optimal choice of language for lightweight scripting was the distilled essence of the most infamously intractible language on the planet, Perl. Perl is much easier to understand than it appears, until you start hitting your head on all manner of subtleties, such as the inability to tell barewords from constants in hash keys. Perl’s symbol overloading is so bad, that Perl resorts to guesswork to “do the right thing”, and this guesswork fails in a variety of obscure ways. Sane Perl code is easy to understand, but so much Perl library code seems to go out of its way to be as bizarre as possible, and Perl has no end of tricks to make programs as difficult to follow as it is possible without resorting to the likes of APL.

With PowerShell, the clue ought to be in the name. The objective seemed to be a simple, straightforward, readable language suitable for powerful but logical and easy-to-construct one-liners, and clear and maintainable script. That is not what happened.

PowerShell is a collection of everything that is wrong with Perl. It replicates and expands on Perl’s symbol overloading, to make it as awkward as possible to make sense of the syntax. It tries desperately to undermine decades of industry knowledge by deliberately choosing syntax forms that confuse anyone accustomed to industry-standard syntax such as C or BASIC. This would be excusable if the new syntax was clean and understandable, but the end result is to layer confusion onto ambiguity. Why are there two different array initialisation syntaxes? Why are hashes initialised using a semicolon as the delimiter? Since { … } can be both a code block and a hash initialiser, the use of a semicolon delimiter means that { …; … } is going to look like a series of statements in a block to anyone used to semicolon being a statement terminator.

There is a reason that languages such as BASIC and Pascal used such clear, readable syntax, and why BASIC has remained so popular. Keywords such as “THEN” and “END IF” clearly indicate where you stand in the code without depleting the painfully limited repertoire of symbols that can be typed on a keyboard (keyboards having never recovered from the decimation of typography brought about by typewriters).

As a Perl programmer, PowerShell should be easier to learn and work with, but it invariably proves more painful. PowerShell is clearly heavily inspired by Perl, but it misses the point spectacularly on so many counts. The commands are verbose, but the grammar is terse to the point of unreadable. It is a shell language that is too complicated to actually pull off one-liner commands without aggravation.

Note that this page is a work in progress, to be updated as more horrors come to light. Feel free to suggest more that I have yet to have had the misfortune to suffer through.

Terminology and grammar



Void context

Every command executed in implied void context writes the output that would have gone into a variable, to the screen instead. When running a sequence of void instructions, every one has to be separately silenced if you are logging output or trying to write out meaningful output without pages of nonsense being thrown in your face. It seems that there is an explicit void context to actually prevent this, analagous to a (void) cast, which is insane.

Scripts should automatically suppress all output in void context, in a manner vaguely analogous to having @echo off in batch files, but automatically.


The strict adherence to classes instead of the flexibility of SQL JOIN or Perl hashes means that you often cannot create one-liner reports, because the object names to list by each result, are trapped in the previous pipeline stage where you cannot get at them. For example, being able to measure the size of each subdirectory in the working directory, as a table of directory names and sizes. The “Shell” in “PowerShell” suggests that you should be able to execute useful one-liner commands, but the language is too poorly-conceived for this to work in far too many cases.

That is, for Get-Mailbox | Get-MailboxStatistics, it is not possible to output columns only returned by Get-Mailbox, as those are thrown away as Get-MailboxStatistics processes each object. Some people try to work around this by trying to remember fields during the pipeline, but none of the “solutions” they present actually work.

Error handling

It doesn’t.


Warnings from compilers, frameworks and libraries exist to alert the programmer when they appear to have done something wrong or ill-advised, but where there is no proof of a mistake. This may include taking shortcuts that are legitimate but where the language runtime cannot distinguish intended shortcuts from actual bugs, such as processing undefined values and accessing nonexistent hash keys.

Warnings can be addressed using several methods:

  1. Do something else: replace the code that generates the warning with another approach that does not;
  2. Adjust your code to avoid the warning, which generally improves code robustness and reduces ambiguity;
  3. Suppress the specific warning or category of warnings, either globally, or within just the relevant block of code.

Perl does this right. Perl warnings always warn of avoidable situations, and warning categories can be selectively suppressed across the whole program or temporarily within a block of code. Perl warning behaviour is also specific to each Perl module. Some development environments number each warning so that individual warnings can be disregarded.

PowerShell warnings are useless. PowerShell has no warning classification: you cannot turn off a specific type of warning that is not relevant. PowerShell warns you about usage you are not even employing, and about what might happen if you use an option that you specifically selected knowing full well its pros and cons. The place for these messages is in the documentation, not the warning stream! For example, Search-Mailbox warns:

The Search-Mailbox cmdlet returns up to 10000 results per mailbox if a search query is specified. To return more than 10000 results, use the New-MailboxSearch cmdlet or the In-Place eDiscovery & Hold console in the Exchange Administration Center

The problem is that this warning is issued even when -SearchQuery is not specified! There is no option to remove the warning, because it does not relate to anything you did.

New-PSSession warns:

PSSession … was created using the EnableNetworkAccess parameter and can only be reconnected from the local computer.

You don’t say. This characteristic is properly documented—just as is the limitation on -SearchQuery—and thus if I choose to use this feature, why should I be subjected to a warning? This is not a warning about a mistake I made: it is a warning against using a feature with intended behaviour.

Worse, Disconnect-PSSession issues this warning even when -EnableNetworkAccess was not set! That makes the warning not only a nuisance, but deceitful.

Thus, PowerShell fails the useful mitigations:

  1. The warnings relate to valid actions and thus they cannot be avoided by better programming; they are not warnings about ambiguous instructions that might be erroneous.
  2. There is no warning classification or identification, and thus no suppression of specific warnings.


PowerShell ISE (which is ghastly) does not have the power to handle constants correctly. When executing a script in ISE that sets constants, those constants are defined not within the context of the script execution, but ISE itself. Should you be so silly as to run the script again, the constants are still there from the prior execution, and attempts to define them become attempts to redefine them, which results in one of PowerShell’s characteristic error tirades.


One of the more insidious ideas in object-oriented development is the idea of live properties: set a new value on a property, and a method is secretly invoked to apply that change. PowerShell is not consistent about how this is applied. Some objects returned (such as an Active Directory user) come back as cached records that you can play with as desired. Other PowerShell objects are live connections to the data source, and changing the PowerShell object will change the original object. There is nothing in the syntax or calling convention that indicates whether you have live data or not, and no consistency about when you might possess live data. It is safer not to use live properties in a shell script: it just adds extra burden to the workload of a system administrator whose job is to maintain systems, not write complex software. (It is safest never to implement live properties at all.)

User interface

Tab complete does not make sense. When using wildcards to complete command names, they match incorrectly: “A*B” matches “ABCD”. There must be a special award for an organisation that has managed to get something as simple as a basic wildcard behaviour wrong.

Also, the Verb–Noun ordering means that typing the noun and pressing the tab key does not cycle through commands related to that noun, as the noun comes last. If you forget whether you were meant to use “Create-Foo”, “New-Foo” or “Make-Foo”, you cannot type “Foo” then press tab. (Here, you would want “*Foo” except that would also match “*FooBar” because of the incorrect wildcard expansion.)

Why does | ft -autosize have to be specified manually to get it to actually figure out how wide to format a table? It’s a computer, it can figure this stuff out by itself by now, right? There is a legitimate reason for this one: in order to format the table, PowerShell needs to know the size of every “cell” in the output, and this means that progressive output isn’t possible. The problem is that it pathologically makes columns too narrow. This is not an easy problem to solve, however, but it just adds to the time consumption of even the simplest of tasks. Additionally, it has a habit of giving you the most useless subset of columns for any object: you have to use select to request the useful ones, after carefully scrutinising the objects in fl to figure out what they happened to be called. Does a reference to a file use FullName or FullPath? Depends who wrote the cmdlet of course.


PowerShell provides access to the Registry via the command line. Why on earth would anyone want to do that? It’s insane. The end result is that Registry paths do not match those used everywhere else, including the long-awaited address bar in Registry Editor. UNIX’s “everything is a file” works as (dubiously) well as it does because it was a fundamental part of the OS. PowerShell is too late to pretend that everything in Windows is a file. It isn’t.


The following is a genuine Microsoft Exchange tip of the day. It is difficult to be determine whether or not this was meant to be a joke.

Tip of the day #85: Wondering how many log files are generated per server every minute? Quickly find out by typing: Get-MailboxDatabase -Server $env:ComputerName | ?{ %{$_.DatabaseCopies | ?{$_.ReplayLagTime -ne [TimeSpan]::Zero -And $_.HostServerName -eq $env:ComputerName} } } | %{ $count = 0; $MinT = [DateTime]::MaxValue; $MaxT = [DateTime]::MinValue; Get-ChildItem -Path $_.LogFolderPath -Filter "*????.log" | %{ $count = $count + 1; if($_.LastWriteTime -gt $MaxT){ $MaxT = $_.LastWriteTime}; if($_.LastWriteTime -lt $MinT){ $MinT= $_.LastWriteTime} }; ($count / ($MaxT.Subtract($MinT)).TotalMinutes) } | Measure-Object -Min -Max -Ave