views:

844

answers:

14

What Powershell pitfalls you have fall into? :-)

Mine are:

# -----------------------------------
function foo()
{
    @("text")
}

# Expected 1, actually 4.
(foo).length

# -----------------------------------
if(@($null, $null))
{
    Write-Host "Expected to be here, and I am here."
}

if(@($null))
{
    Write-Host "Expected to be here, BUT NEVER EVER."
}

# -----------------------------------

function foo($a)
{
    # I thought this is right.
    #if($a -eq $null)
    #{
    #    throw "You can't pass $null as argument."
    #}

    # But actually it should be:
    if($null -eq $a)
    {
        throw "You can't pass $null as argument."
    }
}

foo @($null, $null)

# -----------------------------------

# There is try/catch, but no callstack reported.
function foo() 
{
   bar
}

function bar() 
{
  throw "test"
}

# Expected:
#  At bar() line:XX
#  At foo() line:XX
#  
# Actually some like this:
#  At bar() line:XX
foo

Would like to know yours to walk them around :-)

+4  A: 

My personal favorite is

function foo() {
  param ( $param1, $param2 = $(throw "Need a second parameter"))
  ...
}

foo (1,2)

For those unfamiliar with powershell that line throws because instead of passing 2 parameters it actually creates an array and passes one parameter. You have to call it as follows

foo 1 2
JaredPar
Peter M
Call essentially ignores any return value and forces the use of (). Without call subs must not use () and functions must use ().
EBGreen
+7  A: 

Another fun one. Not handling an expression by default writes it to the pipeline. Really annoying when you don't realize a particular function returns a value.

function example() {
  param ( $p1 ) {
  if ( $p1 ) {
    42
  }
  "done"
}

PS> example $true 
42
"done"
JaredPar
+2  A: 

Here is something Ive stumble upon lately (PowerShell 2.0 CTP):

$items = "item0", "item1", "item2"

$part = ($items | select-string "item0")

$items = ($items | where {$part -notcontains $_})

what do you think that $items be at the end of the script?

I was expecting "item1", "item2" but instead the value of $items is: "item0", "item1", "item2".

Shay Erlichmen
You aren't changing the value of items after you first set it. So I would be surprised if it changed. :)
JasonMArcher
I change the snippet so that it would make sense.
Shay Erlichmen
In that case, what is wrong is that $part contains a MatchInfo object. Since the MatchInfo object doesn't contain anything, the last comparison always returns true.
JasonMArcher
A: 

This works. But almost certainly not in the way you think it's working.

PS> $a = 42;
PS> [scriptblock]$b = { $a }
PS> & $b
42
JaredPar
+2  A: 

alex2k8, I think this example of yours is good to talk about:

# -----------------------------------
function foo($a){
    # I thought this is right.
    #if($a -eq $null)
    #{
    #    throw "You can't pass $null as argument."
    #}
    # But actually it should be:
    if($null -eq $a)
    {
        throw "You can't pass $null as argument." 
    }
}
foo @($null, $null)

PowerShell can use some of the comparators against arrays like this:

$array -eq $value
## Returns all values in $array that equal $value

With that in mind, the original example returns two items (the two $null values in the array), which evalutates to $true because you end up with a collection of more than one item. Reversing the order of the arguments stops the array comparison.

This functionality is very handy in certain situations, but it is something you need to be aware of (just like array handling in PowerShell).

JasonMArcher
+4  A: 

Here are my top 5 PowerShell gotchas

John D. Cook
A: 

This one has tripped me up before, using $o.SomeProperty where it should be $($o.SomeProperty).

Bratch
This is only and always the case within quoted strings.
JasonMArcher
And double-quoted strings at that since single quoted strings and here strings don't support variable expansion, sub-expressions or escape chars.
Keith Hill
+1  A: 

Functions 'foo' and 'bar' looks equivalent.

function foo() { $null  }
function bar() { }

E.g.

(foo) -eq $null
# True

(bar) -eq $null
# True

But:

foo | %{ "foo" }
# Prints: foo

bar | %{ "bar" }
# PRINTS NOTHING

Returning $null and returning nothing is not equivalent dealing with pipes.


This one is inspired by Keith Hill example...

function bar() {}

$list = @(foo)
$list.length
# Prints: 0

# Now let's try the same but with a temporal variable.
$tmp = foo
$list = @($tmp)
$list.length
# Prints: 1
alex2k8
A: 
# $x is not defined
[70]: $x -lt 0
True
[71]: [int]$x -eq 0
True

So, what's $x..?

stej
Two different comparisons, the first is $null to 0, which appears to be less than 1. The second is comparing 0 to 0. Interestingly, $null is less than 0, but greater than any negative number.
JasonMArcher
I know, that was the weird behaviour I wanted to point out. $x is less than 0 and equal to 0 at the same time.
stej
$x is $null since it is undefined, which means $x is all values when you do an inequality comparison it will always be true. When you force null to an int value it will be 0.
JNK
+6  A: 
$files = Get-ChildItem . -inc *.extdoesntexist
foreach ($file in $files) {
    "$($file.Fullname.substring(2))"
}

Fails with:

You cannot call a method on a null-valued expression.
At line:3 char:25
+ $file.Fullname.substring <<<< (2)

Fix it like so:

$files = @(Get-ChildItem . -inc *.extdoesntexist)
foreach ($file in $files) {
    "$($file.Fullname.substring(2))"
}

Bottom line is that the foreach statement will loop on a scalar value even if that scalar value is $null. When Get-ChildItem in the first example returns nothing, $files gets assinged $null. If you are expecting an array of items to be returned by a command but there is a chance it will only return 1 item or zero items, put @() around the command. Then you will always get an array - be it of 0, 1 or N items. Note: If the item is already an array putting @() has no effect - it will still be the very same array (i.e. there is no extra array wrapper).

Keith Hill
Very interesting, thank you! I have updated my response inspired by your answer.
alex2k8
+2  A: 
# The pipeline doesn't enumerate hashtables.
$ht = @{"foo" = 1; "bar" = 2}
$ht | measure

# Workaround: call GetEnumerator
$ht.GetEnumerator() | measure
Richard Berg
A: 

Another one I ran into recently: [string] parameters that accept pipeline input are not strongly typed in practice. You can pipe anything at all and PS will coerce it via ToString().

function Foo 
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, ValueFromPipeline=$True)]
        [string] $param
    )

    process { $param }
}

get-process svchost | Foo

Unfortunately there is no way to turn this off. Best workaround I could think of:

function Bar
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, ValueFromPipeline=$True)]
        [object] $param
    )

    process 
    { 
        if ($param -isnot [string]) {
            throw "Pass a string you fool!"
        }
        # rest of function goes here
    }
}

edit - a better workaround I've started using...

Add this to your custom type XML -

<?xml version="1.0" encoding="utf-8" ?>
<Types>
  <Type>
    <Name>System.String</Name>
    <Members>
      <ScriptProperty>
        <Name>StringValue</Name>
        <GetScriptBlock>
          $this
        </GetScriptBlock>
      </ScriptProperty>
    </Members>
  </Type>
</Types>

Then write functions like this:

function Bar
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)]
        [Alias("StringValue")]
        [string] $param
    )

    process 
    { 
        # rest of function goes here
    }
}
Richard Berg
PowerShell will automatically do any coversion it needs to that is available. You can see this with DateTime as well.
JasonMArcher
+2  A: 

Say you've got the following XML file:

<Root>
    <Child />
    <Child />
</Root>

Run this:

PS > $myDoc = [xml](Get-Content $pathToMyDoc)
PS > @($myDoc.SelectNodes("/Root/Child")).Count
2
PS > @($myDoc.Root.Child).Count
2

Now edit the XML file so it has no Child nodes, just the Root node, and run those statements again:

PS > $myDoc = [xml](Get-Content $pathToMyDoc)
PS > @($myDoc.SelectNodes("/Root/Child")).Count
0
PS > @($myDoc.Root.Child).Count
1

That 1 is annoying when you want to iterate over a collection of nodes using foreach if and only if there actually are any. This is how I learned that you cannot use the XML handler's property (dot) notation as a simple shortcut. I believe what's happening is that SelectNodes returns a collection of 0. When @'ed, it is transformed from an XPathNodeList to an Object[] (check GetType()), but the length is preserved. The dynamically generated $myDoc.Root.Child property (which essentially does not exist) returns $null. When $null is @'ed, it becomes an array of length 1.

A: 

Another one:

$x = 2
$y = 3
$a,$b = $x,$y*5

because of operators precedence there is not 25 in $b; the command is the same as ($x,$y)*5 the correct version is

$a,$b = $x,($y*5)
stej